The Web Crypto API in Node

globalThis.crypto in Node is the same Crypto object browsers expose, and crypto.subtle implements the W3C Web Cryptography API. Everything is promise-based, keys are opaque CryptoKey objects instead of buffers, and the algorithm arrives as an object rather than a string. The same code runs in a browser and a Cloudflare 2 Worker, which is why the jose 7,812 library in JSON Web Tokens is built on it.

ECDSA signing and HKDF key derivation through crypto.subtleJavaScript
const { subtle } = globalThis.crypto;
const enc = new TextEncoder(), hex = (b) => Buffer.from(b).toString('hex');
const alg = { name: 'ECDSA', hash: 'SHA-256' };
const ec = { name: 'ECDSA', namedCurve: 'P-256' };
const pair = await subtle.generateKey(ec, true, ['sign', 'verify']);
const sig = await subtle.sign(alg, pair.privateKey, enc.encode('amount=100'));
const ok = (m) => subtle.verify(alg, pair.publicKey, sig, enc.encode(m));
console.log('signature :', sig.byteLength, 'bytes of raw r||s');
console.log('verify    :', await ok('amount=100'), '| tampered:', await ok('amount=900'));
const raw = crypto.getRandomValues(new Uint8Array(32));
const master = await subtle.importKey('raw', raw, 'HKDF', false, ['deriveBits']);
for (const info of ['cookie-signing', 'field-encryption']) {
  const bits = await subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256',
    salt: new Uint8Array(16), info: enc.encode(info) }, master, 256);
  console.log(info.padEnd(16), hex(bits).slice(0, 32));
}
Output
signature : 64 bytes of raw r||s
verify    : true | tampered: false
cookie-signing   4dc92b539ee8fca73a6c079a276cce71
field-encryption 936a7671934f2562f981e530bf4c1e90

Web Crypto returns ECDSA signatures as raw r||s — 64 bytes for P-256 — where node:crypto returns the DER wrapping used by TLS and X.509. The two need conversion, the usual reason a browser signature fails to verify in Node.

The HKDF block is why this API is worth learning even if you never leave Node: from one 32-byte master secret it derives independent keys that cannot recover each other or the master, purely by changing the info string — one SECRET_KEY variable, but cookie signing and field encryption each get a key of their own. Two traps: generateKey needs extractable: true before exportKey works, and the global methods are bound to the Crypto instance, so a destructured getRandomValues throws ERR_INVALID_THIS.