Comparing version 1.6.0 to 1.7.4
321
node.js
@@ -50,6 +50,5 @@ // Copyright 2018 AJ ONeal. All rights reserved | ||
// remove leading *. on wildcard domains | ||
var hostname = ACME.challengePrefixes['dns-01'] + '.' + auth.hostname.replace(/^\*\./, ''); | ||
return me._dig({ | ||
type: 'TXT' | ||
, name: hostname | ||
, name: auth.dnsHost | ||
}).then(function (ans) { | ||
@@ -66,3 +65,3 @@ var err; | ||
"Error: Failed DNS-01 Pre-Flight Dry Run.\n" | ||
+ "dig TXT '" + hostname + "' does not return '" + auth.dnsAuthorization + "'\n" | ||
+ "dig TXT '" + auth.dnsHost + "' does not return '" + auth.dnsAuthorization + "'\n" | ||
+ "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" | ||
@@ -169,3 +168,3 @@ ); | ||
, { nonce: me._nonce | ||
, alg: 'RS256' | ||
, alg: (me._alg || 'RS256') | ||
, url: me._directoryUrls.newAccount | ||
@@ -267,2 +266,32 @@ , jwk: jwk | ||
ACME._testChallengeOptions = function () { | ||
var chToken = require('crypto').randomBytes(16).toString('hex'); | ||
return [ | ||
{ | ||
"type": "http-01", | ||
"status": "pending", | ||
"url": "https://acme-staging-v02.example.com/0", | ||
"token": "test-" + chToken + "-0" | ||
} | ||
, { | ||
"type": "dns-01", | ||
"status": "pending", | ||
"url": "https://acme-staging-v02.example.com/1", | ||
"token": "test-" + chToken + "-1", | ||
"_wildcard": true | ||
} | ||
, { | ||
"type": "tls-sni-01", | ||
"status": "pending", | ||
"url": "https://acme-staging-v02.example.com/2", | ||
"token": "test-" + chToken + "-2" | ||
} | ||
, { | ||
"type": "tls-alpn-01", | ||
"status": "pending", | ||
"url": "https://acme-staging-v02.example.com/3", | ||
"token": "test-" + chToken + "-3" | ||
} | ||
]; | ||
}; | ||
ACME._testChallenges = function (me, options) { | ||
@@ -273,29 +302,110 @@ if (me.skipChallengeTest) { | ||
var CHECK_DELAY = 0; | ||
return Promise.all(options.domains.map(function (identifierValue) { | ||
// TODO we really only need one to pass, not all to pass | ||
return Promise.all(options.challengeTypes.map(function (chType) { | ||
var chToken = require('crypto').randomBytes(16).toString('hex'); | ||
var thumbprint = me.RSA.thumbprint(options.accountKeypair); | ||
var keyAuthorization = chToken + '.' + thumbprint; | ||
var auth = { | ||
identifier: { type: "dns", value: identifierValue } | ||
, hostname: identifierValue | ||
, type: chType | ||
, token: chToken | ||
, thumbprint: thumbprint | ||
, keyAuthorization: keyAuthorization | ||
, dnsAuthorization: me.RSA.utils.toWebsafeBase64( | ||
require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') | ||
) | ||
var challenges = ACME._testChallengeOptions(); | ||
if (identifierValue.includes("*")) { | ||
challenges = challenges.filter(function (ch) { return ch._wildcard; }); | ||
} | ||
var challenge = ACME._chooseChallenge(options, { challenges: challenges }); | ||
if (!challenge) { | ||
// For example, wildcards require dns-01 and, if we don't have that, we have to bail | ||
var enabled = options.challengeTypes.join(', ') || 'none'; | ||
var suitable = 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 + " )." | ||
)); | ||
} | ||
if ('dns-01' === challenge.type) { | ||
// Give the nameservers a moment to propagate | ||
CHECK_DELAY = 1.5 * 1000; | ||
} | ||
return Promise.resolve().then(function () { | ||
var results = { | ||
identifier: { | ||
type: "dns" | ||
, value: identifierValue.replace(/^\*\./, '') | ||
} | ||
, challenges: [ challenge ] | ||
, expires: new Date(Date.now() + (60 * 1000)).toISOString() | ||
, wildcard: identifierValue.includes('*.') || undefined | ||
}; | ||
var dryrun = true; | ||
var auth = ACME._challengeToAuth(me, options, results, challenge, dryrun); | ||
return ACME._setChallenge(me, options, auth).then(function () { | ||
return ACME.challengeTests[chType](me, auth); | ||
return auth; | ||
}); | ||
})); | ||
})); | ||
}); | ||
})).then(function (auths) { | ||
return ACME._wait(CHECK_DELAY).then(function () { | ||
return Promise.all(auths.map(function (auth) { | ||
return ACME.challengeTests[auth.type](me, auth); | ||
})); | ||
}); | ||
}); | ||
}; | ||
ACME._chooseChallenge = function(options, results) { | ||
// For each of the challenge types that we support | ||
var challenge; | ||
options.challengeTypes.some(function (chType) { | ||
// And for each of the challenge types that are allowed | ||
return results.challenges.some(function (ch) { | ||
// Check to see if there are any matches | ||
if (ch.type === chType) { | ||
challenge = ch; | ||
return true; | ||
} | ||
}); | ||
}); | ||
return challenge; | ||
}; | ||
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']; | ||
if (dryrun) { | ||
dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + Math.random().toString().slice(2,6)); | ||
} | ||
var auth = {}; | ||
// straight copy from the new order response | ||
// { identifier, status, expires, challenges, wildcard } | ||
Object.keys(request).forEach(function (key) { | ||
auth[key] = request[key]; | ||
}); | ||
// copy from the challenge we've chosen | ||
// { type, status, url, token } | ||
// (note the duplicate status overwrites the one above, but they should be the same) | ||
Object.keys(challenge).forEach(function (key) { | ||
auth[key] = challenge[key]; | ||
}); | ||
// batteries-included helpers | ||
auth.hostname = request.identifier.value; | ||
auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); | ||
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) | ||
auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; | ||
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); | ||
auth.dnsAuthorization = ACME._toWebsafeBase64( | ||
require('crypto').createHash('sha256').update(auth.keyAuthorization).digest('base64') | ||
); | ||
// because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases | ||
auth.altname = ACME._untame(request.identifier.value, request.wildcard); | ||
return auth; | ||
}; | ||
ACME._untame = function (name, wild) { | ||
if (wild) { name = '*.' + name.replace('*.', ''); } | ||
return name; | ||
}; | ||
// https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 | ||
ACME._postChallenge = function (me, options, identifier, ch) { | ||
ACME._postChallenge = function (me, options, auth) { | ||
var RETRY_INTERVAL = me.retryInterval || 1000; | ||
@@ -307,17 +417,3 @@ var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; | ||
var thumbprint = me.RSA.thumbprint(options.accountKeypair); | ||
var keyAuthorization = ch.token + '.' + thumbprint; | ||
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) | ||
// /.well-known/acme-challenge/:token | ||
var auth = { | ||
identifier: identifier | ||
, hostname: identifier.value | ||
, type: ch.type | ||
, token: ch.token | ||
, thumbprint: thumbprint | ||
, keyAuthorization: keyAuthorization | ||
, dnsAuthorization: me.RSA.utils.toWebsafeBase64( | ||
require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') | ||
) | ||
}; | ||
var altname = ACME._untame(auth.identifier.value, auth.wildcard); | ||
@@ -346,3 +442,3 @@ /* | ||
, undefined | ||
, { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } | ||
, { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } | ||
, Buffer.from(JSON.stringify({ "status": "deactivated" })) | ||
@@ -353,3 +449,3 @@ ); | ||
method: 'POST' | ||
, url: ch.url | ||
, url: auth.url | ||
, headers: { 'Content-Type': 'application/jose+json' } | ||
@@ -373,3 +469,3 @@ , json: jws | ||
return Promise.reject(new Error( | ||
"[acme-v2] stuck in bad pending/processing state for '" + identifier.value + "'" | ||
"[acme-v2] stuck in bad pending/processing state for '" + altname + "'" | ||
)); | ||
@@ -381,3 +477,3 @@ } | ||
if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } | ||
return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { | ||
return me._request({ method: 'GET', url: auth.url, json: true }).then(function (resp) { | ||
if ('processing' === resp.body.status) { | ||
@@ -406,3 +502,8 @@ if (me.debug) { console.debug('poll: again'); } | ||
} else { | ||
options.removeChallenge(identifier.value, ch.token, function () {}); | ||
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; | ||
} | ||
options.removeChallenge(auth.request.identifier, auth.token, function () {}); | ||
} | ||
@@ -415,9 +516,9 @@ } catch(e) {} | ||
if (!resp.body.status) { | ||
errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + identifier.value + "':"; | ||
errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + altname + "':"; | ||
} | ||
else if ('invalid' === resp.body.status) { | ||
errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + identifier.value + "': '" + resp.body.status + "'"; | ||
errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + altname + "': '" + resp.body.status + "'"; | ||
} | ||
else { | ||
errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + identifier.value + "': '" + resp.body.status + "'"; | ||
errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + altname + "': '" + resp.body.status + "'"; | ||
} | ||
@@ -433,3 +534,3 @@ | ||
, undefined | ||
, { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } | ||
, { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } | ||
, Buffer.from(JSON.stringify({ })) | ||
@@ -440,3 +541,3 @@ ); | ||
method: 'POST' | ||
, url: ch.url | ||
, url: auth.url | ||
, headers: { 'Content-Type': 'application/jose+json' } | ||
@@ -457,3 +558,3 @@ , json: jws | ||
return ACME._setChallenge(me, options, auth).then(respondToChallenge); | ||
return respondToChallenge(); | ||
}; | ||
@@ -477,2 +578,7 @@ ACME._setChallenge = function (me, options, auth) { | ||
}); | ||
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; | ||
} | ||
options.setChallenge(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); | ||
@@ -500,3 +606,3 @@ } | ||
, undefined | ||
, { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } | ||
, { nonce: me._nonce, alg: (me._alg || 'RS256'), url: me._finalize, kid: me._kid } | ||
, Buffer.from(payload) | ||
@@ -582,15 +688,39 @@ ); | ||
if (!options.challengeTypes) { | ||
if (!options.challengeType) { | ||
return Promise.reject(new Error("challenge type must be specified")); | ||
// Lot's of error checking to inform the user of mistakes | ||
if (!(options.challengeTypes||[]).length) { | ||
options.challengeTypes = Object.keys(options.challenges||{}); | ||
} | ||
if (!options.challengeTypes.length) { | ||
options.challengeTypes = [ options.challengeType ].filter(Boolean); | ||
} | ||
if (options.challengeType) { | ||
options.challengeTypes.sort(function (a, b) { | ||
if (a === options.challengeType) { return -1; } | ||
if (b === options.challengeType) { return 1; } | ||
return 0; | ||
}); | ||
if (options.challengeType !== options.challengeTypes[0]) { | ||
return Promise.reject(new Error("options.challengeType is '" + options.challengeType + "'," | ||
+ " which does not exist in the supplied types '" + options.challengeTypes.join(',') + "'")); | ||
} | ||
options.challengeTypes = [ options.challengeType ]; | ||
} | ||
// TODO check that all challengeTypes are represented in challenges | ||
if (!options.challengeTypes.length) { | ||
return Promise.reject(new Error("options.challengeTypes (string array) must be specified" | ||
+ " (and in order of preferential priority).")); | ||
} | ||
if (!(options.domains && options.domains.length)) { | ||
return Promise.reject(new Error("options.domains must be a list of string domain names," | ||
+ " with the first being the subject of the domain (or options.subject must specified).")); | ||
} | ||
// It's just fine if there's no account, we'll go get the key id we need via the public key | ||
if (!me._kid) { | ||
if (options.accountKid) { | ||
me._kid = options.accountKid; | ||
if (options.accountKid || options.account.kid) { | ||
me._kid = options.accountKid || options.account.kid; | ||
} else { | ||
//return Promise.reject(new Error("must include KeyID")); | ||
// This is an idempotent request. It'll return the same account for the same public key. | ||
return ACME._registerAccount(me, options).then(function () { | ||
// start back from the top | ||
return ACME._getCertificate(me, options); | ||
@@ -601,2 +731,3 @@ }); | ||
// Do a little dry-run / self-test | ||
return ACME._testChallenges(me, options).then(function () { | ||
@@ -606,4 +737,11 @@ if (me.debug) { console.debug('[acme-v2] certificates.create'); } | ||
var body = { | ||
identifiers: options.domains.map(function (hostname) { | ||
return { type: "dns" , value: hostname }; | ||
// 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 }; | ||
}) | ||
@@ -615,7 +753,10 @@ //, "notBefore": "2016-01-01T00: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: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } | ||
, Buffer.from(payload) | ||
, { nonce: me._nonce, alg: me._alg, url: me._directoryUrls.newOrder, kid: me._kid } | ||
, Buffer.from(payload, 'utf8') | ||
); | ||
@@ -633,3 +774,4 @@ | ||
var location = resp.toJSON().headers.location; | ||
var auths; | ||
var setAuths; | ||
var auths = []; | ||
if (me.debug) { console.debug(location); } // the account id url | ||
@@ -649,8 +791,6 @@ if (me.debug) { console.debug(resp.toJSON()); } | ||
if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } | ||
setAuths = me._authorizations.slice(0); | ||
//return resp.body; | ||
auths = me._authorizations.slice(0); | ||
function next() { | ||
var authUrl = auths.shift(); | ||
function setNext() { | ||
var authUrl = setAuths.shift(); | ||
if (!authUrl) { return; } | ||
@@ -660,22 +800,11 @@ | ||
// var domain = options.domains[i]; // results.identifier.value | ||
var chType = options.challengeTypes.filter(function (chType) { | ||
return results.challenges.some(function (ch) { | ||
return ch.type === chType; | ||
}); | ||
}).sort(function (aType, bType) { | ||
var a = results.challenges.filter(function (ch) { return ch.type === aType; })[0]; | ||
var b = results.challenges.filter(function (ch) { return ch.type === bType; })[0]; | ||
if ('valid' === a.status) { return 1; } | ||
if ('valid' === b.status) { return -1; } | ||
return 0; | ||
})[0]; | ||
// If it's already valid, we're golden it regardless | ||
if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { | ||
return setNext(); | ||
} | ||
var challenge = results.challenges.filter(function (ch) { | ||
if (chType === ch.type) { | ||
return ch; | ||
} | ||
})[0]; | ||
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( | ||
@@ -686,13 +815,18 @@ "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." | ||
if ("valid" === challenge.status) { | ||
return; | ||
} | ||
return ACME._postChallenge(me, options, results.identifier, challenge); | ||
}).then(function () { | ||
return next(); | ||
var auth = ACME._challengeToAuth(me, options, results, challenge); | ||
auths.push(auth); | ||
return ACME._setChallenge(me, options, auth).then(setNext); | ||
}); | ||
} | ||
return next().then(function () { | ||
function challengeNext() { | ||
var auth = auths.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(challengeNext).then(function () { | ||
if (me.debug) { console.debug("[getCertificate] next.then"); } | ||
@@ -733,2 +867,3 @@ var validatedDomains = body.identifiers.map(function (ident) { | ||
me.RSA = me.RSA || require('rsa-compat').RSA; | ||
//me.Keypairs = me.Keypairs || require('keypairs'); | ||
me.request = me.request || require('@coolaj86/urequest'); | ||
@@ -796,1 +931,5 @@ me._dig = function (query) { | ||
}; | ||
ACME._toWebsafeBase64 = function (b64) { | ||
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); | ||
}; |
{ | ||
"name": "acme-v2", | ||
"version": "1.6.0", | ||
"version": "1.7.4", | ||
"description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", | ||
@@ -5,0 +5,0 @@ "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
77424
1297