Skip to content
ecdsa.com

Generate a CSR · Node.js

How to generate a CSR for an EC P-256 key with SANs in Node.js

node:crypto can create keys but not certificate requests, so Node.js code generates a CSR with @peculiar/x509: Pkcs10CertificateRequestGenerator.create takes a WebCrypto key pair, the subject and a SubjectAlternativeNameExtension, and signs the request with ECDSA P-256 and SHA-256. The run decodes the CSR with OpenSSL.

Tested with @peculiar/x509 2.2.0 (Node.js 24.22.0, node:crypto webcrypto, reflect-metadata 0.2.2).

csr.mjs — P-256 key and CSR with two SANs using @peculiar/x509
import "reflect-metadata"; // @peculiar/x509 2.x needs a Reflect polyfill, imported first
import { writeFileSync } from "node:fs";
import { webcrypto } from "node:crypto";
import * as x509 from "@peculiar/x509";
x509.cryptoProvider.set(webcrypto); // Node.js 20+: the global crypto works as well
const algorithm = { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" };
const keys = await webcrypto.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 = Buffer.from(await webcrypto.subtle.exportKey("pkcs8", keys.privateKey));
writeFileSync("key.pem", x509.PemConverter.encode(pkcs8, "PRIVATE KEY"));
writeFileSync("csr.pem", csr.toString("pem"));
console.log("signature valid:", await csr.verify());

How it works

  1. x509.cryptoProvider.set(webcrypto) tells the library which WebCrypto implementation to use. On current Node.js the global crypto works without it (the browser recipe of this task relies on that); setting it matters when another provider should sign.
  2. generateKey({ name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }, true, …) makes an exportable key pair; the same algorithm object is the signingAlgorithm.
  3. name takes an RFC 4514-style string; components are encoded in the order written.
  4. csr.toString("pem") returns the PEM, and PemConverter.encode(pkcs8, "PRIVATE KEY") wraps the exported PKCS#8 key.

Gotchas

  • @peculiar/x509 2.x needs a Reflect polyfill: without import "reflect-metadata" before the library, it fails with tsyringe requires a reflect polyfill. Please add 'import "reflect-metadata"' to the top of your entry point.
  • The key pair must be extractable (true) to save the private key; a non-extractable key can sign the CSR but can never be written to key.pem.
  • Without the SubjectAlternativeNameExtension, the CSR has only a CN, and certificates issued from it fail host-name checks in browsers and in Node.js's own tls module.
  • node:crypto alone cannot build the request: it can create keys and sign bytes, but has no PKCS#10 encoder — hence the library.

Related recipes