|
|
@ -30,7 +30,7 @@ ACME.challengePrefixes = { |
|
|
|
ACME.challengeTests = { |
|
|
|
'http-01': function (me, auth) { |
|
|
|
var url = 'http://' + auth.hostname + ACME.challengePrefixes['http-01'] + '/' + auth.token; |
|
|
|
return me._request({ method: 'GET', url: url }).then(function (resp) { |
|
|
|
return me.request({ method: 'GET', url: url }).then(function (resp) { |
|
|
|
var err; |
|
|
|
|
|
|
|
// TODO limit the number of bytes that are allowed to be downloaded
|
|
|
@ -76,16 +76,28 @@ ACME.challengeTests = { |
|
|
|
|
|
|
|
ACME._directory = function (me) { |
|
|
|
// GET-as-GET ok
|
|
|
|
return me._request({ method: 'GET', url: me.directoryUrl, json: true }); |
|
|
|
return me.request({ method: 'GET', url: me.directoryUrl, json: true }); |
|
|
|
}; |
|
|
|
ACME._getNonce = function (me) { |
|
|
|
// GET-as-GET, HEAD-as-HEAD ok
|
|
|
|
if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } |
|
|
|
return me._request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { |
|
|
|
me._nonce = resp.toJSON().headers['replay-nonce']; |
|
|
|
return me._nonce; |
|
|
|
var nonce; |
|
|
|
while (true) { |
|
|
|
nonce = me._nonces.shift(); |
|
|
|
if (!nonce) { break; } |
|
|
|
if (Date.now() - nonce.createdAt > (15 * 60 * 1000)) { |
|
|
|
nonce = null; |
|
|
|
} else { |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
if (nonce) { return Promise.resolve(nonce); } |
|
|
|
return me.request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { |
|
|
|
return resp.headers['replay-nonce']; |
|
|
|
}); |
|
|
|
}; |
|
|
|
ACME._setNonce = function (me, nonce) { |
|
|
|
me._nonces.unshift({ nonce: nonce, createdAt: Date.now() }); |
|
|
|
}; |
|
|
|
// ACME RFC Section 7.3 Account Creation
|
|
|
|
/* |
|
|
|
{ |
|
|
@ -109,91 +121,86 @@ ACME._getNonce = function (me) { |
|
|
|
ACME._registerAccount = function (me, options) { |
|
|
|
if (me.debug) { console.debug('[acme-v2] accounts.create'); } |
|
|
|
|
|
|
|
return ACME._getNonce(me).then(function () { |
|
|
|
return new Promise(function (resolve, reject) { |
|
|
|
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"; |
|
|
|
reject(err); |
|
|
|
return; |
|
|
|
} |
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
var jwk = options.accountKeypair.privateKeyJwk; |
|
|
|
var p; |
|
|
|
if (jwk) { |
|
|
|
p = Promise.resolve({ private: jwk, public: Keypairs.neuter(jwk) }); |
|
|
|
} else { |
|
|
|
p = Keypairs.import({ pem: options.accountKeypair.privateKeyPem }); |
|
|
|
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 p.then(function (pair) { |
|
|
|
if (pair.public.kid) { |
|
|
|
pair = JSON.parse(JSON.stringify(pair)); |
|
|
|
delete pair.public.kid; |
|
|
|
delete pair.private.kid; |
|
|
|
} |
|
|
|
return pair; |
|
|
|
}).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 |
|
|
|
}; |
|
|
|
if (options.externalAccount) { |
|
|
|
body.externalAccountBinding = me.RSA.signJws( |
|
|
|
// TODO is HMAC the standard, or is this arbitrary?
|
|
|
|
options.externalAccount.secret |
|
|
|
, undefined |
|
|
|
, { alg: options.externalAccount.alg || "HS256" |
|
|
|
, kid: options.externalAccount.id |
|
|
|
, url: me._directoryUrls.newAccount |
|
|
|
} |
|
|
|
, Buffer.from(JSON.stringify(pair.public)) |
|
|
|
); |
|
|
|
} |
|
|
|
var payload = JSON.stringify(body); |
|
|
|
var jws = Keypairs.signJws( |
|
|
|
options.accountKeypair |
|
|
|
, undefined |
|
|
|
, { nonce: me._nonce |
|
|
|
, alg: (me._alg || 'RS256') |
|
|
|
return pair; |
|
|
|
}).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 |
|
|
|
, jwk: pair.public |
|
|
|
} |
|
|
|
, Buffer.from(payload) |
|
|
|
); |
|
|
|
|
|
|
|
delete jws.header; |
|
|
|
if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); } |
|
|
|
if (me.debug) { console.debug(jws); } |
|
|
|
me._nonce = null; |
|
|
|
return me._request({ |
|
|
|
method: 'POST' |
|
|
|
, payload: Enc.strToBuf(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 |
|
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
|
, json: jws |
|
|
|
, 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(body)); |
|
|
|
throw new Error('account error: ' + JSON.stringify(resp.body)); |
|
|
|
} |
|
|
|
|
|
|
|
me._nonce = resp.toJSON().headers['replay-nonce']; |
|
|
|
var location = resp.toJSON().headers.location; |
|
|
|
var location = resp.headers.location; |
|
|
|
// the account id url
|
|
|
|
me._kid = location; |
|
|
|
options._kid = location; |
|
|
|
if (me.debug) { console.debug('[DEBUG] new account location:'); } |
|
|
|
if (me.debug) { console.debug(location); } |
|
|
|
if (me.debug) { console.debug(resp.toJSON()); } |
|
|
|
if (me.debug) { console.debug(resp); } |
|
|
|
|
|
|
|
/* |
|
|
|
{ |
|
|
@ -205,29 +212,29 @@ ACME._registerAccount = function (me, options) { |
|
|
|
if (!account) { account = { _emptyResponse: true, key: {} }; } |
|
|
|
// https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
|
|
|
|
if (!account.key) { account.key = {}; } |
|
|
|
account.key.kid = me._kid; |
|
|
|
account.key.kid = options._kid; |
|
|
|
return account; |
|
|
|
}).then(resolve, reject); |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } |
|
|
|
if (1 === options.agreeToTerms.length) { |
|
|
|
// newer promise API
|
|
|
|
return 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>; }')); |
|
|
|
} |
|
|
|
}); |
|
|
|
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>; }')); |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
/* |
|
|
@ -250,10 +257,16 @@ ACME._registerAccount = function (me, options) { |
|
|
|
"signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" |
|
|
|
} |
|
|
|
*/ |
|
|
|
ACME._getChallenges = function (me, options, auth) { |
|
|
|
ACME._getChallenges = function (me, options, authUrl) { |
|
|
|
if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); } |
|
|
|
// TODO POST-as-GET
|
|
|
|
return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { |
|
|
|
|
|
|
|
return ACME._jwsRequest(me, { |
|
|
|
options: options |
|
|
|
, protected: {} |
|
|
|
, payload: '' |
|
|
|
, url: authUrl |
|
|
|
}).then(function (resp) { |
|
|
|
return resp.body; |
|
|
|
}); |
|
|
|
}; |
|
|
@ -389,16 +402,18 @@ 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); |
|
|
|
auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); |
|
|
|
// 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 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; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
@ -436,25 +451,13 @@ ACME._postChallenge = function (me, options, auth) { |
|
|
|
} |
|
|
|
*/ |
|
|
|
function deactivate() { |
|
|
|
var jws = me.RSA.signJws( |
|
|
|
options.accountKeypair |
|
|
|
, undefined |
|
|
|
, { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } |
|
|
|
, Buffer.from(JSON.stringify({ "status": "deactivated" })) |
|
|
|
); |
|
|
|
me._nonce = null; |
|
|
|
return me._request({ |
|
|
|
method: 'POST' |
|
|
|
if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } |
|
|
|
return ACME._jwsRequest({ |
|
|
|
options: options |
|
|
|
, url: auth.url |
|
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
|
, json: jws |
|
|
|
, protected: { kid: options._kid } |
|
|
|
, payload: Enc.strToBuf(JSON.stringify({ "status": "deactivated" })) |
|
|
|
}).then(function (resp) { |
|
|
|
if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } |
|
|
|
if (me.debug) { console.debug(resp.headers); } |
|
|
|
if (me.debug) { console.debug(resp.body); } |
|
|
|
if (me.debug) { console.debug(); } |
|
|
|
|
|
|
|
me._nonce = resp.toJSON().headers['replay-nonce']; |
|
|
|
if (me.debug) { console.debug('deactivate challenge: resp.body:'); } |
|
|
|
if (me.debug) { console.debug(resp.body); } |
|
|
|
return ACME._wait(DEAUTH_INTERVAL); |
|
|
@ -472,7 +475,7 @@ ACME._postChallenge = function (me, options, auth) { |
|
|
|
|
|
|
|
if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } |
|
|
|
// TODO POST-as-GET
|
|
|
|
return me._request({ method: 'GET', url: auth.url, json: true }).then(function (resp) { |
|
|
|
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'); } |
|
|
|
return ACME._wait(RETRY_INTERVAL).then(pollStatus); |
|
|
@ -523,25 +526,13 @@ ACME._postChallenge = function (me, options, auth) { |
|
|
|
} |
|
|
|
|
|
|
|
function respondToChallenge() { |
|
|
|
var jws = me.RSA.signJws( |
|
|
|
options.accountKeypair |
|
|
|
, undefined |
|
|
|
, { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } |
|
|
|
, Buffer.from(JSON.stringify({ })) |
|
|
|
); |
|
|
|
me._nonce = null; |
|
|
|
return me._request({ |
|
|
|
method: 'POST' |
|
|
|
if (me.debug) { console.debug('[acme-v2.js] responding to accept challenge:'); } |
|
|
|
return ACME._jwsRequest({ |
|
|
|
options: options |
|
|
|
, url: auth.url |
|
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
|
, json: jws |
|
|
|
, protected: { kid: options._kid } |
|
|
|
, payload: Enc.strToBuf(JSON.stringify({})) |
|
|
|
}).then(function (resp) { |
|
|
|
if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); } |
|
|
|
if (me.debug) { console.debug(resp.headers); } |
|
|
|
if (me.debug) { console.debug(resp.body); } |
|
|
|
if (me.debug) { console.debug(); } |
|
|
|
|
|
|
|
me._nonce = resp.toJSON().headers['replay-nonce']; |
|
|
|
if (me.debug) { console.debug('respond to challenge: resp.body:'); } |
|
|
|
if (me.debug) { console.debug(resp.body); } |
|
|
|
return ACME._wait(RETRY_INTERVAL).then(pollStatus); |
|
|
@ -586,36 +577,26 @@ ACME._setChallenge = function (me, options, auth) { |
|
|
|
}; |
|
|
|
ACME._finalizeOrder = function (me, options, validatedDomains) { |
|
|
|
if (me.debug) { console.debug('finalizeOrder:'); } |
|
|
|
var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); |
|
|
|
var csr = me.Keypairs.generateCsrWeb64(options.domainKeypair, validatedDomains); |
|
|
|
var body = { csr: csr }; |
|
|
|
var payload = JSON.stringify(body); |
|
|
|
|
|
|
|
function pollCert() { |
|
|
|
var jws = me.RSA.signJws( |
|
|
|
options.accountKeypair |
|
|
|
, undefined |
|
|
|
, { nonce: me._nonce, alg: (me._alg || 'RS256'), url: me._finalize, kid: me._kid } |
|
|
|
, Buffer.from(payload) |
|
|
|
); |
|
|
|
|
|
|
|
if (me.debug) { console.debug('finalize:', me._finalize); } |
|
|
|
me._nonce = null; |
|
|
|
return me._request({ |
|
|
|
method: 'POST' |
|
|
|
, url: me._finalize |
|
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
|
, json: jws |
|
|
|
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) { |
|
|
|
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
|
|
|
|
// Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
|
|
|
|
me._nonce = resp.toJSON().headers['replay-nonce']; |
|
|
|
|
|
|
|
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) { |
|
|
|
me._expires = resp.body.expires; |
|
|
|
me._certificate = resp.body.certificate; |
|
|
|
options._expires = resp.body.expires; |
|
|
|
options._certificate = resp.body.certificate; |
|
|
|
|
|
|
|
return resp.body; // return order
|
|
|
|
} |
|
|
@ -672,6 +653,11 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { |
|
|
|
|
|
|
|
return pollCert(); |
|
|
|
}; |
|
|
|
// _kid
|
|
|
|
// registerAccount
|
|
|
|
// postChallenge
|
|
|
|
// finalizeOrder
|
|
|
|
// getCertificate
|
|
|
|
ACME._getCertificate = function (me, options) { |
|
|
|
if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } |
|
|
|
|
|
|
@ -704,139 +690,126 @@ ACME._getCertificate = function (me, options) { |
|
|
|
} |
|
|
|
|
|
|
|
// 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 || options.account && 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); |
|
|
|
}); |
|
|
|
} |
|
|
|
if (options.accountKid || options.account && options.account.kid) { |
|
|
|
options._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); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 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 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"
|
|
|
|
}; |
|
|
|
|
|
|
|
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); |
|
|
|
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 |
|
|
|
, url: me._directoryUrls.newOrder |
|
|
|
, protected: { kid: options._kid } |
|
|
|
, payload: Enc.strToBuf(payload) |
|
|
|
}).then(function (resp) { |
|
|
|
var location = resp.headers.location; |
|
|
|
var setAuths; |
|
|
|
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;
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
function setNext() { |
|
|
|
var authUrl = setAuths.shift(); |
|
|
|
if (!authUrl) { return; } |
|
|
|
function setNext() { |
|
|
|
var authUrl = setAuths.shift(); |
|
|
|
if (!authUrl) { return; } |
|
|
|
|
|
|
|
return ACME._getChallenges(me, options, authUrl).then(function (results) { |
|
|
|
// var domain = options.domains[i]; // results.identifier.value
|
|
|
|
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(); |
|
|
|
} |
|
|
|
// 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() + "'." |
|
|
|
)); |
|
|
|
} |
|
|
|
|
|
|
|
return ACME._challengeToAuth(me, options, results, challenge, false).then(function (auth) { |
|
|
|
auths.push(auth); |
|
|
|
return ACME._setChallenge(me, options, auth).then(setNext); |
|
|
|
}); |
|
|
|
return ACME._challengeToAuth(me, options, results, challenge, false).then(function (auth) { |
|
|
|
auths.push(auth); |
|
|
|
return ACME._setChallenge(me, options, auth).then(setNext); |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function challengeNext() { |
|
|
|
var auth = auths.shift(); |
|
|
|
if (!auth) { return; } |
|
|
|
return ACME._postChallenge(me, options, auth).then(challengeNext); |
|
|
|
} |
|
|
|
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"); } |
|
|
|
var validatedDomains = body.identifiers.map(function (ident) { |
|
|
|
return ident.value; |
|
|
|
}); |
|
|
|
// 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'); } |
|
|
|
// TODO POST-as-GET
|
|
|
|
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; |
|
|
|
}); |
|
|
|
return ACME._finalizeOrder(me, options, validatedDomains); |
|
|
|
}).then(function (order) { |
|
|
|
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; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
@ -847,9 +820,10 @@ ACME.create = function create(me) { |
|
|
|
if (!me) { me = {}; } |
|
|
|
// me.debug = true;
|
|
|
|
me.challengePrefixes = ACME.challengePrefixes; |
|
|
|
me.RSA = me.RSA || require('rsa-compat').RSA; |
|
|
|
me.Keypairs = me.Keypairs || me.RSA || require('rsa-compat').RSA; |
|
|
|
me._nonces = []; |
|
|
|
//me.Keypairs = me.Keypairs || require('keypairs');
|
|
|
|
me.request = me.request || require('@coolaj86/urequest'); |
|
|
|
//me.request = me.request || require('@root/request');
|
|
|
|
if (!me.dig) { |
|
|
|
me.dig = function (query) { |
|
|
|
// TODO use digd.js
|
|
|
@ -860,37 +834,33 @@ ACME.create = function create(me) { |
|
|
|
|
|
|
|
resolve({ |
|
|
|
answer: records.map(function (rr) { |
|
|
|
return { |
|
|
|
data: rr |
|
|
|
}; |
|
|
|
return { data: rr }; |
|
|
|
}) |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
} |
|
|
|
me.promisify = me.promisify || require('util').promisify /*node v8+*/ || require('bluebird').promisify /*node v6*/; |
|
|
|
|
|
|
|
|
|
|
|
if ('function' !== typeof me._request) { |
|
|
|
// MUST have a User-Agent string (see node.js version)
|
|
|
|
me._request = function (opts) { |
|
|
|
return window.fetch(opts.url, opts).then(function (resp) { |
|
|
|
return resp.json().then(function (json) { |
|
|
|
var headers = {}; |
|
|
|
Array.from(resp.headers.entries()).forEach(function (h) { headers[h[0]] = h[1]; }); |
|
|
|
return { headers: headers , body: json }; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
if ('function' !== typeof me.request) { |
|
|
|
me.request = ACME._defaultRequest; |
|
|
|
} |
|
|
|
|
|
|
|
me.init = function (_directoryUrl) { |
|
|
|
me.directoryUrl = me.directoryUrl || _directoryUrl; |
|
|
|
me.init = function (opts) { |
|
|
|
function fin(dir) { |
|
|
|
me._directoryUrls = dir; |
|
|
|
me._tos = dir.meta.termsOfService; |
|
|
|
return dir; |
|
|
|
} |
|
|
|
if (opts && opts.meta && opts.termsOfService) { |
|
|
|
return Promise.resolve(fin(opts)); |
|
|
|
} |
|
|
|
if (!me.directoryUrl) { me.directoryUrl = opts; } |
|
|
|
if ('string' !== typeof me.directoryUrl) { |
|
|
|
throw new Error("you must supply either the ACME directory url as a string or an object of the ACME urls"); |
|
|
|
} |
|
|
|
return ACME._directory(me).then(function (resp) { |
|
|
|
me._directoryUrls = resp.body; |
|
|
|
me._tos = me._directoryUrls.meta.termsOfService; |
|
|
|
return me._directoryUrls; |
|
|
|
return fin(resp.body); |
|
|
|
}); |
|
|
|
}; |
|
|
|
me.accounts = { |
|
|
@ -906,6 +876,84 @@ ACME.create = function create(me) { |
|
|
|
return me; |
|
|
|
}; |
|
|
|
|
|
|
|
// Handle nonce, signing, and request altogether
|
|
|
|
ACME._jwsRequest = function (me, bigopts) { |
|
|
|
return ACME._getNonce(me).then(function (nonce) { |
|
|
|
bigopts.protected.nonce = nonce; |
|
|
|
bigopts.protected.url = bigopts.url; |
|
|
|
// protected.alg: added by Keypairs.signJws
|
|
|
|
return me.Keypairs.signJws( |
|
|
|
{ jwk: 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); } |
|
|
|
return ACME._request(me, { url: bigopts.url, json: jws }); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
// Handle some ACME-specific defaults
|
|
|
|
ACME._request = function (me, opts) { |
|
|
|
if (!opts.headers) { opts.headers = {}; } |
|
|
|
if (opts.json && true !== opts.json) { |
|
|
|
opts.headers['Content-Type'] = 'application/jose+json'; |
|
|
|
opts.body = JSON.stringify(opts.json); |
|
|
|
if (!opts.method) { opts.method = 'POST'; } |
|
|
|
} |
|
|
|
return me.request(opts).then(function (resp) { |
|
|
|
resp = resp.toJSON(); |
|
|
|
if (resp.headers['replay-nonce']) { |
|
|
|
ACME._setNonce(me, resp.headers['replay-nonce']); |
|
|
|
} |
|
|
|
return resp; |
|
|
|
}); |
|
|
|
}; |
|
|
|
// A very generic, swappable request lib
|
|
|
|
ACME._defaultRequest = function (opts) { |
|
|
|
// Note: normally we'd have to supply a User-Agent string, but not here in a browser
|
|
|
|
if (!opts.headers) { opts.headers = {}; } |
|
|
|
if (opts.json) { |
|
|
|
opts.headers.Accept = 'application/json'; |
|
|
|
if (true !== opts.json) { opts.body = JSON.stringify(opts.json); } |
|
|
|
} |
|
|
|
if (!opts.method) { |
|
|
|
opts.method = 'GET'; |
|
|
|
if (opts.body) { opts.method = 'POST'; } |
|
|
|
} |
|
|
|
opts.cors = true; |
|
|
|
return window.fetch(opts.url, opts).then(function (resp) { |
|
|
|
var headers = {}; |
|
|
|
var result = { statusCode: resp.status, headers: headers, toJSON: function () { return this; } }; |
|
|
|
Array.from(resp.headers.entries()).forEach(function (h) { headers[h[0]] = h[1]; }); |
|
|
|
if (!headers['content-type']) { |
|
|
|
return result; |
|
|
|
} |
|
|
|
if (/json/.test(headers['content-type'])) { |
|
|
|
return resp.json().then(function (json) { |
|
|
|
result.body = json; |
|
|
|
return result; |
|
|
|
}); |
|
|
|
} |
|
|
|
return resp.text().then(function (txt) { |
|
|
|
result.body = txt; |
|
|
|
return result; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
/* |
|
|
|
TODO |
|
|
|
Per-Order State Params |
|
|
|
_kty |
|
|
|
_alg |
|
|
|
_finalize |
|
|
|
_expires |
|
|
|
_certificate |
|
|
|
_order |
|
|
|
_authorizations |
|
|
|
*/ |
|
|
|
|
|
|
|
ACME._toWebsafeBase64 = function (b64) { |
|
|
|
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); |
|
|
|
}; |
|
|
|