Security FIELD NOTES · 5 MIN READ
HMAC: How Messages Prove They Weren't Tampered With
How HMAC works: folding a secret key into a hash so a tag proves both integrity and origin, the two-pass construction, and where it signs the modern web.
A hash tells you whether a message changed. It cannot tell you who changed it — because anyone can recompute a hash. HMAC fixes exactly that gap: by mixing a secret key into the hashing process, it produces a tag that only holders of the key can create, and therefore only holders of the key can verify. That single idea sits underneath signed API requests, webhook deliveries, and the HS256 tokens your browser sends every day.
The problem a plain hash can't solve
Suppose a server receives amount=100 together with SHA-256("amount=100"). The hash proves the message arrived intact — but nothing more. An attacker who intercepts both can change the message to amount=9999 and compute the new hash in microseconds; the pair looks just as self-consistent as the original. A hash verifies integrity (the data matches the digest) but not authenticity (the data came from someone who knows a secret). The missing ingredient is a secret only the legitimate sender and receiver share (see How Hashing Works for the hash fundamentals).
The fix: fold a secret key into the hash
HMAC — hash-based message authentication code, standardized in RFC 2104 — takes two inputs instead of one: the message and a key shared between the parties. Change either input and the output changes completely. With the key secret-key:
HMAC-SHA256(key="secret-key", "hello") =
98e7ffb964bb5a3f902db1fc101a5baa98b6f2cd56858210c9d70f26ac762fc7
plain SHA-256("hello") =
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
Same message, different construction, unrelated output. Now watch the two levers independently. Change the message by one letter (hellp) and the tag scrambles to 5b9ef29c57a3c9f1.... Keep the message and change the key by one character (secret-key1) and it scrambles again, to df4285d96ef2dfc2.... Both avalanches are the point: nobody can tune a message to keep its tag, and nobody can guess the tag without the key. Switching the underlying hash changes the tag length — HMAC-SHA512 of the same message and key produces 128 hex characters instead of 64.
Inside the construction
You might think HMAC is just hash(key + message). It is not, for a good reason: appending the key to the front leaves a plain hash of key + message + anything, and hashes built like SHA-256 allow length-extension attacks — given hash(key ‖ message), an attacker can compute the hash of key ‖ message ‖ suffix for a suffix of their choosing without knowing the key. HMAC's nesting closes that door. The key is first padded to the hash's block size (64 bytes for SHA-256), then XORed with a fixed pattern to build two derived keys:
inner = SHA-256((key ⊕ 0x36…36) ‖ message)
HMAC = SHA-256((key ⊕ 0x5c…5c) ‖ inner) (⊕ = XOR)
The message is hashed once inside, and that inner digest is hashed again with the outer, differently-masked key. The two-pass structure is what makes the whole scheme indifferent to the quirks of the underlying hash — HMAC-SHA256 remains secure even though plain SHA-256 has properties that break naive constructions.
Verification is recomputation
HMAC is deterministic: the same message and the same key always produce the same tag. So verifying needs no decryption and no secrets beyond the key both sides already share. The sender computes the tag and sends it alongside the message; the receiver recomputes the tag from the received message with their copy of the key and compares. Matching means both things at once: the message was not altered in transit, and it was produced by someone holding the key — because no one else could have generated a matching tag. The same tag applies to every message, unlike one-time codes: reusing the key across many messages is normal and safe, because seeing old tags does not help forge a new one.
Output comes in the same encodings as any digest. The tag above is 64 hex characters, or 44 characters in Base64: mOf/uWS7Wj+QLbH8EBpbqpi28s1WhYIQydcPJqx2L8c=. Same bytes, different alphabet — pick whichever your protocol expects.
Where HMAC shows up
Once you know the shape, you see it everywhere. Webhook providers sign each delivery with an HMAC so the receiving server can reject forgeries. APIs that sign requests (AWS signatures, OAuth request signing, payment-gateway callbacks) HMAC the request parameters with an access secret. JSON Web Tokens signed with HS256 carry exactly this construction — HMAC-SHA256(base64url(header) + "." + base64url(payload), key) — which you can watch being decoded in Inside a JWT: Decoding the Token. Even TLS 1.2 authenticated every record it sent with an HMAC over the stream — the same verify-by-recomputation idea applied to live traffic.
Using it well
Three habits make the difference between textbook HMAC and working HMAC. First, compare tags in constant time — a naive equality check that bails on the first differing character leaks, character by character, how much of a guess was right. Second, keep one key per purpose: a key used for webhook signing should not be the same one used for API signing, so a leak in one channel does not open the other. Third, note that fast hashes are fine here — plain SHA-256 is too fast for hashing things an attacker wants to guess, but HMAC's security comes from the key being secret, not from slowness; the key never gets brute-forced from a tag because the tag reveals nothing about the key that produced it. To compare two received digests safely — normalized for case, whitespace and prefixes, with a plain match/no-match verdict — the Hash Compare tool does the comparison the constant-time way. To hash a whole list of values at once, the Hash Batch tool processes every line in one pass.
Try it yourself
Open the HMAC Generator, enter message hello and key secret-key, and confirm the SHA-256 tag 98e7ffb9... above. Then run the two experiments from this article: change one letter of the message and watch the tag scramble; restore the message and change one character of the key — a completely different tag from the same text. To see the unkeyed baseline, compute hello in the Hash Generator and compare it with the HMAC output: same algorithm family underneath, entirely different guarantees on top.