Browse Source

backport all the things

v3
AJ ONeal 5 years ago
parent
commit
f05e9db38e
  1. 161
      account.js
  2. 1787
      acme.js
  3. 21
      tests/index.js
  4. 155
      utils.js

161
account.js

@ -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);
};

1787
acme.js

File diff suppressed because it is too large

21
tests/index.js

@ -8,9 +8,6 @@ var PEM = require('@root/pem');
var punycode = require('punycode');
var ACME = require('../acme.js');
var Keypairs = require('@root/keypairs');
var acme = ACME.create({
// debug: true
});
// TODO exec npm install --save-dev CHALLENGE_MODULE
if (!process.env.CHALLENGE_OPTIONS) {
@ -33,6 +30,22 @@ var pluginPrefix = 'acme-' + config.challengeType + '-';
var pluginName = config.challengeModule;
var plugin;
var acme = ACME.create({
// debug: true
maintainerEmail: config.email,
notify: function(ev, params) {
console.info(
ev,
params.subject || params.altname || params.domain,
params.status
);
if ('error' === ev) {
console.error(params);
console.error(params.error);
}
}
});
function badPlugin(err) {
if ('MODULE_NOT_FOUND' !== err.code) {
console.error(err);
@ -88,7 +101,7 @@ async function happyPath(accKty, srvKty, rnd) {
}
var accountKeypair = await Keypairs.generate({ kty: accKty });
var accountKey = accountKeypair.private;
var accountKey = accountKeypair.private;
if (config.debug) {
console.info('Account Key Created');
console.info(JSON.stringify(accountKey, null, 2));

155
utils.js

@ -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…
Cancel
Save