iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Digital Signatures

A digital signature proves a message came from the holder of a private key and was not modified. It is one-way: anyone with the public key verifies; only the key holder signs. Use it for JWTs, software updates, webhook authenticity — anywhere you need authenticity over an untrusted channel.

Ed25519, RSA-PSS, ECDSA, verify path

EXAMPLE
// SCENARIO — webhook signing and verification, defensive perspective.

// ─── 1) Ed25519 — modern default ───────────────────────────────
// Fast, small keys (32 bytes), deterministic, side-channel resistant.

import crypto from 'node:crypto';

const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');

const msg = Buffer.from('order:12345:paid');
const sig = crypto.sign(null, msg, privateKey);     // 64 bytes
const ok  = crypto.verify(null, msg, publicKey, sig);
console.log(ok);                                     // true

// Export / import
const pem = privateKey.export({ type: 'pkcs8', format: 'pem' });
const pub = publicKey.export({ type: 'spki', format: 'pem' });

// ─── 2) RSA-PSS — when you need RSA (interop, HSMs) ────────────
const { publicKey: rsaPub, privateKey: rsaPriv } = crypto.generateKeyPairSync('rsa', {
    modulusLength: 3072,                            // 2048 minimum; 3072+ preferred
});

const sig2 = crypto.sign('sha256', msg, {
    key: rsaPriv,
    padding: crypto.constants.RSA_PKCS1_PSS_PADDING, // PSS, not v1.5
    saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
});

const ok2 = crypto.verify('sha256', msg, {
    key: rsaPub,
    padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
    saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
}, sig2);

// Avoid RSA-PKCS#1 v1.5 for new code (still legal for some interop; PSS is safer).

// ─── 3) ECDSA (P-256) — short signatures, broad support ────────
const { publicKey: ecPub, privateKey: ecPriv } = crypto.generateKeyPairSync('ec', {
    namedCurve: 'P-256',
});
const sig3 = crypto.sign('sha256', msg, ecPriv);
const ok3  = crypto.verify('sha256', msg, ecPub, sig3);

// Avoid: secp192r1 (too short), curves with weak parameters, ECDSA without
// guaranteed RNG quality (deterministic ECDSA / Ed25519 sidestep RNG failure).

// ─── 4) Signing a webhook payload ──────────────────────────────
function signPayload(body, privateKey, keyId) {
    const ts    = Math.floor(Date.now() / 1000);
    const data  = Buffer.from(`${ts}.${body}`);
    const sig   = crypto.sign(null, data, privateKey).toString('base64');
    return { ts, sig, keyId };
}

// Send headers like:
//   X-Signature-Timestamp: 1717000000
//   X-Signature-KeyId: 2024-q3
//   X-Signature: base64sig…

// ─── 5) Verifying — the critical path ──────────────────────────
const keyring = new Map([
    ['2024-q3', publicKey],
    ['2024-q4', publicKey],     // rotation overlap
]);

function verifyWebhook(req) {
    const ts    = parseInt(req.get('X-Signature-Timestamp'), 10);
    const keyId = req.get('X-Signature-KeyId');
    const sigB64 = req.get('X-Signature');

    if (!ts || !keyId || !sigB64) return false;

    // 1. Reject stale timestamps (replay protection)
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - ts) > 300) return false;     // 5 min window

    // 2. Look up the public key for this key id
    const pk = keyring.get(keyId);
    if (!pk) return false;

    // 3. Reconstruct the signed bytes
    const data = Buffer.from(`${ts}.${req.rawBody.toString('utf8')}`);

    // 4. Verify
    try {
        return crypto.verify(null, data, pk, Buffer.from(sigB64, 'base64'));
    } catch {
        return false;
    }
}

// IMPORTANT: webhook bodies must be verified BEFORE JSON.parse runs.
// JSON re-serialization changes whitespace → signature mismatch.
// In Express:
//   app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));

// ─── 6) JWT signatures — use a library ─────────────────────────
import * as jose from 'jose';

const jwt = await new jose.SignJWT({ sub: 'user_42', role: 'admin' })
    .setProtectedHeader({ alg: 'EdDSA' })
    .setIssuer('https://app.example.com')
    .setAudience('api')
    .setExpirationTime('15m')
    .sign(privateKey);

const { payload } = await jose.jwtVerify(jwt, publicKey, {
    issuer:   'https://app.example.com',
    audience: 'api',
});

// Pitfalls:
//   • alg=none in the header → reject
//   • RSA public key being verified against an HS256 token → key confusion
//   • Forgetting audience/issuer checks → token replay across services

// ─── 7) Key rotation ───────────────────────────────────────────
// Always sign with the newest key, verify against the keyring.
// Publish a /.well-known/jwks.json or equivalent so consumers can fetch keys
// by key id and cache them.

// ─── 8) What signatures are NOT ────────────────────────────────
//   • Encryption — signatures don't hide the data, only authenticate it
//   • Authorisation — signature == authentic, not == permitted
//   • Hash — a hash has no key; anyone can compute it
//   • MAC — MAC uses a shared secret; signature uses asymmetric keys

// ─── CHECKLIST ─────────────────────────────────────────────────
// 1. Default to Ed25519 for new systems
// 2. RSA: 3072-bit or larger, PSS padding only
// 3. Always include + verify a timestamp; reject stale requests
// 4. Verify before parsing the body
// 5. Key ID in the request → rotate keys safely
// 6. Use a vetted library for JWTs; reject alg=none and pin acceptable algs
// 7. Use timingSafeEqual / library verify (constant-time)

Why it matters

Use Ed25519 by default; reach for RSA-PSS or ECDSA only when interop demands it. Whatever you pick, sign a timestamp alongside the payload, verify against a key id, and parse the body only after the signature checks out — otherwise replay and key-confusion bugs creep back in.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
import { sign, verify, generateKeyPairSync } from 'crypto';
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
const sig = sign(null, Buffer.from('hi'), privateKey);
const ok  = verify(null, Buffer.from('hi'), publicKey, sig);
Try it Yourself »

Discussion

Loading…