Skip to content

Instantly share code, notes, and snippets.

@mmalone

mmalone/acme.js Secret

Last active March 13, 2024 10:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mmalone/f3c33a2381ffa3d67e86c6d5ad3042c9 to your computer and use it in GitHub Desktop.
Save mmalone/f3c33a2381ffa3d67e86c6d5ad3042c9 to your computer and use it in GitHub Desktop.
Example HTTPS server using ACME to obtain & renew its certificate
const acme = require('acme-client');
const http = require('http');
const https = require('https');
const tls = require('tls');
const fs = require('fs');
const port = 11443;
const email = "you@yours.com";
const domain = "bar.internal";
const acmeDirectoryUrl = "https://acme.internal/acme/acme/directory";
const rootCertificate = "/home/mmalone/.step/certs/root_ca.crt";
const renewFrequency = 15000;
var clientOptions = {
ca: fs.readFileSync(rootCertificate),
ciphers: 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256',
minVersion: 'TLSv1.2',
};
acme.axios.defaults.httpsAgent = new https.Agent(clientOptions);
function log(m) {
process.stdout.write(`${m}\n`);
}
/**
* A map of challenge paths => responses for the standalone ACME server
*/
var challenges = {};
/**
* Function used to satisfy an ACME challenge
*/
async function challengeCreateFn(authz, challenge, keyAuthorization) {
if (challenge.type !== 'http-01') {
throw new Error(`Unsupported challenge type ${challenge.type}`);
}
log(`Creating challenge response for ${authz.identifier.value}`);
challenges[`/.well-known/acme-challenge/${challenge.token}`] = keyAuthorization;
}
/**
* Function used to remove an ACME challenge response
*/
async function challengeRemoveFn(authz, challenge, keyAuthorization) {
if (challenge.type !== 'http-01') {
throw new Error(`Unsupported challenge type ${challenge.type}`);
}
log(`Removing challenge response for ${authz.identifier.value}`);
delete challenges[`/.well-known/acme-challenge/${challenge.token}`];
}
/**
* Obtain a certificate using ACME. Optionally provide a key for "renewal".
*/
async function obtainCertificate(client, existingKey = null) {
/* Create CSR */
const [key, csr] = await acme.forge.createCsr({
commonName: domain,
altNames: [domain]
}, existingKey);
/* Certificate */
const cert = await client.auto({
csr,
email: email,
termsOfServiceAgreed: true,
challengeCreateFn,
challengeRemoveFn
});
log(`CSR:\n${csr.toString()}`);
log(`Private key:\n${key.toString()}`);
log(`Certificate:\n${cert.toString()}`);
return [cert, key]
};
var config = {
ca: fs.readFileSync(rootCertificate),
cert: undefined,
key: undefined,
ciphers: 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256',
minVersion: 'TLSv1.2',
};
async function startServer() {
/* Initialize ACME client */
const client = new acme.Client({
directoryUrl: acmeDirectoryUrl,
accountKey: await acme.forge.createPrivateKey()
});
// Start standalone ACME challenge server on port 80
var acmeServer = http.createServer(function(req, res) {
log(`Challenge server received request for ${req.url}`);
if (req.url in challenges) {
res.write(challenges[req.url]);
} else {
res.statusCode = 404;
}
res.end();
}).listen(80);
// Obtain certificates and configure a server security context
[config.cert, config.key] = await obtainCertificate(client);
var ctx = tls.createSecureContext(config);
// Start our HTTPS server
var httpsServer = https.createServer({
requestCert: true, // Request client certificates,
rejectUnauthorized: false, // but make them optional.
// Use the SNI callback to supply a fresh TLS context, with
// renewed certificates, for every connection.
SNICallback: (servername, cb) => {
cb(null, ctx);
}
}, (req, res) => {
res.writeHead(200);
const clientCert = req.socket.getPeerCertificate();
if (clientCert !== undefined && clientCert.subject !== undefined) {
// `tlsSocket.authorized` returns `true` if the client certificate
// is valid (not expired & signed by our CA).
if (req.socket.authorized === true) {
res.end(`Hello, ${clientCert.subject.CN}\n`);
} else {
log(`Connection had authorization error: ${req.socket.authorizationError}`);
res.end("Hello, invalid client certificate\n");
}
} else {
res.end('Hello, TLS\n');
}
}).listen(port);
console.log(`Listening on :${port} ...`);
// Renew periodically
async function renewCertificate() {
const cert = await acme.forge.readCertificateInfo(config.cert);
// Renew certificate if it's 2/3 expired.
const renewAfter = Math.floor((cert.notAfter.getTime() - cert.notBefore.getTime()) * 2/3);
const renewAt = new Date(cert.notBefore.getTime() + renewAfter);
if (renewAt <= Date.now()) {
log("Renewing certificate");
[config.cert, config.key] = await obtainCertificate(client, config.key);
ctx = tls.createSecureContext(config);
} else {
log(`Waiting to renew at: ${renewAt}`);
}
setTimeout(renewCertificate, renewFrequency);
}
setTimeout(renewCertificate, renewFrequency);
}
startServer();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment