API and test cleanup

This commit is contained in:
AJ ONeal 2019-10-24 18:49:42 -06:00
parent 161e9183c6
commit 90c7154a24
6 changed files with 637 additions and 670 deletions

View File

@ -8,23 +8,18 @@ var Enc = require('@root/encoding/bytes');
A._getAccountKid = function(me, options) { 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 // It's just fine if there's no account, we'll go get the key id we need via the existing key
options._kid = var kid =
options._kid || options.kid ||
options.accountKid || (options.account && (options.account.key && options.account.key.kid));
(options.account &&
(options.account.kid ||
(options.account.key && options.account.key.kid)));
if (options._kid) { if (kid) {
return Promise.resolve(options._kid); return Promise.resolve(kid);
} }
//return Promise.reject(new Error("must include KeyID")); //return Promise.reject(new Error("must include KeyID"));
// This is an idempotent request. It'll return the same account for the same public key. // This is an idempotent request. It'll return the same account for the same public key.
return A._registerAccount(me, options).then(function(account) { return A._registerAccount(me, options).then(function(account) {
options._kid = account.key.kid; return account.key.kid;
// start back from the top
return options._kid;
}); });
}; };
@ -54,50 +49,33 @@ A._registerAccount = function(me, options) {
function agree(tosUrl) { function agree(tosUrl) {
var err; var err;
if (me._tos !== tosUrl) { if (me._tos !== tosUrl) {
err = new Error("You must agree to the ToS at '" + me._tos + "'"); err = new Error("must agree to '" + tosUrl + "'");
err.code = 'E_AGREE_TOS'; err.code = 'E_AGREE_TOS';
throw err; throw err;
} }
return true;
}
return U._importKeypair( function getAccount() {
me, return U._importKeypair(options.accountKey).then(function(pair) {
options.accountKey || options.accountKeypair
).then(function(pair) {
var contact; var contact;
if (options.contact) { if (options.contact) {
contact = options.contact.slice(0); contact = options.contact.slice(0);
} else if (options.subscriberEmail || options.email) { } else if (options.subscriberEmail) {
contact = [ contact = ['mailto:' + options.subscriberEmail];
'mailto:' + (options.subscriberEmail || options.email)
];
} }
var accountRequest = { var accountRequest = {
termsOfServiceAgreed: tosUrl === me._tos, termsOfServiceAgreed: true,
onlyReturnExisting: false, onlyReturnExisting: false,
contact: contact contact: contact
}; };
var pExt;
if (options.externalAccount) { var pub = pair.public;
pExt = Keypairs.signJws({ return attachExtAcc(pub, accountRequest).then(function(accReq) {
// TODO is HMAC the standard, or is this arbitrary? var payload = JSON.stringify(accReq);
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, { return U._jwsRequest(me, {
options: options, accountKey: options.accountKey,
url: me._directoryUrls.newAccount, url: me._directoryUrls.newAccount,
protected: { kid: false, jwk: pair.public }, protected: { kid: false, jwk: pair.public },
payload: Enc.strToBuf(payload) payload: Enc.strToBuf(payload)
@ -118,34 +96,42 @@ A._registerAccount = function(me, options) {
); );
} }
var location = resp.headers.location; // the account id url is the "kid"
// the account id url var kid = resp.headers.location;
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) { if (!account) {
account = { _emptyResponse: true }; account = { _emptyResponse: true };
} }
// https://git.rootprojects.org/root/acme.js/issues/8
if (!account.key) { if (!account.key) {
account.key = {}; account.key = {};
} }
account.key.kid = options._kid; account.key.kid = kid;
return account; return account;
}); });
}); });
}); });
} }
// for external accounts (probably useless, but spec'd)
function attachExtAcc(pubkey, accountRequest) {
if (!options.externalAccount) {
return Promise.resolve(accountRequest);
}
return 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(pubkey))
}).then(function(jws) {
accountRequest.externalAccountBinding = jws;
return accountRequest;
});
}
return Promise.resolve() return Promise.resolve()
.then(function() { .then(function() {
//#console.debug('[ACME.js] agreeToTerms'); //#console.debug('[ACME.js] agreeToTerms');
@ -157,5 +143,6 @@ A._registerAccount = function(me, options) {
} }
return agreeToTerms(me._tos); return agreeToTerms(me._tos);
}) })
.then(agree); .then(agree)
.then(getAccount);
}; };

1074
acme.js

File diff suppressed because it is too large Load Diff

81
errors.js Normal file
View File

@ -0,0 +1,81 @@
'use strict';
var E = module.exports;
E.NO_SUITABLE_CHALLENGE = function(domain, challenges, presenters) {
// Bail with a descriptive message if no usable challenge could be selected
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
var enabled = presenters.join(', ') || 'none';
var suitable =
challenges
.map(function(r) {
return r.type;
})
.join(', ') || 'none';
return new Error(
"None of the challenge types that you've enabled ( " +
enabled +
' )' +
" are suitable for validating the domain you've selected (" +
domain +
').' +
' You must enable one of ( ' +
suitable +
' ).'
);
};
E.UNHANDLED_ORDER_STATUS = function(options, domains, resp) {
return 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: '" +
domains.join(', ') +
"'\n" +
JSON.stringify(resp.body, null, 2) +
'\n\n' +
'Please open an issue at https://git.rootprojects.org/root/acme.js'
);
};
E.DOUBLE_READY_ORDER = function(options, domains, resp) {
return 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: '" +
domains.join(', ') +
"'\n" +
JSON.stringify(resp.body, null, 2) +
'\n\n' +
'Please open an issue at https://git.rootprojects.org/root/acme.js'
);
};
E.ORDER_INVALID = function(options, domains, resp) {
return 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: '" +
domains.join(', ') +
"'\n" +
JSON.stringify(resp.body, null, 2)
);
};
E.NO_AUTHORIZATIONS = function(options, resp) {
return new Error(
"[acme-v2.js] authorizations were not fetched for '" +
options.domains.join() +
"':\n" +
JSON.stringify(resp.body)
);
};

View File

@ -3,6 +3,7 @@
var http = module.exports; var http = module.exports;
http.request = function(opts) { http.request = function(opts) {
opts.cors = true;
return window.fetch(opts.url, opts).then(function(resp) { return window.fetch(opts.url, opts).then(function(resp) {
var headers = {}; var headers = {};
var result = { var result = {

10
tests/index.js Normal file
View File

@ -0,0 +1,10 @@
'use strict';
async function main() {
await require('./generate-cert-key.js')();
await require('./format-pem-chains.js')();
await require('./compute-authorization-response.js')();
await require('./issue-certificates.js')();
}
main();

View File

@ -3,6 +3,7 @@
var U = module.exports; var U = module.exports;
var Keypairs = require('@root/keypairs'); var Keypairs = require('@root/keypairs');
var UserAgent = require('./lib/node/client-user-agent.js');
// Handle nonce, signing, and request altogether // Handle nonce, signing, and request altogether
U._jwsRequest = function(me, bigopts) { U._jwsRequest = function(me, bigopts) {
@ -12,16 +13,14 @@ U._jwsRequest = function(me, bigopts) {
// protected.alg: added by Keypairs.signJws // protected.alg: added by Keypairs.signJws
if (!bigopts.protected.jwk) { if (!bigopts.protected.jwk) {
// protected.kid must be overwritten due to ACME's interpretation of the spec // protected.kid must be overwritten due to ACME's interpretation of the spec
if (!bigopts.protected.kid) { if (!('kid' in bigopts.protected)) {
bigopts.protected.kid = bigopts.options._kid; bigopts.protected.kid = bigopts.kid;
} }
} }
// this will shasum the thumbprint the 2nd time // this will shasum the thumbprint the 2nd time
return Keypairs.signJws({ return Keypairs.signJws({
jwk: jwk: bigopts.accountKey,
bigopts.options.accountKey ||
bigopts.options.accountKeypair.privateKeyJwk,
protected: bigopts.protected, protected: bigopts.protected,
payload: bigopts.payload payload: bigopts.payload
}) })
@ -72,16 +71,36 @@ U._getNonce = function(me) {
// Handle some ACME-specific defaults // Handle some ACME-specific defaults
U._request = function(me, opts) { U._request = function(me, opts) {
// no-op on browser
var ua = UserAgent.get(me, opts);
// Note: the required User-Agent string will be set in node, but not browsers
if (!opts.headers) { if (!opts.headers) {
opts.headers = {}; opts.headers = {};
} }
if (ua && !opts.headers['User-Agent']) {
opts.headers['User-Agent'] = ua;
}
if (opts.json && true !== opts.json) { if (opts.json && true !== opts.json) {
opts.headers['Content-Type'] = 'application/jose+json'; opts.headers['Content-Type'] = 'application/jose+json';
opts.body = JSON.stringify(opts.json); opts.body = JSON.stringify(opts.json);
if (!opts.method) { }
if (!opts.method) {
opts.method = 'GET';
if (opts.body) {
opts.method = 'POST'; opts.method = 'POST';
} }
} }
if (opts.json) {
opts.headers.Accept = 'application/json';
if (true !== opts.json) {
opts.body = JSON.stringify(opts.json);
}
}
//console.log('\n[debug] REQUEST');
//console.log(opts);
return me.request(opts).then(function(resp) { return me.request(opts).then(function(resp) {
if (resp.toJSON) { if (resp.toJSON) {
resp = resp.toJSON(); resp = resp.toJSON();
@ -89,6 +108,9 @@ U._request = function(me, opts) {
if (resp.headers['replay-nonce']) { if (resp.headers['replay-nonce']) {
U._setNonce(me, resp.headers['replay-nonce']); U._setNonce(me, resp.headers['replay-nonce']);
} }
//console.log('[debug] RESPONSE:');
//console.log(resp.headers);
//console.log(resp.body);
var e; var e;
var err; var err;
@ -122,7 +144,7 @@ U._setNonce = function(me, nonce) {
me._nonces.unshift({ nonce: nonce, createdAt: Date.now() }); me._nonces.unshift({ nonce: nonce, createdAt: Date.now() });
}; };
U._importKeypair = function(me, key) { U._importKeypair = function(key) {
var p; var p;
var pub; var pub;