Generator and validation harness for a deterministic positional sweep of modified-ciphertext ML-KEM decapsulation test vectors, contributed to C2SP/wycheproof. The vectors target implementations that skip part of the Fujisaki-Okamoto (FO) re-encryption comparison during implicit rejection.
This repository holds the generator and cross-check code. The generated JSON is submitted to Wycheproof itself: generator code lives here, generated vectors live in the Wycheproof repo.
ML-KEM decapsulation must implicitly reject a modified ciphertext: it recomputes
c' from the decrypted message and, if c != c', returns the reject secret
J(z, c) instead of the real shared secret (FIPS 203 Algorithm 18).
An implementation that compares only part of c accepts ciphertexts modified
solely in the un-compared region, which breaks IND-CCA2. Two public, fixed cases
motivate these vectors:
- CVE-2026-10097 (wolfSSL AVX2): compared 1536 of 1568 bytes, skipping the last 32.
- CVE-2026-6330 (wolfSSL NEON): compared only half the ciphertext.
One new MLKEMTest group per parameter set is added to the main
mlkem_{512,768,1024}_test.json files (7 + 8 + 9 = 24 tests). Each is a
single-byte, message-preserving modification of one valid base ciphertext, so the
only difference from the re-encryption is the target byte, which is what makes a
regional skip detectable. result: valid with the implicit-reject K, flag
MalleableCiphertext.
Positions per set (byte offsets), one per structural region:
| Set | c1 / c2 / N | positions |
|---|---|---|
| ML-KEM-512 | 640 / 128 / 768 | 0, 319, 639, 640, 704, 736, 767 |
| ML-KEM-768 | 960 / 128 / 1088 | 0, 319, 639, 959, 960, 1024, 1056, 1087 |
| ML-KEM-1024 | 1408 / 160 / 1568 | 0, 351, 703, 1055, 1407, 1408, 1488, 1536, 1567 |
Roles: byte 0 (head), the last byte of each 32*du c1 poly-block, and c2
start / middle / N-32 / last. For ML-KEM-1024 this includes bytes 1536 and 1567,
the exact window skipped by CVE-2026-10097. The two last-byte positions
(last of c1, last of c2) coincide with those of Wycheproof PR #254;
they are kept so each file has a self-contained per-region sweep.
pip install kyber-py # tested with kyber-py 1.2.0
git clone https://github.com/C2SP/wycheproof
python generate_vectors.py --corpus wycheproof/testvectors_v1 --out .
The generator calls the reference's internal FIPS 203 routines, so pin
kyber-py 1.2.0 (later releases may rename them). A pre-generated copy
of the vectorgen envelopes is in envelopes/.
validate_and_demo.py trusts the reference implementation only after it matches
the Wycheproof official-seed KATs (100 per set) and the corpus reject-value
vectors (20 per set), then demonstrates that a comparison skipping an interior
region is missed by the two last-byte anchors of PR #254 but caught by
an interior positional vector:
python validate_and_demo.py --corpus wycheproof/testvectors_v1
The generated vectors were additionally cross-checked against OpenSSL 4.1 and
wolfSSL decapsulation (both return the expected reject K, i.e. no false
positives on a conforming implementation); see crosscheck/.
The paper "Testing the Re-encryption Check in ML-KEM: A Detectability Theorem and
a Coverage Measurement" adds three measurements. Each is reproduced by a
self-contained script in crosscheck/ (needs only kyber-py):
usability_scan.py-- the completeness theorem. Over 50 random bases per parameter set, it tests every ciphertext byte for a message-preserving single-bit flip and reports that every position is usable (longest unusable run 0, so L* = 1).margin_dist.py-- Figure 1. It reconstructs the decode inputw = v - s^T uand prints the distribution of the noise|delta_i|the theorem rests on, with the largest value observed (far below the usability thresholdq/4 - q/2^dv).acvp_measure.py-- the ACVP coverage finding. It reads NIST's ACVP ML-KEM decapsulation vectors (usnistgov/ACVP-Server,gen-val/json-files/ML-KEM-encapDecap-FIPS203/internalProjection.json), validates the reference against ACVP's expected shared secrets (15/15), and shows the reject cases modify only the last byte, catching tail skips (5/5) but no interior skip (0/5).
python crosscheck/usability_scan.py
python crosscheck/margin_dist.py
ACVP_JSON=internalProjection.json python crosscheck/acvp_measure.py
CVE-2026-10097 and CVE-2026-6330 are public and fixed. These vectors are published only after the bugs were fixed, to guard against the same class recurring.
- C2SP/wycheproof and PR #254 (the last-byte re-encryption vectors this work generalizes).
- kyber-py (GiacomoPope) - the pure-Python ML-KEM reference used to generate and validate the vectors.
- FIPS 203 - the ML-KEM specification.
- CVE-2026-10097, CVE-2026-6330 - the motivating implementation bugs.
- ml-kem-key-recovery - full key-recovery analysis of the same wolfSSL bugs.