Skip to main content

All weeks · Worksheet · Overview

Week 3 · Lecture slides

Week 3

Contents17 sections

MACs (hash-only auth, length-extension)

Security & Cryptography · Nutthakorn Chalaemwongwan


Today

  • Hashes vs. MACs vs. signatures — what "authenticity" actually requires
  • Why H(secret ‖ data) is not a MAC — SHA-256 length-extension
  • HMAC's nested construction — why it closes the gap
  • 🔓 Game: Forge the Admin Cookie

Recap — Week 2

  • A hash proves integrity only if the attacker can't recompute it
  • Bare H(password) broke because attackers could recompute — at scale, with rainbow tables
  • Fix was salting + slow KDFs (bcrypt/Argon2) — but hashing still had no secret

The spine of this course

Textbook-secure primitive → real-system failure → correct construction.

  • SHA-256 is a mathematically sound, collision-resistant hash function. Full stop.
  • Stick a secret in front of it to build authentication, and the system breaks anyway
  • Today's failure: length-extension — misuse, not a flaw in SHA-256 itself

What a MAC needs to guarantee

  • Integrity — data wasn't modified in transit
  • Authenticity — data came from someone who knows a secret key
  • A hash alone gives you neither if the attacker can compute the same hash function

cookie = username || SHA-3(username) — no secret at all. Anyone can recompute it for username=admin.


The tempting-but-wrong fix: secret-prefix hash

sig = SHA256(MAC_SECRET + data)
  • Looks reasonable: now there IS a secret in the computation
  • This is exactly the vulnerable construction in vulnerable_app.py
  • CWE-347 — Improper Verification of Cryptographic Signature

Why: Merkle-Damgard hashes process in chunks

  • SHA-256 absorbs input in 64-byte blocks, updating internal state block by block
  • The final internal state IS the digest — nothing more, nothing less
  • If you know a valid SHA256(secret ‖ data), you know the exact internal state after processing secret ‖ data

Length-extension: resume, don't restart

  • Attacker doesn't need secret — only sig, len(secret), and len(data)
  • Attacker resumes SHA-256 compression from that known state
  • Appends glue padding (the 0x80 + zero bytes + 8-byte bit-length SHA-256 always adds) then their own extra bytes (&admin=true)
  • Result: a valid SHA256(secret ‖ data ‖ glue ‖ extra) — computed with zero knowledge of secret

CWE-290: this IS authentication bypass by spoofing

  • The server's /admin check: does sig match AND does data contain admin=true?
  • Forged cookie satisfies both — without ever learning MAC_SECRET
  • The server can't tell "legitimately signed" from "resumed from a leaked digest"

The fix: HMAC's nested construction

HMAC(key, msg) = H( (key⊕opad) ‖ H( (key⊕ipad) ‖ msg ) )
  • Inner hash processes key⊕ipad ‖ msg → produces an intermediate digest
  • Outer hash re-hashes that digest with a fresh key-derived prefix — a brand-new Merkle-Damgard chain
  • Attacker never sees the state the outer hash starts from — it depends on key⊕opad, which they don't have

Fixed vs. vulnerable — one line differs

# vulnerable_app.py
sig = hashlib.sha256(MAC_SECRET.encode() + data).hexdigest()

# fixed_app.py
sig = hmac.new(MAC_SECRET.encode(), data, hashlib.sha256).hexdigest()
  • Same secret, same data, same SHA-256 — only the construction changed
  • Length-extension against :8093 (HMAC) → rejected, 403, no flag

One fix doesn't fix everything

if sig != expected:      # plain string comparison — in BOTH apps
    return jsonify({"error": "bad signature"}), 403
  • HMAC defeats length-extension — it does not make != constant-time
  • Byte-by-byte early-exit comparison leaks timing information → an attacker can recover the tag one byte at a time
  • Real fix: hmac.compare_digest() (constant-time by design)

MACs alone don't stop everything else

  • A valid (data, sig) pair can be replayed later — MAC says nothing about when
  • Short tags (e.g. 64-bit) are forgeable via birthday-bound brute force
  • Today's fix is length-extension only — a secure cookie design needs more (nonces/timestamps, tag length, comparison)

ServicePortSchemeLength-extension?
vulnerable_app.py:8092SHA256(MAC_SECRET + data)Yes
fixed_app.py:8093HMAC-SHA256(MAC_SECRET, data)No
  1. Log in as guest on :8092, capture (data, sig)
  2. Forge &admin=true appended, via length-extension — no MAC_SECRET needed
  3. Cash in at /admin → flag
  4. Replay the same trick against :8093 → must be rejected

Lab today

📋 Worksheet 3 — labs/week03-macs/worksheet.md · kickoff: docker compose up -d then python exploit.py

  • Tasks 0–5: read the vulnerable construction, capture a cookie, hand-compute the glue padding, forge it, cash in the flag, confirm HMAC rejects it
  • + Audit the AI — critique an AI's flawed Q6 cookie-design answer (replay claim + == comparison)
  • + EiPE (explain length-extension in plain English) + Prompt Problem on HMAC vs. bare hash

Key takeaways

  • SHA-256 is textbook-sound — the misuse (H(secret ‖ data)) is what breaks
  • Length-extension resumes hash state the attacker was never supposed to see
  • HMAC's nested construction fixes this — but doesn't fix comparison timing or replay

Questions?

Next week: AES — CBC bit-flipping and why unauthenticated encryption fails

All weeks in Security & Cryptography