Hash functions verify that data has not been accidentally corrupted. But what if an attacker deliberately modifies it? MACs add a secret key to the equation, providing both integrity and authenticity.
Hash functions are public algorithms. Anyone can compute SHA-256 of anything. That is exactly the problem.
In the previous chapter, you learned that hash functions create unique fingerprints of data. If the data changes, the fingerprint changes. This sounds like it solves integrity, but there is a catch.
If Alice sends Bob a message with its hash appended, and Eve sits in the middle, Eve can replace the message, compute a fresh hash of her modified version, and forward both to Bob. Bob checks the hash, sees it matches, and trusts the message. The hash gave integrity against accidents (bit flips, network errors) but zero protection against a deliberate attacker. What is missing is a secret.
A Message Authentication Code takes two inputs, a secret key and a message, and produces a fixed-size tag. Only someone who knows the key can produce or verify a valid tag.
The same key and message always produce the same tag. Compute HMAC(key, message) a thousand times and you will get the exact same result every time.
Without the key, producing a valid tag for any message is computationally infeasible. Even if you have seen tags for millions of other messages, you cannot forge a new one.
Bob computes MAC(key, received_message) and compares it with the received tag. If they match, the message is authentic and unmodified.
A MAC does not provide confidentiality. The message itself travels in plaintext. Eve can still read it. She just cannot modify it without detection. Combining confidentiality and integrity in a single operation is the topic of Chapter 6: Authenticated Encryption.
Enter a message and a secret key to compute an HMAC tag in real time. Change a single character in either field and watch the entire tag change.
Enter a message and a secret key to compute an HMAC tag in real time.
Change a single character in the key or the message. The entire tag changes unpredictably.
Your first instinct might be: take a hash function, prepend the secret key, and hash the whole thing. It sounds reasonable. It is dangerously broken.
Hash functions like SHA-256 do not process the entire input at once. They break the data into fixed-size blocks (64 bytes each) and feed them through a compression function one at a time. Each block takes the previous block's output as its starting state. The final block's output becomes the hash.
This is called the Merkle-Damgard construction, and it has a critical consequence: the hash output IS the internal state after the last block. If you know the hash, you know the state. And if you know the state, you can keep feeding in more blocks from that point forward, extending the message without ever knowing what came before.
If Alice computes tag = hash(key || message) and sends the message with the tag, Eve can do the following without knowing the key: take the tag (which is the internal hash state), resume the hash computation from that state, feed in additional data, and obtain hash(key || message || padding || extra_data). She now has a valid tag for a message Alice never sent.
This is not a theoretical weakness. Length extension attacks have been used against real APIs that used hash(key || message) for authentication. The Flickr API signing vulnerability in 2009 is one well-known example.
See why hash(key || message) is broken. Step through the attack.
Alice computes hash(key || message) and sends the message with its tag.
HMAC is the industry-standard MAC construction. It uses the same hash function you already know, but wraps it in a two-pass structure that eliminates length extension entirely.
HMAC starts by creating two derived keys from your original key. It XORs the key with a constant called ipad (0x36 repeated to fill a block) and another constant called opad (0x5c repeated). These two values never change. They are defined in the HMAC specification (RFC 2104).
The inner pass hashes (key XOR ipad) concatenated with the message. This produces a 32-byte intermediate result. The outer pass then hashes (key XOR opad) concatenated with that intermediate result. The output of the outer hash is the final HMAC tag.
Why does this stop length extension? Because the attacker only sees the outer hash output. To extend it, they would need the outer hash's internal state, but that state depends on the key XOR opad, which they do not know. The inner hash result is buried inside the outer computation, completely out of reach.
In a single formula:
The inner hash produces an intermediate digest. The outer hash seals it. An attacker who sees only the final output cannot extend either pass.
Watch HMAC-SHA-256 compute a tag one step at a time, with real intermediate values.
In this interactive scenario, you play Eve. See why a hash alone fails to protect a message, and why a MAC stops you cold.
You are Eve, sitting between Alice and Bob. Try to tamper with a message in both scenarios.
Alice sends a message with its SHA-256 hash. You are Eve. Modify the message and see what happens.
You have a message and its valid HMAC tag, but the key is secret. Can you produce a valid tag for a different message? Try as many times as you want.
Alice sent a message with a valid HMAC-SHA-256 tag. The key is secret. Your mission: produce a valid tag for a different message without knowing the key.
MACs are one of the most deployed cryptographic primitives, working silently in APIs, browsers, and protocols across the internet.
Services like AWS sign every API request with HMAC. The server recomputes the MAC to verify the request came from someone with the secret key and was not modified in transit.
Web frameworks sign session cookies with HMAC. If a user tampers with their session data, the server detects the invalid tag and rejects the request.
Every TLS record includes a MAC or AEAD tag. This prevents an attacker from silently modifying encrypted traffic between your browser and a server.
JSON Web Tokens use HMAC-SHA256 to sign claims. The server verifies the signature before trusting the token contents, ensuring nobody tampered with the payload.