Cryptography
HMAC
A secret key plus a hash — the standard way to prove a message wasn't tampered with
HMAC is a message authentication code built from a hash function and a secret key, hashing the message twice with key-derived inner and outer pads to prove integrity and origin while resisting length-extension attacks.
- Full nameHash-based MAC
- StandardRFC 2104 / FIPS 198-1
- Hash calls2 nested
- Padsipad 0x36 · opad 0x5c
- ProvidesIntegrity + authenticity
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
How HMAC works
You want to send a message over an open channel and let the receiver verify two things: the message wasn't altered in transit (integrity), and it came from someone who shares your secret key (authenticity). A plain hash like SHA-256 gives you integrity against accidental corruption — but anyone can recompute it, so it proves nothing about who sent the message. A message authentication code closes that gap by mixing a secret key into the hash. Only holders of the key can produce a valid tag, and the receiver recomputes it to check.
The obvious construction — tag = H(key ‖ message), the "secret-prefix MAC" — looks fine and is catastrophically broken for the hashes everyone actually uses. HMAC is the fix that Bellare, Canetti, and Krawczyk published in 1996, standardized as RFC 2104 and FIPS 198-1, and it is the keyed hash underneath TLS, JWT, AWS request signing, OAuth 1.0, and password-derivation functions like PBKDF2.
The definition is one line:
HMAC(K, m) = H( (K' ⊕ opad) ‖ H( (K' ⊕ ipad) ‖ m ) )
Reading it from the inside out:
- Normalize the key to K'. If the key is longer than the hash's block size B (64 bytes for SHA-256), replace it with
H(key). Then right-pad with zero bytes up to B. Now K' is exactly one block. - Inner hash. XOR K' with
ipad(the byte0x36repeated B times), prepend that to the message, and hash. This produces a short digest that already depends on the key. - Outer hash. XOR K' with
opad(0x5crepeated B times), prepend it to the inner digest, and hash again. The result is the tag — 32 bytes for HMAC-SHA256.
Because 0x36 ⊕ 0x5c = 0x6a, the inner and outer keys differ in four bits of every byte, so they behave like two independent keys derived from one secret — exactly what the security proof requires.
Why two hashes: the length-extension attack
The whole reason for the nesting is a structural weakness in Merkle–Damgård hashes — MD5, SHA-1, and the SHA-2 family (SHA-224/256/384/512). These functions process the message block by block, and the output is the internal state after the last block. That means if you know H(secret ‖ m) and the length of secret ‖ m, you can resume the computation and compute H(secret ‖ m ‖ padding ‖ extra) for any extra you like — without knowing the secret.
So the secret-prefix MAC H(key ‖ message) lets an attacker forge a valid tag for a message they never authorized. This is not theoretical: Flickr's API signing scheme was broken this way in 2009. HMAC defeats it because the attacker only ever sees the outer hash output. The inner hash's extendable state is wrapped inside a second, independently-keyed hash, so there is nothing to extend.
SHA-3 and BLAKE2/BLAKE3 are not Merkle–Damgård and are immune to length extension, which is why BLAKE2 and SHA-3 offer a built-in keyed mode and you generally don't need HMAC around them. But because so much infrastructure standardized on SHA-256, HMAC-SHA256 remains the default keyed hash in the wild.
When to use HMAC — and when not to
- Verifying API requests. Sign the canonicalized request with HMAC-SHA256 and a shared secret; the server recomputes and rejects mismatches. This is the core of AWS Signature v4 and webhook signing (Stripe, GitHub, Slack).
- Stateless session tokens. JWTs with the
HS256algorithm are HMAC-SHA256 over the header and payload — the server can verify a token without a database lookup. - Key derivation. HKDF and PBKDF2 are built on HMAC; it is the standard PRF for expanding or stretching keys.
- Cookie integrity. Signed cookies (Rails, Django, Flask) HMAC the payload so a client can hold the cookie but cannot tamper with it.
Reach for something else when:
- You need confidentiality. HMAC does not hide the message. Use authenticated encryption (AES-GCM, ChaCha20-Poly1305) instead, or encrypt-then-MAC.
- You need public verification. HMAC is symmetric: anyone who can verify can also forge. If a third party must verify without being able to sign, use a digital signature (RSA, ECDSA, Ed25519).
- You're hashing passwords. HMAC is fast by design; password storage needs a deliberately slow function like bcrypt, scrypt, or Argon2.
HMAC vs other authentication primitives
| HMAC | Plain hash | CMAC (AES) | Poly1305 | KMAC (SHA-3) | Ed25519 signature | |
|---|---|---|---|---|---|---|
| Key type | Symmetric secret | None | Symmetric (AES key) | One-time symmetric | Symmetric secret | Asymmetric keypair |
| Public verification | No | n/a | No | No | No | Yes |
| Built on | Hash (SHA-2) | Hash | Block cipher | Polynomial in GF(2¹³⁰−5) | Keccak / SHA-3 | Edwards curve |
| Length-extension safe | Yes | No (MD-based) | Yes | Yes | Yes | Yes |
| Relative speed | Fast (2 hash calls) | Fastest | Fast | Very fast | Fast | ~100× slower |
| Typical use | TLS, JWT, API signing | Checksums, dedup | Hardware, constrained MAC | AEAD (with ChaCha20) | Modern SHA-3 stacks | SSH, certs, signing |
The headline distinction: HMAC and the other MACs are symmetric — verification and forgery use the same key — while a signature is asymmetric, so the world can verify but only the holder of the private key can sign. HMAC wins on speed and simplicity wherever both parties already share a secret; signatures win when they don't.
What the numbers actually say
- Two hash calls, near-zero overhead. HMAC is one inner hash plus a tiny outer hash over a single block, so for messages of any real size the cost is essentially one hash pass. On a modern x86 core with SHA extensions, HMAC-SHA256 runs at roughly 1.5–2 GB/s.
- Forgery probability ≈ 2⁻²⁵⁶. Without the key, guessing a valid HMAC-SHA256 tag is a 256-bit search. There is no shortcut: the best known attacks need on the order of 2¹²⁸ queries, far beyond feasibility.
- Key-normalization cliff. A key longer than 64 bytes is hashed to 32 bytes first — so a 1,000-byte "key" carries at most 256 bits of effective strength, and worse, two different long keys that hash to the same digest become identical secrets.
- Timing leak is cheap to exploit. A non-constant-time tag comparison turns a 2²⁵⁶ brute force into roughly 32 × 256 ≈ 8,000 timed requests to recover a 32-byte tag one byte at a time.
JavaScript implementation
In production you would never hand-roll HMAC — use the platform's audited primitive:
import { createHmac, timingSafeEqual } from 'node:crypto';
function sign(key, message) {
return createHmac('sha256', key).update(message).digest(); // 32-byte Buffer
}
function verify(key, message, tag) {
const expected = sign(key, message);
// Length guard: timingSafeEqual throws if lengths differ.
if (expected.length !== tag.length) return false;
return timingSafeEqual(expected, tag); // constant-time — no early return
}
const key = Buffer.from('a-32-byte-shared-secret-key!!!!!');
const tag = sign(key, 'transfer $100 to alice');
console.log(verify(key, 'transfer $100 to alice', tag)); // true
console.log(verify(key, 'transfer $900 to mallory', tag)); // false
But the from-scratch version makes the structure concrete — note that it is the textbook nesting, byte for byte:
import { createHash } from 'node:crypto';
const B = 64; // SHA-256 block size in bytes
function hmacSha256(key, message) {
// 1. Normalize the key to one block.
if (key.length > B) key = createHash('sha256').update(key).digest();
const k = Buffer.alloc(B); // zero-padded to block size
key.copy(k);
// 2. Build the two key-derived pads.
const inner = Buffer.alloc(B), outer = Buffer.alloc(B);
for (let i = 0; i < B; i++) {
inner[i] = k[i] ^ 0x36; // ipad
outer[i] = k[i] ^ 0x5c; // opad
}
// 3. Nested hash: H(outer ‖ H(inner ‖ message)).
const innerHash = createHash('sha256').update(inner).update(message).digest();
return createHash('sha256').update(outer).update(innerHash).digest();
}
Python implementation
import hmac
import hashlib
def sign(key: bytes, message: bytes) -> bytes:
return hmac.new(key, message, hashlib.sha256).digest() # 32 bytes
def verify(key: bytes, message: bytes, tag: bytes) -> bool:
expected = sign(key, message)
return hmac.compare_digest(expected, tag) # constant-time
key = b"a-32-byte-shared-secret-key!!!!!"
tag = sign(key, b"transfer $100 to alice")
print(verify(key, b"transfer $100 to alice", tag)) # True
print(verify(key, b"transfer $900 to mallory", tag)) # False
# From scratch, to show the nesting explicitly:
def hmac_sha256(key: bytes, message: bytes) -> bytes:
B = 64 # SHA-256 block size
if len(key) > B:
key = hashlib.sha256(key).digest()
key = key.ljust(B, b"\x00") # zero-pad to one block
inner = bytes(b ^ 0x36 for b in key) # K' ⊕ ipad
outer = bytes(b ^ 0x5c for b in key) # K' ⊕ opad
inner_hash = hashlib.sha256(inner + message).digest()
return hashlib.sha256(outer + inner_hash).digest()
assert hmac_sha256(key, b"hi") == sign(key, b"hi")
Two details worth flagging. First, always verify with hmac.compare_digest (Python) or timingSafeEqual (Node), never == — equality on bytes short-circuits and leaks timing. Second, the .ljust(B, b"\x00") step is what normalizes a short key; skip it and you'd XOR the pad against garbage past the key's end.
Variants worth knowing
HMAC-SHA512 / HMAC-SHA384. Same construction over a 128-byte block; 512-bit output. Often faster than SHA-256 on 64-bit CPUs lacking SHA hardware, because SHA-512 uses wider 64-bit words and processes a larger block. SHA-384 is SHA-512 with a different initial value and the output truncated to 384 bits, used in TLS 1.2 cipher suites.
HKDF. The HMAC-based key derivation function (RFC 5869) — "extract then expand." It runs HMAC once to distill a uniform pseudorandom key from imperfect input entropy, then runs HMAC repeatedly to expand it into as many output bytes as you need. It is the KDF inside TLS 1.3 and Signal.
PBKDF2. Password-based key derivation that iterates HMAC tens of thousands of times to deliberately slow down brute force. Still standardized but superseded by memory-hard functions (Argon2, scrypt) for password storage.
KMAC. The SHA-3 (Keccak) keyed MAC from NIST SP 800-185. Because Keccak isn't Merkle–Damgård, KMAC keys the sponge directly in a single pass — no inner/outer nesting needed.
BLAKE2/BLAKE3 keyed mode. These hashes take a key parameter natively, giving you a MAC in one pass, immune to length extension, and faster than HMAC-SHA256 in software.
Common bugs and edge cases
- Timing-unsafe comparison. Comparing tags with
==ormemcmpleaks the position of the first mismatch. Use a constant-time comparator. This is the single most common HMAC vulnerability. - Verifying the wrong bytes. Sign and verify over the exact same canonical serialization. If the sender HMACs a sorted JSON form and the verifier re-serializes differently, every tag fails — or worse, a tweak in serialization order lets a forged message pass.
- Confusing HMAC with a plain hash.
sha256(secret + msg)is the broken secret-prefix MAC, not HMAC. Length extension forges it. - Reusing the same key for HMAC and encryption. Derive separate keys (e.g. via HKDF) for the cipher and the MAC; key reuse across primitives voids the security proofs.
- MAC-then-encrypt instead of encrypt-then-MAC. Authenticating the plaintext and then encrypting (the order TLS 1.0 used) enabled padding-oracle attacks like Lucky 13. Encrypt first, then MAC the ciphertext.
- Truncating tags too aggressively. RFC 2104 allows truncating the output, but cutting an HMAC-SHA256 tag to, say, 4 bytes drops forgery resistance to 2³² — guessable. Keep at least 16 bytes (128 bits).
- Treating the tag as a secret. The tag is public; the key is the secret. Leaking a tag is harmless, but it does reveal whether two messages were identical.
Frequently asked questions
Why does HMAC hash the message twice instead of once?
A single keyed hash like H(key ‖ message) is broken by a length-extension attack: with Merkle–Damgård hashes (MD5, SHA-1, SHA-256) an attacker who sees one valid tag can forge a tag for message ‖ padding ‖ extra without knowing the key. The nested HMAC(key, m) = H((key ⊕ opad) ‖ H((key ⊕ ipad) ‖ m)) wraps the extendable inner hash in a second outer hash, so the attacker never sees an extendable state.
What are ipad and opad in HMAC?
ipad is the byte 0x36 repeated to the hash's block size, and opad is 0x5c repeated. The key is XOR-ed with ipad for the inner hash and with opad for the outer hash. The two constants differ in many bits (0x36 ⊕ 0x5c = 0x6a) so the inner and outer keys behave like two independent keys, which is what the security proof needs.
Is HMAC the same as encryption?
No. HMAC provides integrity and authenticity, not confidentiality — it does not hide the message, it only proves the message was not altered and came from someone holding the key. To get both confidentiality and authenticity you combine encryption with a MAC, ideally an authenticated-encryption mode like AES-GCM or encrypt-then-MAC.
Why must you compare HMAC tags in constant time?
A naive byte-by-byte comparison returns early on the first mismatched byte. An attacker measuring response time can recover the correct tag one byte at a time, turning a 2^256 brute force into roughly 32 × 256 ≈ 8,000 guesses. Always compare with a constant-time function such as crypto.timingSafeEqual or hmac.compare_digest.
Can HMAC use SHA-1 or MD5 safely?
Surprisingly, yes for HMAC specifically. HMAC's security depends on the compression function being a pseudorandom function, not on full collision resistance, so HMAC-SHA1 and even HMAC-MD5 have no practical break despite collisions in the bare hashes. Still, new systems should use HMAC-SHA256 because the underlying primitives are deprecated everywhere else.
How long should an HMAC key be?
Ideally equal to the hash output size — 32 bytes for HMAC-SHA256. Keys longer than the block size (64 bytes for SHA-256) are first hashed down, which throws away entropy, so very long keys can be weaker. Keys shorter than the output size reduce the effective security level below the hash's strength.