authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-01-25 17:42:01+01:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-01-25 17:42:01+01:00
log8709f53d440ed8479f711d871a2d6c2c35dc1014
treee08bf46306b640e8e73def890dbb40081568e0b8
parent99ec1ee3536b577bd1d14facde523c503108886d

crypto.ff: allow seamless chaining regardless of representation (#30913)

Finite field elements can be in regular or Montgomery form, and chaining different operations use to require manual and error-prone conversions. Now: - `add`, `sub` and `mul` convert the second operand to match the first operand's form - `sq` and `pow` preserve the input's Montgomery form - `toPrimitive` and `toBytes` return `UnexpectedRepresentation` if the element is in Montgomery form, preventing incorrect serialization This is fully backwards compatible and allows seamless chaining of operations regardless of their representation.

1 files changed, 134 insertions(+), 32 deletions(-)

lib/std/crypto/ff.zig+134-32
......@@ -329,7 +329,11 @@ fn Fe_(comptime bits: comptime_int) type {
329329
330330 /// Converts the field element to a primitive.
331331 /// This function may not run in constant time.
332 pub fn toPrimitive(self: Self, comptime T: type) OverflowError!T {
332 /// Returns an error if the element is in Montgomery form.
333 pub fn toPrimitive(self: Self, comptime T: type) (OverflowError || RepresentationError)!T {
334 if (self.montgomery) {
335 return error.UnexpectedRepresentation;
336 }
333337 return self.v.toPrimitive(T);
334338 }
335339
......@@ -343,7 +347,11 @@ fn Fe_(comptime bits: comptime_int) type {
343347 }
344348
345349 /// Converts the field element to a byte string.
346 pub fn toBytes(self: Self, bytes: []u8, comptime endian: Endian) OverflowError!void {
350 /// Returns an error if the element is in Montgomery form.
351 pub fn toBytes(self: Self, bytes: []u8, comptime endian: Endian) (OverflowError || RepresentationError)!void {
352 if (self.montgomery) {
353 return error.UnexpectedRepresentation;
354 }
347355 return self.v.toBytes(bytes, endian);
348356 }
349357
......@@ -530,19 +538,46 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
530538 /// Adds two field elements (mod m).
531539 pub fn add(self: Self, x: Fe, y: Fe) Fe {
532540 var out = x;
533 const overflow = out.v.addWithOverflow(y.v);
534 const underflow: u1 = @bitCast(ct.limbsCmpLt(out.v, self.v));
535 const need_sub = ct.eql(overflow, underflow);
536 _ = out.v.conditionalSubWithOverflow(need_sub, self.v);
537 return out;
541 if (x.montgomery == y.montgomery) {
542 @branchHint(.likely);
543 const overflow = out.v.addWithOverflow(y.v);
544 const underflow: u1 = @bitCast(ct.limbsCmpLt(out.v, self.v));
545 const need_sub = ct.eql(overflow, underflow);
546 _ = out.v.conditionalSubWithOverflow(need_sub, self.v);
547 return out;
548 } else {
549 var y_ = y;
550 if (y.montgomery) {
551 self.fromMontgomery(&y_) catch unreachable;
552 } else {
553 self.toMontgomery(&y_) catch unreachable;
554 }
555 const overflow = out.v.addWithOverflow(y_.v);
556 const underflow: u1 = @bitCast(ct.limbsCmpLt(out.v, self.v));
557 const need_sub = ct.eql(overflow, underflow);
558 _ = out.v.conditionalSubWithOverflow(need_sub, self.v);
559 return out;
560 }
538561 }
539562
540563 /// Subtracts two field elements (mod m).
541564 pub fn sub(self: Self, x: Fe, y: Fe) Fe {
542565 var out = x;
543 const underflow: bool = @bitCast(out.v.subWithOverflow(y.v));
544 _ = out.v.conditionalAddWithOverflow(underflow, self.v);
545 return out;
566 if (x.montgomery == y.montgomery) {
567 const underflow: bool = @bitCast(out.v.subWithOverflow(y.v));
568 _ = out.v.conditionalAddWithOverflow(underflow, self.v);
569 return out;
570 } else {
571 var y_ = y;
572 if (y.montgomery) {
573 self.fromMontgomery(&y_) catch unreachable;
574 } else {
575 self.toMontgomery(&y_) catch unreachable;
576 }
577 const underflow: bool = @bitCast(out.v.subWithOverflow(y_.v));
578 _ = out.v.conditionalAddWithOverflow(underflow, self.v);
579 return out;
580 }
546581 }
547582
548583 /// Converts a field element to the Montgomery form.
......@@ -663,13 +698,15 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
663698 for (e) |b| acc |= b;
664699 if (acc == 0) return error.NullExponent;
665700
701 const was_montgomery = x.montgomery;
702
666703 var out = self.one();
667704 self.toMontgomery(&out) catch unreachable;
668705
669706 if (public and e.len < 3 or (e.len == 3 and e[if (endian == .big) 0 else 2] <= 0b1111)) {
670707 // Do not use a precomputation table for short, public exponents
671708 var x_m = x;
672 if (x.montgomery == false) {
709 if (!x.montgomery) {
673710 self.toMontgomery(&x_m) catch unreachable;
674711 }
675712 var s = switch (endian) {
......@@ -702,7 +739,7 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
702739 } else {
703740 // Use a precomputation table for large exponents
704741 var pc = [1]Fe{x} ++ [_]Fe{self.zero} ** 14;
705 if (x.montgomery == false) {
742 if (!x.montgomery) {
706743 self.toMontgomery(&pc[0]) catch unreachable;
707744 }
708745 for (1..pc.len) |i| {
......@@ -747,38 +784,55 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
747784 }
748785 }
749786 }
750 self.fromMontgomery(&out) catch unreachable;
787 if (!was_montgomery) {
788 self.fromMontgomery(&out) catch unreachable;
789 }
751790 return out;
752791 }
753792
754793 /// Multiplies two field elements.
794 /// Result preserves the first operand's form.
755795 pub fn mul(self: Self, x: Fe, y: Fe) Fe {
756 if (x.montgomery != y.montgomery) {
757 return self.montgomeryMul(x, y);
758 }
759 var a_ = x;
760 if (x.montgomery == false) {
761 self.toMontgomery(&a_) catch unreachable;
796 if (x.montgomery) {
797 const y_ = if (!y.montgomery) blk: {
798 var yy = y;
799 self.toMontgomery(&yy) catch unreachable;
800 break :blk yy;
801 } else y;
802 return self.montgomeryMul(x, y_);
762803 } else {
763 self.fromMontgomery(&a_) catch unreachable;
804 var x_m = x;
805 var y_m = if (y.montgomery) blk: {
806 var yy = y;
807 self.fromMontgomery(&yy) catch unreachable;
808 break :blk yy;
809 } else y;
810 self.toMontgomery(&x_m) catch unreachable;
811 self.toMontgomery(&y_m) catch unreachable;
812 var out = self.montgomeryMul(x_m, y_m);
813 self.fromMontgomery(&out) catch unreachable;
814 return out;
764815 }
765 return self.montgomeryMul(a_, y);
766816 }
767817
768818 /// Squares a field element.
769819 pub fn sq(self: Self, x: Fe) Fe {
770 var out = x;
771 if (x.montgomery == true) {
820 if (x.montgomery) {
821 return self.montgomerySq(x);
822 } else {
823 var out = x;
824 self.toMontgomery(&out) catch unreachable;
825 out = self.montgomerySq(out);
772826 self.fromMontgomery(&out) catch unreachable;
827 return out;
773828 }
774 out = self.montgomerySq(out);
775 out.montgomery = false;
776 self.toMontgomery(&out) catch unreachable;
777 return out;
778829 }
779830
780831 /// Returns x^e (mod m) in constant time.
781 pub fn pow(self: Self, x: Fe, e: Fe) NullExponentError!Fe {
832 pub fn pow(self: Self, x: Fe, e: Fe) (NullExponentError || RepresentationError)!Fe {
833 if (e.montgomery) {
834 return error.UnexpectedRepresentation;
835 }
782836 var buf: [Fe.encoded_bytes]u8 = undefined;
783837 e.toBytes(&buf, native_endian) catch unreachable;
784838 return self.powWithEncodedExponent(x, &buf, native_endian);
......@@ -786,7 +840,10 @@ pub fn Modulus(comptime max_bits: comptime_int) type {
786840
787841 /// Returns x^e (mod m), assuming that the exponent is public.
788842 /// The function remains constant time with respect to `x`.
789 pub fn powPublic(self: Self, x: Fe, e: Fe) NullExponentError!Fe {
843 pub fn powPublic(self: Self, x: Fe, e: Fe) (NullExponentError || RepresentationError)!Fe {
844 if (e.montgomery) {
845 return error.UnexpectedRepresentation;
846 }
790847 var e_normalized = Fe{ .v = e.v.normalize() };
791848 var buf_: [Fe.encoded_bytes]u8 = undefined;
792849 var buf = buf_[0 .. math.divCeil(usize, e_normalized.v.limbs_len * t_bits, 8) catch unreachable];
......@@ -927,6 +984,8 @@ test "finite field arithmetic" {
927984
928985 try m.toMontgomery(&x);
929986 x_y = m.mul(x, y);
987 try testing.expect(x_y.montgomery); // result preserves first operand's form
988 try m.fromMontgomery(&x_y);
930989 try testing.expectEqual(x_y.toPrimitive(u256), 1666576607955767413750776202132407807424848069716933450241);
931990 try m.fromMontgomery(&x);
932991
......@@ -941,8 +1000,11 @@ test "finite field arithmetic" {
9411000
9421001 const x_pow_y = try m.powPublic(x, y);
9431002 try testing.expectEqual(x_pow_y.toPrimitive(u256), 1631933139300737762906024873185789093007782131928298618473);
1003 try testing.expect(!x_pow_y.montgomery);
9441004 try m.toMontgomery(&x);
945 const x_pow_y2 = try m.powPublic(x, y);
1005 var x_pow_y2 = try m.powPublic(x, y);
1006 try testing.expect(x_pow_y2.montgomery);
1007 try m.fromMontgomery(&x_pow_y2);
9461008 try m.fromMontgomery(&x);
9471009 try testing.expect(x_pow_y2.eql(x_pow_y));
9481010 try testing.expectError(error.NullExponent, m.powPublic(x, m.zero));
......@@ -953,13 +1015,53 @@ test "finite field arithmetic" {
9531015
9541016 const x_sq = m.sq(x);
9551017 const x_sq2 = m.mul(x, x);
1018 try testing.expect(!x_sq.montgomery);
1019 try testing.expect(!x_sq2.montgomery);
9561020 try testing.expect(x_sq.eql(x_sq2));
9571021 try m.toMontgomery(&x);
958 const x_sq3 = m.sq(x);
959 const x_sq4 = m.mul(x, x);
1022 var x_sq3 = m.sq(x);
1023 var x_sq4 = m.mul(x, x);
1024 try testing.expect(x_sq3.montgomery);
1025 try testing.expect(x_sq4.montgomery);
1026 try m.fromMontgomery(&x_sq3);
1027 try m.fromMontgomery(&x_sq4);
9601028 try testing.expect(x_sq.eql(x_sq3));
9611029 try testing.expect(x_sq3.eql(x_sq4));
9621030 try m.fromMontgomery(&x);
1031
1032 var x_mont = x;
1033 try m.toMontgomery(&x_mont);
1034
1035 // Non-montgomery + montgomery
1036 const add_nm_m = m.add(x, x_mont);
1037 try testing.expect(!add_nm_m.montgomery);
1038 var add_m_nm = m.add(x_mont, x);
1039 try testing.expect(add_m_nm.montgomery);
1040 try m.fromMontgomery(&add_m_nm);
1041 try testing.expect(add_nm_m.eql(add_m_nm));
1042
1043 // Non-montgomery - montgomery
1044 const sub_nm_m = m.sub(x, y);
1045 try testing.expect(!sub_nm_m.montgomery);
1046 var y_mont = y;
1047 try m.toMontgomery(&y_mont);
1048 var sub_m_nm = m.sub(x_mont, y);
1049 try testing.expect(sub_m_nm.montgomery);
1050 try m.fromMontgomery(&sub_m_nm);
1051 try testing.expect(sub_nm_m.eql(sub_m_nm));
1052
1053 // mul: preserves first operand's form
1054 const mul_nm_m = m.mul(x, x_mont);
1055 try testing.expect(!mul_nm_m.montgomery);
1056 const mul_nm_nm = m.mul(x, x);
1057 try testing.expect(mul_nm_m.eql(mul_nm_nm));
1058 var mul_m_nm = m.mul(x_mont, x);
1059 try testing.expect(mul_m_nm.montgomery);
1060 try m.fromMontgomery(&mul_m_nm);
1061 try testing.expect(mul_m_nm.eql(mul_nm_nm));
1062
1063 try testing.expectEqual(x.toPrimitive(u256), 80169837251094269539116136208111827396136208141182357733);
1064 try testing.expectError(error.UnexpectedRepresentation, x_mont.toPrimitive(u256));
9631065}
9641066
9651067fn testCt(ct_: anytype) !void {