import { readFileSync, writeFileSync } from "node:fs";import { createPrivateKey, createPublicKey } from "node:crypto"; const privateKey = createPrivateKey(readFileSync("key.pem"));const publicKey = createPublicKey(privateKey); // derives the public half const pem = publicKey.export({ type: "spki", format: "pem" });writeFileSync("pub.pem", pem);console.log(pem); // -----BEGIN PUBLIC KEY-----publicKey.export({ type: "spki", format: "der" }); // Buffer (binary SPKI)publicKey.export({ format: "jwk" }); // { kty: 'EC', crv, x, y }How it works
createPublicKeyaccepts a privateKeyObject(or private PEM directly) and returns the corresponding public key — EC public keys are embedded in, or derivable from, the private key.type: "spki"selects SubjectPublicKeyInfo — theBEGIN PUBLIC KEYcontainer. This is what OpenSSL, Go, Python and WebCrypto expect.format: "jwk"is the right shape for JOSE endpoints and WebCrypto'simportKey("jwk", …).
Gotchas
- Node never writes files for you — a script that only calls
exportand logs will leave no pub.pem behind. Pairexportwith an explicit write, as above. - The PEM label matters downstream: SPKI gives
BEGIN PUBLIC KEY. If a consumer demandsBEGIN EC PUBLIC KEY(rare, legacy), that is a different serialization Node does not emit — update the consumer, not the label. - Copy-pasting PEM through chat or editors can smuggle in smart quotes, CRLF endings or lost final newlines; when a consumer rejects your export, hexdump the file before doubting the key.