All articles

HMAC: how a keyed hash differs from a regular hash

GitHub, Stripe, and dozens of other platforms sign every webhook payload with HMAC-SHA256: GitHub sends an X-Hub-Signature-256 header, Stripe sends Stripe-Signature, and both expect the receiving server to recompute the HMAC locally using a shared secret and compare it against the header. This single pattern — sign with a shared secret, verify by recomputation — is how most of the API economy proves "this request really came from us."

Why you need a secret key, not just a hash

A plain hash like SHA-256 only proves data wasn't corrupted in transit — not who sent it. If a server just hashed the webhook body and compared it to a hash in the request, an attacker could forge their own fake payload and attach a matching hash. HMAC fixes this by mixing in a secret key that only the sender and receiver know: without it, producing a valid signature is infeasible even if the attacker knows exactly which hash function is used.

Why it's not just "hash the key plus the message"

Naively concatenating the key and message before hashing (e.g. hash(key + message)) is vulnerable to length extension attacks on some hash functions — an attacker can sometimes append data and compute a new valid hash without ever knowing the full key. HMAC uses a specific construction with double hashing and inner/outer padding that closes off this entire attack class.

Where else HMAC shows up

Beyond webhook signing, HMAC authenticates API requests (AWS's SigV4 request-signing scheme is built on it), and it's the core primitive inside TOTP — the algorithm behind the six-digit codes in authenticator apps.

Why you'd need this

  • Verifying the authenticity of webhooks from payment providers or other third-party services.
  • Signing API requests without ever sending the secret key itself over the network.
  • Understanding the inner workings of TOTP codes and session tokens.

Constant-time comparison

Comparing a received signature with the expected one using a plain === is unsafe: such an operator typically stops at the first differing byte, and the comparison's timing leaks to an attacker how many leading bytes they guessed correctly. Production-grade webhook handlers always use a constant-time comparison function — such as hash_equals in PHP or crypto.timingSafeEqual in Node.js — which checks every byte regardless of where the first mismatch occurs.

Try the tool