forked from root/acme.js
backport all the things
This commit is contained in:
parent
7e6a66c1d8
commit
f05e9db38e
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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