|
|
@ -276,7 +276,10 @@ ACME._registerAccount = function(me, options) { |
|
|
|
} |
|
|
|
if (1 === options.agreeToTerms.length) { |
|
|
|
// newer promise API
|
|
|
|
return options.agreeToTerms(me._tos).then(agree, reject); |
|
|
|
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) { |
|
|
@ -461,6 +464,58 @@ ACME._chooseChallenge = function(options, results) { |
|
|
|
|
|
|
|
return challenge; |
|
|
|
}; |
|
|
|
ACME._getZones = function(me, options, dnsHosts) { |
|
|
|
if ('function' !== typeof options.getZones) { |
|
|
|
options.getZones = function() { |
|
|
|
return Promise.resolve([]); |
|
|
|
}; |
|
|
|
} |
|
|
|
return new Promise(function(resolve, reject) { |
|
|
|
try { |
|
|
|
if (options.getZones.length <= 1) { |
|
|
|
options |
|
|
|
.getZones({ dnsHosts: dnsHosts }) |
|
|
|
.then(resolve) |
|
|
|
.catch(reject); |
|
|
|
} else if (2 === options.getZones.length) { |
|
|
|
options.getZones({ dnsHosts: dnsHosts }, function(err, zonenames) { |
|
|
|
if (err) { |
|
|
|
reject(err); |
|
|
|
} else { |
|
|
|
resolve(zonenames); |
|
|
|
} |
|
|
|
}); |
|
|
|
} else { |
|
|
|
throw new Error( |
|
|
|
'options.getZones should accept opts and Promise an array of zone names' |
|
|
|
); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
reject(e); |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
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]; |
|
|
|
} |
|
|
|
ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { |
|
|
|
// we don't poison the dns cache with our dummy request
|
|
|
|
var dnsPrefix = ACME.challengePrefixes['dns-01']; |
|
|
@ -490,6 +545,7 @@ 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
|
|
|
@ -511,7 +567,15 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { |
|
|
|
.update(auth.keyAuthorization) |
|
|
|
.digest('base64') |
|
|
|
); |
|
|
|
if (zone) { |
|
|
|
auth.dnsZone = zone; |
|
|
|
auth.dnsPrefix = auth.dnsHost |
|
|
|
.replace(newZoneRegExp(zone), '') |
|
|
|
.replace(/\.$/, ''); |
|
|
|
} |
|
|
|
|
|
|
|
// for backwards compat
|
|
|
|
auth.challenge = auth; |
|
|
|
return auth; |
|
|
|
}; |
|
|
|
|
|
|
@ -997,187 +1061,204 @@ 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'); |
|
|
|
} |
|
|
|
return ACME._getNonce(me).then(function() { |
|
|
|
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); |
|
|
|
// determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
|
|
|
|
me._kty = |
|
|
|
(options.accountKeypair.privateKeyJwk && |
|
|
|
options.accountKeypair.privateKeyJwk.kty) || |
|
|
|
'RSA'; |
|
|
|
me._alg = 'EC' === me._kty ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled)
|
|
|
|
var jws = me.RSA.signJws( |
|
|
|
options.accountKeypair, |
|
|
|
undefined, |
|
|
|
{ |
|
|
|
nonce: me._nonce, |
|
|
|
alg: me._alg, |
|
|
|
url: me._directoryUrls.newOrder, |
|
|
|
kid: me._kid |
|
|
|
}, |
|
|
|
Buffer.from(payload, 'utf8') |
|
|
|
); |
|
|
|
|
|
|
|
var dnsHosts = options.domains.map(function(d) { |
|
|
|
return ( |
|
|
|
require('crypto') |
|
|
|
.randomBytes(2) |
|
|
|
.toString('hex') + d |
|
|
|
); |
|
|
|
}); |
|
|
|
return ACME._getZones(me, options, dnsHosts).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('\n[DEBUG] newOrder\n'); |
|
|
|
console.debug('[acme-v2] certificates.create'); |
|
|
|
} |
|
|
|
me._nonce = null; |
|
|
|
return me |
|
|
|
._request({ |
|
|
|
method: 'POST', |
|
|
|
url: me._directoryUrls.newOrder, |
|
|
|
headers: { 'Content-Type': 'application/jose+json' }, |
|
|
|
json: jws |
|
|
|
}) |
|
|
|
.then(function(resp) { |
|
|
|
me._nonce = resp.toJSON().headers['replay-nonce']; |
|
|
|
var location = resp.toJSON().headers.location; |
|
|
|
var setAuths; |
|
|
|
var auths = []; |
|
|
|
if (me.debug) { |
|
|
|
console.debug(location); |
|
|
|
} // the account id url
|
|
|
|
if (me.debug) { |
|
|
|
console.debug(resp.toJSON()); |
|
|
|
} |
|
|
|
me._authorizations = resp.body.authorizations; |
|
|
|
me._order = location; |
|
|
|
me._finalize = resp.body.finalize; |
|
|
|
//if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return;
|
|
|
|
|
|
|
|
if (!me._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 = me._authorizations.slice(0); |
|
|
|
return ACME._getNonce(me).then(function() { |
|
|
|
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"
|
|
|
|
}; |
|
|
|
|
|
|
|
function setNext() { |
|
|
|
var authUrl = setAuths.shift(); |
|
|
|
if (!authUrl) { |
|
|
|
return; |
|
|
|
var payload = JSON.stringify(body); |
|
|
|
// determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
|
|
|
|
me._kty = |
|
|
|
(options.accountKeypair.privateKeyJwk && |
|
|
|
options.accountKeypair.privateKeyJwk.kty) || |
|
|
|
'RSA'; |
|
|
|
me._alg = 'EC' === me._kty ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled)
|
|
|
|
var jws = me.RSA.signJws( |
|
|
|
options.accountKeypair, |
|
|
|
undefined, |
|
|
|
{ |
|
|
|
nonce: me._nonce, |
|
|
|
alg: me._alg, |
|
|
|
url: me._directoryUrls.newOrder, |
|
|
|
kid: me._kid |
|
|
|
}, |
|
|
|
Buffer.from(payload, 'utf8') |
|
|
|
); |
|
|
|
|
|
|
|
if (me.debug) { |
|
|
|
console.debug('\n[DEBUG] newOrder\n'); |
|
|
|
} |
|
|
|
me._nonce = null; |
|
|
|
return me |
|
|
|
._request({ |
|
|
|
method: 'POST', |
|
|
|
url: me._directoryUrls.newOrder, |
|
|
|
headers: { 'Content-Type': 'application/jose+json' }, |
|
|
|
json: jws |
|
|
|
}) |
|
|
|
.then(function(resp) { |
|
|
|
me._nonce = resp.toJSON().headers['replay-nonce']; |
|
|
|
var location = resp.toJSON().headers.location; |
|
|
|
var setAuths; |
|
|
|
var auths = []; |
|
|
|
if (me.debug) { |
|
|
|
console.debug(location); |
|
|
|
} // the account id url
|
|
|
|
if (me.debug) { |
|
|
|
console.debug(resp.toJSON()); |
|
|
|
} |
|
|
|
me._authorizations = resp.body.authorizations; |
|
|
|
me._order = location; |
|
|
|
me._finalize = resp.body.finalize; |
|
|
|
//if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return;
|
|
|
|
|
|
|
|
if (!me._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 = me._authorizations.slice(0); |
|
|
|
|
|
|
|
return ACME._getChallenges(me, options, authUrl).then(function( |
|
|
|
results |
|
|
|
) { |
|
|
|
// var domain = options.domains[i]; // results.identifier.value
|
|
|
|
function setNext() { |
|
|
|
var authUrl = setAuths.shift(); |
|
|
|
if (!authUrl) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// If it's already valid, we're golden it regardless
|
|
|
|
if ( |
|
|
|
results.challenges.some(function(ch) { |
|
|
|
return 'valid' === ch.status; |
|
|
|
}) |
|
|
|
return ACME._getChallenges(me, options, authUrl).then(function( |
|
|
|
results |
|
|
|
) { |
|
|
|
return setNext(); |
|
|
|
} |
|
|
|
// 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(); |
|
|
|
} |
|
|
|
|
|
|
|
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() + |
|
|
|
"'." |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
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() + |
|
|
|
"'." |
|
|
|
) |
|
|
|
var auth = ACME._challengeToAuth( |
|
|
|
me, |
|
|
|
options, |
|
|
|
results, |
|
|
|
challenge |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
var auth = ACME._challengeToAuth(me, options, results, challenge); |
|
|
|
auths.push(auth); |
|
|
|
return ACME._setChallenge(me, options, auth).then(setNext); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function challengeNext() { |
|
|
|
var auth = auths.shift(); |
|
|
|
if (!auth) { |
|
|
|
return; |
|
|
|
auths.push(auth); |
|
|
|
return ACME._setChallenge(me, options, auth).then(setNext); |
|
|
|
}); |
|
|
|
} |
|
|
|
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(challengeNext) |
|
|
|
.then(function() { |
|
|
|
if (me.debug) { |
|
|
|
console.debug('[getCertificate] next.then'); |
|
|
|
function challengeNext() { |
|
|
|
var auth = auths.shift(); |
|
|
|
if (!auth) { |
|
|
|
return; |
|
|
|
} |
|
|
|
var validatedDomains = body.identifiers.map(function(ident) { |
|
|
|
return ident.value; |
|
|
|
}); |
|
|
|
return ACME._postChallenge(me, options, auth).then(challengeNext); |
|
|
|
} |
|
|
|
|
|
|
|
return ACME._finalizeOrder(me, options, validatedDomains); |
|
|
|
}) |
|
|
|
.then(function(order) { |
|
|
|
if (me.debug) { |
|
|
|
console.debug('acme-v2: order was finalized'); |
|
|
|
} |
|
|
|
return me |
|
|
|
._request({ method: 'GET', url: me._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; |
|
|
|
// 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(challengeNext) |
|
|
|
.then(function() { |
|
|
|
if (me.debug) { |
|
|
|
console.debug('[getCertificate] next.then'); |
|
|
|
} |
|
|
|
var validatedDomains = body.identifiers.map(function(ident) { |
|
|
|
return ident.value; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
return ACME._finalizeOrder(me, options, validatedDomains); |
|
|
|
}) |
|
|
|
.then(function(order) { |
|
|
|
if (me.debug) { |
|
|
|
console.debug('acme-v2: order was finalized'); |
|
|
|
} |
|
|
|
return me |
|
|
|
._request({ method: 'GET', url: me._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; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
@ -1190,7 +1271,7 @@ ACME.create = function create(me) { |
|
|
|
me.challengePrefixes = ACME.challengePrefixes; |
|
|
|
me.RSA = me.RSA || require('rsa-compat').RSA; |
|
|
|
//me.Keypairs = me.Keypairs || require('keypairs');
|
|
|
|
me.request = me.request || require('@coolaj86/urequest'); |
|
|
|
me.request = me.request || require('@root/request'); |
|
|
|
me._dig = function(query) { |
|
|
|
// TODO use digd.js
|
|
|
|
return new Promise(function(resolve, reject) { |
|
|
@ -1241,7 +1322,27 @@ ACME.create = function create(me) { |
|
|
|
} |
|
|
|
|
|
|
|
me.init = function(_directoryUrl) { |
|
|
|
me.directoryUrl = me.directoryUrl || _directoryUrl; |
|
|
|
if (_directoryUrl) { |
|
|
|
_directoryUrl = _directoryUrl.directoryUrl || _directoryUrl; |
|
|
|
} |
|
|
|
if ('string' === typeof _directoryUrl) { |
|
|
|
me.directoryUrl = _directoryUrl; |
|
|
|
} |
|
|
|
if (!me.directoryUrl) { |
|
|
|
me.directoryUrl = |
|
|
|
'https://acme-staging-v02.api.letsencrypt.org/directory'; |
|
|
|
console.warn(); |
|
|
|
console.warn( |
|
|
|
"No ACME `directoryUrl` was specified. Using Let's Encrypt's staging environment as the default, which will issue invalid certs." |
|
|
|
); |
|
|
|
console.warn('\t' + me.directoryUrl); |
|
|
|
console.warn(); |
|
|
|
console.warn( |
|
|
|
"To get valid certificates you will need to switch to a production URL. You might like Let's Encrypt v2:" |
|
|
|
); |
|
|
|
console.warn('\t' + me.directoryUrl.replace('-staging', '')); |
|
|
|
console.warn(); |
|
|
|
} |
|
|
|
return ACME._directory(me).then(function(resp) { |
|
|
|
me._directoryUrls = resp.body; |
|
|
|
me._tos = me._directoryUrls.meta.termsOfService; |
|
|
|