Hashing & Integrity
HMAC & MACs
Proving a message is untampered and from the right sender.
A plain hash proves a message was not accidentally corrupted, but it cannot prove who sent it: an attacker who changes the message just recomputes the hash. A Message Authentication Code (MAC) fixes this by mixing a shared secret key into the tag, so only a key holder can produce or verify it. HMAC is the standard, battle-tested way to build a MAC from a hash function, and it appears in TLS, API request signing, JWTs, and session cookies.
Why hash(key || message) fails: length extension
The obvious idea for a keyed hash is hash(key || message): prepend the secret and hash the result. It seems to bind the key to the message, but it is broken against the Merkle-Damgard hash functions (MD5, SHA-1, SHA-256, SHA-512) because of a length-extension attack.
Those hashes work by processing the message block by block, carrying forward an internal state, and the final digest is just that state. An attacker who sees hash(key || message) knows the internal state after key || message, without knowing the key. They can resume from that state and append their own data, producing a valid hash(key || message || padding || evil) tag for a message they extended. So they can forge a valid tag for a message they partly control, defeating the whole point.
Concretely, if a server authenticates user=alice&role=guest with hash(secret || data), an attacker can append &role=admin and compute a matching tag without ever learning secret. This is not theoretical; it has broken real APIs. The takeaway: never build a MAC as hash(key || message). Use HMAC.
HMAC construction and hands-on verification
HMAC (Hash-based MAC, RFC 2104) is designed to resist length extension by hashing twice with two derived keys:
HMAC(K, m) = H( (K' XOR opad) || H( (K' XOR ipad) || m ) )
Here K' is the key sized to the hash block, ipad and opad are fixed padding constants, and H is the underlying hash (SHA-256, SHA-512, and so on). The inner hash absorbs the message, and the outer hash wraps the result with the key again, so the attacker never sees a raw internal state they can extend. HMAC’s security has a solid proof and does not depend on collision resistance of H, which is why HMAC-SHA1 stayed safe for authentication even after SHA-1 collisions broke it for signatures.
Compute and verify an HMAC with OpenSSL:
mkdir -p ~/crypto-lab && cd ~/crypto-lab
KEY=$(openssl rand -hex 32)
printf 'user=alice&role=guest' > req.txt
# Produce a tag (modern openssl mac interface)
openssl mac -digest SHA256 -macopt hexkey:$KEY -in req.txt HMAC
Expected output:
4c9f2a7b1e05d8c3a6f0b9147e2d8815c33ab90de6712f4485a1cc02d9b7e6f1
Verification means recomputing the tag over the received message and comparing. The comparison must be constant-time so an attacker cannot learn the correct tag byte by byte through timing:
cd ~/crypto-lab
EXPECTED=$(openssl mac -digest SHA256 -macopt hexkey:$KEY -in req.txt HMAC)
GOT=$(openssl mac -digest SHA256 -macopt hexkey:$KEY -in req.txt HMAC)
[ "$EXPECTED" = "$GOT" ] && echo "MAC valid" || echo "MAC INVALID: reject"
Expected output:
MAC valid
If any byte of req.txt is altered, the recomputed tag differs entirely (the avalanche effect from Hash Functions), and verification fails. In real code use a library’s constant-time compare (hmac.compare_digest in Python, hmac.Equal in Go) rather than a plain string equality, which can leak timing.
MACs vs signatures: shared key vs public key
MACs and digital signatures both prove a message is authentic and unaltered, but they differ in the trust model, and picking the wrong one is a design error.
| Property | MAC (HMAC) | Digital signature |
|---|---|---|
| Key model | One shared secret key | Private key signs, public key verifies |
| Who can verify | Only holders of the shared key | Anyone with the public key |
| Who could have created it | Any key holder (sender or verifier) | Only the private-key holder |
| Non-repudiation | No | Yes |
| Speed | Very fast | Slower |
| Typical use | API auth, cookies, TLS record integrity | Certificates, software signing, receipts |
The decisive distinction is non-repudiation. With an HMAC, both parties share the key, so either could have produced any valid tag; a verifier cannot prove to a third party that the sender (and not the verifier) created it. A signature can only be made with the private key, so it proves origin to anyone and the signer cannot later deny it. Use a MAC when the same parties both create and check tags and share a secret; use a signature when verifiers should not be able to forge, or when a third party must be convinced.
Practical Guidance
- Never build
hash(key || message)as a MAC on a Merkle-Damgard hash. Use HMAC, which is designed to resist length extension. - Use HMAC-SHA256 as a solid default. HMAC stays secure even against some weaknesses in the underlying hash, but still prefer a modern hash.
- Always verify MACs with a constant-time comparison (
hmac.compare_digest,hmac.Equal). A naive==can leak the correct tag via timing. - Choose a MAC when both sides share a secret and both create and verify. Choose a signature when you need public verifiability or non-repudiation.
- Authenticate everything that matters, including headers and metadata, and consider an AEAD (see
Authenticated Encryption) when you also need encryption. - Follow rule zero: use the standard HMAC in an audited library rather than hand-rolling any keyed-hash scheme.