WIP gets a cert... nice!

This commit is contained in:
AJ ONeal 2019-10-05 05:21:07 -06:00
parent e75c503356
commit 24c3633d75
5 changed files with 503 additions and 285 deletions

View File

@ -165,7 +165,7 @@ ACME._registerAccount = function(me, options) {
} else if (options.email) { } else if (options.email) {
contact = ['mailto:' + options.email]; contact = ['mailto:' + options.email];
} }
var body = { var accountRequest = {
termsOfServiceAgreed: tosUrl === me._tos, termsOfServiceAgreed: tosUrl === me._tos,
onlyReturnExisting: false, onlyReturnExisting: false,
contact: contact contact: contact
@ -182,14 +182,14 @@ ACME._registerAccount = function(me, options) {
}, },
payload: Enc.strToBuf(JSON.stringify(pair.public)) payload: Enc.strToBuf(JSON.stringify(pair.public))
}).then(function(jws) { }).then(function(jws) {
body.externalAccountBinding = jws; accountRequest.externalAccountBinding = jws;
return body; return accountRequest;
}); });
} else { } else {
pExt = Promise.resolve(body); pExt = Promise.resolve(accountRequest);
} }
return pExt.then(function(body) { return pExt.then(function(accountRequest) {
var payload = JSON.stringify(body); var payload = JSON.stringify(accountRequest);
return ACME._jwsRequest(me, { return ACME._jwsRequest(me, {
options: options, options: options,
url: me._directoryUrls.newAccount, url: me._directoryUrls.newAccount,
@ -199,10 +199,20 @@ ACME._registerAccount = function(me, options) {
.then(function(resp) { .then(function(resp) {
var account = resp.body; var account = resp.body;
if (2 !== Math.floor(resp.statusCode / 100)) { if (
resp.statusCode < 200 ||
resp.statusCode >= 300
) {
if ('string' !== typeof account) {
account = JSON.stringify(account);
}
throw new Error( throw new Error(
'account error: ' + 'account error: ' +
JSON.stringify(resp.body) resp.statusCode +
' ' +
account +
'\n' +
JSON.stringify(accountRequest)
); );
} }
@ -344,7 +354,24 @@ ACME._testChallengeOptions = function() {
]; ];
}; };
ACME._testChallenges = function(me, options) { ACME._testChallenges = function(me, options) {
console.log('[debug] testChallenges');
var CHECK_DELAY = 0; var CHECK_DELAY = 0;
// memoized so that it doesn't run until it's first called
var getThumbnail = function() {
var thumbPromise = ACME._importKeypair(me, options.accountKeypair).then(
function(pair) {
return me.Keypairs.thumbprint({
jwk: pair.public
});
}
);
getThumbnail = function() {
return thumbPromise;
};
return thumbPromise;
};
return Promise.all( return Promise.all(
options.domains.map(function(identifierValue) { options.domains.map(function(identifierValue) {
// TODO we really only need one to pass, not all to pass // TODO we really only need one to pass, not all to pass
@ -389,10 +416,11 @@ ACME._testChallenges = function(me, options) {
if ('dns-01' === challenge.type) { if ('dns-01' === challenge.type) {
// Give the nameservers a moment to propagate // Give the nameservers a moment to propagate
CHECK_DELAY = 1.5 * 1000; // TODO get this value from the plugin
CHECK_DELAY = 7 * 1000;
} }
return Promise.resolve().then(function() { return getThumbnail().then(function(accountKeyThumb) {
var results = { var results = {
identifier: { identifier: {
type: 'dns', type: 'dns',
@ -409,6 +437,7 @@ ACME._testChallenges = function(me, options) {
return ACME._challengeToAuth( return ACME._challengeToAuth(
me, me,
options, options,
accountKeyThumb,
results, results,
challenge, challenge,
dryrun dryrun
@ -460,7 +489,14 @@ ACME._chooseChallenge = function(options, results) {
return challenge; return challenge;
}; };
ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { ACME._challengeToAuth = function(
me,
options,
accountKeyThumb,
request,
challenge,
dryrun
) {
// we don't poison the dns cache with our dummy request // we don't poison the dns cache with our dummy request
var dnsPrefix = ACME.challengePrefixes['dns-01']; var dnsPrefix = ACME.challengePrefixes['dns-01'];
if (dryrun) { if (dryrun) {
@ -486,15 +522,15 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
auth[key] = challenge[key]; auth[key] = challenge[key];
}); });
var zone = pluckZone(options.zonenames || [], auth.identifier.value);
// batteries-included helpers // batteries-included helpers
auth.hostname = auth.identifier.value; 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 // 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.altname = ACME._untame(auth.identifier.value, auth.wildcard);
return ACME._importKeypair(me, options.accountKeypair).then(function(pair) { // we must accept JWKs that we didn't generate and we can't guarantee
return me.Keypairs.thumbprint({ jwk: pair.public }).then(function( // that they properly set kid to thumbnail (especially since ACME doesn't do this)
thumb // so we have to regenerate it every time we need it, which is quite often
) { auth.thumbprint = accountKeyThumb;
auth.thumbprint = thumb;
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; auth.keyAuthorization = challenge.token + '.' + auth.thumbprint;
// conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
@ -507,6 +543,9 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
auth.token; auth.token;
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', '');
// Always calculate dnsAuthorization because we
// may need to present to the user for confirmation / instruction
// _as part of_ the decision making process
return sha2 return sha2
.sum(256, auth.keyAuthorization) .sum(256, auth.keyAuthorization)
.then(function(hash) { .then(function(hash) {
@ -514,10 +553,27 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
}) })
.then(function(hash64) { .then(function(hash64) {
auth.dnsAuthorization = hash64; auth.dnsAuthorization = hash64;
if (zone) {
auth.dnsZone = zone;
auth.dnsPrefix = auth.dnsHost
.replace(newZoneRegExp(zone), '')
.replace(/\.$/, '');
}
// For backwards compat with the v2.7 plugins
auth.challenge = auth;
// TODO can we use just { challenge: auth }?
auth.request = function() {
// TODO see https://git.rootprojects.org/root/acme.js/issues/###
console.warn(
"[warn] deprecated use of request on '" +
auth.type +
"' challenge object. Receive from challenger.init() instead."
);
me.request.apply(null, arguments);
};
return auth; return auth;
}); });
});
});
}; };
ACME._untame = function(name, wild) { ACME._untame = function(name, wild) {
@ -597,7 +653,7 @@ ACME._postChallenge = function(me, options, auth) {
.then(function(resp) { .then(function(resp) {
if ('processing' === resp.body.status) { if ('processing' === resp.body.status) {
if (me.debug) { if (me.debug) {
console.debug('poll: again'); console.debug('poll: again', auth.url);
} }
return ACME._wait(RETRY_INTERVAL).then(pollStatus); return ACME._wait(RETRY_INTERVAL).then(pollStatus);
} }
@ -610,14 +666,14 @@ ACME._postChallenge = function(me, options, auth) {
.then(respondToChallenge); .then(respondToChallenge);
} }
if (me.debug) { if (me.debug) {
console.debug('poll: again'); console.debug('poll: again', auth.url);
} }
return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); return ACME._wait(RETRY_INTERVAL).then(respondToChallenge);
} }
if ('valid' === resp.body.status) { if ('valid' === resp.body.status) {
if (me.debug) { if (me.debug) {
console.debug('poll: valid'); console.debug('VALID !!!!!!!!!!!!!!!! poll: valid');
} }
try { try {
@ -637,7 +693,8 @@ ACME._postChallenge = function(me, options, auth) {
"[acme-v2] (E_STATE_INVALID) challenge state for '" + "[acme-v2] (E_STATE_INVALID) challenge state for '" +
altname + altname +
"': '" + "': '" +
resp.body.status + //resp.body.status +
JSON.stringify(resp.body) +
"'"; "'";
} else { } else {
errmsg = errmsg =
@ -675,17 +732,20 @@ ACME._postChallenge = function(me, options, auth) {
return respondToChallenge(); return respondToChallenge();
}; };
ACME._setChallenge = function(me, options, auth) { ACME._setChallenge = function(me, options, auth) {
return new Promise(function(resolve, reject) { return Promise.resolve().then(function() {
var challengers = options.challenges || {}; var challengers = options.challenges || {};
var challenger = var challenger = challengers[auth.type] && challengers[auth.type].set;
(challengers[auth.type] && challengers[auth.type].set) || if (!challenger) {
options.setChallenge; throw new Error(
try { "options.challenges did not have a valid entry for '" +
auth.type +
"'"
);
}
if (1 === challenger.length) { if (1 === challenger.length) {
challenger(auth) return Promise.resolve(challenger(auth));
.then(resolve)
.catch(reject);
} else if (2 === challenger.length) { } else if (2 === challenger.length) {
return new Promise(function(resolve, reject) {
challenger(auth, function(err) { challenger(auth, function(err) {
if (err) { if (err) {
reject(err); reject(err);
@ -693,45 +753,12 @@ ACME._setChallenge = function(me, options, auth) {
resolve(); resolve();
} }
}); });
} else {
// TODO remove this old backwards-compat
var challengeCb = function(err) {
if (err) {
reject(err);
} else {
resolve();
}
};
// for backwards compat adding extra keys without changing params length
Object.keys(auth).forEach(function(key) {
challengeCb[key] = auth[key];
}); });
if (!ACME._setChallengeWarn) { } else {
console.warn( throw new Error(
'Please update to acme-v2 setChallenge(options) <Promise> or setChallenge(options, cb).' "Bad function signature for '" + auth.type + "' challenge.set()"
);
console.warn(
"The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."
);
ACME._setChallengeWarn = true;
}
challenger(
auth.identifier.value,
auth.token,
auth.keyAuthorization,
challengeCb
); );
} }
} catch (e) {
reject(e);
}
}).then(function() {
// TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves?
var DELAY = me.setChallengeWait || 500;
if (me.debug) {
console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY);
}
return ACME._wait(DELAY);
}); });
}; };
ACME._finalizeOrder = function(me, options, validatedDomains) { ACME._finalizeOrder = function(me, options, validatedDomains) {
@ -943,12 +970,45 @@ ACME._getCertificate = function(me, options) {
}); });
} }
// TODO Promise.all()?
Object.keys(options.challenges).forEach(function(key) {
var presenter = options.challenges[key];
if ('function' === typeof presenter.init && !presenter._initialized) {
presenter._initialized = true;
return ACME._depInit(me, presenter);
}
});
var promiseZones;
if (options.challenges['dns-01']) {
// a little bit of random to ensure that getZones()
// actually returns the zones and not the hosts as zones
var dnsHosts = options.domains.map(function(d) {
var rnd = require('crypto')
.randomBytes(2)
.toString('hex');
return rnd + '.' + d;
});
promiseZones = ACME._getZones(
me,
options.challenges['dns-01'],
dnsHosts
);
} else {
promiseZones = Promise.resolve([]);
}
return promiseZones
.then(function(zonenames) {
options.zonenames = zonenames;
// Do a little dry-run / self-test // Do a little dry-run / self-test
return ACME._testChallenges(me, options).then(function() { return ACME._testChallenges(me, options);
})
.then(function() {
if (me.debug) { if (me.debug) {
console.debug('[acme-v2] certificates.create'); console.debug('[acme-v2] certificates.create');
} }
var body = { var certOrder = {
// raw wildcard syntax MUST be used here // raw wildcard syntax MUST be used here
identifiers: options.domains identifiers: options.domains
.sort(function(a, b) { .sort(function(a, b) {
@ -971,7 +1031,7 @@ ACME._getCertificate = function(me, options) {
//, "notAfter": "2016-01-08T00:00:00Z" //, "notAfter": "2016-01-08T00:00:00Z"
}; };
var payload = JSON.stringify(body); var payload = JSON.stringify(certOrder);
if (me.debug) { if (me.debug) {
console.debug('\n[DEBUG] newOrder\n'); console.debug('\n[DEBUG] newOrder\n');
} }
@ -1011,15 +1071,27 @@ ACME._getCertificate = function(me, options) {
} }
setAuths = options._authorizations.slice(0); setAuths = options._authorizations.slice(0);
var accountKeyThumb;
function setThumbnail() {
return ACME._importKeypair(me, options.accountKeypair).then(
function(pair) {
return me.Keypairs.thumbprint({
jwk: pair.public
}).then(function(_thumb) {
accountKeyThumb = _thumb;
});
}
);
}
function setNext() { function setNext() {
var authUrl = setAuths.shift(); var authUrl = setAuths.shift();
if (!authUrl) { if (!authUrl) {
return; return;
} }
return ACME._getChallenges(me, options, authUrl).then(function( return ACME._getChallenges(me, options, authUrl).then(
results function(results) {
) {
// var domain = options.domains[i]; // results.identifier.value // var domain = options.domains[i]; // results.identifier.value
// If it's already valid, we're golden it regardless // If it's already valid, we're golden it regardless
@ -1031,7 +1103,10 @@ ACME._getCertificate = function(me, options) {
return setNext(); return setNext();
} }
var challenge = ACME._chooseChallenge(options, results); var challenge = ACME._chooseChallenge(
options,
results
);
if (!challenge) { if (!challenge) {
// For example, wildcards require dns-01 and, if we don't have that, we have to bail // For example, wildcards require dns-01 and, if we don't have that, we have to bail
return Promise.reject( return Promise.reject(
@ -1046,19 +1121,37 @@ ACME._getCertificate = function(me, options) {
return ACME._challengeToAuth( return ACME._challengeToAuth(
me, me,
options, options,
accountKeyThumb,
results, results,
challenge, challenge,
false false
).then(function(auth) { ).then(function(auth) {
console.log('ADD DUBIOUS AUTH');
auths.push(auth); auths.push(auth);
return ACME._setChallenge(me, options, auth).then( return ACME._setChallenge(
setNext me,
options,
auth
).then(setNext);
});
}
); );
}); }
});
function waitAll() {
// TODO take the max wait of all challenge plugins and wait that long, or 1000ms
var DELAY = me.setChallengeWait || 7000;
if (true || me.debug) {
console.debug(
'\n[DEBUG] waitChallengeDelay %s\n',
DELAY
);
}
return ACME._wait(DELAY);
} }
function checkNext() { function checkNext() {
console.log('CONSUME DUBIOUS AUTH', auths.length);
var auth = auths.shift(); var auth = auths.shift();
if (!auth) { if (!auth) {
return; return;
@ -1068,45 +1161,43 @@ ACME._getCertificate = function(me, options) {
// not so much "valid" as "not invalid" // not so much "valid" as "not invalid"
// but in this case we can't confirm either way // but in this case we can't confirm either way
validAuths.push(auth); validAuths.push(auth);
return Promise.resolve(); console.log('ADD VALID AUTH (skip)', validAuths.length);
return checkNext();
} }
return ACME.challengeTests[auth.type](me, auth) return ACME.challengeTests[auth.type](me, auth)
.then(function() { .then(function() {
console.log('ADD VALID AUTH');
validAuths.push(auth); validAuths.push(auth);
}) })
.then(checkNext); .then(checkNext);
} }
function challengeNext() { function presentNext() {
console.log('CONSUME VALID AUTH', validAuths.length);
var auth = validAuths.shift(); var auth = validAuths.shift();
if (!auth) { if (!auth) {
return; return;
} }
return ACME._postChallenge(me, options, auth).then( return ACME._postChallenge(me, options, auth).then(
challengeNext presentNext
); );
} }
// First we set every challenge function finalizeOrder() {
// 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(checkNext)
.then(challengeNext)
.then(function() {
if (me.debug) { if (me.debug) {
console.debug('[getCertificate] next.then'); console.debug('[getCertificate] next.then');
} }
var validatedDomains = body.identifiers.map(function( var validatedDomains = certOrder.identifiers.map(function(
ident ident
) { ) {
return ident.value; return ident.value;
}); });
return ACME._finalizeOrder(me, options, validatedDomains); return ACME._finalizeOrder(me, options, validatedDomains);
}) }
.then(function(order) {
function retrieveCerts(order) {
if (me.debug) { if (me.debug) {
console.debug('acme-v2: order was finalized'); console.debug('acme-v2: order was finalized');
} }
@ -1141,10 +1232,22 @@ ACME._getCertificate = function(me, options) {
} }
return certs; return certs;
}); });
}); }
// First we set each and 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 setThumbnail()
.then(setNext)
.then(waitAll)
.then(checkNext)
.then(presentNext)
.then(finalizeOrder)
.then(retrieveCerts);
}); });
}); });
}; };
ACME._generateCsrWeb64 = function(me, options, validatedDomains) { ACME._generateCsrWeb64 = function(me, options, validatedDomains) {
var csr; var csr;
if (options.csr) { if (options.csr) {
@ -1153,6 +1256,7 @@ ACME._generateCsrWeb64 = function(me, options, validatedDomains) {
if ('string' !== typeof csr) { if ('string' !== typeof csr) {
csr = Enc.bufToUrlBase64(csr); csr = Enc.bufToUrlBase64(csr);
} }
// TODO PEM.parseBlock()
// nix PEM headers, if any // nix PEM headers, if any
if ('-' === csr[0]) { if ('-' === csr[0]) {
csr = csr csr = csr
@ -1168,13 +1272,11 @@ ACME._generateCsrWeb64 = function(me, options, validatedDomains) {
me, me,
options.serverKeypair || options.domainKeypair options.serverKeypair || options.domainKeypair
).then(function(pair) { ).then(function(pair) {
return me return me.CSR.csr({
.CSR({
jwk: pair.private, jwk: pair.private,
domains: validatedDomains, domains: validatedDomains,
encoding: 'der' encoding: 'der'
}) }).then(function(der) {
.then(function(der) {
return Enc.bufToUrlBase64(der); return Enc.bufToUrlBase64(der);
}); });
}); });
@ -1276,6 +1378,7 @@ ACME._jwsRequest = function(me, bigopts) {
bigopts.protected.kid = bigopts.options._kid; bigopts.protected.kid = bigopts.options._kid;
} }
} }
// this will shasum the thumbnail the 2nd time
return me.Keypairs.signJws({ return me.Keypairs.signJws({
jwk: bigopts.options.accountKeypair.privateKeyJwk, jwk: bigopts.options.accountKeypair.privateKeyJwk,
protected: bigopts.protected, protected: bigopts.protected,
@ -1291,6 +1394,7 @@ ACME._jwsRequest = function(me, bigopts) {
}); });
}); });
}; };
// Handle some ACME-specific defaults // Handle some ACME-specific defaults
ACME._request = function(me, opts) { ACME._request = function(me, opts) {
if (!opts.headers) { if (!opts.headers) {
@ -1430,24 +1534,99 @@ ACME._http01 = function(me, auth) {
ACME._removeChallenge = function(me, options, auth) { ACME._removeChallenge = function(me, options, auth) {
var challengers = options.challenges || {}; var challengers = options.challenges || {};
var removeChallenge = var removeChallenge =
(challengers[auth.type] && challengers[auth.type].remove) || challengers[auth.type] && challengers[auth.type].remove;
options.removeChallenge;
if (1 === removeChallenge.length) { if (1 === removeChallenge.length) {
removeChallenge(auth).then(function() {}, function() {}); return Promise.resolve(removeChallenge(auth)).then(
function() {},
function() {}
);
} else if (2 === removeChallenge.length) { } else if (2 === removeChallenge.length) {
removeChallenge(auth, function(err) { removeChallenge(auth, function(err) {
return err; return err;
}); });
} else { } else {
if (!ACME._removeChallengeWarn) { throw new Error(
console.warn( "Bad function signature for '" + auth.type + "' challenge.remove()"
'Please update to acme-v2 removeChallenge(options) <Promise> or removeChallenge(options, cb).'
); );
console.warn(
"The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."
);
ACME._removeChallengeWarn = true;
}
removeChallenge(auth.request.identifier, auth.token, function() {});
} }
}; };
ACME._depInit = function(me, presenter) {
if ('function' !== typeof presenter.init) {
return Promise.resolve(null);
}
return ACME._wrapCb(
me,
presenter,
'init',
{ type: '*', request: me.request },
'null'
);
};
ACME._getZones = function(me, presenter, dnsHosts) {
if ('function' !== typeof presenter.zones) {
presenter.zones = function() {
return Promise.resolve([]);
};
}
var challenge = {
type: 'dns-01',
dnsHosts: dnsHosts,
request: me.request
};
// back/forwards-compat
challenge.challenge = challenge;
return ACME._wrapCb(
me,
presenter,
'zones',
challenge,
'an array of zone names'
);
};
ACME._wrapCb = function(me, options, _name, args, _desc) {
return new Promise(function(resolve, reject) {
if (options[_name].length <= 1) {
return Promise.resolve(options[_name](args))
.then(resolve)
.catch(reject);
} else if (2 === options[_name].length) {
options[_name](args, function(err, results) {
if (err) {
reject(err);
} else {
resolve(results);
}
});
} else {
throw new Error(
'options.' + _name + ' should accept opts and Promise ' + _desc
);
}
});
};
function newZoneRegExp(zonename) {
// (^|\.)example\.com$
// which matches:
// foo.example.com
// example.com
// but not:
// fooexample.com
return new RegExp('(^|\\.)' + zonename.replace(/\./g, '\\.') + '$');
}
function pluckZone(zonenames, dnsHost) {
return zonenames
.filter(function(zonename) {
// the only character that needs to be escaped for regex
// and is allowed in a domain name is '.'
return newZoneRegExp(zonename).test(dnsHost);
})
.sort(function(a, b) {
// longest match first
return b.length - a.length;
})[0];
}

View File

@ -5,18 +5,19 @@
'use strict'; 'use strict';
/*global Promise*/ /*global Promise*/
var ASN1 = require('./asn1/parser.js'); // DER, actually var ASN1 = require('./asn1/packer.js'); // DER, actually
var Asn1 = ASN1.Any; var Asn1 = ASN1.Any;
var BitStr = ASN1.BitStr; var BitStr = ASN1.BitStr;
var UInt = ASN1.UInt; var UInt = ASN1.UInt;
var Asn1Parser = require('./asn1/packer.js'); // DER, actually var Asn1Parser = require('./asn1/parser.js');
var Enc = require('omnibuffer'); var Enc = require('omnibuffer');
var PEM = require('./pem.js'); var PEM = require('./pem.js');
var X509 = require('./x509.js'); var X509 = require('./x509.js');
var Keypairs = require('./keypairs'); var Keypairs = require('./keypairs');
// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken // TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
var CSR = (exports.CSR = function(opts) { var CSR = module.exports;
CSR.csr = function(opts) {
// We're using a Promise here to be compatible with the browser version // We're using a Promise here to be compatible with the browser version
// which will probably use the webcrypto API for some of the conversions // which will probably use the webcrypto API for some of the conversions
return CSR._prepare(opts).then(function(opts) { return CSR._prepare(opts).then(function(opts) {
@ -24,11 +25,10 @@ var CSR = (exports.CSR = function(opts) {
return CSR._encode(opts, bytes); return CSR._encode(opts, bytes);
}); });
}); });
}); };
CSR._prepare = function(opts) { CSR._prepare = function(opts) {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
var Keypairs;
opts = JSON.parse(JSON.stringify(opts)); opts = JSON.parse(JSON.stringify(opts));
// We do a bit of extra error checking for user convenience // We do a bit of extra error checking for user convenience
@ -66,16 +66,6 @@ CSR._prepare = function(opts) {
throw new Error('You must pass options.key as a JSON web key'); throw new Error('You must pass options.key as a JSON web key');
} }
Keypairs = exports.Keypairs;
if (!exports.Keypairs) {
throw new Error(
'Keypairs.js is an optional dependency for PEM-to-JWK.\n' +
"Install it if you'd like to use it:\n" +
'\tnpm install --save rasha\n' +
'Otherwise supply a jwk as the private key.'
);
}
return Keypairs.import({ pem: opts.pem || opts.key }).then(function( return Keypairs.import({ pem: opts.pem || opts.key }).then(function(
pair pair
) { ) {
@ -119,7 +109,7 @@ CSR._sign = function csrEcSig(jwk, request) {
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a // Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
// TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same) // TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
// TODO have a consistent non-private way to sign // TODO have a consistent non-private way to sign
return Keypairs._sign( return Keypairs.sign(
{ jwk: jwk, format: 'x509' }, { jwk: jwk, format: 'x509' },
Enc.hexToBuf(request) Enc.hexToBuf(request)
).then(function(sig) { ).then(function(sig) {

View File

@ -76,6 +76,7 @@ Keypairs.neuter = function(opts) {
}; };
Keypairs.thumbprint = function(opts) { Keypairs.thumbprint = function(opts) {
//console.log('[debug]', new Error('NOT_ERROR').stack);
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
if (/EC/i.test(opts.jwk.kty)) { if (/EC/i.test(opts.jwk.kty)) {
console.log('[debug] EC thumbprint'); console.log('[debug] EC thumbprint');
@ -121,6 +122,7 @@ Keypairs.publish = function(opts) {
// JWT a.k.a. JWS with Claims using Compact Serialization // JWT a.k.a. JWS with Claims using Compact Serialization
Keypairs.signJwt = function(opts) { Keypairs.signJwt = function(opts) {
console.log('[debug] signJwt');
return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) { return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) {
var header = opts.header || {}; var header = opts.header || {};
var claims = JSON.parse(JSON.stringify(opts.claims || {})); var claims = JSON.parse(JSON.stringify(opts.claims || {}));
@ -255,6 +257,9 @@ Keypairs.signJws = function(opts) {
}); });
}; };
// TODO expose consistently
Keypairs.sign = native._sign;
Keypairs._getBits = function(opts) { Keypairs._getBits = function(opts) {
if (opts.alg) { if (opts.alg) {
return opts.alg.replace(/[a-z\-]/gi, ''); return opts.alg.replace(/[a-z\-]/gi, '');

View File

@ -15,7 +15,7 @@ Keypairs._sign = function(opts, payload) {
.update(payload) .update(payload)
.sign(pem); .sign(pem);
if ('EC' === opts.jwk.kty) { if ('EC' === opts.jwk.kty && !/x509|asn1/i.test(opts.format)) {
// ECDSA JWT signatures differ from "normal" ECDSA signatures // ECDSA JWT signatures differ from "normal" ECDSA signatures
// https://tools.ietf.org/html/rfc7518#section-3.4 // https://tools.ietf.org/html/rfc7518#section-3.4
binsig = Keypairs._ecdsaAsn1SigToJoseSig(binsig); binsig = Keypairs._ecdsaAsn1SigToJoseSig(binsig);

View File

@ -1,18 +1,39 @@
'use strict'; 'use strict';
require('dotenv').config();
var ACME = require('../'); var ACME = require('../');
var Keypairs = require('../lib/keypairs.js'); var Keypairs = require('../lib/keypairs.js');
var acme = ACME.create({}); var acme = ACME.create({ debug: true });
// TODO exec npm install --save-dev CHALLENGE_MODULE
var config = { var config = {
env: process.env.ENV, env: process.env.ENV,
email: process.env.SUBSCRIBER_EMAIL, email: process.env.SUBSCRIBER_EMAIL,
domain: process.env.BASE_DOMAIN domain: process.env.BASE_DOMAIN,
challengeType: process.env.CHALLENGE_TYPE,
challengeModule: process.env.CHALLENGE_MODULE,
challengeOptions: JSON.parse(process.env.CHALLENGE_OPTIONS)
}; };
config.debug = !/^PROD/i.test(config.env); config.debug = !/^PROD/i.test(config.env);
config.challenger = require('acme-' +
config.challengeType +
'-' +
config.challengeModule).create(config.challengeOptions);
if (!config.challengeType || !config.domain) {
console.error(
new Error('Missing config variables. Check you .env and the docs')
.message
);
console.error(config);
process.exit(1);
}
var challenges = {};
challenges[config.challengeType] = config.challenger;
async function happyPath() { async function happyPath() {
var domains = randomDomains();
var agreed = false; var agreed = false;
var metadata = await acme.init( var metadata = await acme.init(
'https://acme-staging-v02.api.letsencrypt.org/directory' 'https://acme-staging-v02.api.letsencrypt.org/directory'
@ -66,8 +87,31 @@ async function happyPath() {
if (config.debug) { if (config.debug) {
console.info('Server Key Created'); console.info('Server Key Created');
console.info(JSON.stringify(serverKeypair, null, 2)); console.info(JSON.stringify(serverKeypair, null, 2));
console.info('');
console.info(); console.info();
console.info();
}
var domains = randomDomains();
if (config.debug) {
console.info('Get certificates for random domains:');
console.info(domains);
}
var results = await acme.certificates.create({
account: account,
accountKeypair: { privateKeyJwk: accountKeypair.private },
serverKeypair: { privateKeyJwk: serverKeypair.private },
domains: domains,
challenges: challenges, // must be implemented
skipDryRun: true
});
if (config.debug) {
console.info('Got SSL Certificate:');
console.info(results.expires);
console.info(results.cert);
console.info(results.chain);
console.info('');
console.info('');
} }
} }