ecdsa.com

Sign a message · WebCrypto

How to sign a message with ECDSA in WebCrypto

crypto.subtle.sign produces ECDSA signatures in the browser with no dependencies — and in Node.js, which ships the same WebCrypto API. The output is always raw r‖s (IEEE P1363), exactly 64 bytes on P-256: perfect for JWS, and in need of conversion for OpenSSL.

Tested against the WebCrypto API in Node.js 26 — the same crypto.subtle interface browsers implement.

Same recipe in:Node.jsPythonGoOpenSSLWebCrypto
import a PKCS#8 private key and sign
function pemToBytes(pem) {
const b64 = pem.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
}
 
const privateKey = await crypto.subtle.importKey(
"pkcs8", // "BEGIN PRIVATE KEY" body
pemToBytes(privateKeyPem),
{ name: "ECDSA", namedCurve: "P-256" },
false,
["sign"],
);
 
const signature = new Uint8Array(
await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
privateKey,
new TextEncoder().encode(message),
),
);
console.log(signature.length); // always 64: raw r‖s, not DER

How it works

  1. importKey("pkcs8", …) accepts only the modern PKCS#8 layout. Legacy BEGIN EC PRIVATE KEY files must be converted once: openssl pkey -in legacy.pem -out pkcs8.pem.
  2. The hash is chosen at sign time in the algorithm object — the key itself carries only the curve.
  3. The resolved ArrayBuffer is fixed-length raw output: 64 bytes (P-256), 96 (P-384), 132 (P-521).
  4. TextEncoder is only for string messages — binary payloads (file bytes, hashes of larger documents) go into subtle.sign as a Uint8Array directly.

Gotchas

  • The signature is not DER: openssl dgst -verify and Python's default verify reject it. Convert raw → DER for those consumers, or verify in Node with dsaEncoding: "ieee-p1363".
  • Setting extractable to false for signing keys is good hygiene — signing still works; only exportKey is blocked.
  • Each call yields a different signature for the same input (random nonce, as the ECDSA spec requires) — caching or deduplicating by signature bytes is a design error.

Related recipes