Post-Quantum Cryptography (Lamport one-time signatures, key reuse)
Security & Cryptography · Nutthakorn Chalaemwongwan
Today
- The quantum threat: Shor vs. Grover
- Harvest now, decrypt later
- The four PQC families + NIST's ML-KEM / ML-DSA
- Hash-based signatures: Lamport OTS — and its one-time-only rule
- 🔓 Signature game: Forge the Admin Signature (key reuse → private-key recovery)
Recap — Week 14
- Authentication proved who you're talking to (passwords, MFA, sessions)
- A signature proves what was said and that it wasn't tampered with — integrity + non-repudiation
- This week: the primitive behind signatures is itself under threat — from quantum computers
Why quantum breaks (some) crypto
- Shor's algorithm — solves factoring / discrete log efficiently → breaks RSA and ECC outright
- Grover's algorithm — quadratic speedup on search → only halves symmetric/hash security
- Grover's mitigation: double the key/output length (AES-256 stays ~AES-128-strength — still fine)
Harvest now, decrypt later
- Adversary records ciphertext today, decrypts it once a quantum computer exists
- Makes PQC migration urgent now — even though no large quantum computer exists yet
- Real today for anything with a long confidentiality lifetime: health records, state secrets, long-lived credentials
The four PQC families
- Lattice-based — NIST's ML-KEM (Kyber, a KEM) and ML-DSA (Dilithium, a signature)
- Hash-based — Lamport, SPHINCS+, XMSS ← this week
- Code-based
- Multivariate
- NIST standards (2024): FIPS 203 (ML-KEM) · FIPS 204 (ML-DSA) · FIPS 205 (SLH-DSA / SPHINCS+)
Lamport one-time signatures — the construction
- Private key: two random preimages per message bit —
sk[i] = (a_i, b_i) - Public key: hashes of both —
pk[i] = (H(a_i), H(b_i)) - To sign bit i: reveal
a_iif that bit is 0,b_iif it's 1 - Security rests on one assumption — hash preimage resistance. No exotic math, quantum-resistant
What one signature reveals
- Signing message
Mreveals exactly one preimage per bit — the one matchingM's bit value - The other preimage for each bit position stays secret
- Lab check (bit 0, message
0x00000000):SHA256(sig[0]) == pk[0][0], butSHA256(sig[0]) != pk[0][1] - One signature ⇒ attacker learns half the private key — never enough to forge a different message
The break: sign twice and the key falls out
- Sign
M = 0x00000000and its bitwise complement~M = 0xFFFFFFFF - They differ in every bit → together the two signatures reveal both preimages, every position
- That is the complete private key — no brute force, just algebra
- CWE-323 — Reusing a Nonce/Key Pair in Encryption (the vulnerable app's exact flaw)
Worked example: forging the admin signature
# vulnerable_app.py — ONE Lamport keypair reused on every /sign call
sig0 = sign(0x00000000) # reveals the "bit=0" preimage for every position
sig1 = sign(0xFFFFFFFF) # reveals the "bit=1" preimage for every position
# assemble ANY message OFFLINE now -- including 0xA5A5C3C3, the admin message
forged = [sig0[i] if bit(admin_msg, i) == 0 else sig1[i] for i in range(32)]
# POST /admin {forged} -> {"flag": FLAG_PQC}
The fix — operational, not mathematical
fixed_app.py: the key signs at most once; any 2nd/signcall →403- No new math, no bigger keys — just refuse reuse
- Rule is "one signature, period" — not "one distinct message" (a 2nd request for the same message is refused too)
- A client retrying a dropped response must cache the first signature, never re-sign
- CWE-347 — Improper Verification of Cryptographic Signature (the class this closes)
Scaling past "one signature ever"
- Stateful (XMSS, LMS) — a tree of one-time keys; must track which leaf was used — lose the state, reuse a key, break everything
- Stateless (SPHINCS+ / SLH-DSA, FIPS 205) — huge tree + pseudorandom key selection; reuse becomes cryptographically negligible, no state to lose
- Real deployed hash-based schemes are Lamport's exact discipline, engineered around
🔓 Signature game — Forge the Admin Signature
- Two identical Flask targets, same Lamport scheme:
:8100reuses its key,:8101allows only one signature POST /signtwo messages → recover the private key → forge0xA5A5C3C3→POST /admin→ flag- Win condition: flag captured on
:8100and the identical attack correctly defeated on:8101
docker compose up -d
python exploit.py # PASS on :8100 (flag captured), PASS on :8101 (attack defeated)
Crypto-agility — the real engineering lesson
- Algorithms keep changing (MD5→SHA-2, RSA→PQC) — systems must swap primitives without a rewrite
- Practical pattern: hybrid classical + ML-KEM channels (see companion
hndl/lab) — safe even if one side is later broken - The Lamport lesson generalizes: correct usage discipline matters as much as the underlying math
Lab today
📋 Worksheet 15 —
labs/week15-pqc/worksheet.md(Part 2 written + Part 3 hands-on) · kickoff:docker compose up -d
- Recover the Lamport private key from two signatures, forge the admin signature, capture the flag
- Confirm one-time enforcement defeats the identical attack on
:8101 - + Audit the AI (a hashing-first Lamport signer that still reuses its key) + EiPE / Prompt Problem
Key takeaways
- Shor breaks RSA/ECC outright; Grover only dents AES/SHA — mitigated by longer keys
- Hash-based signatures are textbook-secure on one assumption — if used strictly one-time
- Real-system failure: reuse the key once and the entire private key falls out — no math needed
- The fix is a usage rule, not a bigger algorithm — "textbook-secure primitive, real-system failure"
Questions?
Next week: Capstone studio — composing the term into CryptoVault