This commit is contained in:
AJ ONeal 2019-05-22 02:38:21 -06:00
parent 330e0e7832
commit 49d5346615
1 changed files with 313 additions and 375 deletions

View File

@ -1067,10 +1067,6 @@ Keypairs.signJws = function (opts) {
protectedHeader = JSON.stringify(protect); protectedHeader = JSON.stringify(protect);
} }
// Not sure how to handle the empty case since ACME POST-as-GET must be empty
//if (!payload) {
// throw new Error("opts.payload should be JSON, string, or ArrayBuffer (it may be empty, but that must be explicit)");
//}
// Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc) // Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc)
if (payload && ('string' !== typeof payload) if (payload && ('string' !== typeof payload)
&& ('undefined' === typeof payload.byteLength) && ('undefined' === typeof payload.byteLength)
@ -1832,104 +1828,87 @@ ACME._setNonce = function (me, nonce) {
} }
*/ */
ACME._registerAccount = function (me, options) { ACME._registerAccount = function (me, options) {
if (me.debug) { console.debug('[acme-v2] accounts.create'); } //#console.debug('[acme-v2] accounts.create');
return new Promise(function (resolve, reject) { 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;
}
function agree(tosUrl) { return ACME._importKeypair(me, options.accountKey || options.accountKeypair).then(function (pair) {
var err; var contact;
if (me._tos !== tosUrl) { if (options.contact) {
err = new Error("You must agree to the ToS at '" + me._tos + "'"); contact = options.contact.slice(0);
err.code = "E_AGREE_TOS"; } else if (options.email) {
reject(err); contact = [ 'mailto:' + options.email ];
return;
} }
var body = {
return ACME._importKeypair(me, options.accountKeypair).then(function (pair) { termsOfServiceAgreed: tosUrl === me._tos
var contact; , onlyReturnExisting: false
if (options.contact) { , contact: contact
contact = options.contact.slice(0); };
} else if (options.email) { var pExt;
contact = [ 'mailto:' + options.email ]; if (options.externalAccount) {
} pExt = me.Keypairs.signJws({
var body = { // TODO is HMAC the standard, or is this arbitrary?
termsOfServiceAgreed: tosUrl === me._tos secret: options.externalAccount.secret
, onlyReturnExisting: false , protected: {
, contact: contact alg: options.externalAccount.alg || "HS256"
}; , kid: options.externalAccount.id
var pExt;
if (options.externalAccount) {
pExt = me.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.binToBuf(JSON.stringify(pair.public))
}).then(function (jws) {
body.externalAccountBinding = jws;
return body;
});
} else {
pExt = Promise.resolve(body);
}
return pExt.then(function (body) {
var payload = JSON.stringify(body);
return ACME._jwsRequest(me, {
options: options
, url: me._directoryUrls.newAccount , url: me._directoryUrls.newAccount
, protected: { kid: false, jwk: pair.public } }
, payload: Enc.binToBuf(payload) , payload: Enc.binToBuf(JSON.stringify(pair.public))
}).then(function (resp) { }).then(function (jws) {
var account = resp.body; body.externalAccountBinding = jws;
return body;
});
} else {
pExt = Promise.resolve(body);
}
return pExt.then(function (body) {
var payload = JSON.stringify(body);
return ACME._jwsRequest(me, {
options: options
, url: me._directoryUrls.newAccount
, protected: { kid: false, jwk: pair.public }
, payload: Enc.binToBuf(payload)
}).then(function (resp) {
var account = resp.body;
if (2 !== Math.floor(resp.statusCode / 100)) { if (2 !== Math.floor(resp.statusCode / 100)) {
throw new Error('account error: ' + JSON.stringify(resp.body)); throw new Error('account error: ' + JSON.stringify(resp.body));
} }
var location = resp.headers.location; var location = resp.headers.location;
// the account id url // the account id url
options._kid = location; options._kid = location;
if (me.debug) { console.debug('[DEBUG] new account location:'); } //#console.debug('[DEBUG] new account location:');
if (me.debug) { console.debug(location); } //#console.debug(location);
if (me.debug) { console.debug(resp); } //#console.debug(resp);
/* /*
{ {
contact: ["mailto:jon@example.com"], contact: ["mailto:jon@example.com"],
orders: "https://some-url", orders: "https://some-url",
status: 'valid' status: 'valid'
} }
*/ */
if (!account) { account = { _emptyResponse: true }; } if (!account) { account = { _emptyResponse: true }; }
// https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
if (!account.key) { account.key = {}; } if (!account.key) { account.key = {}; }
account.key.kid = options._kid; account.key.kid = options._kid;
return account; return account;
}).then(resolve, reject);
}); });
}); });
} });
}
if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } return Promise.resolve().then(function () {
if (1 === options.agreeToTerms.length) { return options.agreeToTerms(me._tos);
// newer promise API }).then(agree);
return Promise.resolve(options.agreeToTerms(me._tos)).then(agree, reject);
}
else if (2 === options.agreeToTerms.length) {
// backwards compat cb API
return options.agreeToTerms(me._tos, function (err, tosUrl) {
if (!err) { agree(tosUrl); return; }
reject(err);
});
}
else {
reject(new Error('agreeToTerms has incorrect function signature.'
+ ' Should be fn(tos) { return Promise<tos>; }'));
}
});
}; };
/* /*
POST /acme/new-order HTTP/1.1 POST /acme/new-order HTTP/1.1
@ -1952,9 +1931,7 @@ ACME._registerAccount = function (me, options) {
} }
*/ */
ACME._getChallenges = function (me, options, authUrl) { ACME._getChallenges = function (me, options, authUrl) {
if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); } //#console.debug('\n[DEBUG] getChallenges\n');
// TODO POST-as-GET
return ACME._jwsRequest(me, { return ACME._jwsRequest(me, {
options: options options: options
, protected: { kid: options._kid } , protected: { kid: options._kid }
@ -1962,7 +1939,7 @@ ACME._getChallenges = function (me, options, authUrl) {
, url: authUrl , url: authUrl
}).then(function (resp) { }).then(function (resp) {
// Pre-emptive rather than lazy for interfaces that need to show the challenges to the user first // Pre-emptive rather than lazy for interfaces that need to show the challenges to the user first
return ACME._challengesToAuth(me, options, resp.body, false).then(function (auths) { return ACME._computeAuths(me, options, resp.body, false).then(function (auths) {
resp.body._rawChallenges = resp.body.challenges; resp.body._rawChallenges = resp.body.challenges;
resp.body.challenges = auths; resp.body.challenges = auths;
return resp.body; return resp.body;
@ -1999,78 +1976,53 @@ ACME._testChallengeOptions = function () {
} }
]; ];
}; };
ACME._testChallenges = function (me, options) { ACME._testChallenges = function (me, reals) {
var CHECK_DELAY = 0; console.log('[DEBUG] testChallenges');
return Promise.all(options.domains.map(function (identifierValue) { if (me.skipDryRun || me.skipChallengeTest) {
// TODO we really only need one to pass, not all to pass return Promise.resolve();
}
var nopts = {};
Object.keys(reals).forEach(function (key) {
nopts[key] = reals[key];
});
nopts.order = {};
return Promise.all(nopts.domains.map(function (name) {
var challenges = ACME._testChallengeOptions(); var challenges = ACME._testChallengeOptions();
if (identifierValue.includes("*")) { var wild = '*.' === name.slice(0, 2);
if (wild) {
challenges = challenges.filter(function (ch) { return ch._wildcard; }); challenges = challenges.filter(function (ch) { return ch._wildcard; });
} }
var resp = {
body: {
identifier: { type: 'dns' , value: name.replace('*.', '') }
, challenges: challenges
, expires: new Date(Date.now() + (60 * 1000)).toISOString()
, wildcard: name.includes('*.') || undefined
}
};
// The dry-run comes first in the spirit of "fail fast" // The dry-run comes first in the spirit of "fail fast"
// (and protecting against challenge failure rate limits) // (and protecting against challenge failure rate limits)
var dryrun = true; var dryrun = true;
var resp = { return ACME._computeAuths(me, nopts, resp.body, dryrun).then(function (auths) {
body: {
identifier: {
type: "dns"
, value: identifierValue.replace(/^\*\./, '')
}
, challenges: challenges
, expires: new Date(Date.now() + (60 * 1000)).toISOString()
, wildcard: identifierValue.includes('*.') || undefined
}
};
return ACME._challengesToAuth(me, options, resp.body, dryrun).then(function (auths) {
resp.body._rawChallenges = resp.body.challenges;
resp.body.challenges = auths; resp.body.challenges = auths;
var auth = ACME._chooseAuth(options, resp.body.challenges);
if (!auth) {
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
var enabled = Object.keys(options.challenges).join(', ') || 'none';
var suitable = resp.body.challenges.map(function (r) { return r.type; }).join(', ') || 'none';
return Promise.reject(new Error(
"None of the challenge types that you've enabled ( " + enabled + " )"
+ " are suitable for validating the domain you've selected (" + identifierValue + ")."
+ " You must enable one of ( " + suitable + " )."
));
}
// TODO remove skipChallengeTest
if (me.skipDryRun || me.skipChallengeTest) {
return null;
}
if ('dns-01' === auth.type) {
// Give the nameservers a moment to propagate
CHECK_DELAY = 1.5 * 1000;
}
if (!me._canUse[auth.type]) { return; }
return ACME._setChallenge(me, options, auth).then(function () {
return auth;
});
}); });
})).then(function (auths) { })).then(function (claims) {
auths = auths.filter(Boolean); nopts.order.claims = claims;
if (!auths.length) { /*skip actual test*/ return; } nopts.setChallengeWait = 0;
return ACME._wait(CHECK_DELAY).then(function () {
return Promise.all(auths.map(function (auth) { return ACME._setChallengesAll(me, nopts).then(function (valids) {
return ACME.challengeTests[auth.type](me, auth).then(function (result) { return Promise.all(valids.map(function (auth) {
// not a blocker ACME._removeChallenge(me, nopts, auth);
ACME._removeChallenge(me, options, auth);
return result;
});
})); }));
}); });
}); });
}; };
ACME._chooseAuth = function(options, auths) { ACME._chooseType = function(options, auths) {
// For each of the challenge types that we support // For each of the challenge types that we support
var auth; var auth;
var challengeTypes = Object.keys(options.challenges); var challengeTypes = Object.keys(options.challenges || ACME._challengesMap);
// ordered from most to least preferred // ordered from most to least preferred
challengeTypes = (options.challengePriority||[ 'tls-alpn-01', 'http-01', 'dns-01' ]).filter(function (chType) { challengeTypes = (options.challengePriority||[ 'tls-alpn-01', 'http-01', 'dns-01' ]).filter(function (chType) {
return challengeTypes.includes(chType); return challengeTypes.includes(chType);
@ -2089,15 +2041,17 @@ ACME._chooseAuth = function(options, auths) {
return auth; return auth;
}; };
ACME._challengesToAuth = function (me, options, request, dryrun) { ACME._challengesMap = {'http-01':0,'dns-01':0,'tls-alpn-01':0};
ACME._computeAuths = function (me, options, request, dryrun) {
console.log('[DEBUG] computeAuths');
// we don't poison the dns cache with our dummy request // we don't poison the dns cache with our dummy request
var dnsPrefix = ACME.challengePrefixes['dns-01']; var dnsPrefix = ACME.challengePrefixes['dns-01'];
if (dryrun) { if (dryrun) {
dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + ACME._prnd(4)); dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + ACME._prnd(4));
} }
var challengeTypes = Object.keys(options.challenges); var challengeTypes = Object.keys(options.challenges || ACME._challengesMap);
return ACME._importKeypair(me, options.accountKeypair).then(function (pair) { return ACME._importKeypair(me, options.accountKey || options.accountKeypair).then(function (pair) {
return me.Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) { return me.Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) {
return Promise.all(request.challenges.map(function (challenge) { return Promise.all(request.challenges.map(function (challenge) {
// Don't do extra work for challenges that we can't satisfy // Don't do extra work for challenges that we can't satisfy
@ -2188,15 +2142,15 @@ ACME._postChallenge = function (me, options, auth) {
} }
*/ */
function deactivate() { function deactivate() {
if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } //#console.debug('[acme-v2.js] deactivate:');
return ACME._jwsRequest(me, { return ACME._jwsRequest(me, {
options: options options: options
, url: auth.url , url: auth.url
, protected: { kid: options._kid } , protected: { kid: options._kid }
, payload: Enc.binToBuf(JSON.stringify({ "status": "deactivated" })) , payload: Enc.binToBuf(JSON.stringify({ "status": "deactivated" }))
}).then(function (resp) { }).then(function (/*#resp*/) {
if (me.debug) { console.debug('deactivate challenge: resp.body:'); } //#console.debug('deactivate challenge: resp.body:');
if (me.debug) { console.debug(resp.body); } //#console.debug(resp.body);
return ACME._wait(DEAUTH_INTERVAL); return ACME._wait(DEAUTH_INTERVAL);
}); });
} }
@ -2210,11 +2164,10 @@ ACME._postChallenge = function (me, options, auth) {
count += 1; count += 1;
if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } //#console.debug('\n[DEBUG] statusChallenge\n');
// TODO POST-as-GET
return me.request({ method: 'GET', url: auth.url, json: true }).then(function (resp) { return me.request({ method: 'GET', url: auth.url, json: true }).then(function (resp) {
if ('processing' === resp.body.status) { if ('processing' === resp.body.status) {
if (me.debug) { console.debug('poll: again'); } //#console.debug('poll: again');
return ACME._wait(RETRY_INTERVAL).then(pollStatus); return ACME._wait(RETRY_INTERVAL).then(pollStatus);
} }
@ -2223,12 +2176,12 @@ ACME._postChallenge = function (me, options, auth) {
if (count >= MAX_PEND) { if (count >= MAX_PEND) {
return ACME._wait(RETRY_INTERVAL).then(deactivate).then(respondToChallenge); return ACME._wait(RETRY_INTERVAL).then(deactivate).then(respondToChallenge);
} }
if (me.debug) { console.debug('poll: again'); } //#console.debug('poll: again');
return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); return ACME._wait(RETRY_INTERVAL).then(respondToChallenge);
} }
if ('valid' === resp.body.status) { if ('valid' === resp.body.status) {
if (me.debug) { console.debug('poll: valid'); } //#console.debug('poll: valid');
try { try {
ACME._removeChallenge(me, options, auth); ACME._removeChallenge(me, options, auth);
@ -2255,226 +2208,227 @@ ACME._postChallenge = function (me, options, auth) {
} }
function respondToChallenge() { function respondToChallenge() {
if (me.debug) { console.debug('[acme-v2.js] responding to accept challenge:'); } //#console.debug('[acme-v2.js] responding to accept challenge:');
return ACME._jwsRequest(me, { return ACME._jwsRequest(me, {
options: options options: options
, url: auth.url , url: auth.url
, protected: { kid: options._kid } , protected: { kid: options._kid }
, payload: Enc.binToBuf(JSON.stringify({})) , payload: Enc.binToBuf(JSON.stringify({}))
}).then(function (resp) { }).then(function (/*#resp*/) {
if (me.debug) { console.debug('respond to challenge: resp.body:'); } //#console.debug('respond to challenge: resp.body:');
if (me.debug) { console.debug(resp.body); } //#console.debug(resp.body);
return ACME._wait(RETRY_INTERVAL).then(pollStatus); return ACME._wait(RETRY_INTERVAL).then(pollStatus);
}); });
} }
return respondToChallenge(); return respondToChallenge();
}; };
ACME._setChallenge = function (me, options, auth) {
return new Promise(function (resolve, reject) { // options = { domains, claims, challenges, challengePriority }
var challengers = options.challenges || {};
var challenger = (challengers[auth.type] && challengers[auth.type].set) || options.setChallenge;
try {
if (1 === challenger.length) {
challenger(auth).then(resolve).catch(reject);
} else if (2 === challenger.length) {
challenger(auth, function (err) {
if(err) { reject(err); } else { resolve(); }
});
} else {
// TODO remove this old backwards-compat
var challengeCb = function(err) {
if(err) { reject(err); } else { resolve(); }
};
// for backwards compat adding extra keys without changing params length
Object.keys(auth).forEach(function (key) {
challengeCb[key] = auth[key];
});
if (!ACME._setChallengeWarn) {
console.warn("Please update to acme-v2 setChallenge(options) <Promise> or setChallenge(options, cb).");
console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types.");
ACME._setChallengeWarn = true;
}
challenger(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb);
}
} catch(e) {
reject(e);
}
}).then(function () {
// TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves?
var DELAY = me.setChallengeWait || 500;
if (me.debug) { console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); }
return ACME._wait(DELAY);
});
};
ACME._setChallengesAll = function (me, options) { ACME._setChallengesAll = function (me, options) {
var order = options.order; console.log("[DEBUG] setChallengesAll");
var setAuths = order.authorizations.slice(0); var claims = options.order.claims.slice(0);
var claims = order.claims.slice(0); var valids = [];
var validAuths = [];
var auths = []; var auths = [];
// TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves?
var DELAY = options.setChallengeWait || me.setChallengeWait || 500;
// Set any challenges, excpting ones that have already been validated
function setNext() { function setNext() {
var authUrl = setAuths.shift();
var claim = claims.shift(); var claim = claims.shift();
if (!authUrl) { return Promise.resolve(); } if (!claim) { return Promise.resolve(); }
// var domain = options.domains[i]; // claim.identifier.value return Promise.resolve().then(function () {
// For any challenges that are already valid,
// add to the list and skip any checks.
if (claim.challenges.some(function (ch) {
if ('valid' === ch.status) {
valids.push(ch);
return true;
}
})) {
return;
}
// If it's already valid, we're golden it regardless // Get the list of challenge types we can validate.
if (claim.challenges.some(function (ch) { return 'valid' === ch.status; })) { // Then order that list by preference
return setNext(); // Select the first matching offered challenge type
} var usable = Object.keys(options.challenges || ACME._challengesMap);
var selected = (options.challengePriority||[ 'tls-alpn-01', 'http-01', 'dns-01' ]).map(function (chType) {
if (!usable.includes(chType)) { return; }
return claim.challenges.filter(function (ch) {
return ch.type === chType;
})[0];
}).filter(Boolean)[0];
var ch;
var auth = ACME._chooseAuth(options, claim.challenges); // Bail with a descriptive message if no usable challenge could be selected
if (!auth) { if (!selected) {
// For example, wildcards require dns-01 and, if we don't have that, we have to bail var enabled = usable.join(', ') || 'none';
return Promise.reject(new Error( var suitable = claim.challenges.map(function (r) { return r.type; }).join(', ') || 'none';
"Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." throw new Error(
)); "None of the challenge types that you've enabled ( " + enabled + " )"
} + " are suitable for validating the domain you've selected (" + claim.altname + ")."
+ " You must enable one of ( " + suitable + " )."
);
}
auths.push(selected);
auths.push(auth); // Give the nameservers a moment to propagate
return ACME._setChallenge(me, options, auth).then(setNext); if ('dns-01' === selected.type) {
DELAY = 1.5 * 1000;
}
if (false === options.challenges) { return; }
ch = options.challenges[selected.type] || {};
if (!ch.set) {
throw new Error("no handler for setting challenge");
}
return ch.set(selected);
}).then(setNext);
} }
function checkNext() { function checkNext() {
var auth = auths.shift(); var auth = auths.shift();
if (!auth) { return; } if (!auth) { return Promise.resolve(valids); }
// These are not as much "valids" as they are "not invalids"
if (!me._canUse[auth.type] || me.skipChallengeTest) { if (!me._canUse[auth.type] || me.skipChallengeTest) {
// not so much "valid" as "not invalid" valids.push(auth);
// but in this case we can't confirm either way return checkNext();
validAuths.push(auth);
return Promise.resolve();
} }
return ACME.challengeTests[auth.type](me, auth).then(function () { return ACME.challengeTests[auth.type](me, auth).then(function () {
validAuths.push(auth); valids.push(auth);
}).then(checkNext); }).then(checkNext);
} }
// Actually sets the challenge via ACME // The reason we set every challenge in a batch first before checking any
function challengeNext() { // is so that we don't poison our own DNS cache with misses.
var auth = validAuths.shift(); return setNext().then(function () {
if (!auth) { return; } //#console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY);
return ACME._postChallenge(me, options, auth).then(challengeNext); return ACME._wait(DELAY);
} }).then(checkNext);
// First we set every challenge
// Then we ask for each challenge to be checked
// Doing otherwise would potentially cause us to poison our own DNS cache with misses
return setNext().then(checkNext).then(challengeNext).then(function () {
if (me.debug) { console.debug("[getCertificate] next.then"); }
console.log('DEBUG 1 order:');
console.log(order);
return order.identifiers.map(function (ident) {
return ident.value;
});
});
}; };
ACME._finalizeOrder = function (me, options) { ACME._finalizeOrder = function (me, options) {
return ACME._getAccountKid(me, options).then(function () { return ACME._getAccountKid(me, options).then(function () {
return ACME._setChallengesAll(me, options).then(function () { if (!options.challenges && !options.challengePriority) {
throw new Error("You must set either challenges or challengePrority");
}
return ACME._setChallengesAll(me, options).then(function (valids) {
// options._kid added // options._kid added
if (me.debug) { console.debug('finalizeOrder:'); } //#console.debug('finalizeOrder:');
var order = options.order; var order = options.order;
var validatedDomains = options.order.identifiers.map(function (ident) { var validatedDomains = options.order.identifiers.map(function (ident) {
return ident.value; return ident.value;
}); });
return ACME._getCsrWeb64(me, options, validatedDomains).then(function (csr) {
var body = { csr: csr };
var payload = JSON.stringify(body);
function pollCert() { // Actually sets the challenge via ACME
if (me.debug) { console.debug('[acme-v2.js] pollCert:'); } function challengeNext() {
return ACME._jwsRequest(me, { var auth = valids.shift();
options: options if (!auth) { return Promise.resolve(); }
, url: options.order.finalizeUrl return ACME._postChallenge(me, options, auth).then(challengeNext);
, protected: { kid: options._kid } }
, payload: Enc.binToBuf(payload) return challengeNext().then(function () {
}).then(function (resp) { //#console.debug("[getCertificate] next.then");
if (me.debug) { console.debug('order finalized: resp.body:'); } console.log('DEBUG 1 order:');
if (me.debug) { console.debug(resp.body); } console.log(options.order);
return options.order.identifiers.map(function (ident) {
return ident.value;
});
}).then(function () {
return ACME._getCsrWeb64(me, options, validatedDomains).then(function (csr) {
var body = { csr: csr };
var payload = JSON.stringify(body);
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3 function pollCert() {
// Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" //#console.debug('[acme-v2.js] pollCert:');
if ('valid' === resp.body.status) { return ACME._jwsRequest(me, {
options._expires = resp.body.expires; options: options
options._certificate = resp.body.certificate; , url: options.order.finalizeUrl
, protected: { kid: options._kid }
, payload: Enc.binToBuf(payload)
}).then(function (resp) {
//#console.debug('order finalized: resp.body:');
//#console.debug(resp.body);
return resp.body; // return order // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
} // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
if ('valid' === resp.body.status) {
options._expires = resp.body.expires;
options._certificate = resp.body.certificate;
if ('processing' === resp.body.status) { return resp.body; // return order
return ACME._wait().then(pollCert); }
}
if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); } if ('processing' === resp.body.status) {
return ACME._wait().then(pollCert);
}
//#console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2));
if ('pending' === resp.body.status) {
return Promise.reject(new Error(
"Did not finalize order: status 'pending'."
+ " Best guess: You have not accepted at least one challenge for each domain:\n"
+ "Requested: '" + options.domains.join(', ') + "'\n"
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
+ JSON.stringify(resp.body, null, 2)
));
}
if ('invalid' === resp.body.status) {
return Promise.reject(new Error(
"Did not finalize order: status 'invalid'."
+ " Best guess: One or more of the domain challenges could not be verified"
+ " (or the order was canceled).\n"
+ "Requested: '" + options.domains.join(', ') + "'\n"
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
+ JSON.stringify(resp.body, null, 2)
));
}
if ('ready' === resp.body.status) {
return Promise.reject(new Error(
"Did not finalize order: status 'ready'."
+ " Hmmm... this state shouldn't be possible here. That was the last state."
+ " This one should at least be 'processing'.\n"
+ "Requested: '" + options.domains.join(', ') + "'\n"
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
+ JSON.stringify(resp.body, null, 2) + "\n\n"
+ "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js"
));
}
if ('pending' === resp.body.status) {
return Promise.reject(new Error( return Promise.reject(new Error(
"Did not finalize order: status 'pending'." "Didn't finalize order: Unhandled status '" + resp.body.status + "'."
+ " Best guess: You have not accepted at least one challenge for each domain:\n" + " This is not one of the known statuses...\n"
+ "Requested: '" + options.domains.join(', ') + "'\n"
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
+ JSON.stringify(resp.body, null, 2)
));
}
if ('invalid' === resp.body.status) {
return Promise.reject(new Error(
"Did not finalize order: status 'invalid'."
+ " Best guess: One or more of the domain challenges could not be verified"
+ " (or the order was canceled).\n"
+ "Requested: '" + options.domains.join(', ') + "'\n"
+ "Validated: '" + validatedDomains.join(', ') + "'\n"
+ JSON.stringify(resp.body, null, 2)
));
}
if ('ready' === resp.body.status) {
return Promise.reject(new Error(
"Did not finalize order: status 'ready'."
+ " Hmmm... this state shouldn't be possible here. That was the last state."
+ " This one should at least be 'processing'.\n"
+ "Requested: '" + options.domains.join(', ') + "'\n" + "Requested: '" + options.domains.join(', ') + "'\n"
+ "Validated: '" + validatedDomains.join(', ') + "'\n" + "Validated: '" + validatedDomains.join(', ') + "'\n"
+ JSON.stringify(resp.body, null, 2) + "\n\n" + JSON.stringify(resp.body, null, 2) + "\n\n"
+ "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js"
)); ));
} });
}
return Promise.reject(new Error( return pollCert();
"Didn't finalize order: Unhandled status '" + resp.body.status + "'." }).then(function () {
+ " This is not one of the known statuses...\n" //#console.debug('acme-v2: order was finalized');
+ "Requested: '" + options.domains.join(', ') + "'\n" return me.request({ method: 'GET', url: options._certificate, json: true }).then(function (resp) {
+ "Validated: '" + validatedDomains.join(', ') + "'\n" //#console.debug('acme-v2: csr submitted and cert received:');
+ JSON.stringify(resp.body, null, 2) + "\n\n" // https://github.com/certbot/certbot/issues/5721
+ "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||'')));
)); // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
// TODO CSR.info
var certs = {
expires: order.expires
, identifiers: order.identifiers
, cert: certsarr.shift()
, chain: certsarr.join('\n')
};
//#console.debug(certs);
return certs;
}); });
}
return pollCert();
}).then(function () {
if (me.debug) { console.debug('acme-v2: order was finalized'); }
// TODO POST-as-GET
return me.request({ method: 'GET', url: options._certificate, json: true }).then(function (resp) {
if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); }
// https://github.com/certbot/certbot/issues/5721
var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||'')));
// cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
var certs = {
expires: order.expires
, identifiers: order.identifiers
//, authorizations: order.authorizations
, cert: certsarr.shift()
//, privkey: privkeyPem
, chain: certsarr.join('\n')
};
if (me.debug) { console.debug(certs); }
return certs;
}); });
}); });
}); });
@ -2482,7 +2436,7 @@ ACME._finalizeOrder = function (me, options) {
}; };
ACME._createOrder = function (me, options) { ACME._createOrder = function (me, options) {
return ACME._getAccountKid(me, options).then(function () { return ACME._getAccountKid(me, options).then(function () {
// options._kid added // options._kid added
var body = { var body = {
// raw wildcard syntax MUST be used here // raw wildcard syntax MUST be used here
identifiers: options.domains.sort(function (a, b) { identifiers: options.domains.sort(function (a, b) {
@ -2499,7 +2453,7 @@ ACME._createOrder = function (me, options) {
}; };
var payload = JSON.stringify(body); var payload = JSON.stringify(body);
if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } //#console.debug('\n[DEBUG] newOrder\n');
return ACME._jwsRequest(me, { return ACME._jwsRequest(me, {
options: options options: options
, url: me._directoryUrls.newOrder , url: me._directoryUrls.newOrder
@ -2513,8 +2467,8 @@ ACME._createOrder = function (me, options) {
, identifiers: body.identifiers , identifiers: body.identifiers
, _response: resp.body , _response: resp.body
}; };
if (me.debug) { console.debug('[ordered]', location); } // the account id url //#console.debug('[ordered]', location); // the account id url
if (me.debug) { console.debug(resp); } //#console.debug(resp);
if (!order.authorizations) { if (!order.authorizations) {
return Promise.reject(new Error( return Promise.reject(new Error(
@ -2526,7 +2480,7 @@ ACME._createOrder = function (me, options) {
return order; return order;
}).then(function (order) { }).then(function (order) {
var claims = []; var claims = [];
if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } //#console.debug("[acme-v2] POST newOrder has authorizations");
var challengeAuths = order.authorizations.slice(0); var challengeAuths = order.authorizations.slice(0);
function getNext() { function getNext() {
@ -2565,13 +2519,12 @@ ACME._getAccountKid = function (me, options) {
return options._kid; return options._kid;
}); });
}; };
// _kid
// registerAccount //
// postChallenge // Helper Methods
// finalizeOrder //
// getCertificate
ACME._getCertificate = function (me, options) { ACME._getCertificate = function (me, options) {
if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } //#console.debug('[acme-v2] DEBUG get cert 1');
if (options.csr) { if (options.csr) {
// TODO validate csr signature // TODO validate csr signature
@ -2585,10 +2538,13 @@ ACME._getCertificate = function (me, options) {
return Promise.reject(new Error("options.domains must be a list of string domain names," return Promise.reject(new Error("options.domains must be a list of string domain names,"
+ " with the first being the subject of the certificate (or options.subject must specified).")); + " with the first being the subject of the certificate (or options.subject must specified)."));
} }
if (!options.challenges) {
return Promise.reject(new Error("You must specify challenge handlers."));
}
// Do a little dry-run / self-test // Do a little dry-run / self-test
return ACME._testChallenges(me, options).then(function () { return ACME._testChallenges(me, options).then(function () {
if (me.debug) { console.debug('[acme-v2] certificates.create'); } //#console.debug('[acme-v2] certificates.create');
return ACME._createOrder(me, options).then(function (/*order*/) { return ACME._createOrder(me, options).then(function (/*order*/) {
// options.order = order; // options.order = order;
return ACME._finalizeOrder(me, options); return ACME._finalizeOrder(me, options);
@ -2607,7 +2563,7 @@ ACME._getCsrWeb64 = function (me, options, validatedDomains) {
return Promise.resolve(csr); return Promise.resolve(csr);
} }
return ACME._importKeypair(me, options.serverKeypair || options.domainKeypair).then(function (pair) { return ACME._importKeypair(me, options.serverKey || options.serverKeypair || options.domainKeypair).then(function (pair) {
return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) { return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) {
return Enc.bufToUrlBase64(der); return Enc.bufToUrlBase64(der);
}); });
@ -2708,13 +2664,13 @@ ACME._jwsRequest = function (me, bigopts) {
if (!bigopts.protected.kid) { bigopts.protected.kid = bigopts.options._kid; } if (!bigopts.protected.kid) { bigopts.protected.kid = bigopts.options._kid; }
} }
return me.Keypairs.signJws( return me.Keypairs.signJws(
{ jwk: bigopts.options.accountKeypair.privateKeyJwk { jwk: bigopts.accountKey || bigopts.options.accountKeypair.privateKeyJwk
, protected: bigopts.protected , protected: bigopts.protected
, payload: bigopts.payload , payload: bigopts.payload
} }
).then(function (jws) { ).then(function (jws) {
if (me.debug) { console.debug('[acme-v2] ' + bigopts.url + ':'); } //#console.debug('[acme-v2] ' + bigopts.url + ':');
if (me.debug) { console.debug(jws); } //#console.debug(jws);
return ACME._request(me, { url: bigopts.url, json: jws }); return ACME._request(me, { url: bigopts.url, json: jws });
}); });
}); });
@ -2769,7 +2725,7 @@ ACME._defaultRequest = function (opts) {
}; };
ACME._importKeypair = function (me, kp) { ACME._importKeypair = function (me, kp) {
var jwk = kp.privateKeyJwk; var jwk = kp.privateKeyJwk || kp.kty && kp;
var p; var p;
if (jwk) { if (jwk) {
// nix the browser jwk extras // nix the browser jwk extras
@ -2791,18 +2747,6 @@ ACME._importKeypair = function (me, kp) {
}); });
}; };
/*
TODO
Per-Order State Params
_kty
_alg
_finalize
_expires
_certificate
_order
_authorizations
*/
ACME._toWebsafeBase64 = function (b64) { ACME._toWebsafeBase64 = function (b64) {
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,"");
}; };
@ -2850,20 +2794,14 @@ ACME._http01 = function (me, auth) {
}); });
}; };
ACME._removeChallenge = function (me, options, auth) { ACME._removeChallenge = function (me, options, auth) {
var challengers = options.challenges || {}; return Promise.resolve().then(function () {
var removeChallenge = (challengers[auth.type] && challengers[auth.type].remove) || options.removeChallenge; if (!options.challenges) { return; }
if (1 === removeChallenge.length) { var ch = options.challenges[auth.type];
removeChallenge(auth).then(function () {}, function () {}); ch.remove(auth).catch(function (e) {
} else if (2 === removeChallenge.length) { console.warn("challenge.remove error:");
removeChallenge(auth, function (err) { return err; }); console.warn(e);
} else { });
if (!ACME._removeChallengeWarn) { });
console.warn("Please update to acme-v2 removeChallenge(options) <Promise> or removeChallenge(options, cb).");
console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types.");
ACME._removeChallengeWarn = true;
}
removeChallenge(auth.request.identifier, auth.token, function () {});
}
}; };
Enc.bufToUrlBase64 = function (u8) { Enc.bufToUrlBase64 = function (u8) {