ecdsa.com

Sign a message · Node.js

How to sign a message with ECDSA in Node.js

Signing a message with ECDSA in Node.js is a one-liner on top of node:crypto — the decisions are all about output format. The same key can emit ASN.1 DER (for OpenSSL, Python, Java) or raw 64-byte r‖s (for WebCrypto and JWS), and picking the wrong one is the top interop failure.

Tested with Node.js 26 (OpenSSL 3.x backend).

Same recipe in:Node.jsPythonGoOpenSSLWebCrypto
sign.mjs — sign with SHA-256, DER output (the default)
import { readFileSync, writeFileSync } from "node:fs";
import { createPrivateKey, sign } from "node:crypto";
 
const privateKey = createPrivateKey(readFileSync("key.pem")); // PKCS#8 or SEC1
const message = readFileSync("message.txt");
 
const der = sign("sha256", message, privateKey); // ASN.1 DER
writeFileSync("sig.der", der);
console.log("DER:", der.length, "bytes"); // 70-72 on P-256, varies per signature
raw r‖s output for WebCrypto / JWS consumers
const raw = sign("sha256", message, {
key: privateKey,
dsaEncoding: "ieee-p1363",
});
writeFileSync("sig.raw", raw);
console.log("raw:", raw.length, "bytes"); // always 64 on P-256

How it works

  1. createPrivateKey reads both modern PKCS#8 (BEGIN PRIVATE KEY) and legacy SEC1 (BEGIN EC PRIVATE KEY) PEMs.
  2. sign("sha256", message, key) hashes and signs in one call — hand it the message, not a digest.
  3. The default output is ASN.1 DER; dsaEncoding: "ieee-p1363" switches to fixed-length raw r‖s.
  4. 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:crypto does 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.

Related recipes