Learn · Technologies we use
What is HMAC? A hash with a key
SHA-256(key + message) falls to length extension, forged here on real numbers. How HMAC fixes it, and how to verify a tag.
HMAC is a way to make a hash function take a secret key. Anyone can compute a plain hash of a message, so a hash alone proves nothing about who sent it. HMAC mixes a key into the hash in a particular way, so only someone who knows the key can compute or check the result, called a tag or MAC (message authentication code). Mihir Bellare, Ran Canetti and Hugo Krawczyk designed it in 1996, and it was standardized as RFC 2104 in 1997. It sits inside PBKDF2, TLS, SSH, JSON web tokens and the request signing of most web APIs.
The obvious idea, and why it fails
The first thing anyone tries is tag = SHA-256(key || message): hash the key followed by the message. It looks fine, and with the SHA-2 and MD5 family it's broken, because of how these hashes work (see what a hash function is). A Merkle–Damgård hash such as SHA-256 processes its input block by block and outputs its internal state after the last block. So given only the tag, an attacker can load that state and keep hashing, appending more data. They never need the key.
Here's the attack on real numbers, from scripts/examples/sha256-worked.mjs. A server signs amount=100 with the secret key (three bytes) as SHA-256(key || amount=100), giving 82843577 1718c26c 2c3fce70 635ade53 371a7bff b07fb7b0 118fb44e 692a0c65. The attacker knows the message, the tag, and that the secret plus message is 13 bytes (and can try each plausible secret length). The hash padded those 13 bytes to a full block: a 80 byte, zeros, and the length 104 bits (68), 51 bytes of padding in all. The attacker starts the hash from the published tag as if it were the state after that block, and hashes &amount=900 on top. The result, c8b72cfb d550e4d6 9e47700e a893c39c ad7cf526 fb6f1d14 5831546c 84c81585, is a valid tag for the message amount=100 + the 51 padding bytes + &amount=900. The attacker never learned the secret, and the test in this site's repository confirms that the forged tag equals the real SHA-256 of that longer message. The flaw isn't in SHA-256 as a hash. It's in using it as a MAC, and it has caused real vulnerabilities.
Putting the key at the end, SHA-256(message || key), avoids that. But any collision in the hash then gives two messages with the same tag, so it inherits every collision weakness. Neither order is safe, which is why a construction with a proof exists.
How HMAC works
HMAC hashes twice, with two different keys derived from the secret:
HMAC(K, m) = H( (K' xor opad) || H( (K' xor ipad) || m ) )
K' is the key padded with zero bytes to the hash's block size (64 bytes for SHA-256, and a longer key is hashed first). ipad is the byte 36 repeated, and opad is 5c repeated. The two constants were chosen so the two derived keys differ in many bit positions. Worked for the key key and the message amount=100:
| Step | Value |
|---|---|
K' | 6b6579 followed by 61 zero bytes |
inner = SHA-256((K' xor ipad) ‖ amount=100) | 6000c628ddd3eda54ed66e0c1e9b0696e3dc2cb83d795bc682f9ef6da485a17b |
outer = SHA-256((K' xor opad) ‖ inner) | ccd6493a2a01d18bd477c78774a0f6d39f85af61d4064bf4effe5f5765bde9a0 |
The outer value is the tag, and it equals what Node's createHmac('sha256', 'key') returns for the same input. You can reproduce it in the HMAC tool. Compare the forged-tag attack. The attacker sees only the outer hash, which ran over a different, secret-keyed block, so there's no inner-hash state to resume from and nothing about the outer key to extend. Length extension is closed off by construction.
What HMAC is good for
- Authenticating messages and requests. Both sides share a key, the sender attaches the tag, and the receiver recomputes it. A changed message or a wrong key gives a different tag.
- Deriving keys. HMAC is the building block of PBKDF2 (the passphrase is the key, the salt the message) and of HKDF (RFC 5869), which turns one secret into several independent keys.
- Tokens. A server can hand out a value that carries data and an HMAC tag (a signed cookie, the HS256 form of a JSON web token) and later check that nothing was altered, without keeping a table.
HMAC proves that someone with the key made the tag. It doesn't hide the message, and it can't tell which of two key-holders made it. For that, use a public-key signature such as ML-DSA or RSA.
Three places you'll meet it
- Webhooks. A service that calls your server signs the request body with a secret you share and puts the tag in a header (GitHub's is
X-Hub-Signature-256: sha256=…). Your server recomputes HMAC-SHA-256 over the raw body bytes, not over a parsed and re-serialized copy, and compares in constant time. Anyone without the secret can't post a convincing event. - Signed cookies and tokens. A session value or a JSON web token with the HS256 algorithm carries its own HMAC-SHA-256 tag. The server needs no database lookup to know the value is the one it issued, though it can't revoke it without keeping a list.
- Request signing. Cloud APIs sign a canonical form of the request, including the time, with a key derived through a chain of HMACs, so a captured request can't be replayed later or altered in transit.
In every case the tag covers exactly the bytes fed in, so the design question is what to include: the time (to limit replay), the method and path (so a tag for one request can't be moved to another), and a clear separator between fields so "ab" + "c" can't be confused with "a" + "bc".
One detail of the definition surprises people. A key shorter than the block is padded with zero bytes, so the keys key and key followed by a zero byte give identical HMACs, and a key longer than the block is replaced by its hash. That's harmless for random keys, and it's a reason to use full-length keys drawn from a secure generator (random numbers in the browser) instead of short words.
Using it correctly
- Compare tags in constant time. A normal string comparison stops at the first differing byte, so how long it takes tells an attacker how many leading bytes were right, and they can forge a tag a byte at a time. Use the library's verify function. The Web Crypto call
crypto.subtle.verify('HMAC', key, tag, data)does the comparison for you. - Use a key at least as long as the hash output. An attacker can guess a short or guessable key offline from one message and its tag.
- Stick with SHA-256 or better. The attacks that broke MD5 and SHA-1 for collisions haven't been shown to give forgeries against HMAC with those hashes, which is a tribute to the construction, but there's no reason to rely on that.
- Don't use it with passwords alone. It's fast. See password hashing.
Sign and verify with the browser's own HMAC.
// Web Crypto: HMAC-SHA-256 tag and constant-time verification
async function tag(keyBytes, message) {
var key = await crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']);
var t = new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message)));
var ok = await crypto.subtle.verify('HMAC', key, t, new TextEncoder().encode(message)); // true; a changed message gives false
return { tag: t, ok: ok };
}How cipher.ink uses this
HMAC runs inside the key derivation: PBKDF2-HMAC-SHA256 executes HMAC-SHA-256 600,000 times in a chain, each round keyed with your passphrase, which is why the work can't be shortcut. The messages themselves carry no separate HMAC. AES-GCM authenticates as part of the encryption, with its own 128-bit tag, so a wrong passphrase or a flipped bit gets caught on opening (authenticated encryption). The HMAC tool on this site computes any HMAC and checks a tag you paste.
Further reading
- RFC 2104: HMAC: Keyed-Hashing for Message Authentication (1997)
- RFC 4231: HMAC-SHA Identifiers and Test Vectors
- Springer: Bellare, Canetti, Krawczyk, Keying Hash Functions for Message Authentication (CRYPTO 1996)
- RFC 5869: HKDF: HMAC-based Extract-and-Expand Key Derivation Function
- FIPS 198-1: The Keyed-Hash Message Authentication Code (HMAC)