How to generate an HTTPS certificate with node-forge
A script that returns a self-signed certificate in Javascript
Generating HTTPS certificates
Many times I need to generate an HTTPS certificate for a development server. For example, I can use the https
module to start a webserver:
import https from "https";
// ... get a certificate somehow
// start HTTPS server
const app = https.createServer({key: cert.privateKey, cert: cert.cert}, async (req, res) => {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end("Hello world!");
});
app.listen(8081);
But the https.createServer
needs a certificate. How to generate it?
The official documentation as well as most articles recommend running OpenSSL commands in advance:
openssl genrsa -out key.pem
openssl req -new -key key.pem -out csr.pem
openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
rm csr.pem
Then on the Javascript-side, dealing with certificates is a matter of reading files:
const options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};
But, as usual, I wanted something simpler: an in-process generator that requires no setup and just works when I run the Javascript code.
PEM
For this I used to use the pem library. It has a createCertificate
function thats result the https.createServer
can use:
import pem from "pem";
const keys = await pem.promisified.createCertificate({days: 1, selfSigned: true});
https.createServer({key: keys.clientKey, cert: keys.certificate}, (req, res) => {
// ...
}).listen(8081);
This works great, but under the hood it simply calls OpenSSL. I always found it a possible problem.
And indeed it broke recently because of this. So I continued searching for some other solution.
Node-forge
During this search, I stumbled upon this article: https://www.techengineer.one/how-to-generate-x-509v3-self-signed-certificate-in-pem-format-with-node-js/. It describes the solution for my problem.
With a few extra tweaks, this is the solution I ended up with:
import forge from "node-forge";
import crypto from "crypto";
export const generateCert = ({altNameIPs, altNameURIs, validityDays}) => {
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
// NOTE: serialNumber is the hex encoded value of an ASN.1 INTEGER.
// Conforming CAs should ensure serialNumber is:
// - no more than 20 octets
// - non-negative (prefix a '00' if your value starts with a '1' bit)
cert.serialNumber = '01' + crypto.randomBytes(19).toString("hex"); // 1 octet = 8 bits = 1 byte = 2 hex chars
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * (validityDays ?? 1));
const attrs = [{
name: 'countryName',
value: 'AU'
}, {
shortName: 'ST',
value: 'Some-State'
}, {
name: 'organizationName',
value: 'Internet Widgits Pty Ltd'
}];
cert.setSubject(attrs);
cert.setIssuer(attrs);
// add alt names so that the browser won't complain
cert.setExtensions([{
name: 'subjectAltName',
altNames: [
...(altNameURIs !== undefined ?
altNameURIs.map((uri) => ({type: 6, value: uri})) :
[]
),
...(altNameIPs !== undefined ?
altNameIPs.map((uri) => ({type: 7, ip: uri})) :
[]
)
]
}]);
// self-sign certificate
cert.sign(keys.privateKey);
// convert a Forge certificate and private key to PEM
const pem = forge.pki.certificateToPem(cert);
const privateKey = forge.pki.privateKeyToPem(keys.privateKey)
return {
cert: pem,
privateKey
};
}
This generates an https
-compatible certificate:
const cert = generateCert({altNameIPs: ["127.0.0.1", "127.0.0.2"], validityDays: 2});
const app = https.createServer({key: cert.privateKey, cert: cert.cert}, async (req, res) => {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end("Hello world!");
});
app.listen(8081);
I added the ability to specify the alt name IP and domains, as well as a validity in days. But otherwise, this is the perfect solution: fully JS-based, no outside dependency, and does not require any additional setup.
How does it work?
It uses node-forge which is a pure JS implementation of all primitives related to TLS and other cryptographic tools. It can do a lot more than just generating and signing certificates, and its documentation contains a ton of examples.