ecdsa.com

Convert DER ⇄ raw · WebCrypto

How to convert an ECDSA signature between DER and raw in WebCrypto

WebCrypto only understands raw r‖s signatures, so a DER signature from OpenSSL or a backend must be converted in browser JavaScript before crypto.subtle.verify can accept it. The conversion is pure byte work — no crypto, no dependencies — and fits in one small function.

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

Same recipe in:Node.jsPythonGoWebCrypto
derToRaw — plain browser JS, no libraries
function derToRaw(der, size = 32) { // size: 32 P-256, 48 P-384, 66 P-521
if (der[0] !== 0x30) throw new Error("not a DER SEQUENCE");
let o = der[1] & 0x80 ? 2 + (der[1] & 0x7f) : 2; // skip long-form length
const readInt = () => {
if (der[o++] !== 0x02) throw new Error("expected INTEGER");
const len = der[o++];
let v = der.subarray(o, o + len);
o += len;
while (v.length > size && v[0] === 0x00) v = v.subarray(1);
const out = new Uint8Array(size);
out.set(v, size - v.length); // left-pad to fixed width
return out;
};
const raw = new Uint8Array(2 * size);
raw.set(readInt(), 0);
raw.set(readInt(), size);
return raw;
}
use it: verify an OpenSSL (DER) signature in the browser
const rawSig = derToRaw(derSignature); // derSignature: Uint8Array
 
const ok = await crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" },
publicKey,
rawSig,
new TextEncoder().encode(message),
);

How it works

  1. DER wraps r and s in SEQUENCE/INTEGER framing with minimal-length rules; raw format is just both integers at fixed width, concatenated.
  2. The function strips the sign-padding 0x00 bytes DER adds and left-pads each value to size — the two operations that differ between the formats.
  3. Work on Uint8Array: if the signature arrived as an ArrayBuffer (e.g. from fetch), wrap it first — new Uint8Array(buffer).

Gotchas

  • Skipping conversion and passing DER to subtle.verify does not throw — it resolves to false, indistinguishable from a bad signature. Length is your tell: DER is ~70–72 bytes on P-256, raw is exactly 64.
  • Both halves need the padding treatment: a conversion that handles r but slices s naively still fails on the subset of signatures where s has a padding byte.
  • The reverse direction (raw → DER, for sending WebCrypto signatures to OpenSSL) must strip zeros and re-add 0x00 when the top bit is set — see the Node.js recipe for a tested rawToDer.

Related recipes