authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-09-22 13:32:43+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-09-22 13:32:43+02:00
log20af0e10a1d4b4159f5530df1af7ec70ed12ee13
tree07fe7cebad922fa8a36e34780d9e1c1217eac763
parentb844605c524b9d7fe7327d7b95463ce70ed6fbf4
parent6df745f1a06b1215235059ad1abff134e694aa83

Merge pull request 'crypto.ff: fix overflow checks in primitive conversions and byte (de)serialization' (#36895) from jedisct1/zig:ff-fixes1 into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/36895

1 files changed, 171 insertions(+), 51 deletions(-)

lib/std/crypto/ff.zig+171-51
......@@ -55,14 +55,18 @@ pub const RepresentationError = error{UnexpectedRepresentation};
5555pub const Error = OverflowError || InvalidModulusError || NullExponentError || FieldElementError || RepresentationError;
5656
5757/// An unsigned big integer with a fixed maximum size (`max_bits`), suitable for cryptographic operations.
58/// Storage rounds up to whole limbs and can hold up to `capacity_bits` bits.
5859/// Unless side-channels mitigations are explicitly disabled, operations are designed to be constant-time.
5960pub fn Uint(comptime max_bits: comptime_int) type {
6061 comptime assert(@bitSizeOf(Limb) % 8 == 0); // Limb size must be a multiple of 8
62 comptime assert(max_bits > 0);
6163
6264 return struct {
6365 const Self = @This();
6466 const max_limbs_count = @divCeil(max_bits, t_bits);
6567
68 pub const capacity_bits = max_limbs_count * t_bits;
69
6670 limbs_buffer: [max_limbs_count]Limb,
6771 /// The number of active limbs.
6872 limbs_len: usize,
......@@ -102,14 +106,15 @@ pub fn Uint(comptime max_bits: comptime_int) type {
102106 /// Creates a new big integer from a primitive type.
103107 /// This function may not run in constant time.
104108 pub fn fromPrimitive(comptime T: type, init_value: T) OverflowError!Self {
105 var x = init_value;
109 const U = @Int(.unsigned, @bitSizeOf(T));
110 var x = math.cast(U, init_value) orelse return error.Overflow;
106111 var out: Self = .{
107112 .limbs_buffer = undefined,
108113 .limbs_len = max_limbs_count,
109114 };
110115 for (&out.limbs_buffer) |*limb| {
111116 limb.* = if (@bitSizeOf(T) > t_bits) @as(TLimb, @truncate(x)) else x;
112 x = math.shr(T, x, t_bits);
117 x = math.shr(U, x, t_bits);
113118 }
114119 if (x != 0) {
115120 return error.Overflow;
......@@ -120,18 +125,21 @@ pub fn Uint(comptime max_bits: comptime_int) type {
120125 /// Converts a big integer to a primitive type.
121126 /// This function may not run in constant time.
122127 pub fn toPrimitive(self: Self, comptime T: type) OverflowError!T {
123 var x: T = 0;
128 const U = @Int(.unsigned, @bitSizeOf(T));
129 var x: U = 0;
124130 var i = self.limbs_len - 1;
125131 while (true) : (i -= 1) {
126 if (@bitSizeOf(T) >= t_bits and math.shr(T, x, @bitSizeOf(T) - t_bits) != 0) {
132 // Check for overflow before shifting, even when the destination is narrower than a limb.
133 const discarded = if (@bitSizeOf(U) >= t_bits) math.shr(U, x, @bitSizeOf(U) - t_bits) else x;
134 if (discarded != 0) {
127135 return error.Overflow;
128136 }
129 x = math.shl(T, x, t_bits);
130 const v = math.cast(T, self.limbsConst()[i]) orelse return error.Overflow;
137 x = math.shl(U, x, t_bits);
138 const v = math.cast(U, self.limbsConst()[i]) orelse return error.Overflow;
131139 x |= v;
132140 if (i == 0) break;
133141 }
134 return x;
142 return math.cast(T, x) orelse error.Overflow;
135143 }
136144
137145 /// Encodes a big integer into a byte array.
......@@ -150,15 +158,15 @@ pub fn Uint(comptime max_bits: comptime_int) type {
150158 var remaining_bits = t_bits;
151159 var limb = self.limbsConst()[i];
152160 while (remaining_bits >= 8) {
153 bytes[out_i] |= math.shl(u8, @as(u8, @truncate(limb)), shift);
161 bytes[out_i] |= math.shl(u8, @truncate(limb), shift);
154162 const consumed = 8 - shift;
155 limb >>= @as(u4, @truncate(consumed));
163 limb >>= @truncate(consumed);
156164 remaining_bits -= consumed;
157165 shift = 0;
158166 switch (endian) {
159167 .big => {
160168 if (out_i == 0) {
161 if (i != self.limbs_len - 1 or limb != 0) {
169 if (limb | orLimbs(self.limbsConst()[i + 1 ..]) != 0) {
162170 return error.Overflow;
163171 }
164172 return;
......@@ -168,7 +176,7 @@ pub fn Uint(comptime max_bits: comptime_int) type {
168176 .little => {
169177 out_i += 1;
170178 if (out_i == bytes.len) {
171 if (i != self.limbs_len - 1 or limb != 0) {
179 if (limb | orLimbs(self.limbsConst()[i + 1 ..]) != 0) {
172180 return error.Overflow;
173181 }
174182 return;
......@@ -176,48 +184,16 @@ pub fn Uint(comptime max_bits: comptime_int) type {
176184 },
177185 }
178186 }
179 bytes[out_i] |= @as(u8, @truncate(limb));
187 bytes[out_i] |= @truncate(limb);
180188 shift = remaining_bits;
181189 }
182190 }
183191
184192 /// Creates a new big integer from a byte array.
185193 pub fn fromBytes(bytes: []const u8, comptime endian: Endian) OverflowError!Self {
186 if (bytes.len == 0) return Self.zero;
187 var shift: usize = 0;
188194 var out = Self.zero;
189 var out_i: usize = 0;
190 var i: usize = switch (endian) {
191 .big => bytes.len - 1,
192 .little => 0,
193 };
194 while (true) {
195 const bi = bytes[i];
196 out.limbs()[out_i] |= math.shl(Limb, bi, shift);
197 shift += 8;
198 if (shift >= t_bits) {
199 shift -= t_bits;
200 out.limbs()[out_i] = @as(TLimb, @truncate(out.limbs()[out_i]));
201 const overflow = math.shr(Limb, bi, 8 - shift);
202 out_i += 1;
203 if (out_i >= out.limbs_len) {
204 if (overflow != 0 or i != 0) {
205 return error.Overflow;
206 }
207 break;
208 }
209 out.limbs()[out_i] = overflow;
210 }
211 switch (endian) {
212 .big => {
213 if (i == 0) break;
214 i -= 1;
215 },
216 .little => {
217 i += 1;
218 if (i == bytes.len) break;
219 },
220 }
195 if (decodeBytes(out.limbs(), bytes, endian) != 0) {
196 return error.Overflow;
221197 }
222198 return out;
223199 }
......@@ -239,11 +215,7 @@ pub fn Uint(comptime max_bits: comptime_int) type {
239215
240216 /// Returns `true` if the integer is zero.
241217 pub fn isZero(x: Self) bool {
242 var t: Limb = 0;
243 for (x.limbsConst()) |elem| {
244 t |= elem;
245 }
246 return ct.eql(t, 0);
218 return ct.eql(orLimbs(x.limbsConst()), 0);
247219 }
248220
249221 /// Returns `true` if the integer is odd.
......@@ -376,6 +348,47 @@ fn Fe_(comptime bits: comptime_int) type {
376348 };
377349}
378350
351// Decodes into zeroed limbs and returns the OR of the bits that didn't fit.
352fn decodeBytes(limbs: []Limb, bytes: []const u8, comptime endian: Endian) Limb {
353 var acc: Limb = 0;
354 var shift: usize = 0;
355 var out_i: usize = 0;
356 for (0..bytes.len) |k| {
357 const bi = bytes[
358 switch (endian) {
359 .big => bytes.len - 1 - k,
360 .little => k,
361 }
362 ];
363 if (out_i >= limbs.len) {
364 acc |= bi;
365 continue;
366 }
367 limbs[out_i] |= math.shl(Limb, bi, shift);
368 shift += 8;
369 if (shift >= t_bits) {
370 shift -= t_bits;
371 limbs[out_i] = @as(TLimb, @truncate(limbs[out_i]));
372 const spill = math.shr(Limb, bi, 8 - shift);
373 out_i += 1;
374 if (out_i < limbs.len) {
375 limbs[out_i] = spill;
376 } else {
377 acc |= spill;
378 }
379 }
380 }
381 return acc;
382}
383
384fn orLimbs(limbs: []const Limb) Limb {
385 var t: Limb = 0;
386 for (limbs) |limb| {
387 t |= limb;
388 }
389 return t;
390}
391
379392/// A modulus, defining a finite field.
380393/// All operations within the field are performed modulo this modulus, without heap allocations.
381394/// `max_bits` represents the number of bits in the maximum value the modulus can be set to.
......@@ -411,6 +424,11 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
411424 return self.limbs_count() * t_bits - self.leading;
412425 }
413426
427 /// Returns the encoded length, in bytes.
428 pub fn encodedLen(self: Self) usize {
429 return @divCeil(self.bits(), 8);
430 }
431
414432 /// Returns the element `1`.
415433 pub fn one(self: Self) Fe {
416434 var fe = self.zero;
......@@ -474,6 +492,11 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
474492 return self.v.toBytes(bytes, endian);
475493 }
476494
495 /// Returns the modulus as an integer
496 pub fn toUint(self: Self) FeUint {
497 return self.v;
498 }
499
477500 /// Rejects field elements that are not in the canonical form.
478501 pub fn rejectNonCanonical(self: Self, fe: Fe) error{NonCanonical}!void {
479502 if (fe.limbs_count() != self.limbs_count() or ct.limbsCmpGeq(fe.v, self.v)) {
......@@ -1085,3 +1108,100 @@ test ct {
10851108 try testCt(ct_protected);
10861109 try testCt(ct_unprotected);
10871110}
1111
1112fn expectWellFormedLimbs(x: anytype) !void {
1113 for (x.limbsConst()) |limb| {
1114 try testing.expect(limb <= math.maxInt(TLimb));
1115 }
1116 for (x.limbs_buffer[x.limbs_len..]) |limb| {
1117 try testing.expectEqual(0, limb);
1118 }
1119}
1120
1121// Big-endian encoding of the largest value a `Uint` can store.
1122fn maxUintBytes(comptime U: type) [@divCeil(U.capacity_bits, 8)]u8 {
1123 var buf: [@divCeil(U.capacity_bits, 8)]u8 = @splat(0xff);
1124 buf[0] = 0xff >> (8 * buf.len - U.capacity_bits);
1125 return buf;
1126}
1127
1128test "modulus creation" {
1129 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
1130
1131 const M = Modulus(256);
1132 try testing.expectError(error.EvenModulus, M.fromPrimitive(u8, 0));
1133 try testing.expectError(error.ModulusTooSmall, M.fromPrimitive(u8, 1));
1134 try testing.expectError(error.EvenModulus, M.fromPrimitive(u8, 2));
1135 for ([_]u65{ 3, 255, (1 << 64) + 1 }) |v| {
1136 const m = try M.fromPrimitive(u65, v);
1137 try testing.expectEqual(v, try m.toUint().toPrimitive(u65));
1138 try testing.expectEqual(@divCeil(65 - @clz(v), 8), m.encodedLen());
1139 try testing.expect((try M.fromUint(m.toUint())).v.eql(m.v));
1140 }
1141 const cap = maxUintBytes(Uint(256));
1142 try testing.expectEqual(cap.len, (try M.fromBytes(&cap, .big)).encodedLen());
1143}
1144
1145test "Uint serialization" {
1146 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
1147
1148 const U = Uint(256);
1149 const x = try U.fromPrimitive(u128, (1 << t_bits) + 5);
1150 try testing.expectError(error.Overflow, Uint(64).fromPrimitive(u256, 1 << 200));
1151 try testing.expectError(error.Overflow, x.toPrimitive(u8));
1152 try testing.expectError(error.Overflow, x.toPrimitive(@Int(.unsigned, t_bits)));
1153 try testing.expectEqual((1 << t_bits) + 5, try x.toPrimitive(@Int(.unsigned, t_bits + 1)));
1154 const max = try U.fromPrimitive(u128, (1 << t_bits) - 1);
1155 try testing.expectEqual((1 << t_bits) - 1, try max.toPrimitive(@Int(.unsigned, t_bits)));
1156 try testing.expectError(error.Overflow, max.toPrimitive(@Int(.unsigned, t_bits - 1)));
1157 const signed_max = try U.fromPrimitive(u128, math.maxInt(i128));
1158 try testing.expectEqual(math.maxInt(i128), try signed_max.toPrimitive(i128));
1159 try testing.expectError(error.Overflow, (try U.fromPrimitive(u128, 1 << 127)).toPrimitive(i128));
1160
1161 inline for (.{ .big, .little }) |endian| {
1162 const zero = try U.fromBytes(&.{}, endian);
1163 var empty: [0]u8 = .{};
1164 try zero.toBytes(&empty, endian);
1165 try testing.expectError(error.Overflow, x.toBytes(&empty, endian));
1166 var tight: [16]u8 = undefined;
1167 try x.toBytes(&tight, endian);
1168 try testing.expectEqual((1 << t_bits) + 5, mem.readInt(u128, &tight, endian));
1169 try testing.expect(x.eql(try U.fromBytes(&tight, endian)));
1170 var normalized: [16]u8 = undefined;
1171 try x.normalize().toBytes(&normalized, endian);
1172 try testing.expectEqualSlices(u8, &tight, &normalized);
1173
1174 var cap = maxUintBytes(U);
1175 if (endian == .little) mem.reverse(u8, &cap);
1176 const full = try U.fromBytes(&cap, endian);
1177 try expectWellFormedLimbs(full);
1178 var out: [cap.len]u8 = undefined;
1179 try full.toBytes(&out, endian);
1180 try testing.expectEqualSlices(u8, &cap, &out);
1181 out[if (endian == .big) 0 else out.len - 1] |= 1 << (U.capacity_bits % 8);
1182 try testing.expectError(error.Overflow, U.fromBytes(&out, endian));
1183 var padded = if (endian == .big) [_]u8{0} ++ cap else cap ++ [_]u8{0};
1184 try testing.expect(full.eql(try U.fromBytes(&padded, endian)));
1185 padded[if (endian == .big) 0 else padded.len - 1] = 1;
1186 try testing.expectError(error.Overflow, U.fromBytes(&padded, endian));
1187 }
1188}
1189
1190test "field element decoding" {
1191 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
1192
1193 const M = Modulus(256);
1194 const mv: u256 = (1 << 190) + 33;
1195 const m = try M.fromPrimitive(u256, mv);
1196 try testing.expectError(error.NonCanonical, M.Fe.fromPrimitive(u256, m, mv));
1197 try testing.expectError(error.Overflow, M.Fe.fromPrimitive(u256, m, 1 << 255));
1198
1199 inline for (.{ .big, .little }) |endian| {
1200 var buf: [32]u8 = undefined;
1201 try m.toBytes(&buf, endian);
1202 try testing.expectError(error.NonCanonical, M.Fe.fromBytes(m, &buf, endian));
1203 const x = try M.Fe.fromPrimitive(u256, m, mv - 1);
1204 try x.toBytes(&buf, endian);
1205 try testing.expect(x.eql(try M.Fe.fromBytes(m, &buf, endian)));
1206 }
1207}