A tempting homemade MAC: tag = SHA256(secret ‖ message). It looks sound — you'd
need the secret to recompute it. But for any Merkle–Damgård hash (MD5, SHA-1, the
SHA-2 family), the digest is the internal state at the end, and that's all you
need to keep hashing from where it left off.
- construction
- tag = H(secret ‖ message), H ∈ {MD5, SHA-1, SHA-256, ...}
- known
- the message, its tag, and len(secret) (or just brute it)
- goal
- a valid tag for message ‖ glue-padding ‖ attacker-data — no secret
why the digest is a resume point#
A Merkle–Damgård hash chops the input into fixed blocks and folds each into a running state with a compression function; the final state, serialized, is the digest. There's no finalization that mixes in a secret key — the output is just the chaining variable after the last block.
So if you know H(secret ‖ message), you know the exact internal state the hash
reached after consuming secret ‖ message ‖ glue_padding (the padding the hash
appended to reach a block boundary). Load that state back into the algorithm and
keep feeding it bytes, and you compute H(secret ‖ message ‖ glue_padding ‖extension) — a valid digest for a message you've extended, without ever knowing
the secret.
The only unknown is len(secret) (it shifts the glue padding). You either know it,
or you try lengths 0,1,2,… and submit each forgery until one is accepted.
doing it#
The arithmetic is fiddly (reconstructing the exact glue padding, byte-swapping the state into the right endianness), so the tools do it:
guest to admin, no key
Last-write-wins parsers (role=guest…&role=admin) make the appended field override
the original — which is why this maps so cleanly onto signed query strings and
cookies.
the fix is HMAC#
- Use HMAC.
HMAC(k, m) = H(k⊕opad ‖ H(k⊕ipad ‖ m))hashes twice with the key wrapping both ends, so the published tag isn't a resumable state for the outer message — length extension dies. - Or a sponge hash. SHA-3/Keccak isn't Merkle–Damgård and isn't extendable; BLAKE⅔ have keyed modes designed for MACs.
- Never roll
H(secret ‖ message). It's the textbook footgun this attack exists to punish.
The takeaway: a hash digest is not a sealed fingerprint, it's a snapshot of a
resumable computation. Treat H(secret ‖ m) as a MAC and you've published the
exact state an attacker needs to keep typing where your secret left off.