Skip to content

About

Deterministic positional test vectors and cross-implementation validation for the ML-KEM Fujisaki-Okamoto re-encryption comparison (implicit-rejection integrity); contributed to C2SP/wycheproof.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

Repository files navigation

ML-KEM re-encryption positional test vectors

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.

The bug class

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.

The vectors

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.

Regenerate

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/.

Validation

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/.

Reproducing the paper's claims

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 input w = v - s^T u and prints the distribution of the noise |delta_i| the theorem rests on, with the largest value observed (far below the usability threshold q/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

Responsible disclosure

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.

References

About

Deterministic positional test vectors and cross-implementation validation for the ML-KEM Fujisaki-Okamoto re-encryption comparison (implicit-rejection integrity); contributed to C2SP/wycheproof.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages