AJ ONeal
5 years ago
4 changed files with 1152 additions and 972 deletions
@ -0,0 +1,161 @@ |
|||
'use strict'; |
|||
|
|||
var A = module.exports; |
|||
var U = require('./utils.js'); |
|||
|
|||
var Keypairs = require('@root/keypairs'); |
|||
var Enc = require('@root/encoding/bytes'); |
|||
|
|||
A._getAccountKid = function(me, options) { |
|||
// 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.resolve(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 A._registerAccount(me, options).then(function(account) { |
|||
options._kid = account.key.kid; |
|||
// start back from the top
|
|||
return options._kid; |
|||
}); |
|||
}; |
|||
|
|||
// ACME RFC Section 7.3 Account Creation
|
|||
/* |
|||
{ |
|||
"protected": base64url({ |
|||
"alg": "ES256", |
|||
"jwk": {...}, |
|||
"nonce": "6S8IqOGY7eL2lsGoTZYifg", |
|||
"url": "https://example.com/acme/new-account" |
|||
}), |
|||
"payload": base64url({ |
|||
"termsOfServiceAgreed": true, |
|||
"onlyReturnExisting": false, |
|||
"contact": [ |
|||
"mailto:cert-admin@example.com", |
|||
"mailto:admin@example.com" |
|||
] |
|||
}), |
|||
"signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" |
|||
} |
|||
*/ |
|||
A._registerAccount = function(me, options) { |
|||
//#console.debug('[ACME.js] accounts.create');
|
|||
|
|||
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'; |
|||
throw err; |
|||
} |
|||
|
|||
return U._importKeypair( |
|||
me, |
|||
options.accountKey || options.accountKeypair |
|||
).then(function(pair) { |
|||
var contact; |
|||
if (options.contact) { |
|||
contact = options.contact.slice(0); |
|||
} else if (options.subscriberEmail || options.email) { |
|||
contact = [ |
|||
'mailto:' + (options.subscriberEmail || options.email) |
|||
]; |
|||
} |
|||
var accountRequest = { |
|||
termsOfServiceAgreed: tosUrl === me._tos, |
|||
onlyReturnExisting: false, |
|||
contact: contact |
|||
}; |
|||
var pExt; |
|||
if (options.externalAccount) { |
|||
pExt = 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 |
|||
}, |
|||
payload: Enc.strToBuf(JSON.stringify(pair.public)) |
|||
}).then(function(jws) { |
|||
accountRequest.externalAccountBinding = jws; |
|||
return accountRequest; |
|||
}); |
|||
} else { |
|||
pExt = Promise.resolve(accountRequest); |
|||
} |
|||
return pExt.then(function(accountRequest) { |
|||
var payload = JSON.stringify(accountRequest); |
|||
return U._jwsRequest(me, { |
|||
options: options, |
|||
url: me._directoryUrls.newAccount, |
|||
protected: { kid: false, jwk: pair.public }, |
|||
payload: Enc.strToBuf(payload) |
|||
}).then(function(resp) { |
|||
var account = resp.body; |
|||
|
|||
if (resp.statusCode < 200 || resp.statusCode >= 300) { |
|||
if ('string' !== typeof account) { |
|||
account = JSON.stringify(account); |
|||
} |
|||
throw new Error( |
|||
'account error: ' + |
|||
resp.statusCode + |
|||
' ' + |
|||
account + |
|||
'\n' + |
|||
payload |
|||
); |
|||
} |
|||
|
|||
var location = resp.headers.location; |
|||
// the account id url
|
|||
options._kid = location; |
|||
//#console.debug('[DEBUG] new account location:');
|
|||
//#console.debug(location);
|
|||
//#console.debug(resp);
|
|||
|
|||
/* |
|||
{ |
|||
contact: ["mailto:jon@example.com"], |
|||
orders: "https://some-url", |
|||
status: 'valid' |
|||
} |
|||
*/ |
|||
if (!account) { |
|||
account = { _emptyResponse: true }; |
|||
} |
|||
// https://git.rootprojects.org/root/acme.js/issues/8
|
|||
if (!account.key) { |
|||
account.key = {}; |
|||
} |
|||
account.key.kid = options._kid; |
|||
return account; |
|||
}); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
return Promise.resolve() |
|||
.then(function() { |
|||
//#console.debug('[ACME.js] agreeToTerms');
|
|||
var agreeToTerms = options.agreeToTerms; |
|||
if (true === agreeToTerms) { |
|||
agreeToTerms = function(tos) { |
|||
return tos; |
|||
}; |
|||
} |
|||
return agreeToTerms(me._tos); |
|||
}) |
|||
.then(agree); |
|||
}; |
File diff suppressed because it is too large
@ -0,0 +1,155 @@ |
|||
'use strict'; |
|||
|
|||
var U = module.exports; |
|||
|
|||
var Keypairs = require('@root/keypairs'); |
|||
|
|||
// Handle nonce, signing, and request altogether
|
|||
U._jwsRequest = function(me, bigopts) { |
|||
return U._getNonce(me).then(function(nonce) { |
|||
bigopts.protected.nonce = nonce; |
|||
bigopts.protected.url = bigopts.url; |
|||
// protected.alg: added by Keypairs.signJws
|
|||
if (!bigopts.protected.jwk) { |
|||
// protected.kid must be overwritten due to ACME's interpretation of the spec
|
|||
if (!bigopts.protected.kid) { |
|||
bigopts.protected.kid = bigopts.options._kid; |
|||
} |
|||
} |
|||
|
|||
// this will shasum the thumbprint the 2nd time
|
|||
return Keypairs.signJws({ |
|||
jwk: |
|||
bigopts.options.accountKey || |
|||
bigopts.options.accountKeypair.privateKeyJwk, |
|||
protected: bigopts.protected, |
|||
payload: bigopts.payload |
|||
}) |
|||
.then(function(jws) { |
|||
//#console.debug('[ACME.js] url: ' + bigopts.url + ':');
|
|||
//#console.debug(jws);
|
|||
return U._request(me, { url: bigopts.url, json: jws }); |
|||
}) |
|||
.catch(function(e) { |
|||
if (/badNonce$/.test(e.urn)) { |
|||
// retry badNonces
|
|||
var retryable = bigopts._retries >= 2; |
|||
if (!retryable) { |
|||
bigopts._retries = (bigopts._retries || 0) + 1; |
|||
return U._jwsRequest(me, bigopts); |
|||
} |
|||
} |
|||
throw e; |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
U._getNonce = function(me) { |
|||
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.nonce); |
|||
} |
|||
|
|||
// HEAD-as-HEAD ok
|
|||
return U._request(me, { |
|||
method: 'HEAD', |
|||
url: me._directoryUrls.newNonce |
|||
}).then(function(resp) { |
|||
return resp.headers['replay-nonce']; |
|||
}); |
|||
}; |
|||
|
|||
// Handle some ACME-specific defaults
|
|||
U._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) { |
|||
if (resp.toJSON) { |
|||
resp = resp.toJSON(); |
|||
} |
|||
if (resp.headers['replay-nonce']) { |
|||
U._setNonce(me, resp.headers['replay-nonce']); |
|||
} |
|||
|
|||
var e; |
|||
var err; |
|||
if (resp.body) { |
|||
err = resp.body.error; |
|||
e = new Error(''); |
|||
if (400 === resp.body.status) { |
|||
err = { type: resp.body.type, detail: resp.body.detail }; |
|||
} |
|||
if (err) { |
|||
e.status = resp.body.status; |
|||
e.code = 'E_ACME'; |
|||
if (e.status) { |
|||
e.message = '[' + e.status + '] '; |
|||
} |
|||
e.detail = err.detail; |
|||
e.message += err.detail || JSON.stringify(err); |
|||
e.urn = err.type; |
|||
e.uri = resp.body.url; |
|||
e._rawError = err; |
|||
e._rawBody = resp.body; |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
return resp; |
|||
}); |
|||
}; |
|||
|
|||
U._setNonce = function(me, nonce) { |
|||
me._nonces.unshift({ nonce: nonce, createdAt: Date.now() }); |
|||
}; |
|||
|
|||
U._importKeypair = function(me, kp) { |
|||
var jwk = kp.privateKeyJwk; |
|||
if (kp.kty) { |
|||
jwk = kp; |
|||
kp = {}; |
|||
} |
|||
var pub; |
|||
var p; |
|||
if (jwk) { |
|||
// nix the browser jwk extras
|
|||
jwk.key_ops = undefined; |
|||
jwk.ext = undefined; |
|||
pub = Keypairs.neuter({ jwk: jwk }); |
|||
p = Promise.resolve({ |
|||
private: jwk, |
|||
public: pub |
|||
}); |
|||
} else { |
|||
p = 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; |
|||
}); |
|||
}; |
Loading…
Reference in new issue