import "reflect-metadata"; // @peculiar/x509 2.x needs a Reflect polyfill, imported firstimport { writeFileSync } from "node:fs";import * as x509 from "@peculiar/x509"; const algorithm = { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" };const keys = await crypto.subtle.generateKey(algorithm, true, ["sign", "verify"]); const notBefore = new Date();const notAfter = new Date(notBefore.getTime() + 365 * 24 * 60 * 60 * 1000); // Without serialNumber, the library picks a random positive 128-bit serialconst cert = await x509.X509CertificateGenerator.createSelfSigned({ name: "CN=localhost", notBefore, notAfter, keys, signingAlgorithm: algorithm, extensions: [ new x509.SubjectAlternativeNameExtension([ { type: "dns", value: "localhost" }, { type: "ip", value: "127.0.0.1" }, ]), new x509.BasicConstraintsExtension(false, undefined, true), // CA:FALSE, critical new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true), new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage.serverAuth]), ],}); const pkcs8 = await crypto.subtle.exportKey("pkcs8", keys.privateKey);writeFileSync("key.pem", x509.PemConverter.encode(pkcs8, "PRIVATE KEY"));writeFileSync("cert.pem", cert.toString("pem"));// Use it right away: an HTTPS server, and a client that trusts exactly this certificateimport { readFileSync } from "node:fs";import https from "node:https"; const server = https.createServer({ key: readFileSync("key.pem"), cert: readFileSync("cert.pem") }, (req, res) => res.end("hello over TLS\n"));server.listen(8443, "127.0.0.1", () => { https.get("https://localhost:8443/", { ca: readFileSync("cert.pem") }, (res) => { res.pipe(process.stdout); // stdout never emits "finish": wait for the response's "end" res.on("end", () => server.close()); });});How it works
crypto.subtle.generateKeywith{ name: "ECDSA", namedCurve: "P-256" }creates an exportable key pair; the same algorithm object, withhash: "SHA-256", is thesigningAlgorithm.createSelfSignedusesnameas both subject and issuer; withoutserialNumber, it generates a random positive 16-byte serial.- The extensions are objects:
SubjectAlternativeNameExtensionwithdnsandipentries,BasicConstraintsExtension(false, undefined, true)for a critical CA:FALSE,KeyUsagesExtension(…, true)andExtendedKeyUsageExtension([serverAuth]). - In use.mjs,
https.createServer({ key, cert })serves TLS with the two files, andhttps.get(…, { ca })trusts this certificate as its only root;localhostmatches the DNS SAN. The server closes on the response'sendevent — piping intoprocess.stdoutnever emitsfinish.
Gotchas
- A client without
carejects the server withDEPTH_ZERO_SELF_SIGNED_CERT: self-signed certificate; if the root CA is installed locally, try running Node.js with --use-system-ca.NODE_EXTRA_CA_CERTS=cert.pemmakes bothhttpsandfetch()trust it without code changes. - A DNS SAN does not cover addresses: connecting to 127.0.0.1 with a certificate that lacks the IP SAN fails with
ERR_TLS_CERT_ALTNAME_INVALID: Hostname/IP does not match certificate's altnames: IP: 127.0.0.1 is not in the cert's list. NODE_TLS_REJECT_UNAUTHORIZED=0hides the error by switching off certificate checks for every connection of the process; Node.js warns that itmakes TLS connections and HTTPS requests insecure by disabling certificate verification.- @peculiar/x509 2.x needs a Reflect polyfill: without
import "reflect-metadata"before the library, it throwstsyringe requires a reflect polyfill.