From a0e9130b894f6eeb810f5ad25ff34fe9782b24ba Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Tue, 9 Dec 2025 00:37:19 +0100 Subject: [PATCH] crypto.mlkem: return J(z||c) on implicit rejection The ML-KEM decapsulation was returning z directly when implicit rejection was triggered, but FIPS 203 specifies it should return J(z || c) = SHAKE256(z || c). --- lib/std/crypto/ml_kem.zig | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/std/crypto/ml_kem.zig b/lib/std/crypto/ml_kem.zig index 9badac5d79..62e21f60a9 100644 --- a/lib/std/crypto/ml_kem.zig +++ b/lib/std/crypto/ml_kem.zig @@ -329,17 +329,19 @@ fn Kyber(comptime p: Params) type { // ct' = innerEnc(pk, m', r') const ct2 = sk.pk.encrypt(&m2, kr2[32..64]); - // Compute H(ct) and put in the second slot of kr2 which will be (K'', H(ct)). - sha3.Sha3_256.hash(ct, kr2[32..], .{}); - - // Replace K'' by z in the first slot of kr2 if ct ≠ ct'. - cmov(32, kr2[0..32], sk.z, ctneq(ciphertext_length, ct.*, ct2)); - if (p.ml_kem) { - // ML-KEM: K = K''/z + // ML-KEM: K = K'' if ct == ct', else K = J(z || c) per FIPS 203 + var k_bar: [shared_length]u8 = undefined; + var j = sha3.Shake256.init(.{}); + j.update(&sk.z); + j.update(ct); + j.squeeze(&k_bar); + cmov(shared_length, kr2[0..shared_length], k_bar, ctneq(ciphertext_length, ct.*, ct2)); return kr2[0..shared_length].*; } else { // Kyber: K = KDF(K''/z ‖ H(c)) + sha3.Sha3_256.hash(ct, kr2[32..], .{}); + cmov(32, kr2[0..32], sk.z, ctneq(ciphertext_length, ct.*, ct2)); var ss: [shared_length]u8 = undefined; sha3.Shake256.hash(&kr2, &ss, .{}); return ss;