|
|
@ -165,7 +165,7 @@ ACME._registerAccount = function(me, options) { |
|
|
|
} else if (options.email) { |
|
|
|
contact = ['mailto:' + options.email]; |
|
|
|
} |
|
|
|
var body = { |
|
|
|
var accountRequest = { |
|
|
|
termsOfServiceAgreed: tosUrl === me._tos, |
|
|
|
onlyReturnExisting: false, |
|
|
|
contact: contact |
|
|
@ -182,14 +182,14 @@ ACME._registerAccount = function(me, options) { |
|
|
|
}, |
|
|
|
payload: Enc.strToBuf(JSON.stringify(pair.public)) |
|
|
|
}).then(function(jws) { |
|
|
|
body.externalAccountBinding = jws; |
|
|
|
return body; |
|
|
|
accountRequest.externalAccountBinding = jws; |
|
|
|
return accountRequest; |
|
|
|
}); |
|
|
|
} else { |
|
|
|
pExt = Promise.resolve(body); |
|
|
|
pExt = Promise.resolve(accountRequest); |
|
|
|
} |
|
|
|
return pExt.then(function(body) { |
|
|
|
var payload = JSON.stringify(body); |
|
|
|
return pExt.then(function(accountRequest) { |
|
|
|
var payload = JSON.stringify(accountRequest); |
|
|
|
return ACME._jwsRequest(me, { |
|
|
|
options: options, |
|
|
|
url: me._directoryUrls.newAccount, |
|
|
@ -199,10 +199,20 @@ ACME._registerAccount = function(me, options) { |
|
|
|
.then(function(resp) { |
|
|
|
var account = resp.body; |
|
|
|
|
|
|
|
if (2 !== Math.floor(resp.statusCode / 100)) { |
|
|
|
if ( |
|
|
|
resp.statusCode < 200 || |
|
|
|
resp.statusCode >= 300 |
|
|
|
) { |
|
|
|
if ('string' !== typeof account) { |
|
|
|
account = JSON.stringify(account); |
|
|
|
} |
|
|
|
throw new Error( |
|
|
|
'account error: ' + |
|
|
|
JSON.stringify(resp.body) |
|
|
|
resp.statusCode + |
|
|
|
' ' + |
|
|
|
account + |
|
|
|
'\n' + |
|
|
|
JSON.stringify(accountRequest) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
@ -344,7 +354,24 @@ ACME._testChallengeOptions = function() { |
|
|
|
]; |
|
|
|
}; |
|
|
|
ACME._testChallenges = function(me, options) { |
|
|
|
console.log('[debug] testChallenges'); |
|
|
|
var CHECK_DELAY = 0; |
|
|
|
|
|
|
|
// memoized so that it doesn't run until it's first called
|
|
|
|
var getThumbnail = function() { |
|
|
|
var thumbPromise = ACME._importKeypair(me, options.accountKeypair).then( |
|
|
|
function(pair) { |
|
|
|
return me.Keypairs.thumbprint({ |
|
|
|
jwk: pair.public |
|
|
|
}); |
|
|
|
} |
|
|
|
); |
|
|
|
getThumbnail = function() { |
|
|
|
return thumbPromise; |
|
|
|
}; |
|
|
|
return thumbPromise; |
|
|
|
}; |
|
|
|
|
|
|
|
return Promise.all( |
|
|
|
options.domains.map(function(identifierValue) { |
|
|
|
// TODO we really only need one to pass, not all to pass
|
|
|
@ -389,10 +416,11 @@ ACME._testChallenges = function(me, options) { |
|
|
|
|
|
|
|
if ('dns-01' === challenge.type) { |
|
|
|
// Give the nameservers a moment to propagate
|
|
|
|
CHECK_DELAY = 1.5 * 1000; |
|
|
|
// TODO get this value from the plugin
|
|
|
|
CHECK_DELAY = 7 * 1000; |
|
|
|
} |
|
|
|
|
|
|
|
return Promise.resolve().then(function() { |
|
|
|
return getThumbnail().then(function(accountKeyThumb) { |
|
|
|
var results = { |
|
|
|
identifier: { |
|
|
|
type: 'dns', |
|
|
@ -409,6 +437,7 @@ ACME._testChallenges = function(me, options) { |
|
|
|
return ACME._challengeToAuth( |
|
|
|
me, |
|
|
|
options, |
|
|
|
accountKeyThumb, |
|
|
|
results, |
|
|
|
challenge, |
|
|
|
dryrun |
|
|
@ -460,7 +489,14 @@ ACME._chooseChallenge = function(options, results) { |
|
|
|
|
|
|
|
return challenge; |
|
|
|
}; |
|
|
|
ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { |
|
|
|
ACME._challengeToAuth = function( |
|
|
|
me, |
|
|
|
options, |
|
|
|
accountKeyThumb, |
|
|
|
request, |
|
|
|
challenge, |
|
|
|
dryrun |
|
|
|
) { |
|
|
|
// we don't poison the dns cache with our dummy request
|
|
|
|
var dnsPrefix = ACME.challengePrefixes['dns-01']; |
|
|
|
if (dryrun) { |
|
|
@ -486,38 +522,58 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { |
|
|
|
auth[key] = challenge[key]; |
|
|
|
}); |
|
|
|
|
|
|
|
var zone = pluckZone(options.zonenames || [], auth.identifier.value); |
|
|
|
// batteries-included helpers
|
|
|
|
auth.hostname = auth.identifier.value; |
|
|
|
// because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases
|
|
|
|
auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); |
|
|
|
return ACME._importKeypair(me, options.accountKeypair).then(function(pair) { |
|
|
|
return me.Keypairs.thumbprint({ jwk: pair.public }).then(function( |
|
|
|
thumb |
|
|
|
) { |
|
|
|
auth.thumbprint = thumb; |
|
|
|
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
|
|
|
|
auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; |
|
|
|
// conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
|
|
|
|
// TODO auth.http01Url ?
|
|
|
|
auth.challengeUrl = |
|
|
|
'http://' + |
|
|
|
auth.identifier.value + |
|
|
|
ACME.challengePrefixes['http-01'] + |
|
|
|
'/' + |
|
|
|
auth.token; |
|
|
|
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); |
|
|
|
|
|
|
|
return sha2 |
|
|
|
.sum(256, auth.keyAuthorization) |
|
|
|
.then(function(hash) { |
|
|
|
return Enc.bufToUrlBase64(new Uint8Array(hash)); |
|
|
|
}) |
|
|
|
.then(function(hash64) { |
|
|
|
auth.dnsAuthorization = hash64; |
|
|
|
return auth; |
|
|
|
}); |
|
|
|
// we must accept JWKs that we didn't generate and we can't guarantee
|
|
|
|
// that they properly set kid to thumbnail (especially since ACME doesn't do this)
|
|
|
|
// so we have to regenerate it every time we need it, which is quite often
|
|
|
|
auth.thumbprint = accountKeyThumb; |
|
|
|
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
|
|
|
|
auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; |
|
|
|
// conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
|
|
|
|
// TODO auth.http01Url ?
|
|
|
|
auth.challengeUrl = |
|
|
|
'http://' + |
|
|
|
auth.identifier.value + |
|
|
|
ACME.challengePrefixes['http-01'] + |
|
|
|
'/' + |
|
|
|
auth.token; |
|
|
|
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); |
|
|
|
|
|
|
|
// Always calculate dnsAuthorization because we
|
|
|
|
// may need to present to the user for confirmation / instruction
|
|
|
|
// _as part of_ the decision making process
|
|
|
|
return sha2 |
|
|
|
.sum(256, auth.keyAuthorization) |
|
|
|
.then(function(hash) { |
|
|
|
return Enc.bufToUrlBase64(new Uint8Array(hash)); |
|
|
|
}) |
|
|
|
.then(function(hash64) { |
|
|
|
auth.dnsAuthorization = hash64; |
|
|
|
if (zone) { |
|
|
|
auth.dnsZone = zone; |
|
|
|
auth.dnsPrefix = auth.dnsHost |
|
|
|
.replace(newZoneRegExp(zone), '') |
|
|
|
.replace(/\.$/, ''); |
|
|
|
} |
|
|
|
|
|
|
|
// For backwards compat with the v2.7 plugins
|
|
|
|
auth.challenge = auth; |
|
|
|
// TODO can we use just { challenge: auth }?
|
|
|
|
auth.request = function() { |
|
|
|
// TODO see https://git.rootprojects.org/root/acme.js/issues/###
|
|
|
|
console.warn( |
|
|
|
"[warn] deprecated use of request on '" + |
|
|
|
auth.type + |
|
|
|
"' challenge object. Receive from challenger.init() instead." |
|
|
|
); |
|
|
|
me.request.apply(null, arguments); |
|
|
|
}; |
|
|
|
return auth; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
ACME._untame = function(name, wild) { |
|
|
@ -597,7 +653,7 @@ ACME._postChallenge = function(me, options, auth) { |
|
|
|
.then(function(resp) { |
|
|
|
if ('processing' === resp.body.status) { |
|
|
|
if (me.debug) { |
|
|
|
console.debug('poll: again'); |
|
|
|
console.debug('poll: again', auth.url); |
|
|
|
} |
|
|
|
return ACME._wait(RETRY_INTERVAL).then(pollStatus); |
|
|
|
} |
|
|
@ -610,14 +666,14 @@ ACME._postChallenge = function(me, options, auth) { |
|
|
|
.then(respondToChallenge); |
|
|
|
} |
|
|
|
if (me.debug) { |
|
|
|
console.debug('poll: again'); |
|
|
|
console.debug('poll: again', auth.url); |
|
|
|
} |
|
|
|
return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); |
|
|
|
} |
|
|
|
|
|
|
|
if ('valid' === resp.body.status) { |
|
|
|
if (me.debug) { |
|
|
|
console.debug('poll: valid'); |
|
|
|
console.debug('VALID !!!!!!!!!!!!!!!! poll: valid'); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
@ -637,7 +693,8 @@ ACME._postChallenge = function(me, options, auth) { |
|
|
|
"[acme-v2] (E_STATE_INVALID) challenge state for '" + |
|
|
|
altname + |
|
|
|
"': '" + |
|
|
|
resp.body.status + |
|
|
|
//resp.body.status +
|
|
|
|
JSON.stringify(resp.body) + |
|
|
|
"'"; |
|
|
|
} else { |
|
|
|
errmsg = |
|
|
@ -675,17 +732,20 @@ ACME._postChallenge = function(me, options, auth) { |
|
|
|
return respondToChallenge(); |
|
|
|
}; |
|
|
|
ACME._setChallenge = function(me, options, auth) { |
|
|
|
return new Promise(function(resolve, reject) { |
|
|
|
return Promise.resolve().then(function() { |
|
|
|
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) { |
|
|
|
var challenger = challengers[auth.type] && challengers[auth.type].set; |
|
|
|
if (!challenger) { |
|
|
|
throw new Error( |
|
|
|
"options.challenges did not have a valid entry for '" + |
|
|
|
auth.type + |
|
|
|
"'" |
|
|
|
); |
|
|
|
} |
|
|
|
if (1 === challenger.length) { |
|
|
|
return Promise.resolve(challenger(auth)); |
|
|
|
} else if (2 === challenger.length) { |
|
|
|
return new Promise(function(resolve, reject) { |
|
|
|
challenger(auth, function(err) { |
|
|
|
if (err) { |
|
|
|
reject(err); |
|
|
@ -693,45 +753,12 @@ ACME._setChallenge = function(me, options, auth) { |
|
|
|
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); |
|
|
|
}); |
|
|
|
} else { |
|
|
|
throw new Error( |
|
|
|
"Bad function signature for '" + auth.type + "' challenge.set()" |
|
|
|
); |
|
|
|
} |
|
|
|
return ACME._wait(DELAY); |
|
|
|
}); |
|
|
|
}; |
|
|
|
ACME._finalizeOrder = function(me, options, validatedDomains) { |
|
|
@ -943,170 +970,234 @@ ACME._getCertificate = function(me, options) { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// Do a little dry-run / self-test
|
|
|
|
return ACME._testChallenges(me, options).then(function() { |
|
|
|
if (me.debug) { |
|
|
|
console.debug('[acme-v2] certificates.create'); |
|
|
|
// TODO Promise.all()?
|
|
|
|
Object.keys(options.challenges).forEach(function(key) { |
|
|
|
var presenter = options.challenges[key]; |
|
|
|
if ('function' === typeof presenter.init && !presenter._initialized) { |
|
|
|
presenter._initialized = true; |
|
|
|
return ACME._depInit(me, presenter); |
|
|
|
} |
|
|
|
var body = { |
|
|
|
// raw wildcard syntax MUST be used here
|
|
|
|
identifiers: options.domains |
|
|
|
.sort(function(a, b) { |
|
|
|
// the first in the list will be the subject of the certificate, I believe (and hope)
|
|
|
|
if (!options.subject) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
if (options.subject === a) { |
|
|
|
return -1; |
|
|
|
} |
|
|
|
if (options.subject === b) { |
|
|
|
return 1; |
|
|
|
} |
|
|
|
return 0; |
|
|
|
}) |
|
|
|
.map(function(hostname) { |
|
|
|
return { type: 'dns', value: hostname }; |
|
|
|
}) |
|
|
|
//, "notBefore": "2016-01-01T00:00:00Z"
|
|
|
|
//, "notAfter": "2016-01-08T00:00:00Z"
|
|
|
|
}; |
|
|
|
}); |
|
|
|
|
|
|
|
var payload = JSON.stringify(body); |
|
|
|
if (me.debug) { |
|
|
|
console.debug('\n[DEBUG] newOrder\n'); |
|
|
|
} |
|
|
|
return ACME._jwsRequest(me, { |
|
|
|
options: options, |
|
|
|
url: me._directoryUrls.newOrder, |
|
|
|
protected: { kid: options._kid }, |
|
|
|
payload: Enc.strToBuf(payload) |
|
|
|
}).then(function(resp) { |
|
|
|
var location = resp.headers.location; |
|
|
|
var setAuths; |
|
|
|
var validAuths = []; |
|
|
|
var auths = []; |
|
|
|
if (me.debug) { |
|
|
|
console.debug('[ordered]', location); |
|
|
|
} // the account id url
|
|
|
|
var promiseZones; |
|
|
|
if (options.challenges['dns-01']) { |
|
|
|
// a little bit of random to ensure that getZones()
|
|
|
|
// actually returns the zones and not the hosts as zones
|
|
|
|
var dnsHosts = options.domains.map(function(d) { |
|
|
|
var rnd = require('crypto') |
|
|
|
.randomBytes(2) |
|
|
|
.toString('hex'); |
|
|
|
return rnd + '.' + d; |
|
|
|
}); |
|
|
|
promiseZones = ACME._getZones( |
|
|
|
me, |
|
|
|
options.challenges['dns-01'], |
|
|
|
dnsHosts |
|
|
|
); |
|
|
|
} else { |
|
|
|
promiseZones = Promise.resolve([]); |
|
|
|
} |
|
|
|
|
|
|
|
return promiseZones |
|
|
|
.then(function(zonenames) { |
|
|
|
options.zonenames = zonenames; |
|
|
|
// Do a little dry-run / self-test
|
|
|
|
return ACME._testChallenges(me, options); |
|
|
|
}) |
|
|
|
.then(function() { |
|
|
|
if (me.debug) { |
|
|
|
console.debug(resp); |
|
|
|
console.debug('[acme-v2] certificates.create'); |
|
|
|
} |
|
|
|
options._authorizations = resp.body.authorizations; |
|
|
|
options._order = location; |
|
|
|
options._finalize = resp.body.finalize; |
|
|
|
//if (me.debug) console.debug('[DEBUG] finalize:', options._finalize); return;
|
|
|
|
var certOrder = { |
|
|
|
// raw wildcard syntax MUST be used here
|
|
|
|
identifiers: options.domains |
|
|
|
.sort(function(a, b) { |
|
|
|
// the first in the list will be the subject of the certificate, I believe (and hope)
|
|
|
|
if (!options.subject) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
if (options.subject === a) { |
|
|
|
return -1; |
|
|
|
} |
|
|
|
if (options.subject === b) { |
|
|
|
return 1; |
|
|
|
} |
|
|
|
return 0; |
|
|
|
}) |
|
|
|
.map(function(hostname) { |
|
|
|
return { type: 'dns', value: hostname }; |
|
|
|
}) |
|
|
|
//, "notBefore": "2016-01-01T00:00:00Z"
|
|
|
|
//, "notAfter": "2016-01-08T00:00:00Z"
|
|
|
|
}; |
|
|
|
|
|
|
|
if (!options._authorizations) { |
|
|
|
return Promise.reject( |
|
|
|
new Error( |
|
|
|
"[acme-v2.js] authorizations were not fetched for '" + |
|
|
|
options.domains.join() + |
|
|
|
"':\n" + |
|
|
|
JSON.stringify(resp.body) |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
var payload = JSON.stringify(certOrder); |
|
|
|
if (me.debug) { |
|
|
|
console.debug('[acme-v2] POST newOrder has authorizations'); |
|
|
|
console.debug('\n[DEBUG] newOrder\n'); |
|
|
|
} |
|
|
|
setAuths = options._authorizations.slice(0); |
|
|
|
|
|
|
|
function setNext() { |
|
|
|
var authUrl = setAuths.shift(); |
|
|
|
if (!authUrl) { |
|
|
|
return; |
|
|
|
return ACME._jwsRequest(me, { |
|
|
|
options: options, |
|
|
|
url: me._directoryUrls.newOrder, |
|
|
|
protected: { kid: options._kid }, |
|
|
|
payload: Enc.strToBuf(payload) |
|
|
|
}).then(function(resp) { |
|
|
|
var location = resp.headers.location; |
|
|
|
var setAuths; |
|
|
|
var validAuths = []; |
|
|
|
var auths = []; |
|
|
|
if (me.debug) { |
|
|
|
console.debug('[ordered]', location); |
|
|
|
} // the account id url
|
|
|
|
if (me.debug) { |
|
|
|
console.debug(resp); |
|
|
|
} |
|
|
|
options._authorizations = resp.body.authorizations; |
|
|
|
options._order = location; |
|
|
|
options._finalize = resp.body.finalize; |
|
|
|
//if (me.debug) console.debug('[DEBUG] finalize:', options._finalize); return;
|
|
|
|
|
|
|
|
return ACME._getChallenges(me, options, authUrl).then(function( |
|
|
|
results |
|
|
|
) { |
|
|
|
// var domain = options.domains[i]; // results.identifier.value
|
|
|
|
if (!options._authorizations) { |
|
|
|
return Promise.reject( |
|
|
|
new Error( |
|
|
|
"[acme-v2.js] authorizations were not fetched for '" + |
|
|
|
options.domains.join() + |
|
|
|
"':\n" + |
|
|
|
JSON.stringify(resp.body) |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
if (me.debug) { |
|
|
|
console.debug('[acme-v2] POST newOrder has authorizations'); |
|
|
|
} |
|
|
|
setAuths = options._authorizations.slice(0); |
|
|
|
|
|
|
|
var accountKeyThumb; |
|
|
|
function setThumbnail() { |
|
|
|
return ACME._importKeypair(me, options.accountKeypair).then( |
|
|
|
function(pair) { |
|
|
|
return me.Keypairs.thumbprint({ |
|
|
|
jwk: pair.public |
|
|
|
}).then(function(_thumb) { |
|
|
|
accountKeyThumb = _thumb; |
|
|
|
}); |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// If it's already valid, we're golden it regardless
|
|
|
|
if ( |
|
|
|
results.challenges.some(function(ch) { |
|
|
|
return 'valid' === ch.status; |
|
|
|
}) |
|
|
|
) { |
|
|
|
return setNext(); |
|
|
|
function setNext() { |
|
|
|
var authUrl = setAuths.shift(); |
|
|
|
if (!authUrl) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
var challenge = ACME._chooseChallenge(options, results); |
|
|
|
if (!challenge) { |
|
|
|
// 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() + |
|
|
|
"'." |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
return ACME._getChallenges(me, options, authUrl).then( |
|
|
|
function(results) { |
|
|
|
// var domain = options.domains[i]; // results.identifier.value
|
|
|
|
|
|
|
|
// If it's already valid, we're golden it regardless
|
|
|
|
if ( |
|
|
|
results.challenges.some(function(ch) { |
|
|
|
return 'valid' === ch.status; |
|
|
|
}) |
|
|
|
) { |
|
|
|
return setNext(); |
|
|
|
} |
|
|
|
|
|
|
|
return ACME._challengeToAuth( |
|
|
|
me, |
|
|
|
options, |
|
|
|
results, |
|
|
|
challenge, |
|
|
|
false |
|
|
|
).then(function(auth) { |
|
|
|
auths.push(auth); |
|
|
|
return ACME._setChallenge(me, options, auth).then( |
|
|
|
setNext |
|
|
|
); |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
var challenge = ACME._chooseChallenge( |
|
|
|
options, |
|
|
|
results |
|
|
|
); |
|
|
|
if (!challenge) { |
|
|
|
// 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() + |
|
|
|
"'." |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
function checkNext() { |
|
|
|
var auth = auths.shift(); |
|
|
|
if (!auth) { |
|
|
|
return; |
|
|
|
return ACME._challengeToAuth( |
|
|
|
me, |
|
|
|
options, |
|
|
|
accountKeyThumb, |
|
|
|
results, |
|
|
|
challenge, |
|
|
|
false |
|
|
|
).then(function(auth) { |
|
|
|
console.log('ADD DUBIOUS AUTH'); |
|
|
|
auths.push(auth); |
|
|
|
return ACME._setChallenge( |
|
|
|
me, |
|
|
|
options, |
|
|
|
auth |
|
|
|
).then(setNext); |
|
|
|
}); |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
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(); |
|
|
|
function waitAll() { |
|
|
|
// TODO take the max wait of all challenge plugins and wait that long, or 1000ms
|
|
|
|
var DELAY = me.setChallengeWait || 7000; |
|
|
|
if (true || me.debug) { |
|
|
|
console.debug( |
|
|
|
'\n[DEBUG] waitChallengeDelay %s\n', |
|
|
|
DELAY |
|
|
|
); |
|
|
|
} |
|
|
|
return ACME._wait(DELAY); |
|
|
|
} |
|
|
|
|
|
|
|
return ACME.challengeTests[auth.type](me, auth) |
|
|
|
.then(function() { |
|
|
|
function checkNext() { |
|
|
|
console.log('CONSUME DUBIOUS AUTH', auths.length); |
|
|
|
var auth = auths.shift(); |
|
|
|
if (!auth) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
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); |
|
|
|
}) |
|
|
|
.then(checkNext); |
|
|
|
} |
|
|
|
console.log('ADD VALID AUTH (skip)', validAuths.length); |
|
|
|
return checkNext(); |
|
|
|
} |
|
|
|
|
|
|
|
function challengeNext() { |
|
|
|
var auth = validAuths.shift(); |
|
|
|
if (!auth) { |
|
|
|
return; |
|
|
|
return ACME.challengeTests[auth.type](me, auth) |
|
|
|
.then(function() { |
|
|
|
console.log('ADD VALID AUTH'); |
|
|
|
validAuths.push(auth); |
|
|
|
}) |
|
|
|
.then(checkNext); |
|
|
|
} |
|
|
|
|
|
|
|
function presentNext() { |
|
|
|
console.log('CONSUME VALID AUTH', validAuths.length); |
|
|
|
var auth = validAuths.shift(); |
|
|
|
if (!auth) { |
|
|
|
return; |
|
|
|
} |
|
|
|
return ACME._postChallenge(me, options, auth).then( |
|
|
|
presentNext |
|
|
|
); |
|
|
|
} |
|
|
|
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() { |
|
|
|
function finalizeOrder() { |
|
|
|
if (me.debug) { |
|
|
|
console.debug('[getCertificate] next.then'); |
|
|
|
} |
|
|
|
var validatedDomains = body.identifiers.map(function( |
|
|
|
var validatedDomains = certOrder.identifiers.map(function( |
|
|
|
ident |
|
|
|
) { |
|
|
|
return ident.value; |
|
|
|
}); |
|
|
|
|
|
|
|
return ACME._finalizeOrder(me, options, validatedDomains); |
|
|
|
}) |
|
|
|
.then(function(order) { |
|
|
|
} |
|
|
|
|
|
|
|
function retrieveCerts(order) { |
|
|
|
if (me.debug) { |
|
|
|
console.debug('acme-v2: order was finalized'); |
|
|
|
} |
|
|
@ -1141,10 +1232,22 @@ ACME._getCertificate = function(me, options) { |
|
|
|
} |
|
|
|
return certs; |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// First we set each and 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 setThumbnail() |
|
|
|
.then(setNext) |
|
|
|
.then(waitAll) |
|
|
|
.then(checkNext) |
|
|
|
.then(presentNext) |
|
|
|
.then(finalizeOrder) |
|
|
|
.then(retrieveCerts); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
ACME._generateCsrWeb64 = function(me, options, validatedDomains) { |
|
|
|
var csr; |
|
|
|
if (options.csr) { |
|
|
@ -1153,6 +1256,7 @@ ACME._generateCsrWeb64 = function(me, options, validatedDomains) { |
|
|
|
if ('string' !== typeof csr) { |
|
|
|
csr = Enc.bufToUrlBase64(csr); |
|
|
|
} |
|
|
|
// TODO PEM.parseBlock()
|
|
|
|
// nix PEM headers, if any
|
|
|
|
if ('-' === csr[0]) { |
|
|
|
csr = csr |
|
|
@ -1168,15 +1272,13 @@ ACME._generateCsrWeb64 = function(me, options, validatedDomains) { |
|
|
|
me, |
|
|
|
options.serverKeypair || options.domainKeypair |
|
|
|
).then(function(pair) { |
|
|
|
return me |
|
|
|
.CSR({ |
|
|
|
jwk: pair.private, |
|
|
|
domains: validatedDomains, |
|
|
|
encoding: 'der' |
|
|
|
}) |
|
|
|
.then(function(der) { |
|
|
|
return Enc.bufToUrlBase64(der); |
|
|
|
}); |
|
|
|
return me.CSR.csr({ |
|
|
|
jwk: pair.private, |
|
|
|
domains: validatedDomains, |
|
|
|
encoding: 'der' |
|
|
|
}).then(function(der) { |
|
|
|
return Enc.bufToUrlBase64(der); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
@ -1276,6 +1378,7 @@ ACME._jwsRequest = function(me, bigopts) { |
|
|
|
bigopts.protected.kid = bigopts.options._kid; |
|
|
|
} |
|
|
|
} |
|
|
|
// this will shasum the thumbnail the 2nd time
|
|
|
|
return me.Keypairs.signJws({ |
|
|
|
jwk: bigopts.options.accountKeypair.privateKeyJwk, |
|
|
|
protected: bigopts.protected, |
|
|
@ -1291,6 +1394,7 @@ ACME._jwsRequest = function(me, bigopts) { |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
// Handle some ACME-specific defaults
|
|
|
|
ACME._request = function(me, opts) { |
|
|
|
if (!opts.headers) { |
|
|
@ -1430,24 +1534,99 @@ 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; |
|
|
|
challengers[auth.type] && challengers[auth.type].remove; |
|
|
|
if (1 === removeChallenge.length) { |
|
|
|
removeChallenge(auth).then(function() {}, function() {}); |
|
|
|
return Promise.resolve(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." |
|
|
|
throw new Error( |
|
|
|
"Bad function signature for '" + auth.type + "' challenge.remove()" |
|
|
|
); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
ACME._depInit = function(me, presenter) { |
|
|
|
if ('function' !== typeof presenter.init) { |
|
|
|
return Promise.resolve(null); |
|
|
|
} |
|
|
|
return ACME._wrapCb( |
|
|
|
me, |
|
|
|
presenter, |
|
|
|
'init', |
|
|
|
{ type: '*', request: me.request }, |
|
|
|
'null' |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
ACME._getZones = function(me, presenter, dnsHosts) { |
|
|
|
if ('function' !== typeof presenter.zones) { |
|
|
|
presenter.zones = function() { |
|
|
|
return Promise.resolve([]); |
|
|
|
}; |
|
|
|
} |
|
|
|
var challenge = { |
|
|
|
type: 'dns-01', |
|
|
|
dnsHosts: dnsHosts, |
|
|
|
request: me.request |
|
|
|
}; |
|
|
|
// back/forwards-compat
|
|
|
|
challenge.challenge = challenge; |
|
|
|
return ACME._wrapCb( |
|
|
|
me, |
|
|
|
presenter, |
|
|
|
'zones', |
|
|
|
challenge, |
|
|
|
'an array of zone names' |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
ACME._wrapCb = function(me, options, _name, args, _desc) { |
|
|
|
return new Promise(function(resolve, reject) { |
|
|
|
if (options[_name].length <= 1) { |
|
|
|
return Promise.resolve(options[_name](args)) |
|
|
|
.then(resolve) |
|
|
|
.catch(reject); |
|
|
|
} else if (2 === options[_name].length) { |
|
|
|
options[_name](args, function(err, results) { |
|
|
|
if (err) { |
|
|
|
reject(err); |
|
|
|
} else { |
|
|
|
resolve(results); |
|
|
|
} |
|
|
|
}); |
|
|
|
} else { |
|
|
|
throw new Error( |
|
|
|
'options.' + _name + ' should accept opts and Promise ' + _desc |
|
|
|
); |
|
|
|
ACME._removeChallengeWarn = true; |
|
|
|
} |
|
|
|
removeChallenge(auth.request.identifier, auth.token, function() {}); |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
function newZoneRegExp(zonename) { |
|
|
|
// (^|\.)example\.com$
|
|
|
|
// which matches:
|
|
|
|
// foo.example.com
|
|
|
|
// example.com
|
|
|
|
// but not:
|
|
|
|
// fooexample.com
|
|
|
|
return new RegExp('(^|\\.)' + zonename.replace(/\./g, '\\.') + '$'); |
|
|
|
} |
|
|
|
|
|
|
|
function pluckZone(zonenames, dnsHost) { |
|
|
|
return zonenames |
|
|
|
.filter(function(zonename) { |
|
|
|
// the only character that needs to be escaped for regex
|
|
|
|
// and is allowed in a domain name is '.'
|
|
|
|
return newZoneRegExp(zonename).test(dnsHost); |
|
|
|
}) |
|
|
|
.sort(function(a, b) { |
|
|
|
// longest match first
|
|
|
|
return b.length - a.length; |
|
|
|
})[0]; |
|
|
|
} |
|
|
|