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 {...@@ -443,14 +443,14 @@ fn PolyVec(comptime len: u8) type {
443 }443 }
444444
445 /// Unpack vector with coefficients in [-eta, eta]445 /// 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 {
447 const poly_size = if (eta == 2) 96 else 128;447 const poly_size = if (eta == 2) 96 else 128;
448 const unpack_fn = struct {448 var result: Self = undefined;
449 fn unpack(b: []const u8) Poly {449 inline for (0..len) |i| {
450 return polyUnpackLeqEta(eta, b);450 const offset = i * poly_size;
451 }451 result.ps[i] = try polyUnpackLeqEta(eta, buf[offset..][0..poly_size]);
452 }.unpack;452 }
453 return unpackWith(poly_size, unpack_fn, buf);453 return result;
454 }454 }
455455
456 /// Pack vector of polynomials with coefficients < gamma1456 /// Pack vector of polynomials with coefficients < gamma1
...@@ -996,7 +996,7 @@ fn polyPackLeqEta(p: Poly, comptime eta: u8, buf: []u8) void {...@@ -996,7 +996,7 @@ fn polyPackLeqEta(p: Poly, comptime eta: u8, buf: []u8) void {
996996
997/// Unpack polynomial with coefficients in [Q-eta, Q+eta] from bytes.997/// Unpack polynomial with coefficients in [Q-eta, Q+eta] from bytes.
998/// Output coefficients will not be normalized, but in [q-η, q+η].998/// 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 {
1000 comptime {1000 comptime {
1001 if (eta != 2 and eta != 4) {1001 if (eta != 2 and eta != 4) {
1002 @compileError("eta must be 2 or 4");1002 @compileError("eta must be 2 or 4");
...@@ -1010,22 +1010,31 @@ fn polyUnpackLeqEta(comptime eta: u8, buf: []const u8) Poly {...@@ -1010,22 +1010,31 @@ fn polyUnpackLeqEta(comptime eta: u8, buf: []const u8) Poly {
1010 var j: usize = 0;1010 var j: usize = 0;
1011 var i: usize = 0;1011 var i: usize = 0;
1012 while (i < buf.len) : (i += 3) {1012 while (i < buf.len) : (i += 3) {
1013 p.cs[j] = Q + eta - (buf[i] & 7);1013 const values = [_]u8{
1014 p.cs[j + 1] = Q + eta - ((buf[i] >> 3) & 7);1014 buf[i] & 7,
1015 p.cs[j + 2] = Q + eta - ((buf[i] >> 6) | ((buf[i + 1] << 2) & 7));1015 (buf[i] >> 3) & 7,
1016 p.cs[j + 3] = Q + eta - ((buf[i + 1] >> 1) & 7);1016 (buf[i] >> 6) | ((buf[i + 1] << 2) & 7),
1017 p.cs[j + 4] = Q + eta - ((buf[i + 1] >> 4) & 7);1017 (buf[i + 1] >> 1) & 7,
1018 p.cs[j + 5] = Q + eta - ((buf[i + 1] >> 7) | ((buf[i + 2] << 1) & 7));1018 (buf[i + 1] >> 4) & 7,
1019 p.cs[j + 6] = Q + eta - ((buf[i + 2] >> 2) & 7);1019 (buf[i + 1] >> 7) | ((buf[i + 2] << 1) & 7),
1020 p.cs[j + 7] = Q + eta - ((buf[i + 2] >> 5) & 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 }
1021 j += 8;1027 j += 8;
1022 }1028 }
1023 } else { // eta == 41029 } else { // eta == 4
1024 // 4 bits per coefficient: unpack 2 coefficients from 1 byte1030 // 4 bits per coefficient: unpack 2 coefficients from 1 byte
1025 var j: usize = 0;1031 var j: usize = 0;
1026 for (0..buf.len) |i| {1032 for (0..buf.len) |i| {
1027 p.cs[j] = Q + eta - (buf[i] & 15);1033 const values = [_]u8{ buf[i] & 15, buf[i] >> 4 };
1028 p.cs[j + 1] = Q + eta - (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 }
1029 j += 2;1038 j += 2;
1030 }1039 }
1031 }1040 }
...@@ -1589,15 +1598,15 @@ fn MLDSAImpl(comptime p: Params) type {...@@ -1589,15 +1598,15 @@ fn MLDSAImpl(comptime p: Params) type {
1589 offset += p.tr_size;1598 offset += p.tr_size;
15901599
1591 sk.s1 = if (p.eta == 2)1600 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()])
1593 else1602 else
1594 PolyVecL.unpackLeqEta(4, bytes[offset..][0 .. p.l * polyLeqEtaPackedSize()]);1603 try PolyVecL.unpackLeqEta(4, bytes[offset..][0 .. p.l * polyLeqEtaPackedSize()]);
1595 offset += p.l * polyLeqEtaPackedSize();1604 offset += p.l * polyLeqEtaPackedSize();
15961605
1597 sk.s2 = if (p.eta == 2)1606 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()])
1599 else1608 else
1600 PolyVecK.unpackLeqEta(4, bytes[offset..][0 .. p.k * polyLeqEtaPackedSize()]);1609 try PolyVecK.unpackLeqEta(4, bytes[offset..][0 .. p.k * polyLeqEtaPackedSize()]);
1601 offset += p.k * polyLeqEtaPackedSize();1610 offset += p.k * polyLeqEtaPackedSize();
16021611
1603 sk.t0 = PolyVecK.unpackT0(bytes[offset..][0 .. p.k * polyT0PackedSize()]);1612 sk.t0 = PolyVecK.unpackT0(bytes[offset..][0 .. p.k * polyT0PackedSize()]);
...@@ -2443,7 +2452,7 @@ test "polyPackLeqEta / polyUnpackLeqEta roundtrip for eta=2" {...@@ -2443,7 +2452,7 @@ test "polyPackLeqEta / polyUnpackLeqEta roundtrip for eta=2" {
2443 polyPackLeqEta(p, eta, &buf);2452 polyPackLeqEta(p, eta, &buf);
24442453
2445 // Unpack it2454 // Unpack it
2446 const p2 = polyUnpackLeqEta(eta, &buf);2455 const p2 = try polyUnpackLeqEta(eta, &buf);
24472456
2448 // Should be identical2457 // Should be identical
2449 for (0..N) |i| {2458 for (0..N) |i| {
...@@ -2468,7 +2477,7 @@ test "polyPackLeqEta / polyUnpackLeqEta roundtrip for eta=4" {...@@ -2468,7 +2477,7 @@ test "polyPackLeqEta / polyUnpackLeqEta roundtrip for eta=4" {
2468 polyPackLeqEta(p, eta, &buf);2477 polyPackLeqEta(p, eta, &buf);
24692478
2470 // Unpack it2479 // Unpack it
2471 const p2 = polyUnpackLeqEta(eta, &buf);2480 const p2 = try polyUnpackLeqEta(eta, &buf);
24722481
2473 // Should be identical2482 // Should be identical
2474 for (0..N) |i| {2483 for (0..N) |i| {
...@@ -2860,6 +2869,19 @@ test "Key generation basic - all variants" {...@@ -2860,6 +2869,19 @@ test "Key generation basic - all variants" {
2860 }2869 }
2861}2870}
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
2863test "Key generation determinism" {2885test "Key generation determinism" {
2864 if (builtin.zig_backend == .stage2_llvm and builtin.target.cpu.arch.isPowerpc64()) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/2296202886 if (builtin.zig_backend == .stage2_llvm and builtin.target.cpu.arch.isPowerpc64()) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/229620
28652887