What is HMAC (and why SHA-256 alone isn't enough)?
HMAC is how you prove a message wasn't tampered with. It's SHA-256 plus a clever wrapping you should never try to reinvent.
# The problem
You want to send a message M from A to B and prove on arrival that:
1. It really came from A (who shares a secret with B)
2. Nobody modified a byte in transit
Simple-sounding. Naive solutions all fail.
# Why you can't just prepend a secret
"Just send SHA-256(secret + M) alongside the message."
For Merkle-Damgård hashes (SHA-1, SHA-2 family), this is vulnerable to a length-extension attack. An attacker who sees the hash can append their own bytes to M and compute a valid hash for M + attacker_bytes without knowing the secret.
HMAC fixes this.
# What HMAC is
HMAC wraps a hash function with two extra passes and a pair of constants. The construction:
HMAC(K, M) = H((K ⊕ opad) || H((K ⊕ ipad) || M))
Where:
- H is your hash function (SHA-256, SHA-384, etc.)
- K is the secret key (padded/hashed to the block size)
- ipad = 0x36 repeated
- opad = 0x5C repeated
- || is concatenation, ⊕ is XOR
You don't need to understand the internals to use it. But recognize that HMAC is not just "hash with a secret" — the two-layer structure is what defeats length extension.
# Where you'll meet HMAC
- Webhooks (Stripe, GitHub, Slack): the provider HMAC-signs the body with a secret you share, you verify before processing.
- AWS Signature V4: every API request to AWS is HMAC-signed.
- JWT HS256: the signature part of a JWT with algorithm HS256 is HMAC-SHA256.
- Session cookies: many frameworks sign cookies with HMAC so the browser can't forge them.
# How to verify a webhook
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, headerSignature, secret) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
return timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(headerSignature, "hex"),
);
}
<div class="callout callout-warning" role="note"><div class="callout-title">Warning</div><div class="callout-body"><p>Use a <strong>constant-time comparison</strong> (like <code>timingSafeEqual</code>), not <code>===</code>. String equality leaks the first position that differs via timing, enabling attackers to brute-force the signature byte by byte.</p></div></div>
# HMAC vs signatures
- HMAC uses a shared secret. Both sides can generate and verify. Fast.
- Digital signatures (RSA, Ed25519, ECDSA) use public/private keys. Only the holder of the private key can sign; anyone with the public key can verify. Slower, but doesn't require sharing the secret.
Use HMAC when you control both ends. Use signatures when you need third parties to verify without trusting each other.
# Generate and verify HMAC in your browser
Paste a message and a secret into our HMAC Generator — it produces SHA-1, SHA-256, SHA-384 and SHA-512 variants, in hex and Base64.
# Related tools
- HMAC Generator
- Hash Generator — for plain (unkeyed) hashes
- JWT Decoder — HS256 JWTs use HMAC-SHA256
Frequently asked questions
›Is HMAC the same as just hashing secret + message?
No, and it matters. Naive `SHA-256(secret + message)` is vulnerable to length-extension attacks on Merkle-Damgård hashes like SHA-2. HMAC's double-hash construction prevents that.
›Can I use HMAC with SHA-1?
HMAC-SHA1 is still considered secure for authentication (HMAC doesn't need collision resistance like signing does). But SHA-256 is just as fast and forward-safe; no reason to pick SHA-1 on new code.
›How long should my HMAC key be?
At least the output size of the hash (32 bytes for HMAC-SHA256). Longer doesn't help; shorter is acceptable but wasteful.