hashing · level 4

HMAC

Integrity + authenticity with a shared key.

150 XP

HMAC — Hash-based MAC

A plain hash gives you integrity — "has this message been changed?" — but not authenticity — "is this message from someone who holds the secret key?"

An HMAC gives you both. It's defined in RFC 2104:

Analogy

Picture a medieval noble sending a letter. The letter itself is plain writing — anyone who intercepts it can read the words. But the noble folds it, drips hot wax across the seam, and presses their personal signet ring into the wax. The recipient, who has the matching ring, can see at a glance that (a) the letter hasn't been opened en route and (b) it came from someone who owns the same ring. An HMAC is that wax seal: a plain hash confirms the wax is unbroken, but adding the shared-secret ring press proves it came from an authorised sender, not just anyone with a hash calculator.

HMAC(K, M) = H( (K' XOR opad) || H( (K' XOR ipad) || M ) )

where:

  • K' is the key, normalised to the hash's block size (hashed if too long, zero-padded if too short)
  • ipad = 0x36 repeated for the whole block
  • opad = 0x5C repeated for the whole block

Why the double-hash?

Two reasons.

1. It stops length-extension attacks. Plain SHA-2 suffers from length extension: given H(secret || m) and the length of secret, an attacker can compute H(secret || m || padding || extra) without ever knowing secret. The outer hash in HMAC kills this, because the attacker would have to extend H(...)'s final digest, which isn't usable state.

2. It separates inner and outer key derivation. If one pad leaked, the other one wouldn't. The two XOR constants (ipad/opad) differ by a huge Hamming weight, so even small key leaks don't collapse.

Use cases

  • Webhooks — Stripe, GitHub, Shopify all sign payloads with HMAC-SHA256 under a secret you share with them. You verify before trusting.
  • Cookies / session tokens — cookies of the form payload.hmac let servers detect tampering without database lookups.
  • API request signing — AWS v4, Twilio, Slack. Client HMACs canonicalised request → server re-derives and compares.
  • JWTs with HS256 — HMAC-SHA256 is the "HS" in HS256. (Prefer asymmetric RS256/EdDSA when multiple verifiers exist — they don't need the secret.)

Constant-time comparison — important

// WRONG — leaks how many leading bytes matched via timing
if (computedHex === providedHex) { /* accept */ }

// RIGHT — constant-time compare
// Node:
const ok = crypto.timingSafeEqual(Buffer.from(computedHex, "hex"), Buffer.from(providedHex, "hex"));
// Browser:
// Pad/compare byte-by-byte with XOR accumulation to stay constant-time.

Without this, an attacker can use timing differences to guess the tag one byte at a time.

Key rotation

HMACs are only secure while the key is secret. Rotate periodically:

  1. Add the new key, start signing with it, accept both old and new for verification.
  2. After grace period, stop accepting the old key.
  3. Delete the old key.

HMAC vs signature

  • HMAC — shared secret, symmetric. Anyone who can verify can forge. Fast.
  • Signature (RSA/Ed25519) — private key signs, public key verifies. Verifiers can't forge. Slower but much better trust model.

Use HMAC when the verifier is trusted (your own server, a known partner). Use signatures when you're broadcasting to untrusted verifiers.