You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ML-KEM: KEM layer, input checks and zeroization #10
Second half of the core: FIPS 203 Algorithms 16 to 21 on top of #9.
keygen(), keygen_from_seed(d, z), encaps(ek), decaps(dk, c)
Encaps: (K, r) = G(m || H(ek)). No hashing m first and no H(c) in the key, unlike our Kyber code
Decaps: implicit rejection with J(z || c), picked without a branch. The reject flag must never leave the function
Input checks from sections 7.2 and 7.3: ek length and modulus check, ciphertext length on every call, dk length and hash check. They run before anything touches secrets
Zeroize every secret intermediate on every return path, including errors
Randomness from the OS RNG; if it fails, return an error
The _internal functions (caller picks the randomness) stay pub(crate) and are only exposed behind cfg(any(test, feature = "acvp")). FIPS 203 section 3.3 says they're for testing only
Open question: do we expose ML-KEM in the public Rust crate API for 2.0, or keep the crate Python-only for now? I'm leaning towards Python-only, but I'm open to arguments.
Done when: round trips work for all three sets, every input check has a negative test, and the PR checklist is ticked.
Open question: do we expose ML-KEM in the public Rust crate API for 2.0, or keep the crate Python-only for now? I'm leaning towards Python-only, but I'm open to arguments.
Notes for whoever picks this up, from WP-3 part one (#9, merged in #29). Read the module docs at the top of src/mlkem/mod.rs first.
What exists now in src/mlkem/ (still crate-private, still under the dead_code allow in src/lib.rs, keep it until WP-5 wires something in):
kpke.rs: keygen::(d: &[u8; 32]) -> (ek_PKE Vec, dk_PKE Zeroizing), encrypt::(ek, m, r) -> Result<Vec, KyberError>, decrypt::(dk_PKE, c) -> Result<Zeroizing<[u8; 32]>, KyberError>. They only check lengths (KyberError::InvalidInput). The section 7.2 modulus check, the 7.3 hash check and the full dk length check are yours.
compress.rs: poly_compress(r, a, d), poly_decompress(r, a, d) and poly_from_msg. d is a runtime argument because stable Rust cannot take ParamSet constants as const generics. Call generic code with the pattern kpke.rs uses in its tests, kpke::keygen::<{ MlKem768::K }>(..).
acvp_tests.rs: decaps_case is a test-only copy of Algorithm 18 built from kpke::decrypt and encrypt. Replace it with the real decaps_internal once it exists and keep the cases.
Things you will have to write yourself:
The constant-time compare of c and c' and the K' / K-bar select. Do not use kyber/verify.rs (no barrier, caller-supplied length). poly_from_msg shows the idiom used so far (mask through core::hint::black_box). That is best effort, so check the release assembly for the compare and the select and put the grep output in the PR.
H is sha3_256, G is sha3_512, J(z || c) is shake256 with 32 bytes of output, all from kyber::fips202. Test code in acvp_tests.rs has small wrappers you can copy.
Randomness: rand 0.8 is already a dependency and has OsRng, so no new crate is needed. try_fill_bytes and return KyberError::RandomBytesGeneration on failure; do not use thread_rng for key material.
The reject flag must not be returned or stored, and dk, z, m, r, K and K-bar all need zeroizing on the error paths too.
Things that will bite:
PolyVec::from_bytes reduces mod q, which is what the modulus check compares against. For dk_PKE it also decodes a secret; the reduction is branch-free arithmetic, but keep it that way.
Done in #30 (merged). Every box above is covered: keygen, keygen_from_seed, encaps and decaps in src/mlkem/kem.rs, the section 7.2 and 7.3 checks in check.rs, the branch-free select in ct.rs, OsRng with RandomBytesGeneration on failure, zeroization on the error paths, and the *_internal functions behind kem::acvp under cfg(any(test, feature = "acvp")). Round trips pass for all three sets and each input check has a negative test. The assembly evidence is in the PR.
What is left from this issue is only what the PR description lists as believed: the barrier is best effort, armv7/i686 and a dynamic timing test belong to #16, and compiler-made stack copies of secrets belong with the Keccak-spill item in #18.
Activity
Zikithezikit commentedon Oct 29, 2026
In the meantime we'll keep it python only.
Zikithezikit commentedon Oct 30, 2026
Notes for whoever picks this up, from WP-3 part one (#9, merged in #29). Read the module docs at the top of src/mlkem/mod.rs first.
What exists now in src/mlkem/ (still crate-private, still under the dead_code allow in src/lib.rs, keep it until WP-5 wires something in):
Things you will have to write yourself:
Things that will bite:
The open question about a public Rust API stays as decided: Python-only for now.
Zikithezikit commentedon Oct 30, 2026
Done in #30 (merged). Every box above is covered: keygen, keygen_from_seed, encaps and decaps in src/mlkem/kem.rs, the section 7.2 and 7.3 checks in check.rs, the branch-free select in ct.rs, OsRng with RandomBytesGeneration on failure, zeroization on the error paths, and the *_internal functions behind kem::acvp under cfg(any(test, feature = "acvp")). Round trips pass for all three sets and each input check has a negative test. The assembly evidence is in the PR.
What is left from this issue is only what the PR description lists as believed: the barrier is best effort, armv7/i686 and a dynamic timing test belong to #16, and compiler-made stack copies of secrets belong with the Keccak-spill item in #18.