update API and tests

This commit is contained in:
AJ ONeal 2019-10-24 11:39:25 -06:00
parent f05e9db38e
commit 0efa94eeb0
7 changed files with 570 additions and 436 deletions

263
acme.js
View File

@ -179,17 +179,17 @@ ACME._testChallengeOptions = function() {
ACME._thumber = function(me, options, thumb) {
var thumbPromise;
return function() {
return function(key) {
if (thumb) {
return Promise.resolve(thumb);
}
if (thumbPromise) {
return thumbPromise;
}
thumbPromise = U._importKeypair(
me,
options.accountKey || options.accountKeypair
).then(function(pair) {
if (!key) {
key = options.accountKey || options.accountKeypair;
}
thumbPromise = U._importKeypair(null, key).then(function(pair) {
return Keypairs.thumbprint({
jwk: pair.public
});
@ -266,7 +266,14 @@ ACME._dryRun = function(me, realOptions) {
type: ch.type
//challenge: ch
});
noopts.challenges[ch.type].remove({ challenge: ch });
noopts.challenges[ch.type]
.remove({ challenge: ch })
.catch(function(err) {
err.action = 'challenge_remove';
err.altname = ch.altname;
err.type = ch.type;
ACME._notify(me, noopts, 'error', err);
});
});
}
@ -310,95 +317,117 @@ ACME._computeAuths = function(me, options, thumb, request, dryrun) {
);
}
var getThumbprint = ACME._thumber(me, options, thumb);
var getThumbprint = ACME._thumber(null, options, thumb);
return getThumbprint().then(function(thumb) {
return Promise.all(
request.challenges.map(function(challenge) {
// Don't do extra work for challenges that we can't satisfy
if (!options._presenterTypes.includes(challenge.type)) {
return null;
}
return Promise.all(
request.challenges.map(function(challenge) {
// Don't do extra work for challenges that we can't satisfy
if (!options._presenterTypes.includes(challenge.type)) {
return null;
}
var auth = {};
var auth = {};
// straight copy from the new order response
// { identifier, status, expires, challenges, wildcard }
Object.keys(request).forEach(function(key) {
auth[key] = request[key];
// straight copy from the new order response
// { identifier, status, expires, challenges, wildcard }
Object.keys(request).forEach(function(key) {
auth[key] = request[key];
});
// copy from the challenge we've chosen
// { type, status, url, token }
// (note the duplicate status overwrites the one above, but they should be the same)
Object.keys(challenge).forEach(function(key) {
// don't confused devs with the id url
auth[key] = challenge[key];
});
// batteries-included helpers
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
auth.altname = ACME._untame(auth.identifier.value, auth.wildcard);
var zone = pluckZone(
options.zonenames || [],
auth.identifier.value
);
return ACME.computeChallenge({
accountKey: options.accountKey,
_getThumbprint: getThumbprint,
challenge: auth,
zone: zone,
dnsPrefix: dnsPrefix
}).then(function(resp) {
Object.keys(resp).forEach(function(k) {
auth[k] = resp[k];
});
return auth;
});
})
).then(function(auths) {
return auths.filter(Boolean);
});
};
// copy from the challenge we've chosen
// { type, status, url, token }
// (note the duplicate status overwrites the one above, but they should be the same)
Object.keys(challenge).forEach(function(key) {
// don't confused devs with the id url
auth[key] = challenge[key];
});
ACME.computeChallenge = function(opts) {
var auth = opts.challenge;
var hostname = auth.hostname || opts.hostname;
var zone = opts.zone;
var thumb = opts.thumbprint || '';
var accountKey = opts.accountKey;
var getThumbprint = opts._getThumbprint || ACME._thumber(null, opts, thumb);
var dnsPrefix = opts.dnsPrefix || ACME.challengePrefixes['dns-01'];
// batteries-included helpers
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
auth.altname = ACME._untame(
auth.identifier.value,
auth.wildcard
);
return getThumbprint(accountKey).then(function(thumb) {
var resp = {};
resp.thumbprint = thumb;
// keyAuthorization = token + '.' + base64url(JWK_Thumbprint(accountKey))
resp.keyAuthorization = auth.token + '.' + thumb;
auth.thumbprint = thumb;
// keyAuthorization = token + '.' + base64url(JWK_Thumbprint(accountKey))
auth.keyAuthorization = challenge.token + '.' + auth.thumbprint;
if ('http-01' === auth.type) {
// conflicts with ACME challenge id url is already in use,
// so we call this challengeUrl instead
// TODO auth.http01Url ?
resp.challengeUrl =
'http://' +
// `hostname` is an alias of `auth.indentifier.value`
hostname +
ACME.challengePrefixes['http-01'] +
'/' +
auth.token;
}
if ('http-01' === auth.type) {
// conflicts with ACME challenge id url is already in use,
// so we call this challengeUrl instead
// TODO auth.http01Url ?
auth.challengeUrl =
'http://' +
auth.identifier.value +
ACME.challengePrefixes['http-01'] +
'/' +
auth.token;
return auth;
}
if ('dns-01' !== auth.type) {
return resp;
}
if ('dns-01' !== auth.type) {
return auth;
}
var zone = pluckZone(
options.zonenames || [],
auth.identifier.value
);
// Always calculate dnsAuthorization because we
// may need to present to the user for confirmation / instruction
// _as part of_ the decision making process
return sha2
.sum(256, auth.keyAuthorization)
.then(function(hash) {
return Enc.bufToUrlBase64(new Uint8Array(hash));
})
.then(function(hash64) {
auth.dnsHost =
dnsPrefix + '.' + auth.hostname.replace('*.', '');
auth.dnsAuthorization = hash64;
auth.keyAuthorizationDigest = hash64;
if (zone) {
auth.dnsZone = zone;
auth.dnsPrefix = auth.dnsHost
.replace(newZoneRegExp(zone), '')
.replace(/\.$/, '');
}
return auth;
});
// Always calculate dnsAuthorization because we
// may need to present to the user for confirmation / instruction
// _as part of_ the decision making process
return sha2
.sum(256, resp.keyAuthorization)
.then(function(hash) {
return Enc.bufToUrlBase64(Uint8Array.from(hash));
})
).then(function(auths) {
return auths.filter(Boolean);
});
.then(function(hash64) {
resp.dnsHost = dnsPrefix + '.' + hostname; // .replace('*.', '');
// deprecated
resp.dnsAuthorization = hash64;
// should use this instead
resp.keyAuthorizationDigest = hash64;
if (zone) {
resp.dnsZone = zone;
resp.dnsPrefix = resp.dnsHost
.replace(newZoneRegExp(zone), '')
.replace(/\.$/, '');
}
return resp;
});
});
};
@ -583,6 +612,7 @@ ACME._setChallenges = function(me, options, order) {
var claims = order._claims.slice(0);
var valids = [];
var auths = [];
var placed = [];
var USE_DNS = false;
var DNS_DELAY = 0;
@ -618,6 +648,7 @@ ACME._setChallenges = function(me, options, order) {
);
}
auths.push(selected);
placed.push(selected);
ACME._notify(me, options, 'challenge_select', {
// API-locked
altname: ACME._untame(
@ -651,10 +682,13 @@ ACME._setChallenges = function(me, options, order) {
function waitAll() {
//#console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY);
if (!DNS_DELAY || DNS_DELAY <= 0) {
console.warn(
'the given dns-01 challenge did not specify `propagationDelay`'
);
console.warn('the default of 5000ms will be used');
if (!ACME._propagationDelayWarning) {
console.warn(
'warn: the given dns-01 challenge did not specify `propagationDelay`'
);
console.warn('warn: the default of 5000ms will be used');
ACME._propagationDelayWarning = true;
}
DNS_DELAY = 5000;
}
return ACME._wait(DNS_DELAY);
@ -683,7 +717,22 @@ ACME._setChallenges = function(me, options, order) {
// is so that we don't poison our own DNS cache with misses.
return setNext()
.then(waitAll)
.then(checkNext);
.then(checkNext)
.catch(function(err) {
if (!options.debug) {
placed.forEach(function(ch) {
options.challenges[ch.type]
.remove({ challenge: ch })
.catch(function(err) {
err.action = 'challenge_remove';
err.altname = ch.altname;
err.type = ch.type;
ACME._notify(me, options, 'error', err);
});
});
}
throw err;
});
};
ACME._normalizePresenters = function(me, options, presenters) {
@ -1283,40 +1332,6 @@ ACME._prnd = function(n) {
ACME._toHex = function(pair) {
return parseInt(pair, 10).toString(16);
};
ACME._removeChallenge = function(me, options, auth) {
var challengers = options.challenges || {};
var ch = auth.challenge;
var removeChallenge = challengers[ch.type] && challengers[ch.type].remove;
if (!removeChallenge) {
throw new Error('challenge plugin is missing remove()');
}
// TODO normalize, warn, and just use promises
if (1 === removeChallenge.length) {
return Promise.resolve(removeChallenge(auth)).then(
function() {},
function(e) {
console.error('Error during remove challenge:');
console.error(e);
}
);
} else if (2 === removeChallenge.length) {
return new Promise(function(resolve) {
removeChallenge(auth, function(err) {
resolve();
if (err) {
console.error('Error during remove challenge:');
console.error(err);
}
return err;
});
});
} else {
throw new Error(
"Bad function signature for '" + auth.type + "' challenge.remove()"
);
}
};
ACME._depInit = function(me, presenter) {
if ('function' !== typeof presenter.init) {

View File

@ -0,0 +1,111 @@
'use strict';
var ACME = require('../');
var accountKey = require('../fixtures/account.jwk.json').private;
var authorization = {
identifier: {
type: 'dns',
value: 'example.com'
},
status: 'pending',
expires: '2018-04-25T00:23:57Z',
challenges: [
{
type: 'dns-01',
status: 'pending',
url:
'https://acme-staging-v02.api.letsencrypt.org/acme/challenge/cMkwXI8pIeKN04Ynfem8ErHK3GeqAPdSt2x6q7PvVGU/118755342',
token: 'LZdlUiZ-kWPs6q5WTmQFYQHZKpz9szn2vxEUu0XhyyM'
},
{
type: 'http-01',
status: 'pending',
url:
'https://acme-staging-v02.api.letsencrypt.org/acme/challenge/cMkwXI8pIeKN04Ynfem8ErHK3GeqAPdSt2x6q7PvVGU/118755343',
token: '1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU'
}
]
};
var expectedChallengeUrl =
'http://example.com/.well-known/acme-challenge/1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU';
var expectedKeyAuth =
'1S4zBG5YVhwSBaIY4ksI_KNMRrSmH0DZfNM9v7PYjDU.UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs';
var expectedKeyAuthDigest = 'iQiMcQUDiAeD0TJV1RHJuGnI5D2-PuSpxKz9JqUaZ2M';
var expectedDnsHost = '_test-challenge.example.com';
async function main() {
console.info('\n[Test] computing challenge authorizatin responses');
var challenges = authorization.challenges.slice(0);
function next() {
var ch = challenges.shift();
if (!ch) {
return null;
}
var hostname = authorization.identifier.value;
return ACME.computeChallenge({
accountKey: accountKey,
hostname: hostname,
challenge: ch,
dnsPrefix: '_test-challenge'
})
.then(function(auth) {
if ('dns-01' === ch.type) {
if (auth.keyAuthorizationDigest !== expectedKeyAuthDigest) {
console.error('[keyAuthorizationDigest]');
console.error(auth.keyAuthorizationDigest);
console.error(expectedKeyAuthDigest);
throw new Error('bad keyAuthDigest');
}
if (auth.dnsHost !== expectedDnsHost) {
console.error('[dnsHost]');
console.error(auth.dnsHost);
console.error(expectedDnsHost);
throw new Error('bad dnsHost');
}
} else if ('http-01' === ch.type) {
if (auth.challengeUrl !== expectedChallengeUrl) {
console.error('[challengeUrl]');
console.error(auth.challengeUrl);
console.error(expectedChallengeUrl);
throw new Error('bad challengeUrl');
}
if (auth.challengeUrl !== expectedChallengeUrl) {
console.error('[keyAuthorization]');
console.error(auth.keyAuthorization);
console.error(expectedKeyAuth);
throw new Error('bad keyAuth');
}
} else {
throw new Error('bad authorization inputs');
}
console.info('PASS', hostname, ch.type);
return next();
})
.catch(function(err) {
err.message =
'Error computing ' +
ch.type +
' for ' +
hostname +
':' +
err.message;
throw err;
});
}
return next();
}
module.exports = function() {
return main(authorization)
.then(function() {
console.info('PASS');
})
.catch(function(err) {
console.error(err.stack);
process.exit(1);
});
};

View File

@ -35,55 +35,46 @@ var tests = [
'----\nxxxx\nyyyy\n----\r\n----\nxxxx\ryyyy\n----\n'
];
function formatPemChain(str) {
return (
str
.trim()
.replace(/[\r\n]+/g, '\n')
.replace(/\-\n\-/g, '-\n\n-') + '\n'
);
}
function splitPemChain(str) {
return str
.trim()
.split(/[\r\n]{2,}/g)
.map(function(str) {
return str + '\n';
});
}
var ACME = require('../');
tests.forEach(function(str) {
var actual = formatPemChain(str);
if (expected !== actual) {
console.error('input: ', JSON.stringify(str));
console.error('expected:', JSON.stringify(expected));
console.error('actual: ', JSON.stringify(actual));
throw new Error('did not pass');
module.exports = function() {
console.info('\n[Test] can split and format PEM chain properly');
tests.forEach(function(str) {
var actual = ACME.formatPemChain(str);
if (expected !== actual) {
console.error('input: ', JSON.stringify(str));
console.error('expected:', JSON.stringify(expected));
console.error('actual: ', JSON.stringify(actual));
throw new Error('did not pass');
}
});
if (
'----\nxxxx\nyyyy\n----\n' !==
ACME.formatPemChain('\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n')
) {
throw new Error('Not proper for single cert in chain');
}
});
if (
'----\nxxxx\nyyyy\n----\n' !==
formatPemChain('\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n')
) {
throw new Error('Not proper for single cert in chain');
}
if (
'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' !==
formatPemChain(
'\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n'
)
) {
throw new Error('Not proper for three certs in chain');
}
splitPemChain(
'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n'
).forEach(function(str) {
if ('--B--\nxxxx\nyyyy\n--E--\n' !== str) {
throw new Error('bad thingy');
if (
'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' !==
ACME.formatPemChain(
'\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n'
)
) {
throw new Error('Not proper for three certs in chain');
}
});
console.info('PASS');
ACME.splitPemChain(
'--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n'
).forEach(function(str) {
if ('--B--\nxxxx\nyyyy\n--E--\n' !== str) {
throw new Error('bad thingy');
}
});
console.info('PASS');
return Promise.resolve();
};

View File

@ -1,15 +1,27 @@
'use strict';
async function run() {
module.exports = async function() {
console.log('[Test] can generate, export, and import key');
var Keypairs = require('@root/keypairs');
var certKeypair = await Keypairs.generate({ kty: 'RSA' });
console.log(certKeypair);
//console.log(certKeypair);
var pem = await Keypairs.export({
jwk: certKeypair.private,
encoding: 'pem'
});
console.log(pem);
}
var jwk = await Keypairs.import({
pem: pem
});
['kty', 'd', 'n', 'e'].forEach(function(k) {
if (!jwk[k] || jwk[k] !== certKeypair.private[k]) {
throw new Error('bad export/import');
}
});
//console.log(pem);
console.log('PASS');
};
run();
if (require.main === module) {
module.exports();
}

View File

@ -1,245 +0,0 @@
'use strict';
require('dotenv').config();
var CSR = require('@root/csr');
var Enc = require('@root/encoding/base64');
var PEM = require('@root/pem');
var punycode = require('punycode');
var ACME = require('../acme.js');
var Keypairs = require('@root/keypairs');
// TODO exec npm install --save-dev CHALLENGE_MODULE
if (!process.env.CHALLENGE_OPTIONS) {
console.error(
'Please create a .env in the format of examples/example.env to run the tests'
);
process.exit(1);
}
var config = {
env: process.env.ENV,
email: process.env.SUBSCRIBER_EMAIL,
domain: process.env.BASE_DOMAIN,
challengeType: process.env.CHALLENGE_TYPE,
challengeModule: process.env.CHALLENGE_PLUGIN,
challengeOptions: JSON.parse(process.env.CHALLENGE_OPTIONS)
};
config.debug = !/^PROD/i.test(config.env);
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);
return;
}
console.error("Couldn't find '" + pluginName + "'. Is it installed?");
console.error("\tnpm install --save-dev '" + pluginName + "'");
}
try {
plugin = require(pluginName);
} catch (err) {
if (
'MODULE_NOT_FOUND' !== err.code ||
0 === pluginName.indexOf(pluginPrefix)
) {
badPlugin(err);
process.exit(1);
}
try {
pluginName = pluginPrefix + pluginName;
plugin = require(pluginName);
} catch (e) {
badPlugin(e);
process.exit(1);
}
}
config.challenger = plugin.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(accKty, srvKty, rnd) {
var agreed = false;
var metadata = await acme.init(
'https://acme-staging-v02.api.letsencrypt.org/directory'
);
// Ready to use, show page
if (config.debug) {
console.info('ACME.js initialized');
console.info(metadata);
console.info();
console.info();
}
var accountKeypair = await Keypairs.generate({ kty: accKty });
var accountKey = accountKeypair.private;
if (config.debug) {
console.info('Account Key Created');
console.info(JSON.stringify(accountKey, null, 2));
console.info();
console.info();
}
var account = await acme.accounts.create({
agreeToTerms: agree,
// TODO detect jwk/pem/der?
accountKey: accountKey,
subscriberEmail: config.email
});
// TODO top-level agree
function agree(tos) {
if (config.debug) {
console.info('Agreeing to Terms of Service:');
console.info(tos);
console.info();
console.info();
}
agreed = true;
return Promise.resolve(tos);
}
if (config.debug) {
console.info('New Subscriber Account');
console.info(JSON.stringify(account, null, 2));
console.info();
console.info();
}
if (!agreed) {
throw new Error('Failed to ask the user to agree to terms');
}
var certKeypair = await Keypairs.generate({ kty: srvKty });
var pem = await Keypairs.export({
jwk: certKeypair.private,
encoding: 'pem'
});
if (config.debug) {
console.info('Server Key Created');
console.info('privkey.jwk.json');
console.info(JSON.stringify(certKeypair, null, 2));
// This should be saved as `privkey.pem`
console.info();
console.info('privkey.' + srvKty.toLowerCase() + '.pem:');
console.info(pem);
console.info();
}
// 'subject' should be first in list
var domains = randomDomains(rnd);
if (config.debug) {
console.info('Get certificates for random domains:');
console.info(
domains
.map(function(puny) {
var uni = punycode.toUnicode(puny);
if (puny !== uni) {
return puny + ' (' + uni + ')';
}
return puny;
})
.join('\n')
);
console.info();
}
// Create CSR
var csrDer = await CSR.csr({
jwk: certKeypair.private,
domains: domains,
encoding: 'der'
});
var csr = Enc.bufToUrlBase64(csrDer);
var csrPem = PEM.packBlock({
type: 'CERTIFICATE REQUEST',
bytes: csrDer /* { jwk: jwk, domains: opts.domains } */
});
if (config.debug) {
console.info('Certificate Signing Request');
console.info(csrPem);
console.info();
}
var results = await acme.certificates.create({
account: account,
accountKey: accountKey,
csr: csr,
domains: domains,
challenges: challenges, // must be implemented
customerEmail: null
});
if (config.debug) {
console.info('Got SSL Certificate:');
console.info(Object.keys(results));
console.info(results.expires);
console.info(results.cert);
console.info(results.chain);
console.info();
console.info();
}
}
// Try EC + RSA
var rnd = random();
happyPath('EC', 'RSA', rnd)
.then(function() {
// Now try RSA + EC
rnd = random();
return happyPath('RSA', 'EC', rnd).then(function() {
console.info('success');
});
})
.catch(function(err) {
console.error('Error:');
console.error(err.stack);
});
function randomDomains(rnd) {
return ['foo-acmejs', 'bar-acmejs', '*.baz-acmejs', 'baz-acmejs'].map(
function(pre) {
return punycode.toASCII(pre + '-' + rnd + '.' + config.domain);
}
);
}
function random() {
return (
parseInt(
Math.random()
.toString()
.slice(2, 99),
10
)
.toString(16)
.slice(0, 4) + '例'
);
}

253
tests/issue-certificates.js Normal file
View File

@ -0,0 +1,253 @@
'use strict';
require('dotenv').config();
var CSR = require('@root/csr');
var Enc = require('@root/encoding/base64');
var PEM = require('@root/pem');
var punycode = require('punycode');
var ACME = require('../acme.js');
var Keypairs = require('@root/keypairs');
// TODO exec npm install --save-dev CHALLENGE_MODULE
if (!process.env.CHALLENGE_OPTIONS) {
console.error(
'Please create a .env in the format of examples/example.env to run the tests'
);
process.exit(1);
}
var config = {
env: process.env.ENV,
email: process.env.SUBSCRIBER_EMAIL,
domain: process.env.BASE_DOMAIN,
challengeType: process.env.CHALLENGE_TYPE,
challengeModule: process.env.CHALLENGE_PLUGIN,
challengeOptions: JSON.parse(process.env.CHALLENGE_OPTIONS)
};
//config.debug = !/^PROD/i.test(config.env);
var pluginPrefix = 'acme-' + config.challengeType + '-';
var pluginName = config.challengeModule;
var plugin;
module.exports = function() {
console.info('\n[Test] end-to-end issue certificates');
var acme = ACME.create({
// debug: true
maintainerEmail: config.email,
notify: function(ev, params) {
console.info(
'\t' + ev,
params.subject || params.altname || params.domain || '',
params.status || ''
);
if ('error' === ev) {
console.error(params.action || params.type || '');
console.error(params);
}
}
});
function badPlugin(err) {
if ('MODULE_NOT_FOUND' !== err.code) {
console.error(err);
return;
}
console.error("Couldn't find '" + pluginName + "'. Is it installed?");
console.error("\tnpm install --save-dev '" + pluginName + "'");
}
try {
plugin = require(pluginName);
} catch (err) {
if (
'MODULE_NOT_FOUND' !== err.code ||
0 === pluginName.indexOf(pluginPrefix)
) {
badPlugin(err);
process.exit(1);
}
try {
pluginName = pluginPrefix + pluginName;
plugin = require(pluginName);
} catch (e) {
badPlugin(e);
process.exit(1);
}
}
config.challenger = plugin.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(accKty, srvKty, rnd) {
var agreed = false;
var metadata = await acme.init(
'https://acme-staging-v02.api.letsencrypt.org/directory'
);
// Ready to use, show page
if (config.debug) {
console.info('ACME.js initialized');
console.info(metadata);
console.info();
console.info();
}
var accountKeypair = await Keypairs.generate({ kty: accKty });
var accountKey = accountKeypair.private;
if (config.debug) {
console.info('Account Key Created');
console.info(JSON.stringify(accountKey, null, 2));
console.info();
console.info();
}
var account = await acme.accounts.create({
agreeToTerms: agree,
// TODO detect jwk/pem/der?
accountKey: accountKey,
subscriberEmail: config.email
});
// TODO top-level agree
function agree(tos) {
if (config.debug) {
console.info('Agreeing to Terms of Service:');
console.info(tos);
console.info();
console.info();
}
agreed = true;
return Promise.resolve(tos);
}
if (config.debug) {
console.info('New Subscriber Account');
console.info(JSON.stringify(account, null, 2));
console.info();
console.info();
}
if (!agreed) {
throw new Error('Failed to ask the user to agree to terms');
}
var certKeypair = await Keypairs.generate({ kty: srvKty });
var pem = await Keypairs.export({
jwk: certKeypair.private,
encoding: 'pem'
});
if (config.debug) {
console.info('Server Key Created');
console.info('privkey.jwk.json');
console.info(JSON.stringify(certKeypair, null, 2));
// This should be saved as `privkey.pem`
console.info();
console.info('privkey.' + srvKty.toLowerCase() + '.pem:');
console.info(pem);
console.info();
}
// 'subject' should be first in list
var domains = randomDomains(rnd);
if (config.debug) {
console.info('Get certificates for random domains:');
console.info(
domains
.map(function(puny) {
var uni = punycode.toUnicode(puny);
if (puny !== uni) {
return puny + ' (' + uni + ')';
}
return puny;
})
.join('\n')
);
console.info();
}
// Create CSR
var csrDer = await CSR.csr({
jwk: certKeypair.private,
domains: domains,
encoding: 'der'
});
var csr = Enc.bufToUrlBase64(csrDer);
var csrPem = PEM.packBlock({
type: 'CERTIFICATE REQUEST',
bytes: csrDer /* { jwk: jwk, domains: opts.domains } */
});
if (config.debug) {
console.info('Certificate Signing Request');
console.info(csrPem);
console.info();
}
var results = await acme.certificates.create({
account: account,
accountKey: accountKey,
csr: csr,
domains: domains,
challenges: challenges, // must be implemented
customerEmail: null
});
if (config.debug) {
console.info('Got SSL Certificate:');
console.info(Object.keys(results));
console.info(results.expires);
console.info(results.cert);
console.info(results.chain);
console.info();
console.info();
}
}
// Try EC + RSA
var rnd = random();
happyPath('EC', 'RSA', rnd)
.then(function() {
console.info('PASS: ECDSA account key with RSA server key');
// Now try RSA + EC
rnd = random();
return happyPath('RSA', 'EC', rnd).then(function() {
console.info('PASS: RSA account key with ECDSA server key');
});
})
.then(function() {
console.info('PASS');
})
.catch(function(err) {
console.error('Error:');
console.error(err.stack);
});
function randomDomains(rnd) {
return ['foo-acmejs', 'bar-acmejs', '*.baz-acmejs', 'baz-acmejs'].map(
function(pre) {
return punycode.toASCII(pre + '-' + rnd + '.' + config.domain);
}
);
}
function random() {
return (
parseInt(
Math.random()
.toString()
.slice(2, 99),
10
)
.toString(16)
.slice(0, 4) + '例'
);
}
};

View File

@ -122,29 +122,26 @@ 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;
U._importKeypair = function(me, key) {
var p;
if (jwk) {
var pub;
if (key && key.kty) {
// nix the browser jwk extras
jwk.key_ops = undefined;
jwk.ext = undefined;
pub = Keypairs.neuter({ jwk: jwk });
key.key_ops = undefined;
key.ext = undefined;
pub = Keypairs.neuter({ jwk: key });
p = Promise.resolve({
private: jwk,
private: key,
public: pub
});
} else if ('string' === typeof key) {
p = Keypairs.import({ pem: key });
} else {
p = Keypairs.import({ pem: kp.privateKeyPem });
throw new Error('no private key given');
}
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;