authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-01 12:06:17+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-01 12:06:17+02:00
logb4ee9c1fee470aa8fb05610803bccc5075256571
tree6d5f4bc94ab0fc752c4d588285303052e9c104d7
parent230a1a0ac2935d8734115862319616e7a0143183
parentcdc92b64bd1e1ae3b54b9cc77b2e646e47dd88cd

Merge pull request 'crypto.ff: add additional helpers for public values' (#36997) from jedisct1/zig:ff-fixes5 into master

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

1 files changed, 256 insertions(+), 1 deletions(-)

lib/std/crypto/ff.zig+256-1
...@@ -3,6 +3,8 @@...@@ -3,6 +3,8 @@
3//! Unlike `std.math.big`, these integers have a fixed maximum length and are only designed to be used for modular arithmetic.3//! Unlike `std.math.big`, these integers have a fixed maximum length and are only designed to be used for modular arithmetic.
4//! Arithmetic operations are meant to run in constant-time for a given modulus, making them suitable for cryptography.4//! Arithmetic operations are meant to run in constant-time for a given modulus, making them suitable for cryptography.
5//!5//!
6//! Functions with `Public` in their name are the exception.
7//!
6//! Parts of that code was ported from the BSD-licensed crypto/internal/bigmod/nat.go file in the Go language, itself inspired from BearSSL.8//! Parts of that code was ported from the BSD-licensed crypto/internal/bigmod/nat.go file in the Go language, itself inspired from BearSSL.
79
8const std = @import("std");10const std = @import("std");
...@@ -51,8 +53,10 @@ pub const FieldElementError = error{NonCanonical};...@@ -51,8 +53,10 @@ pub const FieldElementError = error{NonCanonical};
51/// Invalid representation (Montgomery vs non-Montgomery domain.)53/// Invalid representation (Montgomery vs non-Montgomery domain.)
52pub const RepresentationError = error{UnexpectedRepresentation};54pub const RepresentationError = error{UnexpectedRepresentation};
5355
56pub const DivisionByZeroError = error{DivisionByZero};
57
54/// The set of all possible errors `std.crypto.ff` functions can return.58/// The set of all possible errors `std.crypto.ff` functions can return.
55pub const Error = OverflowError || InvalidModulusError || NullExponentError || FieldElementError || RepresentationError;59pub const Error = OverflowError || InvalidModulusError || NullExponentError || FieldElementError || RepresentationError || DivisionByZeroError;
5660
57/// An unsigned big integer with a fixed maximum size (`max_bits`), suitable for cryptographic operations.61/// 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.62/// Storage rounds up to whole limbs and can hold up to `capacity_bits` bits.
...@@ -240,6 +244,110 @@ pub fn Uint(comptime max_bits: comptime_int) type {...@@ -240,6 +244,110 @@ pub fn Uint(comptime max_bits: comptime_int) type {
240 return x.conditionalSubWithOverflow(true, y);244 return x.conditionalSubWithOverflow(true, y);
241 }245 }
242246
247 /// Adds the product of `x` and `y` to `acc`, wrapping at the active width.
248 /// Returns 1 if any part of the result was discarded.
249 pub fn mulAddWithOverflow(acc: *Self, x: Self, y: Self) u1 {
250 assert(x.limbs_len == acc.limbs_len);
251 assert(y.limbs_len == acc.limbs_len);
252 const n = acc.limbs_len;
253 var wide: [2 * max_limbs_count]Limb = undefined;
254 @memcpy(wide[0..n], acc.limbsConst());
255 mulAddWide(wide[0 .. 2 * n], x.limbsConst(), y.limbsConst());
256 @memcpy(acc.limbs(), wide[0..n]);
257 return @intFromBool(!ct.eql(orLimbs(wide[n..][0..n]), 0));
258 }
259
260 /// Returns the bit length, or 0 for zero.
261 pub fn bitLenPublic(x: Self) usize {
262 var i = x.limbs_len;
263 while (i != 0) {
264 i -= 1;
265 const limb = x.limbsConst()[i];
266 if (limb != 0) {
267 return i * t_bits + t_bits - @clz(@as(TLimb, @intCast(limb)));
268 }
269 }
270 return 0;
271 }
272
273 /// Returns the number of trailing zero bits, or the active width for zero.
274 pub fn trailingZeroBitsPublic(x: Self) usize {
275 for (x.limbsConst(), 0..) |limb, i| {
276 if (limb != 0) {
277 return i * t_bits + @ctz(@as(TLimb, @intCast(limb)));
278 }
279 }
280 return x.limbs_len * t_bits;
281 }
282
283 pub fn shiftRightPublic(x: *Self, shift: usize) void {
284 const limb_shift = shift / t_bits;
285 const bit_shift = shift % t_bits;
286 const x_limbs = x.limbs();
287 if (limb_shift >= x_limbs.len) {
288 @memset(x_limbs, 0);
289 return;
290 }
291 const active = x_limbs.len - limb_shift;
292 for (0..active) |i| {
293 const lo = math.shr(Limb, x_limbs[i + limb_shift], bit_shift);
294 const hi = if (i + limb_shift + 1 < x_limbs.len)
295 math.shl(Limb, x_limbs[i + limb_shift + 1], t_bits - bit_shift)
296 else
297 0;
298 x_limbs[i] = @as(TLimb, @truncate(lo | hi));
299 }
300 @memset(x_limbs[active..], 0);
301 }
302
303 /// Divides by `divisor` in place and returns the remainder.
304 /// Returns `error.DivisionByZero` without changing the integer if `divisor` is zero.
305 pub fn divRemPublic(x: *Self, divisor: usize) DivisionByZeroError!usize {
306 if (divisor == 0) return error.DivisionByZero;
307 const Wide = @Int(.unsigned, 2 * @bitSizeOf(Limb));
308 const x_limbs = x.limbs();
309 var rem: Limb = 0;
310 var i = x.limbs_len;
311 while (i != 0) {
312 i -= 1;
313 const num = (@as(Wide, rem) << t_bits) | x_limbs[i];
314 x_limbs[i] = @intCast(num / divisor);
315 rem = @intCast(num % divisor);
316 }
317 return rem;
318 }
319
320 /// Returns the greatest common divisor, with gcd(x, 0) = x.
321 /// Returns `error.DivisionByZero` if both operands are zero.
322 pub fn gcdPublic(x: Self, y: Self) DivisionByZeroError!Self {
323 if (x.isZero() and y.isZero()) return error.DivisionByZero;
324 if (x.isZero()) return y;
325 if (y.isZero()) return x;
326
327 var a = x;
328 var b = y;
329 const len = @max(a.limbs_len, b.limbs_len);
330 a.expandTo(len);
331 b.expandTo(len);
332
333 const a_shift = a.trailingZeroBitsPublic();
334 const b_shift = b.trailingZeroBitsPublic();
335 a.shiftRightPublic(a_shift);
336 b.shiftRightPublic(b_shift);
337 while (true) {
338 switch (a.compare(b)) {
339 .eq => break,
340 .lt => mem.swap(Self, &a, &b),
341 .gt => {},
342 }
343 _ = a.subWithOverflow(b);
344 a.shiftRightPublic(a.trailingZeroBitsPublic());
345 }
346
347 a.shiftLeft(@min(a_shift, b_shift)); // GCD always fits in either input, overflow is never an issue
348 return a;
349 }
350
243 fn expandTo(x: *Self, new_len: usize) void {351 fn expandTo(x: *Self, new_len: usize) void {
244 assert(new_len >= x.limbs_len and new_len <= x.limbs_buffer.len);352 assert(new_len >= x.limbs_len and new_len <= x.limbs_buffer.len);
245 @memset(x.limbs_buffer[x.limbs_len..new_len], 0);353 @memset(x.limbs_buffer[x.limbs_len..new_len], 0);
...@@ -276,6 +384,40 @@ pub fn Uint(comptime max_bits: comptime_int) type {...@@ -276,6 +384,40 @@ pub fn Uint(comptime max_bits: comptime_int) type {
276 }384 }
277 return borrow;385 return borrow;
278 }386 }
387
388 fn shiftLeft(x: *Self, shift: usize) void {
389 assert(x.bitLenPublic() + shift <= x.limbs_len * t_bits);
390 const limb_shift = shift / t_bits;
391 const bit_shift = shift % t_bits;
392 const x_limbs = x.limbs();
393 var i = x_limbs.len;
394 while (i != 0) {
395 i -= 1;
396 const hi = if (i >= limb_shift)
397 math.shl(Limb, x_limbs[i - limb_shift], bit_shift)
398 else
399 0;
400 const lo = if (i >= limb_shift + 1)
401 math.shr(Limb, x_limbs[i - limb_shift - 1], t_bits - bit_shift)
402 else
403 0;
404 x_limbs[i] = @as(TLimb, @truncate(hi | lo));
405 }
406 }
407
408 // Shifts in `carry` at the top and returns the low bit shifted out.
409 fn shiftRightByOneWithCarry(x: *Self, carry: u1) u1 {
410 var c: Limb = carry;
411 var i = x.limbs_len;
412 const x_limbs = x.limbs();
413 while (i != 0) {
414 i -= 1;
415 const limb = x_limbs[i];
416 x_limbs[i] = (limb >> 1) | (c << (t_bits - 1));
417 c = @as(u1, @truncate(limb));
418 }
419 return @truncate(c);
420 }
279 };421 };
280}422}
281423
...@@ -402,6 +544,30 @@ fn orLimbs(limbs: []const Limb) Limb {...@@ -402,6 +544,30 @@ fn orLimbs(limbs: []const Limb) Limb {
402 return t;544 return t;
403}545}
404546
547// Adds `x * y` to `z` and returns the carry.
548fn mulAddLimb(z: []Limb, x: []const Limb, y: Limb) Limb {
549 assert(z.len == x.len);
550 var carry: Limb = 0;
551 for (z, x) |*z_limb, x_limb| {
552 const wide = ct.mulWide(x_limb, y);
553 var z_lo = @addWithOverflow(z_limb.*, wide.lo);
554 var z_hi = wide.hi +% z_lo[1];
555 z_lo = @addWithOverflow(z_lo[0], carry);
556 z_hi +%= z_lo[1];
557 z_limb.* = @as(TLimb, @truncate(z_lo[0]));
558 carry = (z_hi << 1) | (z_lo[0] >> t_bits);
559 }
560 return carry;
561}
562
563// Adds `x * y` to the low `x.len` limbs of `z`. The upper limbs need no initialization.
564fn mulAddWide(z: []Limb, x: []const Limb, y: []const Limb) void {
565 assert(z.len == x.len + y.len);
566 for (y, 0..) |y_limb, i| {
567 z[i + x.len] = mulAddLimb(z[i..][0..x.len], x, y_limb);
568 }
569}
570
405/// A modulus, defining a finite field.571/// A modulus, defining a finite field.
406/// All operations within the field are performed modulo this modulus, without heap allocations.572/// All operations within the field are performed modulo this modulus, without heap allocations.
407/// `max_bits` represents the number of bits in the maximum value the modulus can be set to.573/// `max_bits` represents the number of bits in the maximum value the modulus can be set to.
...@@ -1219,6 +1385,70 @@ test "field element decoding" {...@@ -1219,6 +1385,70 @@ test "field element decoding" {
1219 }1385 }
1220}1386}
12211387
1388test "Uint bit measurement and shifts" {
1389 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
1390
1391 const U = Uint(256);
1392 try testing.expectEqual(0, U.zero.bitLenPublic());
1393 try testing.expectEqual(U.max_limbs_count * t_bits, U.zero.trailingZeroBitsPublic());
1394 try testing.expectEqual(t_bits, U.zero.normalize().trailingZeroBitsPublic());
1395 for ([_]usize{ 0, t_bits - 1, t_bits, t_bits + 1, 255 }) |offset| {
1396 const x = try U.fromPrimitive(u256, math.shl(u256, 1, offset));
1397 try testing.expectEqual(offset + 1, x.bitLenPublic());
1398 try testing.expectEqual(offset, x.trailingZeroBitsPublic());
1399 }
1400
1401 const v: u256 = (1 << 255) | (1 << t_bits) | 3;
1402 for ([_]usize{ 0, 1, t_bits - 1, t_bits, t_bits + 1, U.max_limbs_count * t_bits, 10_000 }) |shift| {
1403 var x = try U.fromPrimitive(u256, v);
1404 x.shiftRightPublic(shift);
1405 try testing.expectEqual(math.shr(u256, v, shift), try x.toPrimitive(u256));
1406 try testing.expectEqual(U.max_limbs_count, x.limbs_len);
1407 try expectWellFormedLimbs(x);
1408 }
1409 for ([_]usize{ 0, 1, t_bits - 1, t_bits, t_bits + 1 }) |shift| {
1410 var x = try U.fromPrimitive(u256, (1 << t_bits) + 3);
1411 x.shiftLeft(shift);
1412 try testing.expectEqual(math.shl(u256, (1 << t_bits) + 3, shift), try x.toPrimitive(u256));
1413 }
1414
1415 var x = (try U.fromPrimitive(u8, 5)).normalize();
1416 try testing.expectEqual(1, x.shiftRightByOneWithCarry(1));
1417 try testing.expectEqual((1 << (t_bits - 1)) + 2, try x.toPrimitive(u64));
1418 try testing.expectEqual(0, x.shiftRightByOneWithCarry(0));
1419 x.shiftRightPublic(t_bits);
1420 try testing.expect(x.isZero());
1421 try testing.expectEqual(1, x.limbs_len);
1422}
1423
1424test "Uint division and gcd" {
1425 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
1426
1427 const U = Uint(256);
1428 const v: u256 = (1 << 255) + (1 << t_bits) + 13;
1429 for ([_]usize{ 1, 7, math.maxInt(TLimb), (1 << t_bits) + 5, math.maxInt(usize) }) |divisor| {
1430 var x = try U.fromPrimitive(u256, v);
1431 try testing.expectEqual(v % divisor, try x.divRemPublic(divisor));
1432 try testing.expectEqual(v / divisor, try x.toPrimitive(u256));
1433 try expectWellFormedLimbs(x);
1434 }
1435 var x = (try U.fromPrimitive(u16, 1000)).normalize();
1436 try testing.expectError(error.DivisionByZero, x.divRemPublic(0));
1437 try testing.expectEqual(6, try x.divRemPublic(7));
1438 try testing.expectEqual(142, try x.toPrimitive(u16));
1439 try testing.expectEqual(1, x.limbs_len);
1440 try testing.expectEqual(142, try x.divRemPublic(1000));
1441 try testing.expectEqual(0, try x.divRemPublic(7));
1442
1443 for ([_][3]u256{ .{ 0, 5, 5 }, .{ 6, 6, 6 }, .{ 17, 4, 1 }, .{ 15 << 200, 21 << 100, 3 << 100 } }) |c| {
1444 const a = (try U.fromPrimitive(u256, c[0])).normalize();
1445 const b = try U.fromPrimitive(u256, c[1]);
1446 try testing.expectEqual(c[2], try (try a.gcdPublic(b)).toPrimitive(u256));
1447 try testing.expectEqual(c[2], try (try b.gcdPublic(a)).toPrimitive(u256));
1448 }
1449 try testing.expectError(error.DivisionByZero, U.zero.gcdPublic(U.zero));
1450}
1451
1222test "Uint addition and multiply-add" {1452test "Uint addition and multiply-add" {
1223 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;1453 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
12241454
...@@ -1236,4 +1466,29 @@ test "Uint addition and multiply-add" {...@@ -1236,4 +1466,29 @@ test "Uint addition and multiply-add" {
1236 try testing.expect(wrapped.eql(full));1466 try testing.expect(wrapped.eql(full));
1237 try testing.expectEqual(1, wrapped.addWithOverflow(one));1467 try testing.expectEqual(1, wrapped.addWithOverflow(one));
1238 try testing.expect(wrapped.isZero());1468 try testing.expect(wrapped.isZero());
1469
1470 for ([_]struct { acc: U, x: U, y: U, overflow: u1, expected: U }{
1471 .{ .acc = U.zero, .x = full, .y = one, .overflow = 0, .expected = full },
1472 .{ .acc = full, .x = U.zero, .y = full, .overflow = 0, .expected = full },
1473 .{ .acc = full, .x = full, .y = U.zero, .overflow = 0, .expected = full },
1474 .{ .acc = U.zero, .x = full, .y = full, .overflow = 1, .expected = one },
1475 .{ .acc = full, .x = one, .y = one, .overflow = 1, .expected = U.zero },
1476 }) |c| {
1477 var acc = c.acc;
1478 try testing.expectEqual(c.overflow, acc.mulAddWithOverflow(c.x, c.y));
1479 try testing.expect(acc.eql(c.expected));
1480 try expectWellFormedLimbs(acc);
1481 }
1482 const a: u256 = (1 << 100) + 3;
1483 const b: u256 = (1 << 80) + 7;
1484 var acc = try U.fromPrimitive(u256, a);
1485 try testing.expectEqual(0, acc.mulAddWithOverflow(try U.fromPrimitive(u256, a), try U.fromPrimitive(u256, b)));
1486 try testing.expectEqual(a + a * b, try acc.toPrimitive(u256));
1487
1488 var short = one.normalize();
1489 const high = (try U.fromPrimitive(u64, 1 << (t_bits - 1))).normalize();
1490 const two = (try U.fromPrimitive(u8, 2)).normalize();
1491 try testing.expectEqual(1, short.mulAddWithOverflow(high, two));
1492 try testing.expectEqual(1, short.limbs_len);
1493 try testing.expect(short.isOne());
1239}1494}