authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-25 08:38:01+01:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-25 08:38:01+01:00
log064c78505596ae2bbfec63236df3b93a85e79aad
tree01882e8d24b68d013d11fed45b252f5e94a42c0f
parent720939f8c9cee80963a0089645e42ebbb4545906
parent4707a01ad3ab437e4d8165e2b41b17ad346e6e99

Merge pull request 'crypto.ff: support even moduli / products of arbitrary integers' (#37330) from jedisct1/zig:ff-fixes14 into master

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

1 files changed, 209 insertions(+), 85 deletions(-)

lib/std/crypto/ff.zig+209-85
......@@ -472,6 +472,21 @@ fn Fe_(comptime bits: comptime_int) type {
472472 return fe;
473473 }
474474
475 /// Decodes and reduces a byte string whose value is below 2^m.bits().
476 pub fn fromBytesReduced(m: Modulus(bits), bytes: []const u8, comptime endian: Endian) OverflowError!Self {
477 var fe = Self{ .v = FeUint.zero };
478 const m_limbs_count = m.limbs_count();
479 const fe_limbs = fe.v.limbs()[0..m_limbs_count];
480 var acc = decodeBytes(fe_limbs, bytes, endian);
481
482 acc |= math.shr(Limb, fe_limbs[m_limbs_count - 1], t_bits - m.leading);
483 if (!ct.eql(acc, 0)) return error.Overflow;
484 fe.v.limbs_len = m_limbs_count;
485
486 _ = fe.v.conditionalSubWithOverflow(ct.limbsCmpGeq(fe.v, m.v), m.v);
487 return fe;
488 }
489
475490 /// Converts the field element to a byte string.
476491 /// Returns an error if the element is in Montgomery form.
477492 pub fn toBytes(self: Self, bytes: []u8, comptime endian: Endian) (OverflowError || RepresentationError)!void {
......@@ -586,6 +601,12 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
586601
587602 const FeUint = Fe.FeUint;
588603
604 pub const Options = struct {
605 /// Accept even moduli and treat their parity as public.
606 /// Disabled by default: with even moduli, we can't use Montgomery form, so multiplication is slower.
607 allow_even: bool = false,
608 };
609
589610 /// The neutral element.
590611 zero: Fe,
591612
......@@ -683,15 +704,71 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
683704 /// Creates a new modulus from a primitive value.
684705 /// The modulus must be odd and larger than 2.
685706 pub fn fromPrimitive(comptime T: type, x: T) (InvalidModulusError || OverflowError)!Self {
686 comptime assert(@bitSizeOf(T) <= max_bits); // Primitive type is larger than the modulus type.
687 const v = try FeUint.fromPrimitive(T, x);
688 return try Self.fromUint(v);
707 return fromPrimitiveWithOptions(T, x, .{});
689708 }
690709
691710 /// Creates a new modulus from a byte string.
692711 pub fn fromBytes(bytes: []const u8, comptime endian: Endian) (InvalidModulusError || OverflowError)!Self {
712 return fromBytesWithOptions(bytes, endian, .{});
713 }
714
715 /// Creates a new modulus from a Uint value, with options.
716 pub fn fromUintWithOptions(v_: FeUint, options: Options) InvalidModulusError!Self {
717 if (v_.isOdd()) return fromUint(v_);
718 if (!options.allow_even) return error.EvenModulus;
719
720 const v = v_.normalize();
721 if (v.limbs_len < 2 and v.limbsConst()[0] < 2) {
722 return error.ModulusTooSmall;
723 }
724 const hi = v.limbsConst()[v.limbs_len - 1];
725
726 var m = initWithoutRR(v, @clz(hi) - carry_bits, 0);
727 m.rr = m.zero;
728
729 return m;
730 }
731
732 /// Creates a new big integer from a primitive type, with options.
733 pub fn fromPrimitiveWithOptions(comptime T: type, x: T, options: Options) (InvalidModulusError || OverflowError)!Self {
734 comptime assert(@bitSizeOf(T) <= max_bits);
735 const v = try FeUint.fromPrimitive(T, x);
736 return try Self.fromUintWithOptions(v, options);
737 }
738
739 /// Creates a new big integer from a byte array, with options.
740 pub fn fromBytesWithOptions(bytes: []const u8, comptime endian: Endian, options: Options) (InvalidModulusError || OverflowError)!Self {
693741 const v = try FeUint.fromBytes(bytes, endian);
694 return try Self.fromUint(v);
742 return try Self.fromUintWithOptions(v, options);
743 }
744
745 const ProductUint = Uint(2 * FeUint.capacity_bits);
746
747 fn fullProduct(a: []const Limb, b: []const Limb) ProductUint {
748 var out = ProductUint.zero;
749 out.limbs_len = a.len + b.len;
750 mulAddWide(out.limbs(), a, b);
751 return out;
752 }
753
754 /// Creates an odd modulus from the product of two integers.
755 pub fn fromProduct(a: FeUint, b: FeUint) (InvalidModulusError || OverflowError)!Self {
756 return fromProductWithOptions(a, b, .{});
757 }
758
759 /// Creates a modulus from the product of two integers, with options (set `allow_even` to `true` to allow the product to be even)
760 pub fn fromProductWithOptions(a: FeUint, b: FeUint, options: Options) (InvalidModulusError || OverflowError)!Self {
761 const product = fullProduct(a.limbsConst(), b.limbsConst());
762
763 if (orLimbs(product.limbs_buffer[FeUint.max_limbs_count..]) != 0) {
764 return error.Overflow;
765 }
766
767 const v = FeUint{
768 .limbs_buffer = product.limbs_buffer[0..FeUint.max_limbs_count].*,
769 .limbs_len = FeUint.max_limbs_count,
770 };
771 return try Self.fromUintWithOptions(v, options);
695772 }
696773
697774 /// Serializes the modulus to a byte string.
......@@ -852,8 +929,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
852929 }
853930
854931 /// Converts a field element to the Montgomery form.
932 /// Rejects even moduli, which have no Montgomery form.
855933 pub fn toMontgomery(self: Self, x: *Fe) RepresentationError!void {
856 if (x.montgomery) {
934 if (x.montgomery or !self.v.isOdd()) {
857935 return error.UnexpectedRepresentation;
858936 }
859937 self.shrink(x) catch unreachable;
......@@ -862,8 +940,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
862940 }
863941
864942 /// Takes a field element out of the Montgomery form.
943 /// Rejects even moduli, which have no Montgomery form.
865944 pub fn fromMontgomery(self: Self, x: *Fe) RepresentationError!void {
866 if (!x.montgomery) {
945 if (!x.montgomery or !self.v.isOdd()) {
867946 return error.UnexpectedRepresentation;
868947 }
869948 self.shrink(x) catch unreachable;
......@@ -970,110 +1049,99 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
9701049 return d;
9711050 }
9721051
973 // Returns x^e (mod m), with the exponent provided as a byte string.
974 // `public` must be set to `false` if the exponent it secret.
1052 fn plainMul(self: Self, x: Fe, y: Fe) Fe {
1053 assert(x.limbs_count() == self.limbs_count());
1054 assert(y.limbs_count() == self.limbs_count());
1055 return self.reduce(fullProduct(x.v.limbsConst(), y.v.limbsConst()));
1056 }
1057
1058 // Set `public` to `false` for secret exponents.
9751059 fn powWithEncodedExponentInternal(self: Self, x: Fe, e: []const u8, endian: Endian, comptime public: bool) NullExponentError!Fe {
9761060 var acc: u8 = 0;
9771061 for (e) |b| acc |= b;
9781062 if (acc == 0) return error.NullExponent;
9791063
980 const was_montgomery = x.montgomery;
1064 // Modulus parity is public; even moduli need plain arithmetic.
1065 if (!self.v.isOdd()) {
1066 return self.powLadder(x, e, endian, public, false);
1067 }
9811068
982 var out = self.one();
983 self.toMontgomery(&out) catch unreachable;
1069 var x_m = x;
1070 if (!x.montgomery) {
1071 self.toMontgomery(&x_m) catch unreachable;
1072 }
1073 var out = self.powLadder(x_m, e, endian, public, true);
1074 if (!x.montgomery) {
1075 self.fromMontgomery(&out) catch unreachable;
1076 }
1077 return out;
1078 }
9841079
1080 // `x` and the result use the representation selected by `montgomery`; the exponent must be nonzero.
1081 fn powLadder(self: Self, x: Fe, e: []const u8, endian: Endian, comptime public: bool, comptime montgomery: bool) Fe {
9851082 if (public and
9861083 (e.len < 3 or (e.len == 3 and e[if (endian == .big) 0 else 2] <= 0b1111)))
9871084 {
9881085 // Do not use a precomputation table for short, public exponents
989 var x_m = x;
990 if (!x.montgomery) {
991 self.toMontgomery(&x_m) catch unreachable;
1086 var e_int: u32 = 0;
1087 for (0..e.len) |i| {
1088 e_int = (e_int << 8) | e[if (endian == .big) i else e.len - 1 - i];
9921089 }
993 var s = switch (endian) {
994 .big => 0,
995 .little => e.len - 1,
996 };
997 while (true) {
998 const b = e[s];
999 var j: u3 = 7;
1000 while (true) : (j -= 1) {
1001 out = self.montgomerySq(out);
1002 const k: u1 = @truncate(b >> j);
1003 if (k != 0) {
1004 const t = self.montgomeryMul(out, x_m);
1005 @memcpy(out.v.limbs(), t.v.limbsConst());
1006 }
1007 if (j == 0) break;
1008 }
1009 switch (endian) {
1010 .big => {
1011 s += 1;
1012 if (s == e.len) break;
1013 },
1014 .little => {
1015 if (s == 0) break;
1016 s -= 1;
1017 },
1090 var out = x;
1091 var j = math.log2_int(u32, e_int);
1092 while (j > 0) {
1093 j -= 1;
1094 out = if (montgomery) self.montgomerySq(out) else self.plainMul(out, out);
1095 if ((e_int >> j) & 1 != 0) {
1096 out = if (montgomery) self.montgomeryMul(out, x) else self.plainMul(out, x);
10181097 }
10191098 }
1020 } else {
1021 // Use a precomputation table for large exponents
1022 var pc: [15]Fe = [1]Fe{x} ++ @as([14]Fe, @splat(self.zero));
1023 if (!x.montgomery) {
1024 self.toMontgomery(&pc[0]) catch unreachable;
1025 }
1026 for (1..pc.len) |i| {
1027 pc[i] = self.montgomeryMul(pc[i - 1], pc[0]);
1028 }
1029 var t0 = self.zero;
1030 var s = switch (endian) {
1031 .big => 0,
1032 .little => e.len - 1,
1033 };
1034 while (true) {
1035 const b = e[s];
1036 for ([_]u3{ 4, 0 }) |j| {
1037 for (0..4) |_| {
1038 out = self.montgomerySq(out);
1039 }
1040 const k = (b >> j) & 0b1111;
1041 if (public or std.options.side_channels_mitigations == .none) {
1042 if (k == 0) continue;
1043 t0 = pc[k - 1];
1044 } else {
1045 for (pc, 0..) |t, i| {
1046 t0.v.cmov(ct.eql(k, @as(u8, @truncate(i + 1))), t.v);
1047 }
1048 }
1049 const t1 = self.montgomeryMul(out, t0);
1050 if (public) {
1051 @memcpy(out.v.limbs(), t1.v.limbsConst());
1052 } else {
1053 out.v.cmov(!ct.eql(k, 0), t1.v);
1099 return out;
1100 }
1101
1102 var pc: [15]Fe = [1]Fe{x} ++ @as([14]Fe, @splat(self.zero));
1103 for (1..pc.len) |i| {
1104 pc[i] = if (montgomery) self.montgomeryMul(pc[i - 1], pc[0]) else self.plainMul(pc[i - 1], pc[0]);
1105 }
1106 var out = self.one();
1107 if (montgomery) {
1108 self.toMontgomery(&out) catch unreachable;
1109 }
1110
1111 var t0 = self.zero;
1112 t0.montgomery = montgomery;
1113 for (0..e.len) |s| {
1114 const b = e[if (endian == .big) s else e.len - 1 - s];
1115 for ([_]u3{ 4, 0 }) |j| {
1116 for (0..4) |_| {
1117 out = if (montgomery) self.montgomerySq(out) else self.plainMul(out, out);
1118 }
1119 const k = (b >> j) & 0b1111;
1120 if (public or std.options.side_channels_mitigations == .none) {
1121 if (k == 0) continue;
1122 t0 = pc[k - 1];
1123 } else {
1124 for (pc, 0..) |t, i| {
1125 t0.v.cmov(ct.eql(k, @truncate(i + 1)), t.v);
10541126 }
10551127 }
1056 switch (endian) {
1057 .big => {
1058 s += 1;
1059 if (s == e.len) break;
1060 },
1061 .little => {
1062 if (s == 0) break;
1063 s -= 1;
1064 },
1128 const t1 = if (montgomery) self.montgomeryMul(out, t0) else self.plainMul(out, t0);
1129 if (public) {
1130 @memcpy(out.v.limbs(), t1.v.limbsConst());
1131 } else {
1132 out.v.cmov(!ct.eql(k, 0), t1.v);
10651133 }
10661134 }
10671135 }
1068 if (!was_montgomery) {
1069 self.fromMontgomery(&out) catch unreachable;
1070 }
10711136 return out;
10721137 }
10731138
10741139 /// Multiplies two field elements.
10751140 /// Result preserves the first operand's form.
10761141 pub fn mul(self: Self, x: Fe, y: Fe) Fe {
1142 if (!self.v.isOdd()) {
1143 return self.plainMul(x, y);
1144 }
10771145 if (x.montgomery) {
10781146 const y_ = if (!y.montgomery) blk: {
10791147 var yy = y;
......@@ -1098,6 +1166,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
10981166
10991167 /// Squares a field element.
11001168 pub fn sq(self: Self, x: Fe) Fe {
1169 if (!self.v.isOdd()) {
1170 return self.plainMul(x, x);
1171 }
11011172 if (x.montgomery) {
11021173 return self.montgomerySq(x);
11031174 } else {
......@@ -1612,3 +1683,56 @@ test "field element constants and subOne" {
16121683 try testing.expectEqual(4, try four.toPrimitive(u8));
16131684 }
16141685}
1686
1687test "modulus from a product" {
1688 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
1689
1690 const M = Modulus(256);
1691 const U = Uint(256);
1692 const one = try U.fromPrimitive(u8, 1);
1693 const a = (try U.fromPrimitive(u32, 65537)).normalize();
1694 const b = try U.fromPrimitive(u256, (1 << 200) + 12345);
1695 const m = try M.fromProduct(a, b);
1696 try testing.expectEqual(65537 * ((1 << 200) + 12345), try m.toUint().toPrimitive(u256));
1697 try testing.expect((try M.fromProduct(b, a)).v.eql(m.v));
1698 try testing.expectError(error.EvenModulus, M.fromProduct(U.zero, a));
1699 try testing.expectError(error.ModulusTooSmall, M.fromProduct(one, one));
1700 const two = try U.fromPrimitive(u8, 2);
1701 try testing.expectError(error.EvenModulus, M.fromProduct(a, two));
1702 try testing.expectEqual(2 * 65537, try (try M.fromProductWithOptions(a, two, .{ .allow_even = true })).toUint().toPrimitive(u32));
1703 try testing.expectError(error.ModulusTooSmall, M.fromProductWithOptions(U.zero, a, .{ .allow_even = true }));
1704
1705 const full = try U.fromBytes(&maxUintBytes(U), .big);
1706 try testing.expect((try M.fromProduct(full, one)).toUint().eql(full));
1707 try testing.expectError(error.Overflow, M.fromProduct(full, a));
1708 try testing.expectError(error.Overflow, M.fromProduct(a, full));
1709 const power = try U.fromPrimitive(u256, 1 << 160);
1710 try testing.expectError(error.Overflow, M.fromProductWithOptions(power, power, .{ .allow_even = true }));
1711}
1712
1713test "even modulus arithmetic" {
1714 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
1715
1716 const M = Modulus(256);
1717 const opts: M.Options = .{ .allow_even = true };
1718 try testing.expectError(error.ModulusTooSmall, M.fromPrimitiveWithOptions(u8, 0, opts));
1719
1720 const m2 = try M.fromPrimitiveWithOptions(u8, 2, opts);
1721 var one = m2.one();
1722 try testing.expect(m2.isOne(m2.mul(one, one)));
1723 try testing.expectError(error.UnexpectedRepresentation, m2.toMontgomery(&one));
1724 one.montgomery = true;
1725 try testing.expectError(error.UnexpectedRepresentation, m2.fromMontgomery(&one));
1726
1727 const m = try M.fromPrimitiveWithOptions(u256, ((1 << 127) - 1) * 2, opts);
1728 const x = try M.Fe.fromPrimitive(u256, m, (1 << 126) + 12345);
1729 const e = [_]u8{ 0x0f, 0xff, 0xff };
1730 const expected: u256 = 13725650424532690062485077506887663427;
1731 try testing.expectEqual(expected, try (try m.powWithEncodedPublicExponent(x, &e, .big)).toPrimitive(u256));
1732 try testing.expectEqual(expected, try (try m.powWithEncodedExponent(x, &e, .big)).toPrimitive(u256));
1733
1734 var cap = maxUintBytes(Uint(256));
1735 cap[cap.len - 1] = 0xfe;
1736 const mc = try M.fromBytesWithOptions(&cap, .big, opts);
1737 try testing.expect(mc.isOne(mc.sq(mc.subOne(mc.zero))));
1738}