Chapter 9 — Programming II
Lesson map
Lesson 09 published. Map, example, EX017.
flowchart TD
N0["Learning outcomes"]
N1["9.1 Interfaces over implementations"]
N2["9.2 Input validation"]
N3["9.3 Testing beyond examples"]
N4["9.4 Dependencies"]
N5["Engineering note"]
N6["Synthesis"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6
N6 --> EX["Worked example"]
EX --> Q["Exercise EX017"]
Logical drawing
flowchart LR
N0["Learning outcomes"]
N1["9.1 Interfaces over implementations"]
N2["9.2 Input validation"]
N3["9.3 Testing beyond examples"]
N4["9.4 Dependencies"]
N5["Engineering note"]
N6["Synthesis"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> Q["EX017"]
[Learning outcomes]
|
v
[9.1 Interfaces over implementations]
|
v
[9.2 Input validation]
|
v
[9.3 Testing beyond examples]
|
v
[9.4 Dependencies]
|
v
[Engineering note]
|
v
[Synthesis]
|
v
[Worked example] --> [Exercise EX017]
Learning outcomes
You will (1) structure code so that algorithms are replaceable; (2) parse and validate untrusted inputs such as certificates and key shares; (3) write property-based and known-answer tests; and (4) manage dependencies on cryptographic libraries. It builds on Chapter 4 and prepares agility (Chapter 39).
9.1 Interfaces over implementations
Write code against interfaces — kem.encapsulate(public_key), signer.sign(message) — not against algorithm-specific functions. An algorithm identifier, chosen by policy, selects the implementation. This is the programming basis of crypto-agility (CSWP 39): migration becomes a configuration change plus testing, not a rewrite.
The policy file names an algorithm per use, such as ML-DSA-65 for e-document signatures, and the code never mentions it. When BPA later moves from a hybrid to a pure post-quantum setting, the change is one line in policy plus the test run. Code review checks that no module bypasses the interface by importing an algorithm library directly.
9.2 Input validation
Post-quantum keys and ciphertexts have fixed lengths per parameter set. FIPS 203 requires checks on encapsulation keys (a modulus check that encoded coefficients are below q) and decapsulation keys before use. Code should reject wrong lengths before calling a library and never "pad" or "truncate" to make inputs fit.
An ML-KEM-768 encapsulation key must be exactly 1,184 bytes and a ciphertext exactly 1,088. Checking these lengths first gives a clear error message and avoids passing malformed data into code that might behave unpredictably. The modulus check catches keys whose coefficients were encoded with values of 3329 or higher, which a malicious peer might send to probe for implementation weaknesses.
9.3 Testing beyond examples
Known-answer tests confirm exact outputs. Property-based tests check invariants over many random inputs: decapsulate(encapsulate(pk)) yields the same key; a signature verifies for the signed message and fails for any one-bit change. Negative tests matter as much as positive ones.
A property test for BPA's signer runs a thousand random messages, signs each, verifies each, then flips one random bit in the message and in the signature and expects both checks to fail. A test that only checks successful verification would pass even if verify always returned true, which is precisely the bug negative tests exist to catch.
9.4 Dependencies
Pin versions of cryptographic libraries, track their advisories and record which algorithms and parameter sets each exposes. Draft-era names (e.g. "Kyber768") and final names (ML-KEM-768) are not interchangeable: the final standard changed details, so draft implementations do not interoperate with FIPS 203.
BPA's dependency file records the library name, the exact version, and the algorithms that version provides under final names. When a library release renames or removes a draft algorithm, the build fails at upgrade time rather than at a partner's handshake. Advisory tracking matters because post-quantum libraries are newer and have had more security fixes per year than mature RSA code.
9.5 Worked example
BPA's e-document service calls rsa_sign() in 41 places. Refactoring introduces Signer with implementations RSA-PSS-3072 and ML-DSA-65, selected by a policy file. A test suite runs every call path with both. The diff is large once; the next migration will be small.
The 41 call sites are first found by searching for rsa_sign, then each is checked for assumptions such as a fixed 384-byte signature length in a database column. ML-DSA-65 signatures are 3,309 bytes, so the schema change is part of the same work item. The policy file starts with RSA-PSS-3072 so nothing changes for users until the switch.
9.6 Common errors and limits
- Hard-coding algorithm names and key lengths.
- Treating draft and final algorithm versions as the same.
- Testing only success paths.
Hard-coded names turn each future migration into a code change at every call site, which is exactly what the Signer refactoring removed. Draft and final versions differ in details, so a library still offering Kyber768 will not interoperate with ML-KEM-768. Testing only success paths leaves validation and error handling unchecked, and attackers target precisely those paths with malformed keys and ciphertexts.
Engineering note
Interfaces also need versioned algorithm identifiers that travel with data. A signature stored today must carry an identifier naming its algorithm and parameter set, so that a verifier in ten years selects the right implementation and can refuse retired ones. Without identifiers, systems guess from lengths, which breaks as soon as two algorithms share a size. Treat the identifier as part of the signed or authenticated content wherever the format allows, so an attacker cannot relabel a weak signature as a strong one. This small design habit is what later makes policy-driven migration in Chapter 39 safe rather than merely convenient.
Exercises
- EX017 — Interface design. Write a
KEMinterface (method names and types) suitable for ML-KEM and a future HQC. Rubric: 3 = no algorithm-specific assumptions such as sizes. - EX018 — Length checks. Write a validator that rejects an ML-KEM-768 encapsulation key of the wrong length (correct: 1,184 bytes). Rubric: 3 = rejects, never pads.
Synthesis
Replaceable algorithms start in code: interfaces, policy-chosen identifiers, strict validation, property and negative tests, and dependency tracking that knows draft from final.
Continue studying — chapter 09 of 50
← Chapter 08 (draft) · Course chapter index · Chapter 10 (draft) →