Skip to content
ecdsa.com

Generate a CSR · WebCrypto

How to generate a CSR for an EC P-256 key with SANs in WebCrypto

WebCrypto has no certificate API, but a CSR can still be generated in the browser: crypto.subtle creates the P-256 key pair, and @peculiar/x509 — a library built on WebCrypto — assembles and signs the PKCS#10 request with the SANs. The private key never has to leave the page; only the CSR goes to the CA.

Tested against the WebCrypto API in @peculiar/x509 2.2.0 (WebCrypto in Node.js 24.22.0 (globalThis.crypto), reflect-metadata 0.2.2) — the same crypto.subtle interface browsers implement.

createCsr — key pair and CSR in the browser with @peculiar/x509
import "reflect-metadata"; // @peculiar/x509 2.x needs a Reflect polyfill, imported first
import * 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 CA
async 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

  1. crypto.subtle.generateKey with { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" } creates the key pair; the library picks up the global crypto by default.
  2. Pkcs10CertificateRequestGenerator.create encodes the subject, the public key and the extensionRequest with the SANs, and signs the result with the private key.
  3. csr.toString("pem") is what the CA needs; the PKCS#8 export of the private key is for the user to keep.
  4. 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's reflect) before @peculiar/x509, or it throws at load time with tsyringe requires a reflect polyfill.
  • Exporting the private key requires extractable: true. To keep the key inside the browser, generate it non-extractable and store the CryptoKey in IndexedDB instead of exporting PKCS#8.
  • crypto.subtle is available only in secure contexts — HTTPS or localhost.
  • 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.

Related recipes