const keyPair = await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256" }, true, // extractable — required for exportKey below ["sign", "verify"],); const pkcs8 = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);const spki = await crypto.subtle.exportKey("spki", keyPair.publicKey); const toPem = (buf, label) => { const b64 = btoa(String.fromCharCode(...new Uint8Array(buf))); const lines = b64.match(/.{1,64}/g).join("\n"); return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----\n`;}; console.log(toPem(pkcs8, "PRIVATE KEY"));console.log(toPem(spki, "PUBLIC KEY"));const jwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);// { kty: "EC", crv: "P-256", d: "...", x: "...", y: "..." }How it works
generateKeytakes the curve, the extractability flag and the allowed usages — the pair comes back as liveCryptoKeyobjects.exportKey("pkcs8")andexportKey("spki")return DER asArrayBuffers; base64 plus header lines turns them into the PEMs other tools read.- PEM base64 is wrapped at 64 characters per line — some strict parsers actually check.
Gotchas
extractable: falselocks the private key into the browser: signing works,exportKeythrows. Great for session keys, wrong for keys you need to back up — decide before generating.- Only P-256, P-384 and P-521 are available. No secp256k1, no Brainpool — for those, generate elsewhere or use a pure-JS library.
- Keys generated here are ephemeral: reloading the page loses them unless you export or store the
CryptoKeyin IndexedDB (which can persist even non-extractable keys).