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.

PropertyMAC (HMAC)Digital signature
Key modelOne shared secret keyPrivate key signs, public key verifies
Who can verifyOnly holders of the shared keyAnyone with the public key
Who could have created itAny key holder (sender or verifier)Only the private-key holder
Non-repudiationNoYes
SpeedVery fastSlower
Typical useAPI auth, cookies, TLS record integrityCertificates, 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

  1. Never build hash(key || message) as a MAC on a Merkle-Damgard hash. Use HMAC, which is designed to resist length extension.
  2. Use HMAC-SHA256 as a solid default. HMAC stays secure even against some weaknesses in the underlying hash, but still prefer a modern hash.
  3. Always verify MACs with a constant-time comparison (hmac.compare_digest, hmac.Equal). A naive == can leak the correct tag via timing.
  4. 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.
  5. Authenticate everything that matters, including headers and metadata, and consider an AEAD (see Authenticated Encryption) when you also need encryption.
  6. Follow rule zero: use the standard HMAC in an audited library rather than hand-rolling any keyed-hash scheme.