import "reflect-metadata"; // @peculiar/x509 2.x needs a Reflect polyfill, imported firstimport * as x509 from "@peculiar/x509"; // built on WebCrypto; works in browsers unchanged // The key pair never leaves the page: only the CSR (public) is sent to the CAasync function createCsr() { const algorithm = { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }; const keys = await crypto.subtle.generateKey(algorithm, true, ["sign", "verify"]); const csr = await x509.Pkcs10CertificateRequestGenerator.create({ name: "C=US, O=Example Corp, CN=api.example.com", keys, signingAlgorithm: algorithm, extensions: [ new x509.SubjectAlternativeNameExtension([ { type: "dns", value: "api.example.com" }, { type: "dns", value: "www.example.com" }, ]), ], }); const pkcs8 = await crypto.subtle.exportKey("pkcs8", keys.privateKey); return { csrPem: csr.toString("pem"), keyPem: x509.PemConverter.encode(pkcs8, "PRIVATE KEY") };}How it works
crypto.subtle.generateKeywith{ name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }creates the key pair; the library picks up the globalcryptoby default.Pkcs10CertificateRequestGenerator.createencodes the subject, the public key and the extensionRequest with the SANs, and signs the result with the private key.csr.toString("pem")is what the CA needs; the PKCS#8 export of the private key is for the user to keep.- The run executes the same function in Node.js and checks the CSR with
openssl req.
Gotchas
- The library needs a Reflect polyfill: import
reflect-metadata(or core-js'sreflect) before@peculiar/x509, or it throws at load time withtsyringe requires a reflect polyfill. - Exporting the private key requires
extractable: true. To keep the key inside the browser, generate it non-extractable and store theCryptoKeyin IndexedDB instead of exporting PKCS#8. crypto.subtleis available only in secure contexts — HTTPS orlocalhost.- A key generated in a browser profile is lost with that profile's data; make sure the user saves it, or the certificate becomes useless at the next cleanup.