mirror of
https://github.com/certd/certd.git
synced 2026-04-28 07:57:25 +08:00
162e10909b
Small crypto docs fix 2 Small crypto docs fix Bump v5.3.1 Discourage use of cert subject common name, examples and docs Style refactor docs and examples Bump dependencies
177 lines
5.4 KiB
JavaScript
177 lines
5.4 KiB
JavaScript
/**
|
|
* Example using tls-alpn-01 challenge to generate certificates on-demand
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const https = require('https');
|
|
const tls = require('tls');
|
|
const acme = require('./../../');
|
|
|
|
const HTTPS_SERVER_PORT = 4443;
|
|
const ALPN_RESPONDER_PORT = 4444;
|
|
const VALID_DOMAINS = ['example.com', 'example.org'];
|
|
const FALLBACK_KEY = fs.readFileSync(path.join(__dirname, '..', 'fallback.key'));
|
|
const FALLBACK_CERT = fs.readFileSync(path.join(__dirname, '..', 'fallback.crt'));
|
|
|
|
const pendingDomains = {};
|
|
const alpnResponses = {};
|
|
const certificateStore = {};
|
|
|
|
function log(m) {
|
|
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
|
|
}
|
|
|
|
/**
|
|
* On-demand certificate generation using tls-alpn-01
|
|
*/
|
|
|
|
async function getCertOnDemand(client, servername, attempt = 0) {
|
|
/* Invalid domain */
|
|
if (!VALID_DOMAINS.includes(servername)) {
|
|
throw new Error(`Invalid domain: ${servername}`);
|
|
}
|
|
|
|
/* Certificate exists */
|
|
if (servername in certificateStore) {
|
|
return certificateStore[servername];
|
|
}
|
|
|
|
/* Waiting on certificate order to go through */
|
|
if (servername in pendingDomains) {
|
|
if (attempt >= 10) {
|
|
throw new Error(`Gave up waiting on certificate for ${servername}`);
|
|
}
|
|
|
|
await new Promise((resolve) => { setTimeout(resolve, 1000); });
|
|
return getCertOnDemand(client, servername, (attempt + 1));
|
|
}
|
|
|
|
/* Create CSR */
|
|
log(`Creating CSR for ${servername}`);
|
|
const [key, csr] = await acme.crypto.createCsr({
|
|
altNames: [servername],
|
|
});
|
|
|
|
/* Order certificate */
|
|
log(`Ordering certificate for ${servername}`);
|
|
const cert = await client.auto({
|
|
csr,
|
|
email: 'test@example.com',
|
|
termsOfServiceAgreed: true,
|
|
challengePriority: ['tls-alpn-01'],
|
|
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
|
|
alpnResponses[authz.identifier.value] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
|
|
},
|
|
challengeRemoveFn: (authz) => {
|
|
delete alpnResponses[authz.identifier.value];
|
|
},
|
|
});
|
|
|
|
/* Done, store certificate */
|
|
log(`Certificate for ${servername} created successfully`);
|
|
certificateStore[servername] = [key, cert];
|
|
delete pendingDomains[servername];
|
|
return certificateStore[servername];
|
|
}
|
|
|
|
/**
|
|
* Main
|
|
*/
|
|
|
|
(async () => {
|
|
try {
|
|
/**
|
|
* Initialize ACME client
|
|
*/
|
|
|
|
log('Initializing ACME client');
|
|
const client = new acme.Client({
|
|
directoryUrl: acme.directory.letsencrypt.staging,
|
|
accountKey: await acme.crypto.createPrivateKey(),
|
|
});
|
|
|
|
/**
|
|
* ALPN responder
|
|
*/
|
|
|
|
const alpnResponder = https.createServer({
|
|
/* Fallback cert */
|
|
key: FALLBACK_KEY,
|
|
cert: FALLBACK_CERT,
|
|
|
|
/* Allow acme-tls/1 ALPN protocol */
|
|
ALPNProtocols: ['acme-tls/1'],
|
|
|
|
/* Serve ALPN certificate based on servername */
|
|
SNICallback: async (servername, cb) => {
|
|
try {
|
|
log(`Handling ALPN SNI request for ${servername}`);
|
|
if (!Object.keys(alpnResponses).includes(servername)) {
|
|
throw new Error(`No ALPN certificate found for ${servername}`);
|
|
}
|
|
|
|
/* Serve ALPN challenge response */
|
|
log(`Found ALPN certificate for ${servername}, serving secure context`);
|
|
cb(null, tls.createSecureContext({
|
|
key: alpnResponses[servername][0],
|
|
cert: alpnResponses[servername][1],
|
|
}));
|
|
}
|
|
catch (e) {
|
|
log(`[ERROR] ${e.message}`);
|
|
cb(e.message);
|
|
}
|
|
},
|
|
});
|
|
|
|
/* Terminate once TLS handshake has been established */
|
|
alpnResponder.on('secureConnection', (socket) => {
|
|
socket.end();
|
|
});
|
|
|
|
alpnResponder.listen(ALPN_RESPONDER_PORT, () => {
|
|
log(`ALPN responder listening on port ${ALPN_RESPONDER_PORT}`);
|
|
});
|
|
|
|
/**
|
|
* HTTPS server
|
|
*/
|
|
|
|
const requestListener = (req, res) => {
|
|
log(`HTTP 200 ${req.headers.host}${req.url}`);
|
|
res.writeHead(200);
|
|
res.end('Hello world\n');
|
|
};
|
|
|
|
const httpsServer = https.createServer({
|
|
/* Fallback cert */
|
|
key: FALLBACK_KEY,
|
|
cert: FALLBACK_CERT,
|
|
|
|
/* Serve certificate based on servername */
|
|
SNICallback: async (servername, cb) => {
|
|
try {
|
|
log(`Handling SNI request for ${servername}`);
|
|
const [key, cert] = await getCertOnDemand(client, servername);
|
|
|
|
log(`Found certificate for ${servername}, serving secure context`);
|
|
cb(null, tls.createSecureContext({ key, cert }));
|
|
}
|
|
catch (e) {
|
|
log(`[ERROR] ${e.message}`);
|
|
cb(e.message);
|
|
}
|
|
},
|
|
}, requestListener);
|
|
|
|
httpsServer.listen(HTTPS_SERVER_PORT, () => {
|
|
log(`HTTPS server listening on port ${HTTPS_SERVER_PORT}`);
|
|
});
|
|
}
|
|
catch (e) {
|
|
log(`[FATAL] ${e.message}`);
|
|
process.exit(1);
|
|
}
|
|
})();
|