This commit is contained in:
AJ ONeal 2019-05-22 02:38:21 -06:00
parent f56dafad5d
commit 352e334b5d
1 changed files with 313 additions and 375 deletions

View File

@ -1067,10 +1067,6 @@ Keypairs.signJws = function (opts) {
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)
if (payload && ('string' !== typeof payload)
&& ('undefined' === typeof payload.byteLength)
@ -1832,104 +1828,87 @@ ACME._setNonce = function (me, nonce) {
}
*/
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) {
var err;
if (me._tos !== tosUrl) {
err = new Error("You must agree to the ToS at '" + me._tos + "'");
err.code = "E_AGREE_TOS";
reject(err);
return;
return ACME._importKeypair(me, options.accountKey || options.accountKeypair).then(function (pair) {
var contact;
if (options.contact) {
contact = options.contact.slice(0);
} else if (options.email) {
contact = [ 'mailto:' + options.email ];
}
return ACME._importKeypair(me, options.accountKeypair).then(function (pair) {
var contact;
if (options.contact) {
contact = options.contact.slice(0);
} else if (options.email) {
contact = [ 'mailto:' + options.email ];
}
var body = {
termsOfServiceAgreed: tosUrl === me._tos
, onlyReturnExisting: false
, contact: contact
};
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
var body = {
termsOfServiceAgreed: tosUrl === me._tos
, onlyReturnExisting: false
, contact: contact
};
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
, protected: { kid: false, jwk: pair.public }
, payload: Enc.binToBuf(payload)
}).then(function (resp) {
var account = resp.body;
}
, 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
, protected: { kid: false, jwk: pair.public }
, payload: Enc.binToBuf(payload)
}).then(function (resp) {
var account = resp.body;
if (2 !== Math.floor(resp.statusCode / 100)) {
throw new Error('account error: ' + JSON.stringify(resp.body));
}
if (2 !== Math.floor(resp.statusCode / 100)) {
throw new Error('account error: ' + JSON.stringify(resp.body));
}
var location = resp.headers.location;
// the account id url
options._kid = location;
if (me.debug) { console.debug('[DEBUG] new account location:'); }
if (me.debug) { console.debug(location); }
if (me.debug) { console.debug(resp); }
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.coolaj86.com/coolaj86/acme-v2.js/issues/8
if (!account.key) { account.key = {}; }
account.key.kid = options._kid;
return account;
}).then(resolve, reject);
/*
{
contact: ["mailto:jon@example.com"],
orders: "https://some-url",
status: 'valid'
}
*/
if (!account) { account = { _emptyResponse: true }; }
// https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
if (!account.key) { account.key = {}; }
account.key.kid = options._kid;
return account;
});
});
}
});
}
if (me.debug) { console.debug('[acme-v2] agreeToTerms'); }
if (1 === options.agreeToTerms.length) {
// newer promise API
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>; }'));
}
});
return Promise.resolve().then(function () {
return options.agreeToTerms(me._tos);
}).then(agree);
};
/*
POST /acme/new-order HTTP/1.1
@ -1952,9 +1931,7 @@ ACME._registerAccount = function (me, options) {
}
*/
ACME._getChallenges = function (me, options, authUrl) {
if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); }
// TODO POST-as-GET
//#console.debug('\n[DEBUG] getChallenges\n');
return ACME._jwsRequest(me, {
options: options
, protected: { kid: options._kid }
@ -1962,7 +1939,7 @@ ACME._getChallenges = function (me, options, authUrl) {
, url: authUrl
}).then(function (resp) {
// 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.challenges = auths;
return resp.body;
@ -1999,78 +1976,53 @@ ACME._testChallengeOptions = function () {
}
];
};
ACME._testChallenges = function (me, options) {
var CHECK_DELAY = 0;
return Promise.all(options.domains.map(function (identifierValue) {
// TODO we really only need one to pass, not all to pass
ACME._testChallenges = function (me, reals) {
console.log('[DEBUG] testChallenges');
if (me.skipDryRun || me.skipChallengeTest) {
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();
if (identifierValue.includes("*")) {
var wild = '*.' === name.slice(0, 2);
if (wild) {
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"
// (and protecting against challenge failure rate limits)
var dryrun = true;
var resp = {
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;
return ACME._computeAuths(me, nopts, resp.body, dryrun).then(function (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) {
auths = auths.filter(Boolean);
if (!auths.length) { /*skip actual test*/ return; }
return ACME._wait(CHECK_DELAY).then(function () {
return Promise.all(auths.map(function (auth) {
return ACME.challengeTests[auth.type](me, auth).then(function (result) {
// not a blocker
ACME._removeChallenge(me, options, auth);
return result;
});
})).then(function (claims) {
nopts.order.claims = claims;
nopts.setChallengeWait = 0;
return ACME._setChallengesAll(me, nopts).then(function (valids) {
return Promise.all(valids.map(function (auth) {
ACME._removeChallenge(me, nopts, auth);
}));
});
});
};
ACME._chooseAuth = function(options, auths) {
ACME._chooseType = function(options, auths) {
// For each of the challenge types that we support
var auth;
var challengeTypes = Object.keys(options.challenges);
var challengeTypes = Object.keys(options.challenges || ACME._challengesMap);
// ordered from most to least preferred
challengeTypes = (options.challengePriority||[ 'tls-alpn-01', 'http-01', 'dns-01' ]).filter(function (chType) {
return challengeTypes.includes(chType);
@ -2089,15 +2041,17 @@ ACME._chooseAuth = function(options, auths) {
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
var dnsPrefix = ACME.challengePrefixes['dns-01'];
if (dryrun) {
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 Promise.all(request.challenges.map(function (challenge) {
// Don't do extra work for challenges that we can't satisfy
@ -2188,15 +2142,15 @@ ACME._postChallenge = function (me, options, auth) {
}
*/
function deactivate() {
if (me.debug) { console.debug('[acme-v2.js] deactivate:'); }
//#console.debug('[acme-v2.js] deactivate:');
return ACME._jwsRequest(me, {
options: options
, url: auth.url
, protected: { kid: options._kid }
, payload: Enc.binToBuf(JSON.stringify({ "status": "deactivated" }))
}).then(function (resp) {
if (me.debug) { console.debug('deactivate challenge: resp.body:'); }
if (me.debug) { console.debug(resp.body); }
}).then(function (/*#resp*/) {
//#console.debug('deactivate challenge: resp.body:');
//#console.debug(resp.body);
return ACME._wait(DEAUTH_INTERVAL);
});
}
@ -2210,11 +2164,10 @@ ACME._postChallenge = function (me, options, auth) {
count += 1;
if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); }
// TODO POST-as-GET
//#console.debug('\n[DEBUG] statusChallenge\n');
return me.request({ method: 'GET', url: auth.url, json: true }).then(function (resp) {
if ('processing' === resp.body.status) {
if (me.debug) { console.debug('poll: again'); }
//#console.debug('poll: again');
return ACME._wait(RETRY_INTERVAL).then(pollStatus);
}
@ -2223,12 +2176,12 @@ ACME._postChallenge = function (me, options, auth) {
if (count >= MAX_PEND) {
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);
}
if ('valid' === resp.body.status) {
if (me.debug) { console.debug('poll: valid'); }
//#console.debug('poll: valid');
try {
ACME._removeChallenge(me, options, auth);
@ -2255,226 +2208,227 @@ ACME._postChallenge = function (me, options, auth) {
}
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, {
options: options
, url: auth.url
, protected: { kid: options._kid }
, payload: Enc.binToBuf(JSON.stringify({}))
}).then(function (resp) {
if (me.debug) { console.debug('respond to challenge: resp.body:'); }
if (me.debug) { console.debug(resp.body); }
}).then(function (/*#resp*/) {
//#console.debug('respond to challenge: resp.body:');
//#console.debug(resp.body);
return ACME._wait(RETRY_INTERVAL).then(pollStatus);
});
}
return respondToChallenge();
};
ACME._setChallenge = function (me, options, auth) {
return new Promise(function (resolve, reject) {
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);
});
};
// options = { domains, claims, challenges, challengePriority }
ACME._setChallengesAll = function (me, options) {
var order = options.order;
var setAuths = order.authorizations.slice(0);
var claims = order.claims.slice(0);
var validAuths = [];
console.log("[DEBUG] setChallengesAll");
var claims = options.order.claims.slice(0);
var valids = [];
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() {
var authUrl = setAuths.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
if (claim.challenges.some(function (ch) { return 'valid' === ch.status; })) {
return setNext();
}
// Get the list of challenge types we can validate.
// Then order that list by preference
// 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);
if (!auth) {
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
return Promise.reject(new Error(
"Server didn't offer any challenge we can handle for '" + options.domains.join() + "'."
));
}
// Bail with a descriptive message if no usable challenge could be selected
if (!selected) {
var enabled = usable.join(', ') || 'none';
var suitable = claim.challenges.map(function (r) { return r.type; }).join(', ') || 'none';
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);
return ACME._setChallenge(me, options, auth).then(setNext);
// Give the nameservers a moment to propagate
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() {
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) {
// not so much "valid" as "not invalid"
// but in this case we can't confirm either way
validAuths.push(auth);
return Promise.resolve();
valids.push(auth);
return checkNext();
}
return ACME.challengeTests[auth.type](me, auth).then(function () {
validAuths.push(auth);
valids.push(auth);
}).then(checkNext);
}
// Actually sets the challenge via ACME
function challengeNext() {
var auth = validAuths.shift();
if (!auth) { return; }
return ACME._postChallenge(me, options, auth).then(challengeNext);
}
// 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;
});
});
// The reason we set every challenge in a batch first before checking any
// is so that we don't poison our own DNS cache with misses.
return setNext().then(function () {
//#console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY);
return ACME._wait(DELAY);
}).then(checkNext);
};
ACME._finalizeOrder = function (me, options) {
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
if (me.debug) { console.debug('finalizeOrder:'); }
//#console.debug('finalizeOrder:');
var order = options.order;
var validatedDomains = options.order.identifiers.map(function (ident) {
return ident.value;
});
return ACME._getCsrWeb64(me, options, validatedDomains).then(function (csr) {
var body = { csr: csr };
var payload = JSON.stringify(body);
function pollCert() {
if (me.debug) { console.debug('[acme-v2.js] pollCert:'); }
return ACME._jwsRequest(me, {
options: options
, url: options.order.finalizeUrl
, protected: { kid: options._kid }
, payload: Enc.binToBuf(payload)
}).then(function (resp) {
if (me.debug) { console.debug('order finalized: resp.body:'); }
if (me.debug) { console.debug(resp.body); }
// Actually sets the challenge via ACME
function challengeNext() {
var auth = valids.shift();
if (!auth) { return Promise.resolve(); }
return ACME._postChallenge(me, options, auth).then(challengeNext);
}
return challengeNext().then(function () {
//#console.debug("[getCertificate] next.then");
console.log('DEBUG 1 order:');
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
// Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
if ('valid' === resp.body.status) {
options._expires = resp.body.expires;
options._certificate = resp.body.certificate;
function pollCert() {
//#console.debug('[acme-v2.js] pollCert:');
return ACME._jwsRequest(me, {
options: options
, 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 ACME._wait().then(pollCert);
}
return resp.body; // return order
}
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(
"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"
"Didn't finalize order: Unhandled status '" + resp.body.status + "'."
+ " This is not one of the known statuses...\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"
));
}
});
}
return Promise.reject(new Error(
"Didn't finalize order: Unhandled status '" + resp.body.status + "'."
+ " This is not one of the known statuses...\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"
));
return pollCert();
}).then(function () {
//#console.debug('acme-v2: order was finalized');
return me.request({ method: 'GET', url: options._certificate, json: true }).then(function (resp) {
//#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 */
// 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) {
return ACME._getAccountKid(me, options).then(function () {
// options._kid added
// options._kid added
var body = {
// raw wildcard syntax MUST be used here
identifiers: options.domains.sort(function (a, b) {
@ -2499,7 +2453,7 @@ ACME._createOrder = function (me, options) {
};
var payload = JSON.stringify(body);
if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); }
//#console.debug('\n[DEBUG] newOrder\n');
return ACME._jwsRequest(me, {
options: options
, url: me._directoryUrls.newOrder
@ -2513,8 +2467,8 @@ ACME._createOrder = function (me, options) {
, identifiers: body.identifiers
, _response: resp.body
};
if (me.debug) { console.debug('[ordered]', location); } // the account id url
if (me.debug) { console.debug(resp); }
//#console.debug('[ordered]', location); // the account id url
//#console.debug(resp);
if (!order.authorizations) {
return Promise.reject(new Error(
@ -2526,7 +2480,7 @@ ACME._createOrder = function (me, options) {
return order;
}).then(function (order) {
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);
function getNext() {
@ -2565,13 +2519,12 @@ ACME._getAccountKid = function (me, options) {
return options._kid;
});
};
// _kid
// registerAccount
// postChallenge
// finalizeOrder
// getCertificate
//
// Helper Methods
//
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) {
// 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,"
+ " 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
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*/) {
// options.order = order;
return ACME._finalizeOrder(me, options);
@ -2607,7 +2563,7 @@ ACME._getCsrWeb64 = function (me, options, validatedDomains) {
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 Enc.bufToUrlBase64(der);
});
@ -2708,13 +2664,13 @@ ACME._jwsRequest = function (me, bigopts) {
if (!bigopts.protected.kid) { bigopts.protected.kid = bigopts.options._kid; }
}
return me.Keypairs.signJws(
{ jwk: bigopts.options.accountKeypair.privateKeyJwk
{ jwk: bigopts.accountKey || bigopts.options.accountKeypair.privateKeyJwk
, protected: bigopts.protected
, payload: bigopts.payload
}
).then(function (jws) {
if (me.debug) { console.debug('[acme-v2] ' + bigopts.url + ':'); }
if (me.debug) { console.debug(jws); }
//#console.debug('[acme-v2] ' + bigopts.url + ':');
//#console.debug(jws);
return ACME._request(me, { url: bigopts.url, json: jws });
});
});
@ -2769,7 +2725,7 @@ ACME._defaultRequest = function (opts) {
};
ACME._importKeypair = function (me, kp) {
var jwk = kp.privateKeyJwk;
var jwk = kp.privateKeyJwk || kp.kty && kp;
var p;
if (jwk) {
// 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) {
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,"");
};
@ -2850,20 +2794,14 @@ ACME._http01 = function (me, auth) {
});
};
ACME._removeChallenge = function (me, options, auth) {
var challengers = options.challenges || {};
var removeChallenge = (challengers[auth.type] && challengers[auth.type].remove) || options.removeChallenge;
if (1 === removeChallenge.length) {
removeChallenge(auth).then(function () {}, function () {});
} else if (2 === removeChallenge.length) {
removeChallenge(auth, function (err) { return err; });
} 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 () {});
}
return Promise.resolve().then(function () {
if (!options.challenges) { return; }
var ch = options.challenges[auth.type];
ch.remove(auth).catch(function (e) {
console.warn("challenge.remove error:");
console.warn(e);
});
});
};
Enc.bufToUrlBase64 = function (u8) {