forked from root/acme.js
225 lines
5.1 KiB
JavaScript
225 lines
5.1 KiB
JavaScript
'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');
|
|
var acme = ACME.create({
|
|
// debug: true
|
|
});
|
|
|
|
// TODO exec npm install --save-dev CHALLENGE_MODULE
|
|
|
|
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;
|
|
|
|
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 });
|
|
if (config.debug) {
|
|
console.info('Account Key Created');
|
|
console.info(JSON.stringify(accountKeypair, null, 2));
|
|
console.info();
|
|
console.info();
|
|
}
|
|
|
|
var account = await acme.accounts.create({
|
|
agreeToTerms: agree,
|
|
// TODO detect jwk/pem/der?
|
|
accountKeypair: { privateKeyJwk: accountKeypair.private },
|
|
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,
|
|
accountKeypair: { privateKeyJwk: accountKeypair.private },
|
|
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) + '例'
|
|
);
|
|
}
|