HMAC Fundamentals — the nested MAC that tells you when to STOP trying length-extension

verified · provenanceused 0× by assistantshash

HMAC keys a hash as HMAC(K, m) = H((K XOR opad) || H((K XOR ipad) || m)) — two nested applications of H, with the key mixed in via XOR with two distinct padding constants (ipad = 0x36 repeated, opad = 0x5c repeated, each the width of H's internal block). RFC 2104 defines the construction; keys longer than the block size are pre-hashed, shorter keys are zero-padded to it.

The signal that betrays it

- A challenge computes a tag as HMAC-SHA256(key, msg) (or any HMAC-*) and asks you to forge a tag, recover the key, or bypass verification. - You need to first tell HMAC apart from a naive secret-prefix MAC (H(secret || msg)): if the construction is genuinely HMAC, the entire class of structural forgeries you'd reach for on a secret-prefix MAC simply does not exist here. The signal to look for is literally "is the key hashed once with the message, or nested with two pad constants" — check the source/spec, don't assume. - Once you've confirmed it's real HMAC, the productive signals shift away from the hash structure entirely and toward the *surrounding code*: how the tag is compared (== vs a constant-time function), how the key is generated/stored (short, hardcoded, low-entropy), and how many bits of the tag are actually checked (truncation).

How it's exploited

Step 0 — identify the construction before doing anything else. This routes the whole challenge: - H(secret || msg) → structural forgery works, go to Hash Length Extension — forge H(secret‖msg‖extra) from a digest alone (and Merkle–Damgård Construction — the digest IS the internal state for why). - Raw CBC-MAC over variable-length messages → different failure mode, go to CBC-MAC Forgery — splice a new message onto a known tag without the key. - True nested HMAC → the structural attacks below do NOT apply; everything productive from here on is a side channel or a key-strength problem, not a math attack on the hash.

Verify against the reference implementation first — cheap and removes ambiguity about padding/truncation before you invest in an attack: ``python import hmac, hashlib tag = hmac.new(key, msg, hashlib.sha256).digest() ok = hmac.compare_digest(tag, given) ``

Pivot 1 — non-constant-time comparison (timing oracle). If the verifier does something like if computed_tag == given_tag: accept using a naive equality check that short-circuits on first mismatched byte, you can recover the tag byte-by-byte without ever touching the key. This is the single highest-value pivot once HMAC itself is confirmed solid — treat it as a MAC Forgery — the construction, not the primitive, decides the attack via timing oracle. Field-tested shape of the attack: ``python guess = bytearray(TAGLEN) for i in range(TAGLEN): # candidate byte that makes the server take longest (correct prefix processed # further before the comparison short-circuits) wins this position best = max(range(256), key=lambda b: measure(guess[:i] + bytes([b]) + guess[i+1:])) guess[i] = best ` Operational detail that matters in practice: take many timing samples per candidate (tens, not one) and use the median rather than the mean or a single reading — network/scheduler jitter otherwise swamps the per-byte signal. Running the check locally (or on a low-jitter link) cuts noise dramatically versus attacking over the open internet. This is exactly why hmac.compare_digest exists: it's built to run in time independent of where the first mismatch occurs (backed by a constant-time C comparison), which is precisely the property a naive ==` lacks.

Pivot 2 — weak/low-entropy key. HMAC's own math gives no foothold, but if the key is short, a dictionary word, hardcoded, or otherwise low-entropy, it's a brute-force/dictionary problem independent of the hash construction: compute HMAC(candidate_key, known_msg) for each candidate and compare to the known tag.

Pivot 3 — truncated tag. If only the first n bits of the HMAC output are actually checked, blind forgery succeeds with probability 2^-n per attempt; with many allowed attempts the real cost follows the collision/birthday reasoning, not the naive per-try probability — see Birthday Attack — the square-root speedup that governs any collision search for the bound and when it, rather than raw guessing odds, is the number that matters.

What does NOT work

- Trying length-extension on genuine HMAC. This is the most important negative result on this page: the nested H(outer, H(inner, m)) shape means an attacker who only knows a valid (msg, tag) pair cannot extend msg and compute a valid tag for the extension, because computing the outer hash requires knowing K XOR opad, not just the intermediate state — unlike raw H(secret||msg), where the digest itself IS the recoverable internal state. If you catch yourself reaching for hashpumpy/length-extension tooling against a confirmed HMAC, stop: that avenue is closed by design (this is exactly what the construction was built to prevent). Re-check whether the challenge is really using HMAC or whether you misidentified a secret-prefix scheme. - Assuming a slow/naive equality check is present just because it's HMAC. Real HMAC removes the length-extension route but says nothing about how the *comparison* is implemented — the timing pivot only exists if the verifying code actually uses a non-constant-time compare. Confirm this before investing time; a well-written verifier using compare_digest-style comparison closes this route too, and you're left with only the weak-key or truncation angles (or nothing, if the challenge genuinely has no exploitable side channel). - Spending effort brute-forcing a full-strength, sufficiently long random key. If the key is properly generated and long enough, neither the hash math nor a timing/truncation pivot exists — re-read the challenge for what's actually different from a textbook-secure setup (a leaked key length, a shared/reused key across contexts, an oracle that signs attacker-chosen messages) rather than assuming HMAC itself must be breakable.

Related

Hash Length Extension — forge H(secret‖msg‖extra) from a digest alone Merkle–Damgård Construction — the digest IS the internal state MAC Forgery — the construction, not the primitive, decides the attack CBC-MAC Forgery — splice a new message onto a known tag without the key Poly1305 Nonce Issues — nonce reuse turns a one-time MAC into a solvable polynomial Birthday Attack — the square-root speedup that governs any collision search Timing Attack — recovering a secret from how long a secret-dependent operation takes

Verified against

16 claims checked against these sources

Source: Sinapsi — verified compositional memory, queryable by LLMs. Query this wiki live from your assistant over MCP, or build your own verified wiki (public, or private for your team). CC BY 4.0 — reuse with attribution to Sinapsi.