From 8a353860179a1663987dae856ba08ccbbd037400 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Mon, 7 Sep 2026 21:02:45 +0200 Subject: [PATCH 1/4] crypto.ff: fix primitive conversion overflow checks Reject negative inputs and values that don't fit the destination type. And check for overflow before each limb shift, including when the destination is smaller than a limb. --- lib/std/crypto/ff.zig | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/std/crypto/ff.zig b/lib/std/crypto/ff.zig index c59dc92c61d66a457aabd11175f9ee0c5f83e582..b78fb61e36e324cb1ad6b5bb3957b67072f10a25 100644 --- a/lib/std/crypto/ff.zig +++ b/lib/std/crypto/ff.zig @@ -102,14 +102,15 @@ pub fn Uint(comptime max_bits: comptime_int) type { /// Creates a new big integer from a primitive type. /// This function may not run in constant time. pub fn fromPrimitive(comptime T: type, init_value: T) OverflowError!Self { - var x = init_value; + const U = @Int(.unsigned, @bitSizeOf(T)); + var x = math.cast(U, init_value) orelse return error.Overflow; var out: Self = .{ .limbs_buffer = undefined, .limbs_len = max_limbs_count, }; for (&out.limbs_buffer) |*limb| { limb.* = if (@bitSizeOf(T) > t_bits) @as(TLimb, @truncate(x)) else x; - x = math.shr(T, x, t_bits); + x = math.shr(U, x, t_bits); } if (x != 0) { return error.Overflow; @@ -120,18 +121,21 @@ pub fn Uint(comptime max_bits: comptime_int) type { /// Converts a big integer to a primitive type. /// This function may not run in constant time. pub fn toPrimitive(self: Self, comptime T: type) OverflowError!T { - var x: T = 0; + const U = @Int(.unsigned, @bitSizeOf(T)); + var x: U = 0; var i = self.limbs_len - 1; while (true) : (i -= 1) { - if (@bitSizeOf(T) >= t_bits and math.shr(T, x, @bitSizeOf(T) - t_bits) != 0) { + // Check for overflow before shifting, even when the destination is narrower than a limb. + const discarded = if (@bitSizeOf(U) >= t_bits) math.shr(U, x, @bitSizeOf(U) - t_bits) else x; + if (discarded != 0) { return error.Overflow; } - x = math.shl(T, x, t_bits); - const v = math.cast(T, self.limbsConst()[i]) orelse return error.Overflow; + x = math.shl(U, x, t_bits); + const v = math.cast(U, self.limbsConst()[i]) orelse return error.Overflow; x |= v; if (i == 0) break; } - return x; + return math.cast(T, x) orelse error.Overflow; } /// Encodes a big integer into a byte array. @@ -1085,3 +1089,20 @@ test ct { try testCt(ct_protected); try testCt(ct_unprotected); } + +test "Uint serialization and predicates" { + if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; + + const U = Uint(256); + const x = try U.fromPrimitive(u128, (1 << t_bits) + 5); + try testing.expectError(error.Overflow, Uint(64).fromPrimitive(u256, 1 << 200)); + try testing.expectError(error.Overflow, x.toPrimitive(u8)); + try testing.expectError(error.Overflow, x.toPrimitive(@Int(.unsigned, t_bits))); + try testing.expectEqual((1 << t_bits) + 5, try x.toPrimitive(@Int(.unsigned, t_bits + 1))); + const max = try U.fromPrimitive(u128, (1 << t_bits) - 1); + try testing.expectEqual((1 << t_bits) - 1, try max.toPrimitive(@Int(.unsigned, t_bits))); + try testing.expectError(error.Overflow, max.toPrimitive(@Int(.unsigned, t_bits - 1))); + const signed_max = try U.fromPrimitive(u128, math.maxInt(i128)); + try testing.expectEqual(math.maxInt(i128), try signed_max.toPrimitive(i128)); + try testing.expectError(error.Overflow, (try U.fromPrimitive(u128, 1 << 127)).toPrimitive(i128)); +} -- 2.54.0 From 53b508142ee9d53fd503541b7e4583b36529106c Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Mon, 7 Sep 2026 21:02:48 +0200 Subject: [PATCH 2/4] crypto.ff: fix byte encoding and decoding with padding We should accept zero padding and encodings that use the full rounded storage capacity, while rejecting nonzero bits that don't fit. And allow shorter output buffers when the limbs we skip are all zero. --- lib/std/crypto/ff.zig | 179 +++++++++++++++++++++++++++++++----------- 1 file changed, 134 insertions(+), 45 deletions(-) diff --git a/lib/std/crypto/ff.zig b/lib/std/crypto/ff.zig index b78fb61e36e324cb1ad6b5bb3957b67072f10a25..2432acbe53643176c86cbc2199647a8caf569209 100644 --- a/lib/std/crypto/ff.zig +++ b/lib/std/crypto/ff.zig @@ -55,14 +55,18 @@ pub const RepresentationError = error{UnexpectedRepresentation}; pub const Error = OverflowError || InvalidModulusError || NullExponentError || FieldElementError || RepresentationError; /// An unsigned big integer with a fixed maximum size (`max_bits`), suitable for cryptographic operations. +/// Storage rounds up to whole limbs and can hold up to `capacity_bits` bits. /// Unless side-channels mitigations are explicitly disabled, operations are designed to be constant-time. pub fn Uint(comptime max_bits: comptime_int) type { comptime assert(@bitSizeOf(Limb) % 8 == 0); // Limb size must be a multiple of 8 + comptime assert(max_bits > 0); return struct { const Self = @This(); const max_limbs_count = @divCeil(max_bits, t_bits); + pub const capacity_bits = max_limbs_count * t_bits; + limbs_buffer: [max_limbs_count]Limb, /// The number of active limbs. limbs_len: usize, @@ -154,15 +158,15 @@ pub fn Uint(comptime max_bits: comptime_int) type { var remaining_bits = t_bits; var limb = self.limbsConst()[i]; while (remaining_bits >= 8) { - bytes[out_i] |= math.shl(u8, @as(u8, @truncate(limb)), shift); + bytes[out_i] |= math.shl(u8, @truncate(limb), shift); const consumed = 8 - shift; - limb >>= @as(u4, @truncate(consumed)); + limb >>= @truncate(consumed); remaining_bits -= consumed; shift = 0; switch (endian) { .big => { if (out_i == 0) { - if (i != self.limbs_len - 1 or limb != 0) { + if (limb | orLimbs(self.limbsConst()[i + 1 ..]) != 0) { return error.Overflow; } return; @@ -172,7 +176,7 @@ pub fn Uint(comptime max_bits: comptime_int) type { .little => { out_i += 1; if (out_i == bytes.len) { - if (i != self.limbs_len - 1 or limb != 0) { + if (limb | orLimbs(self.limbsConst()[i + 1 ..]) != 0) { return error.Overflow; } return; @@ -180,48 +184,16 @@ pub fn Uint(comptime max_bits: comptime_int) type { }, } } - bytes[out_i] |= @as(u8, @truncate(limb)); + bytes[out_i] |= @truncate(limb); shift = remaining_bits; } } /// Creates a new big integer from a byte array. pub fn fromBytes(bytes: []const u8, comptime endian: Endian) OverflowError!Self { - if (bytes.len == 0) return Self.zero; - var shift: usize = 0; var out = Self.zero; - var out_i: usize = 0; - var i: usize = switch (endian) { - .big => bytes.len - 1, - .little => 0, - }; - while (true) { - const bi = bytes[i]; - out.limbs()[out_i] |= math.shl(Limb, bi, shift); - shift += 8; - if (shift >= t_bits) { - shift -= t_bits; - out.limbs()[out_i] = @as(TLimb, @truncate(out.limbs()[out_i])); - const overflow = math.shr(Limb, bi, 8 - shift); - out_i += 1; - if (out_i >= out.limbs_len) { - if (overflow != 0 or i != 0) { - return error.Overflow; - } - break; - } - out.limbs()[out_i] = overflow; - } - switch (endian) { - .big => { - if (i == 0) break; - i -= 1; - }, - .little => { - i += 1; - if (i == bytes.len) break; - }, - } + if (decodeBytes(out.limbs(), bytes, endian) != 0) { + return error.Overflow; } return out; } @@ -243,11 +215,7 @@ pub fn Uint(comptime max_bits: comptime_int) type { /// Returns `true` if the integer is zero. pub fn isZero(x: Self) bool { - var t: Limb = 0; - for (x.limbsConst()) |elem| { - t |= elem; - } - return ct.eql(t, 0); + return ct.eql(orLimbs(x.limbsConst()), 0); } /// Returns `true` if the integer is odd. @@ -380,6 +348,47 @@ fn Fe_(comptime bits: comptime_int) type { }; } +// Decodes into zeroed limbs and returns the OR of the bits that didn't fit. +fn decodeBytes(limbs: []Limb, bytes: []const u8, comptime endian: Endian) Limb { + var acc: Limb = 0; + var shift: usize = 0; + var out_i: usize = 0; + for (0..bytes.len) |k| { + const bi = bytes[ + switch (endian) { + .big => bytes.len - 1 - k, + .little => k, + } + ]; + if (out_i >= limbs.len) { + acc |= bi; + continue; + } + limbs[out_i] |= math.shl(Limb, bi, shift); + shift += 8; + if (shift >= t_bits) { + shift -= t_bits; + limbs[out_i] = @as(TLimb, @truncate(limbs[out_i])); + const spill = math.shr(Limb, bi, 8 - shift); + out_i += 1; + if (out_i < limbs.len) { + limbs[out_i] = spill; + } else { + acc |= spill; + } + } + } + return acc; +} + +fn orLimbs(limbs: []const Limb) Limb { + var t: Limb = 0; + for (limbs) |limb| { + t |= limb; + } + return t; +} + /// A modulus, defining a finite field. /// All operations within the field are performed modulo this modulus, without heap allocations. /// `max_bits` represents the number of bits in the maximum value the modulus can be set to. @@ -1090,7 +1099,40 @@ test ct { try testCt(ct_unprotected); } -test "Uint serialization and predicates" { +fn expectWellFormedLimbs(x: anytype) !void { + for (x.limbsConst()) |limb| { + try testing.expect(limb <= math.maxInt(TLimb)); + } + for (x.limbs_buffer[x.limbs_len..]) |limb| { + try testing.expectEqual(0, limb); + } +} + +// Big-endian encoding of the largest value a `Uint` can store. +fn maxUintBytes(comptime U: type) [@divCeil(U.capacity_bits, 8)]u8 { + var buf: [@divCeil(U.capacity_bits, 8)]u8 = @splat(0xff); + buf[0] = 0xff >> (8 * buf.len - U.capacity_bits); + return buf; +} + +test "modulus creation" { + if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; + + const M = Modulus(256); + try testing.expectError(error.EvenModulus, M.fromPrimitive(u8, 0)); + try testing.expectError(error.ModulusTooSmall, M.fromPrimitive(u8, 1)); + try testing.expectError(error.EvenModulus, M.fromPrimitive(u8, 2)); + for ([_]u65{ 3, 255, (1 << 64) + 1 }) |v| { + const m = try M.fromPrimitive(u65, v); + try testing.expectEqual(v, try m.toUint().toPrimitive(u65)); + try testing.expectEqual(@divCeil(65 - @clz(v), 8), m.encodedLen()); + try testing.expect((try M.fromUint(m.toUint())).v.eql(m.v)); + } + const cap = maxUintBytes(Uint(256)); + try testing.expectEqual(cap.len, (try M.fromBytes(&cap, .big)).encodedLen()); +} + +test "Uint serialization" { if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; const U = Uint(256); @@ -1105,4 +1147,51 @@ test "Uint serialization and predicates" { const signed_max = try U.fromPrimitive(u128, math.maxInt(i128)); try testing.expectEqual(math.maxInt(i128), try signed_max.toPrimitive(i128)); try testing.expectError(error.Overflow, (try U.fromPrimitive(u128, 1 << 127)).toPrimitive(i128)); + + inline for (.{ .big, .little }) |endian| { + const zero = try U.fromBytes(&.{}, endian); + var empty: [0]u8 = .{}; + try zero.toBytes(&empty, endian); + try testing.expectError(error.Overflow, x.toBytes(&empty, endian)); + var tight: [16]u8 = undefined; + try x.toBytes(&tight, endian); + try testing.expectEqual((1 << t_bits) + 5, mem.readInt(u128, &tight, endian)); + try testing.expect(x.eql(try U.fromBytes(&tight, endian))); + var normalized: [16]u8 = undefined; + try x.normalize().toBytes(&normalized, endian); + try testing.expectEqualSlices(u8, &tight, &normalized); + + var cap = maxUintBytes(U); + if (endian == .little) mem.reverse(u8, &cap); + const full = try U.fromBytes(&cap, endian); + try expectWellFormedLimbs(full); + var out: [cap.len]u8 = undefined; + try full.toBytes(&out, endian); + try testing.expectEqualSlices(u8, &cap, &out); + out[if (endian == .big) 0 else out.len - 1] |= 1 << (U.capacity_bits % 8); + try testing.expectError(error.Overflow, U.fromBytes(&out, endian)); + var padded = if (endian == .big) [_]u8{0} ++ cap else cap ++ [_]u8{0}; + try testing.expect(full.eql(try U.fromBytes(&padded, endian))); + padded[if (endian == .big) 0 else padded.len - 1] = 1; + try testing.expectError(error.Overflow, U.fromBytes(&padded, endian)); + } +} + +test "field element decoding" { + if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; + + const M = Modulus(256); + const mv: u256 = (1 << 190) + 33; + const m = try M.fromPrimitive(u256, mv); + try testing.expectError(error.NonCanonical, M.Fe.fromPrimitive(u256, m, mv)); + try testing.expectError(error.Overflow, M.Fe.fromPrimitive(u256, m, 1 << 255)); + + inline for (.{ .big, .little }) |endian| { + var buf: [32]u8 = undefined; + try m.toBytes(&buf, endian); + try testing.expectError(error.NonCanonical, M.Fe.fromBytes(m, &buf, endian)); + const x = try M.Fe.fromPrimitive(u256, m, mv - 1); + try x.toBytes(&buf, endian); + try testing.expect(x.eql(try M.Fe.fromBytes(m, &buf, endian))); + } } -- 2.54.0 From 199a5a2700eeb8f3eaed36255fd3f0c0756f0935 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Mon, 7 Sep 2026 21:02:52 +0200 Subject: [PATCH 3/4] crypto.ff: add functions to access the modulus and encoded length Add encodedLen() to size serialization buffers, and toUint() to get the modulus as an integer. --- lib/std/crypto/ff.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/std/crypto/ff.zig b/lib/std/crypto/ff.zig index 2432acbe53643176c86cbc2199647a8caf569209..9192dc2179d4d7a14c30c892c331df248038517c 100644 --- a/lib/std/crypto/ff.zig +++ b/lib/std/crypto/ff.zig @@ -424,6 +424,10 @@ pub fn Modulus(comptime max_bits: comptime_int) type { return self.limbs_count() * t_bits - self.leading; } + pub fn encodedLen(self: Self) usize { + return @divCeil(self.bits(), 8); + } + /// Returns the element `1`. pub fn one(self: Self) Fe { var fe = self.zero; @@ -487,6 +491,10 @@ pub fn Modulus(comptime max_bits: comptime_int) type { return self.v.toBytes(bytes, endian); } + pub fn toUint(self: Self) FeUint { + return self.v; + } + /// Rejects field elements that are not in the canonical form. pub fn rejectNonCanonical(self: Self, fe: Fe) error{NonCanonical}!void { if (fe.limbs_count() != self.limbs_count() or ct.limbsCmpGeq(fe.v, self.v)) { -- 2.54.0 From 6df745f1a06b1215235059ad1abff134e694aa83 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 20 Sep 2026 13:18:10 +0200 Subject: [PATCH 4/4] Add comments for the new functions --- lib/std/crypto/ff.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/std/crypto/ff.zig b/lib/std/crypto/ff.zig index 9192dc2179d4d7a14c30c892c331df248038517c..4e599b67c48fff0e8f7241e821398ce36b2972e8 100644 --- a/lib/std/crypto/ff.zig +++ b/lib/std/crypto/ff.zig @@ -424,6 +424,7 @@ pub fn Modulus(comptime max_bits: comptime_int) type { return self.limbs_count() * t_bits - self.leading; } + /// Returns the encoded length, in bytes. pub fn encodedLen(self: Self) usize { return @divCeil(self.bits(), 8); } @@ -491,6 +492,7 @@ pub fn Modulus(comptime max_bits: comptime_int) type { return self.v.toBytes(bytes, endian); } + /// Returns the modulus as an integer pub fn toUint(self: Self) FeUint { return self.v; } -- 2.54.0