import { readFileSync, writeFileSync } from "node:fs";import { createPrivateKey, sign } from "node:crypto"; const privateKey = createPrivateKey(readFileSync("key.pem")); // PKCS#8 or SEC1const message = readFileSync("message.txt"); const der = sign("sha256", message, privateKey); // ASN.1 DERwriteFileSync("sig.der", der);console.log("DER:", der.length, "bytes"); // 70-72 on P-256, varies per signatureconst raw = sign("sha256", message, { key: privateKey, dsaEncoding: "ieee-p1363",});writeFileSync("sig.raw", raw);console.log("raw:", raw.length, "bytes"); // always 64 on P-256How it works
createPrivateKeyreads both modern PKCS#8 (BEGIN PRIVATE KEY) and legacy SEC1 (BEGIN EC PRIVATE KEY) PEMs.sign("sha256", message, key)hashes and signs in one call — hand it the message, not a digest.- The default output is ASN.1 DER;
dsaEncoding: "ieee-p1363"switches to fixed-length rawr‖s. - Pick the format your verifier expects: DER for OpenSSL/Python/Java, raw for WebCrypto and anything JWS-shaped.
Gotchas
- Signatures differ on every run — ECDSA uses a fresh random nonce. That is correct behavior, not a bug; only RFC 6979 deterministic ECDSA (which
node:cryptodoes not implement) gives repeatable signatures. - Never hash before signing:
sign("sha256", …)already hashes, so pre-hashing double-hashes and produces a signature nobody else can verify. - About half of all signatures come out "high-S". Most verifiers accept them, but Bitcoin-style stacks require low-S — normalize (s → n − s) if your consumer enforces it; our converter flags high-S values.