|
|
@ -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<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; }); |
|
|
|
} |
|
|
|
|
|
|
|
// 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) <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(); } |
|
|
|
|
|
|
|
// 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) <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) { |
|
|
|