forked from root/acme.js
backport all the things
This commit is contained in:
parent
7e6a66c1d8
commit
f05e9db38e
|
@ -0,0 +1,161 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var A = module.exports;
|
||||||
|
var U = require('./utils.js');
|
||||||
|
|
||||||
|
var Keypairs = require('@root/keypairs');
|
||||||
|
var Enc = require('@root/encoding/bytes');
|
||||||
|
|
||||||
|
A._getAccountKid = function(me, options) {
|
||||||
|
// It's just fine if there's no account, we'll go get the key id we need via the existing key
|
||||||
|
options._kid =
|
||||||
|
options._kid ||
|
||||||
|
options.accountKid ||
|
||||||
|
(options.account &&
|
||||||
|
(options.account.kid ||
|
||||||
|
(options.account.key && options.account.key.kid)));
|
||||||
|
|
||||||
|
if (options._kid) {
|
||||||
|
return Promise.resolve(options._kid);
|
||||||
|
}
|
||||||
|
|
||||||
|
//return Promise.reject(new Error("must include KeyID"));
|
||||||
|
// This is an idempotent request. It'll return the same account for the same public key.
|
||||||
|
return A._registerAccount(me, options).then(function(account) {
|
||||||
|
options._kid = account.key.kid;
|
||||||
|
// start back from the top
|
||||||
|
return options._kid;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ACME RFC Section 7.3 Account Creation
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
"protected": base64url({
|
||||||
|
"alg": "ES256",
|
||||||
|
"jwk": {...},
|
||||||
|
"nonce": "6S8IqOGY7eL2lsGoTZYifg",
|
||||||
|
"url": "https://example.com/acme/new-account"
|
||||||
|
}),
|
||||||
|
"payload": base64url({
|
||||||
|
"termsOfServiceAgreed": true,
|
||||||
|
"onlyReturnExisting": false,
|
||||||
|
"contact": [
|
||||||
|
"mailto:cert-admin@example.com",
|
||||||
|
"mailto:admin@example.com"
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
"signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
A._registerAccount = function(me, options) {
|
||||||
|
//#console.debug('[ACME.js] accounts.create');
|
||||||
|
|
||||||
|
function agree(tosUrl) {
|
||||||
|
var err;
|
||||||
|
if (me._tos !== tosUrl) {
|
||||||
|
err = new Error("You must agree to the ToS at '" + me._tos + "'");
|
||||||
|
err.code = 'E_AGREE_TOS';
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return U._importKeypair(
|
||||||
|
me,
|
||||||
|
options.accountKey || options.accountKeypair
|
||||||
|
).then(function(pair) {
|
||||||
|
var contact;
|
||||||
|
if (options.contact) {
|
||||||
|
contact = options.contact.slice(0);
|
||||||
|
} else if (options.subscriberEmail || options.email) {
|
||||||
|
contact = [
|
||||||
|
'mailto:' + (options.subscriberEmail || options.email)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
var accountRequest = {
|
||||||
|
termsOfServiceAgreed: tosUrl === me._tos,
|
||||||
|
onlyReturnExisting: false,
|
||||||
|
contact: contact
|
||||||
|
};
|
||||||
|
var pExt;
|
||||||
|
if (options.externalAccount) {
|
||||||
|
pExt = Keypairs.signJws({
|
||||||
|
// TODO is HMAC the standard, or is this arbitrary?
|
||||||
|
secret: options.externalAccount.secret,
|
||||||
|
protected: {
|
||||||
|
alg: options.externalAccount.alg || 'HS256',
|
||||||
|
kid: options.externalAccount.id,
|
||||||
|
url: me._directoryUrls.newAccount
|
||||||
|
},
|
||||||
|
payload: Enc.strToBuf(JSON.stringify(pair.public))
|
||||||
|
}).then(function(jws) {
|
||||||
|
accountRequest.externalAccountBinding = jws;
|
||||||
|
return accountRequest;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pExt = Promise.resolve(accountRequest);
|
||||||
|
}
|
||||||
|
return pExt.then(function(accountRequest) {
|
||||||
|
var payload = JSON.stringify(accountRequest);
|
||||||
|
return U._jwsRequest(me, {
|
||||||
|
options: options,
|
||||||
|
url: me._directoryUrls.newAccount,
|
||||||
|
protected: { kid: false, jwk: pair.public },
|
||||||
|
payload: Enc.strToBuf(payload)
|
||||||
|
}).then(function(resp) {
|
||||||
|
var account = resp.body;
|
||||||
|
|
||||||
|
if (resp.statusCode < 200 || resp.statusCode >= 300) {
|
||||||
|
if ('string' !== typeof account) {
|
||||||
|
account = JSON.stringify(account);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
'account error: ' +
|
||||||
|
resp.statusCode +
|
||||||
|
' ' +
|
||||||
|
account +
|
||||||
|
'\n' +
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var location = resp.headers.location;
|
||||||
|
// the account id url
|
||||||
|
options._kid = location;
|
||||||
|
//#console.debug('[DEBUG] new account location:');
|
||||||
|
//#console.debug(location);
|
||||||
|
//#console.debug(resp);
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
contact: ["mailto:jon@example.com"],
|
||||||
|
orders: "https://some-url",
|
||||||
|
status: 'valid'
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if (!account) {
|
||||||
|
account = { _emptyResponse: true };
|
||||||
|
}
|
||||||
|
// https://git.rootprojects.org/root/acme.js/issues/8
|
||||||
|
if (!account.key) {
|
||||||
|
account.key = {};
|
||||||
|
}
|
||||||
|
account.key.kid = options._kid;
|
||||||
|
return account;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(function() {
|
||||||
|
//#console.debug('[ACME.js] agreeToTerms');
|
||||||
|
var agreeToTerms = options.agreeToTerms;
|
||||||
|
if (true === agreeToTerms) {
|
||||||
|
agreeToTerms = function(tos) {
|
||||||
|
return tos;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return agreeToTerms(me._tos);
|
||||||
|
})
|
||||||
|
.then(agree);
|
||||||
|
};
|
|
@ -8,9 +8,6 @@ var PEM = require('@root/pem');
|
||||||
var punycode = require('punycode');
|
var punycode = require('punycode');
|
||||||
var ACME = require('../acme.js');
|
var ACME = require('../acme.js');
|
||||||
var Keypairs = require('@root/keypairs');
|
var Keypairs = require('@root/keypairs');
|
||||||
var acme = ACME.create({
|
|
||||||
// debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO exec npm install --save-dev CHALLENGE_MODULE
|
// TODO exec npm install --save-dev CHALLENGE_MODULE
|
||||||
if (!process.env.CHALLENGE_OPTIONS) {
|
if (!process.env.CHALLENGE_OPTIONS) {
|
||||||
|
@ -33,6 +30,22 @@ var pluginPrefix = 'acme-' + config.challengeType + '-';
|
||||||
var pluginName = config.challengeModule;
|
var pluginName = config.challengeModule;
|
||||||
var plugin;
|
var plugin;
|
||||||
|
|
||||||
|
var acme = ACME.create({
|
||||||
|
// debug: true
|
||||||
|
maintainerEmail: config.email,
|
||||||
|
notify: function(ev, params) {
|
||||||
|
console.info(
|
||||||
|
ev,
|
||||||
|
params.subject || params.altname || params.domain,
|
||||||
|
params.status
|
||||||
|
);
|
||||||
|
if ('error' === ev) {
|
||||||
|
console.error(params);
|
||||||
|
console.error(params.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function badPlugin(err) {
|
function badPlugin(err) {
|
||||||
if ('MODULE_NOT_FOUND' !== err.code) {
|
if ('MODULE_NOT_FOUND' !== err.code) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@ -88,7 +101,7 @@ async function happyPath(accKty, srvKty, rnd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var accountKeypair = await Keypairs.generate({ kty: accKty });
|
var accountKeypair = await Keypairs.generate({ kty: accKty });
|
||||||
var accountKey = accountKeypair.private;
|
var accountKey = accountKeypair.private;
|
||||||
if (config.debug) {
|
if (config.debug) {
|
||||||
console.info('Account Key Created');
|
console.info('Account Key Created');
|
||||||
console.info(JSON.stringify(accountKey, null, 2));
|
console.info(JSON.stringify(accountKey, null, 2));
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var U = module.exports;
|
||||||
|
|
||||||
|
var Keypairs = require('@root/keypairs');
|
||||||
|
|
||||||
|
// Handle nonce, signing, and request altogether
|
||||||
|
U._jwsRequest = function(me, bigopts) {
|
||||||
|
return U._getNonce(me).then(function(nonce) {
|
||||||
|
bigopts.protected.nonce = nonce;
|
||||||
|
bigopts.protected.url = bigopts.url;
|
||||||
|
// protected.alg: added by Keypairs.signJws
|
||||||
|
if (!bigopts.protected.jwk) {
|
||||||
|
// protected.kid must be overwritten due to ACME's interpretation of the spec
|
||||||
|
if (!bigopts.protected.kid) {
|
||||||
|
bigopts.protected.kid = bigopts.options._kid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this will shasum the thumbprint the 2nd time
|
||||||
|
return Keypairs.signJws({
|
||||||
|
jwk:
|
||||||
|
bigopts.options.accountKey ||
|
||||||
|
bigopts.options.accountKeypair.privateKeyJwk,
|
||||||
|
protected: bigopts.protected,
|
||||||
|
payload: bigopts.payload
|
||||||
|
})
|
||||||
|
.then(function(jws) {
|
||||||
|
//#console.debug('[ACME.js] url: ' + bigopts.url + ':');
|
||||||
|
//#console.debug(jws);
|
||||||
|
return U._request(me, { url: bigopts.url, json: jws });
|
||||||
|
})
|
||||||
|
.catch(function(e) {
|
||||||
|
if (/badNonce$/.test(e.urn)) {
|
||||||
|
// retry badNonces
|
||||||
|
var retryable = bigopts._retries >= 2;
|
||||||
|
if (!retryable) {
|
||||||
|
bigopts._retries = (bigopts._retries || 0) + 1;
|
||||||
|
return U._jwsRequest(me, bigopts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
U._getNonce = function(me) {
|
||||||
|
var nonce;
|
||||||
|
while (true) {
|
||||||
|
nonce = me._nonces.shift();
|
||||||
|
if (!nonce) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (Date.now() - nonce.createdAt > 15 * 60 * 1000) {
|
||||||
|
nonce = null;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nonce) {
|
||||||
|
return Promise.resolve(nonce.nonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HEAD-as-HEAD ok
|
||||||
|
return U._request(me, {
|
||||||
|
method: 'HEAD',
|
||||||
|
url: me._directoryUrls.newNonce
|
||||||
|
}).then(function(resp) {
|
||||||
|
return resp.headers['replay-nonce'];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle some ACME-specific defaults
|
||||||
|
U._request = function(me, opts) {
|
||||||
|
if (!opts.headers) {
|
||||||
|
opts.headers = {};
|
||||||
|
}
|
||||||
|
if (opts.json && true !== opts.json) {
|
||||||
|
opts.headers['Content-Type'] = 'application/jose+json';
|
||||||
|
opts.body = JSON.stringify(opts.json);
|
||||||
|
if (!opts.method) {
|
||||||
|
opts.method = 'POST';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return me.request(opts).then(function(resp) {
|
||||||
|
if (resp.toJSON) {
|
||||||
|
resp = resp.toJSON();
|
||||||
|
}
|
||||||
|
if (resp.headers['replay-nonce']) {
|
||||||
|
U._setNonce(me, resp.headers['replay-nonce']);
|
||||||
|
}
|
||||||
|
|
||||||
|
var e;
|
||||||
|
var err;
|
||||||
|
if (resp.body) {
|
||||||
|
err = resp.body.error;
|
||||||
|
e = new Error('');
|
||||||
|
if (400 === resp.body.status) {
|
||||||
|
err = { type: resp.body.type, detail: resp.body.detail };
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
e.status = resp.body.status;
|
||||||
|
e.code = 'E_ACME';
|
||||||
|
if (e.status) {
|
||||||
|
e.message = '[' + e.status + '] ';
|
||||||
|
}
|
||||||
|
e.detail = err.detail;
|
||||||
|
e.message += err.detail || JSON.stringify(err);
|
||||||
|
e.urn = err.type;
|
||||||
|
e.uri = resp.body.url;
|
||||||
|
e._rawError = err;
|
||||||
|
e._rawBody = resp.body;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
U._setNonce = function(me, nonce) {
|
||||||
|
me._nonces.unshift({ nonce: nonce, createdAt: Date.now() });
|
||||||
|
};
|
||||||
|
|
||||||
|
U._importKeypair = function(me, kp) {
|
||||||
|
var jwk = kp.privateKeyJwk;
|
||||||
|
if (kp.kty) {
|
||||||
|
jwk = kp;
|
||||||
|
kp = {};
|
||||||
|
}
|
||||||
|
var pub;
|
||||||
|
var p;
|
||||||
|
if (jwk) {
|
||||||
|
// nix the browser jwk extras
|
||||||
|
jwk.key_ops = undefined;
|
||||||
|
jwk.ext = undefined;
|
||||||
|
pub = Keypairs.neuter({ jwk: jwk });
|
||||||
|
p = Promise.resolve({
|
||||||
|
private: jwk,
|
||||||
|
public: pub
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
p = Keypairs.import({ pem: kp.privateKeyPem });
|
||||||
|
}
|
||||||
|
return p.then(function(pair) {
|
||||||
|
kp.privateKeyJwk = pair.private;
|
||||||
|
kp.publicKeyJwk = pair.public;
|
||||||
|
if (pair.public.kid) {
|
||||||
|
pair = JSON.parse(JSON.stringify(pair));
|
||||||
|
delete pair.public.kid;
|
||||||
|
delete pair.private.kid;
|
||||||
|
}
|
||||||
|
return pair;
|
||||||
|
});
|
||||||
|
};
|
Loading…
Reference in New Issue