From 352e334b5ddc4621c1fa672ace43d2ea208fa3ed Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 22 May 2019 02:38:21 -0600 Subject: [PATCH] refactor --- app/js/bluecrypt-acme.js | 698 ++++++++++++++++++--------------------- 1 file changed, 318 insertions(+), 380 deletions(-) diff --git a/app/js/bluecrypt-acme.js b/app/js/bluecrypt-acme.js index 4d9f0d4..f78b066 100644 --- a/app/js/bluecrypt-acme.js +++ b/app/js/bluecrypt-acme.js @@ -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; - - 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); } - - /* - { - 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); + } + , 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)); + } + + 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; }); }); - } + }); + } - 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; }')); - } - }); + 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; }); } - - // 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(/^\*\./, '') - } + identifier: { type: 'dns' , value: name.replace('*.', '') } , challenges: challenges , expires: new Date(Date.now() + (60 * 1000)).toISOString() - , wildcard: identifierValue.includes('*.') || undefined + , wildcard: name.includes('*.') || undefined } }; - return ACME._challengesToAuth(me, options, resp.body, dryrun).then(function (auths) { - resp.body._rawChallenges = resp.body.challenges; + // The dry-run comes first in the spirit of "fail fast" + // (and protecting against challenge failure rate limits) + var dryrun = true; + 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) 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(); } - - // var domain = options.domains[i]; // claim.identifier.value + if (!claim) { return Promise.resolve(); } + + 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; + + // 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); - 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() + "'." - )); - } + // Give the nameservers a moment to propagate + if ('dns-01' === selected.type) { + DELAY = 1.5 * 1000; + } - auths.push(auth); - return ACME._setChallenge(me, options, auth).then(setNext); + 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); } - - // 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; - - return resp.body; // return order - } - - if ('processing' === resp.body.status) { - return ACME._wait().then(pollCert); - } - - if (me.debug) { 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) - )); - } + // 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); - 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) - )); - } + 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); + + // 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; + + return resp.body; // return order + } + + 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 ('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) 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) {