authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-04 12:48:16+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-15 10:03:55+02:00
log3ba52a1741f6d1514d818dfc032094a16edad9db
tree90bb4f454069fe1a3a67ee8bf3fe7fec4dcb7cbc
parent9343d0fe4bb700cf58bc861596cf0d9f2093b494

crypto.ml_dsa: reject invalid encodings of small secret polynomials

Packed coefficients have a limited valid range: - With `eta = 2` (ML-DSA-44 and ML-DSA-87), 3-bit values must be 0...4. Values `5` through `7` are reserved. - With `eta = 4` (ML-DSA-65), 4-bit must be values 0...8. Values 9 through 15 are reserved. We were previously accepting reserved values, allowing malformed secret key encodings. This is not a security issue (secret keys are trusted anyway), and the specification doesn't require these coefficients to be checked. In fact, most implementations including the reference implementations currently don't do it. But: 1) the Zig crypto implementations generally try to be strict for validation 2) the checks are very cheap 3) this makes sure the invariants are enforced 4) SecretKey.fromBytes() already allows returning an error, so this is not really an API change 5) OpenSSL, BoringSSL and Go reject these encodings.

1 files changed, 46 insertions(+), 24 deletions(-)

lib/std/crypto/ml_dsa.zig+46-24
......@@ -443,14 +443,14 @@ fn PolyVec(comptime len: u8) type {
443443 }
444444
445445 /// Unpack vector with coefficients in [-eta, eta]
446 fn unpackLeqEta(comptime eta: u8, buf: []const u8) Self {
446 fn unpackLeqEta(comptime eta: u8, buf: []const u8) EncodingError!Self {
447447 const poly_size = if (eta == 2) 96 else 128;
448 const unpack_fn = struct {
449 fn unpack(b: []const u8) Poly {
450 return polyUnpackLeqEta(eta, b);
451 }
452 }.unpack;
453 return unpackWith(poly_size, unpack_fn, buf);
448 var result: Self = undefined;
449 inline for (0..len) |i| {
450 const offset = i * poly_size;
451 result.ps[i] = try polyUnpackLeqEta(eta, buf[offset..][0..poly_size]);
452 }
453 return result;
454454 }
455455
456456 /// Pack vector of polynomials with coefficients < gamma1
......@@ -996,7 +996,7 @@ fn polyPackLeqEta(p: Poly, comptime eta: u8, buf: []u8) void {
996996
997997/// Unpack polynomial with coefficients in [Q-eta, Q+eta] from bytes.
998998/// Output coefficients will not be normalized, but in [q-η, q+η].
999fn polyUnpackLeqEta(comptime eta: u8, buf: []const u8) Poly {
999fn polyUnpackLeqEta(comptime eta: u8, buf: []const u8) EncodingError!Poly {
10001000 comptime {
10011001 if (eta != 2 and eta != 4) {
10021002 @compileError("eta must be 2 or 4");
......@@ -1010,22 +1010,31 @@ fn polyUnpackLeqEta(comptime eta: u8, buf: []const u8) Poly {
10101010 var j: usize = 0;
10111011 var i: usize = 0;
10121012 while (i < buf.len) : (i += 3) {
1013 p.cs[j] = Q + eta - (buf[i] & 7);
1014 p.cs[j + 1] = Q + eta - ((buf[i] >> 3) & 7);
1015 p.cs[j + 2] = Q + eta - ((buf[i] >> 6) | ((buf[i + 1] << 2) & 7));
1016 p.cs[j + 3] = Q + eta - ((buf[i + 1] >> 1) & 7);
1017 p.cs[j + 4] = Q + eta - ((buf[i + 1] >> 4) & 7);
1018 p.cs[j + 5] = Q + eta - ((buf[i + 1] >> 7) | ((buf[i + 2] << 1) & 7));
1019 p.cs[j + 6] = Q + eta - ((buf[i + 2] >> 2) & 7);
1020 p.cs[j + 7] = Q + eta - ((buf[i + 2] >> 5) & 7);
1013 const values = [_]u8{
1014 buf[i] & 7,
1015 (buf[i] >> 3) & 7,
1016 (buf[i] >> 6) | ((buf[i + 1] << 2) & 7),
1017 (buf[i + 1] >> 1) & 7,
1018 (buf[i + 1] >> 4) & 7,
1019 (buf[i + 1] >> 7) | ((buf[i + 2] << 1) & 7),
1020 (buf[i + 2] >> 2) & 7,
1021 (buf[i + 2] >> 5) & 7,
1022 };
1023 for (p.cs[j..][0..values.len], values) |*coefficient, value| {
1024 if (value > 2 * eta) return error.InvalidEncoding;
1025 coefficient.* = Q + eta - value;
1026 }
10211027 j += 8;
10221028 }
10231029 } else { // eta == 4
10241030 // 4 bits per coefficient: unpack 2 coefficients from 1 byte
10251031 var j: usize = 0;
10261032 for (0..buf.len) |i| {
1027 p.cs[j] = Q + eta - (buf[i] & 15);
1028 p.cs[j + 1] = Q + eta - (buf[i] >> 4);
1033 const values = [_]u8{ buf[i] & 15, buf[i] >> 4 };
1034 for (p.cs[j..][0..values.len], values) |*coefficient, value| {
1035 if (value > 2 * eta) return error.InvalidEncoding;
1036 coefficient.* = Q + eta - value;
1037 }
10291038 j += 2;
10301039 }
10311040 }
......@@ -1589,15 +1598,15 @@ fn MLDSAImpl(comptime p: Params) type {
15891598 offset += p.tr_size;
15901599
15911600 sk.s1 = if (p.eta == 2)
1592 PolyVecL.unpackLeqEta(2, bytes[offset..][0 .. p.l * polyLeqEtaPackedSize()])
1601 try PolyVecL.unpackLeqEta(2, bytes[offset..][0 .. p.l * polyLeqEtaPackedSize()])
15931602 else
1594 PolyVecL.unpackLeqEta(4, bytes[offset..][0 .. p.l * polyLeqEtaPackedSize()]);
1603 try PolyVecL.unpackLeqEta(4, bytes[offset..][0 .. p.l * polyLeqEtaPackedSize()]);
15951604 offset += p.l * polyLeqEtaPackedSize();
15961605
15971606 sk.s2 = if (p.eta == 2)
1598 PolyVecK.unpackLeqEta(2, bytes[offset..][0 .. p.k * polyLeqEtaPackedSize()])
1607 try PolyVecK.unpackLeqEta(2, bytes[offset..][0 .. p.k * polyLeqEtaPackedSize()])
15991608 else
1600 PolyVecK.unpackLeqEta(4, bytes[offset..][0 .. p.k * polyLeqEtaPackedSize()]);
1609 try PolyVecK.unpackLeqEta(4, bytes[offset..][0 .. p.k * polyLeqEtaPackedSize()]);
16011610 offset += p.k * polyLeqEtaPackedSize();
16021611
16031612 sk.t0 = PolyVecK.unpackT0(bytes[offset..][0 .. p.k * polyT0PackedSize()]);
......@@ -2443,7 +2452,7 @@ test "polyPackLeqEta / polyUnpackLeqEta roundtrip for eta=2" {
24432452 polyPackLeqEta(p, eta, &buf);
24442453
24452454 // Unpack it
2446 const p2 = polyUnpackLeqEta(eta, &buf);
2455 const p2 = try polyUnpackLeqEta(eta, &buf);
24472456
24482457 // Should be identical
24492458 for (0..N) |i| {
......@@ -2468,7 +2477,7 @@ test "polyPackLeqEta / polyUnpackLeqEta roundtrip for eta=4" {
24682477 polyPackLeqEta(p, eta, &buf);
24692478
24702479 // Unpack it
2471 const p2 = polyUnpackLeqEta(eta, &buf);
2480 const p2 = try polyUnpackLeqEta(eta, &buf);
24722481
24732482 // Should be identical
24742483 for (0..N) |i| {
......@@ -2860,6 +2869,19 @@ test "Key generation basic - all variants" {
28602869 }
28612870}
28622871
2872test "Reject reserved eta coefficients encodings" {
2873 inline for (.{
2874 .{ .variant = MLDSA44, .reserved = 7 },
2875 .{ .variant = MLDSA65, .reserved = 15 },
2876 }) |config| {
2877 var encoded: [config.variant.SecretKey.encoded_length]u8 = @splat(0);
2878 _ = try config.variant.SecretKey.fromBytes(encoded);
2879 const s1_offset = 32 + 32 + config.variant.params.tr_size;
2880 encoded[s1_offset] = config.reserved;
2881 try testing.expectError(error.InvalidEncoding, config.variant.SecretKey.fromBytes(encoded));
2882 }
2883}
2884
28632885test "Key generation determinism" {
28642886 if (builtin.zig_backend == .stage2_llvm and builtin.target.cpu.arch.isPowerpc64()) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/229620
28652887