authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-09-07 21:07:26+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-25 08:38:01+01:00
log00795888e851bd3c082a16cae8507d7cad7cc4ba
treefaf05bd91eade8d601677ccbc1cfa0b3789d2ce7
parent0f17c8c1e7fa214b8a563a73c88b8096359b2282

crypto.ff: add support for even moduli

Allow options to create a modulus. Right now, there's one option, to allow even moduli (starting at 2). Even moduli are disabled by default: even moduli can't use the Montgomery form, so multiplication and exponentiation are slower. The Option struct is there to future-proof the interface, as we can later use it to add support for blinding, etc.

1 files changed, 79 insertions(+), 16 deletions(-)

lib/std/crypto/ff.zig+79-16
...@@ -601,6 +601,12 @@ pub fn Modulus(comptime max_bits: comptime_int) type {...@@ -601,6 +601,12 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
601601
602 const FeUint = Fe.FeUint;602 const FeUint = Fe.FeUint;
603603
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
604 /// The neutral element.610 /// The neutral element.
605 zero: Fe,611 zero: Fe,
606612
...@@ -698,15 +704,51 @@ pub fn Modulus(comptime max_bits: comptime_int) type {...@@ -698,15 +704,51 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
698 /// Creates a new modulus from a primitive value.704 /// Creates a new modulus from a primitive value.
699 /// The modulus must be odd and larger than 2.705 /// The modulus must be odd and larger than 2.
700 pub fn fromPrimitive(comptime T: type, x: T) (InvalidModulusError || OverflowError)!Self {706 pub fn fromPrimitive(comptime T: type, x: T) (InvalidModulusError || OverflowError)!Self {
701 comptime assert(@bitSizeOf(T) <= max_bits); // Primitive type is larger than the modulus type.707 return fromPrimitiveWithOptions(T, x, .{});
702 const v = try FeUint.fromPrimitive(T, x);
703 return try Self.fromUint(v);
704 }708 }
705709
706 /// Creates a new modulus from a byte string.710 /// Creates a new modulus from a byte string.
707 pub fn fromBytes(bytes: []const u8, comptime endian: Endian) (InvalidModulusError || OverflowError)!Self {711 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 {
708 const v = try FeUint.fromBytes(bytes, endian);741 const v = try FeUint.fromBytes(bytes, endian);
709 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;
710 }752 }
711753
712 /// Serializes the modulus to a byte string.754 /// Serializes the modulus to a byte string.
...@@ -867,8 +909,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {...@@ -867,8 +909,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
867 }909 }
868910
869 /// Converts a field element to the Montgomery form.911 /// Converts a field element to the Montgomery form.
912 /// Rejects even moduli, which have no Montgomery form.
870 pub fn toMontgomery(self: Self, x: *Fe) RepresentationError!void {913 pub fn toMontgomery(self: Self, x: *Fe) RepresentationError!void {
871 if (x.montgomery) {914 if (x.montgomery or !self.v.isOdd()) {
872 return error.UnexpectedRepresentation;915 return error.UnexpectedRepresentation;
873 }916 }
874 self.shrink(x) catch unreachable;917 self.shrink(x) catch unreachable;
...@@ -877,8 +920,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {...@@ -877,8 +920,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
877 }920 }
878921
879 /// Takes a field element out of the Montgomery form.922 /// Takes a field element out of the Montgomery form.
923 /// Rejects even moduli, which have no Montgomery form.
880 pub fn fromMontgomery(self: Self, x: *Fe) RepresentationError!void {924 pub fn fromMontgomery(self: Self, x: *Fe) RepresentationError!void {
881 if (!x.montgomery) {925 if (!x.montgomery or !self.v.isOdd()) {
882 return error.UnexpectedRepresentation;926 return error.UnexpectedRepresentation;
883 }927 }
884 self.shrink(x) catch unreachable;928 self.shrink(x) catch unreachable;
...@@ -985,25 +1029,36 @@ pub fn Modulus(comptime max_bits: comptime_int) type {...@@ -985,25 +1029,36 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
985 return d;1029 return d;
986 }1030 }
9871031
1032 fn plainMul(self: Self, x: Fe, y: Fe) Fe {
1033 assert(x.limbs_count() == self.limbs_count());
1034 assert(y.limbs_count() == self.limbs_count());
1035 return self.reduce(fullProduct(x.v.limbsConst(), y.v.limbsConst()));
1036 }
1037
988 // Set `public` to `false` for secret exponents.1038 // Set `public` to `false` for secret exponents.
989 fn powWithEncodedExponentInternal(self: Self, x: Fe, e: []const u8, endian: Endian, comptime public: bool) NullExponentError!Fe {1039 fn powWithEncodedExponentInternal(self: Self, x: Fe, e: []const u8, endian: Endian, comptime public: bool) NullExponentError!Fe {
990 var acc: u8 = 0;1040 var acc: u8 = 0;
991 for (e) |b| acc |= b;1041 for (e) |b| acc |= b;
992 if (acc == 0) return error.NullExponent;1042 if (acc == 0) return error.NullExponent;
9931043
1044 // Modulus parity is public; even moduli need plain arithmetic.
1045 if (!self.v.isOdd()) {
1046 return self.powLadder(x, e, endian, public, false);
1047 }
1048
994 var x_m = x;1049 var x_m = x;
995 if (!x.montgomery) {1050 if (!x.montgomery) {
996 self.toMontgomery(&x_m) catch unreachable;1051 self.toMontgomery(&x_m) catch unreachable;
997 }1052 }
998 var out = self.powLadder(x_m, e, endian, public);1053 var out = self.powLadder(x_m, e, endian, public, true);
999 if (!x.montgomery) {1054 if (!x.montgomery) {
1000 self.fromMontgomery(&out) catch unreachable;1055 self.fromMontgomery(&out) catch unreachable;
1001 }1056 }
1002 return out;1057 return out;
1003 }1058 }
10041059
1005 // `x` and the result use Montgomery form; the exponent must be nonzero.1060 // `x` and the result use the representation selected by `montgomery`; the exponent must be nonzero.
1006 fn powLadder(self: Self, x: Fe, e: []const u8, endian: Endian, comptime public: bool) Fe {1061 fn powLadder(self: Self, x: Fe, e: []const u8, endian: Endian, comptime public: bool, comptime montgomery: bool) Fe {
1007 if (public and1062 if (public and
1008 (e.len < 3 or (e.len == 3 and e[if (endian == .big) 0 else 2] <= 0b1111)))1063 (e.len < 3 or (e.len == 3 and e[if (endian == .big) 0 else 2] <= 0b1111)))
1009 {1064 {
...@@ -1016,9 +1071,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {...@@ -1016,9 +1071,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
1016 var j = math.log2_int(u32, e_int);1071 var j = math.log2_int(u32, e_int);
1017 while (j > 0) {1072 while (j > 0) {
1018 j -= 1;1073 j -= 1;
1019 out = self.montgomerySq(out);1074 out = if (montgomery) self.montgomerySq(out) else self.plainMul(out, out);
1020 if ((e_int >> j) & 1 != 0) {1075 if ((e_int >> j) & 1 != 0) {
1021 out = self.montgomeryMul(out, x);1076 out = if (montgomery) self.montgomeryMul(out, x) else self.plainMul(out, x);
1022 }1077 }
1023 }1078 }
1024 return out;1079 return out;
...@@ -1026,18 +1081,20 @@ pub fn Modulus(comptime max_bits: comptime_int) type {...@@ -1026,18 +1081,20 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
10261081
1027 var pc: [15]Fe = [1]Fe{x} ++ @as([14]Fe, @splat(self.zero));1082 var pc: [15]Fe = [1]Fe{x} ++ @as([14]Fe, @splat(self.zero));
1028 for (1..pc.len) |i| {1083 for (1..pc.len) |i| {
1029 pc[i] = self.montgomeryMul(pc[i - 1], pc[0]);1084 pc[i] = if (montgomery) self.montgomeryMul(pc[i - 1], pc[0]) else self.plainMul(pc[i - 1], pc[0]);
1030 }1085 }
1031 var out = self.one();1086 var out = self.one();
1032 self.toMontgomery(&out) catch unreachable;1087 if (montgomery) {
1088 self.toMontgomery(&out) catch unreachable;
1089 }
10331090
1034 var t0 = self.zero;1091 var t0 = self.zero;
1035 t0.montgomery = true;1092 t0.montgomery = montgomery;
1036 for (0..e.len) |s| {1093 for (0..e.len) |s| {
1037 const b = e[if (endian == .big) s else e.len - 1 - s];1094 const b = e[if (endian == .big) s else e.len - 1 - s];
1038 for ([_]u3{ 4, 0 }) |j| {1095 for ([_]u3{ 4, 0 }) |j| {
1039 for (0..4) |_| {1096 for (0..4) |_| {
1040 out = self.montgomerySq(out);1097 out = if (montgomery) self.montgomerySq(out) else self.plainMul(out, out);
1041 }1098 }
1042 const k = (b >> j) & 0b1111;1099 const k = (b >> j) & 0b1111;
1043 if (public or std.options.side_channels_mitigations == .none) {1100 if (public or std.options.side_channels_mitigations == .none) {
...@@ -1048,7 +1105,7 @@ pub fn Modulus(comptime max_bits: comptime_int) type {...@@ -1048,7 +1105,7 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
1048 t0.v.cmov(ct.eql(k, @truncate(i + 1)), t.v);1105 t0.v.cmov(ct.eql(k, @truncate(i + 1)), t.v);
1049 }1106 }
1050 }1107 }
1051 const t1 = self.montgomeryMul(out, t0);1108 const t1 = if (montgomery) self.montgomeryMul(out, t0) else self.plainMul(out, t0);
1052 if (public) {1109 if (public) {
1053 @memcpy(out.v.limbs(), t1.v.limbsConst());1110 @memcpy(out.v.limbs(), t1.v.limbsConst());
1054 } else {1111 } else {
...@@ -1062,6 +1119,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {...@@ -1062,6 +1119,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
1062 /// Multiplies two field elements.1119 /// Multiplies two field elements.
1063 /// Result preserves the first operand's form.1120 /// Result preserves the first operand's form.
1064 pub fn mul(self: Self, x: Fe, y: Fe) Fe {1121 pub fn mul(self: Self, x: Fe, y: Fe) Fe {
1122 if (!self.v.isOdd()) {
1123 return self.plainMul(x, y);
1124 }
1065 if (x.montgomery) {1125 if (x.montgomery) {
1066 const y_ = if (!y.montgomery) blk: {1126 const y_ = if (!y.montgomery) blk: {
1067 var yy = y;1127 var yy = y;
...@@ -1086,6 +1146,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {...@@ -1086,6 +1146,9 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
10861146
1087 /// Squares a field element.1147 /// Squares a field element.
1088 pub fn sq(self: Self, x: Fe) Fe {1148 pub fn sq(self: Self, x: Fe) Fe {
1149 if (!self.v.isOdd()) {
1150 return self.plainMul(x, x);
1151 }
1089 if (x.montgomery) {1152 if (x.montgomery) {
1090 return self.montgomerySq(x);1153 return self.montgomerySq(x);
1091 } else {1154 } else {