|
|
@ -7,7 +7,7 @@ |
|
|
|
/* globals Promise */ |
|
|
|
|
|
|
|
var ACME = exports.ACME = {}; |
|
|
|
var Keypairs = exports.Keypairs || {}; |
|
|
|
//var Keypairs = exports.Keypairs || {};
|
|
|
|
var Enc = exports.Enc || {}; |
|
|
|
var Crypto = exports.Crypto || {}; |
|
|
|
|
|
|
@ -90,7 +90,7 @@ ACME._getNonce = function (me) { |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
if (nonce) { return Promise.resolve(nonce); } |
|
|
|
if (nonce) { return Promise.resolve(nonce.nonce); } |
|
|
|
return me.request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { |
|
|
|
return resp.headers['replay-nonce']; |
|
|
|
}); |
|
|
@ -132,26 +132,7 @@ ACME._registerAccount = function (me, options) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
var jwk = options.accountKeypair.privateKeyJwk; |
|
|
|
var p; |
|
|
|
if (jwk) { |
|
|
|
// nix the browser jwk extras
|
|
|
|
jwk.key_ops = undefined; |
|
|
|
jwk.ext = undefined; |
|
|
|
p = Promise.resolve({ private: jwk, public: Keypairs.neuter({ jwk: jwk }) }); |
|
|
|
} else { |
|
|
|
p = Keypairs.import({ pem: options.accountKeypair.privateKeyPem }); |
|
|
|
} |
|
|
|
return p.then(function (pair) { |
|
|
|
options.accountKeypair.privateKeyJwk = pair.private; |
|
|
|
options.accountKeypair.publicKeyJwk = pair.public; |
|
|
|
if (pair.public.kid) { |
|
|
|
pair = JSON.parse(JSON.stringify(pair)); |
|
|
|
delete pair.public.kid; |
|
|
|
delete pair.private.kid; |
|
|
|
} |
|
|
|
return pair; |
|
|
|
}).then(function (pair) { |
|
|
|
return ACME._importKeypair(me, options.accountKeypair).then(function (pair) { |
|
|
|
var contact; |
|
|
|
if (options.contact) { |
|
|
|
contact = options.contact.slice(0); |
|
|
@ -209,7 +190,7 @@ ACME._registerAccount = function (me, options) { |
|
|
|
status: 'valid' |
|
|
|
} |
|
|
|
*/ |
|
|
|
if (!account) { account = { _emptyResponse: true, key: {} }; } |
|
|
|
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; |
|
|
@ -346,9 +327,10 @@ ACME._testChallenges = function (me, options) { |
|
|
|
, 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 auth; |
|
|
|
return ACME._challengeToAuth(me, options, results, challenge, dryrun).then(function (auth) { |
|
|
|
return ACME._setChallenge(me, options, auth).then(function () { |
|
|
|
return auth; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
})).then(function (auths) { |
|
|
@ -402,17 +384,19 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { |
|
|
|
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 me.Keypairs.thumbprint({ jwk: options.accountKeypair.publicKeyJwk }).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
|
|
|
|
auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; |
|
|
|
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); |
|
|
|
|
|
|
|
return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { |
|
|
|
auth.dnsAuthorization = hash; |
|
|
|
return auth; |
|
|
|
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
|
|
|
|
auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; |
|
|
|
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); |
|
|
|
|
|
|
|
return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { |
|
|
|
auth.dnsAuthorization = hash; |
|
|
|
return auth; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
@ -542,15 +526,20 @@ ACME._postChallenge = function (me, options, auth) { |
|
|
|
return respondToChallenge(); |
|
|
|
}; |
|
|
|
ACME._setChallenge = function (me, options, auth) { |
|
|
|
console.log('challenge auth:', auth); |
|
|
|
console.log('challenges:', options.challenges); |
|
|
|
return new Promise(function (resolve, reject) { |
|
|
|
var challengers = options.challenges || {}; |
|
|
|
var challenger = (challengers[auth.type] && challengers[auth.type].set) || options.setChallenge; |
|
|
|
try { |
|
|
|
if (1 === options.setChallenge.length) { |
|
|
|
options.setChallenge(auth).then(resolve).catch(reject); |
|
|
|
} else if (2 === options.setChallenge.length) { |
|
|
|
options.setChallenge(auth, function (err) { |
|
|
|
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(); } |
|
|
|
}; |
|
|
@ -563,7 +552,7 @@ ACME._setChallenge = function (me, options, auth) { |
|
|
|
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); |
|
|
|
challenger(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); |
|
|
|
} |
|
|
|
} catch(e) { |
|
|
|
reject(e); |
|
|
@ -577,81 +566,82 @@ ACME._setChallenge = function (me, options, auth) { |
|
|
|
}; |
|
|
|
ACME._finalizeOrder = function (me, options, validatedDomains) { |
|
|
|
if (me.debug) { console.debug('finalizeOrder:'); } |
|
|
|
var csr = me.Keypairs.generateCsrWeb64(options.domainKeypair, validatedDomains); |
|
|
|
var body = { csr: csr }; |
|
|
|
var payload = JSON.stringify(body); |
|
|
|
|
|
|
|
function pollCert() { |
|
|
|
if (me.debug) { console.debug('[acme-v2.js] pollCert:'); } |
|
|
|
return ACME._jwsRequest({ |
|
|
|
options: options |
|
|
|
, url: options._finalize |
|
|
|
, protected: { kid: options._kid } |
|
|
|
, payload: Enc.strToBuf(payload) |
|
|
|
}).then(function (resp) { |
|
|
|
if (me.debug) { console.debug('order finalized: resp.body:'); } |
|
|
|
if (me.debug) { console.debug(resp.body); } |
|
|
|
return ACME._generateCsrWeb64(me, options, validatedDomains).then(function (csr) { |
|
|
|
var body = { csr: csr }; |
|
|
|
var payload = JSON.stringify(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; |
|
|
|
function pollCert() { |
|
|
|
if (me.debug) { console.debug('[acme-v2.js] pollCert:'); } |
|
|
|
return ACME._jwsRequest({ |
|
|
|
options: options |
|
|
|
, url: options._finalize |
|
|
|
, protected: { kid: options._kid } |
|
|
|
, payload: Enc.strToBuf(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
|
|
|
|
} |
|
|
|
|
|
|
|
return resp.body; // return order
|
|
|
|
} |
|
|
|
if ('processing' === resp.body.status) { |
|
|
|
return ACME._wait().then(pollCert); |
|
|
|
} |
|
|
|
|
|
|
|
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 (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) |
|
|
|
)); |
|
|
|
} |
|
|
|
|
|
|
|
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 ('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(); |
|
|
|
return pollCert(); |
|
|
|
}); |
|
|
|
}; |
|
|
|
// _kid
|
|
|
|
// registerAccount
|
|
|
@ -686,16 +676,18 @@ ACME._getCertificate = function (me, options) { |
|
|
|
} |
|
|
|
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).")); |
|
|
|
+ " with the first being the subject of the certificate (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 (options.accountKid || options.account && options.account.kid) { |
|
|
|
options._kid = options.accountKid || options.account.kid; |
|
|
|
} else { |
|
|
|
// It's just fine if there's no account, we'll go get the key id we need via the existing key
|
|
|
|
options._kid = options._kid || options.accountKid |
|
|
|
|| (options.account && (options.account.kid |
|
|
|
|| (options.account.key && options.account.key.kid))); |
|
|
|
if (!options._kid) { |
|
|
|
//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 () { |
|
|
|
return ACME._registerAccount(me, options).then(function (account) { |
|
|
|
options._kid = account.key.kid; |
|
|
|
// start back from the top
|
|
|
|
return ACME._getCertificate(me, options); |
|
|
|
}); |
|
|
@ -720,9 +712,6 @@ ACME._getCertificate = function (me, options) { |
|
|
|
}; |
|
|
|
|
|
|
|
var payload = JSON.stringify(body); |
|
|
|
// determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
|
|
|
|
options._kty = (options.accountKeypair.privateKeyJwk && options.accountKeypair.privateKeyJwk.kty || 'RSA'); |
|
|
|
options._alg = ('EC' === options._kty) ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled)
|
|
|
|
if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } |
|
|
|
return ACME._jwsRequest({ |
|
|
|
options: options |
|
|
@ -815,6 +804,13 @@ ACME._getCertificate = function (me, options) { |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
ACME._generateCsrWeb64 = function (me, options, validatedDomains) { |
|
|
|
return ACME._importKeypair(me, options.domainKeypair).then(function (/*pair*/) { |
|
|
|
return me.Keypairs.generateCsr(options.domainKeypair, validatedDomains).then(function (der) { |
|
|
|
return Enc.bufToUrlBase64(der); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
ACME.create = function create(me) { |
|
|
|
if (!me) { me = {}; } |
|
|
@ -942,6 +938,30 @@ ACME._defaultRequest = function (opts) { |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
ACME._importKeypair = function (me, kp) { |
|
|
|
var jwk = kp.privateKeyJwk; |
|
|
|
var p; |
|
|
|
if (jwk) { |
|
|
|
// nix the browser jwk extras
|
|
|
|
jwk.key_ops = undefined; |
|
|
|
jwk.ext = undefined; |
|
|
|
p = Promise.resolve({ private: jwk, public: me.Keypairs.neuter({ jwk: jwk }) }); |
|
|
|
} else { |
|
|
|
p = me.Keypairs.import({ pem: kp.privateKeyPem }); |
|
|
|
} |
|
|
|
return p.then(function (pair) { |
|
|
|
kp.privateKeyJwk = pair.private; |
|
|
|
kp.publicKeyJwk = pair.public; |
|
|
|
if (pair.public.kid) { |
|
|
|
pair = JSON.parse(JSON.stringify(pair)); |
|
|
|
delete pair.public.kid; |
|
|
|
delete pair.private.kid; |
|
|
|
} |
|
|
|
return pair; |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
/* |
|
|
|
TODO |
|
|
|
Per-Order State Params |
|
|
|