commit a0e9130b894f6eeb810f5ad25ff34fe9782b24ba (tree)
parent 7f36c4c7d3c8f3bfabb49a922a6156e4f1c61f67
Author: Frank Denis <github@pureftpd.org>
Date: Tue, 9 Dec 2025 00:37:19 +0100
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).
Diffstat:
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git 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;