authorgravatar for david@vortan.devDavid Rubin <david@vortan.dev> 2026-10-23 11:39:07-05:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-10-24 23:02:22+02:00
log735e31ad99bc87621f68dc99806743f64c5128a6
treed18553d6e52c2b13cd4edfbab7a0660687e532c1
parent16126cf14165bf97ed3c588fbc10f13c1970f22f

pcurves: fix addMixed for x = 0

On P-256 and P-384 (a = -3), the curve equation at x = 0 reduces to y^2 = B. B is a quadratic residue mod p on both curves, so the points (0, +/-sqrt(B)) exist, are valid, and lie in the prime-order group since the cofactor is 1. The final select only checks , so for q = (0, +/-sqrt(B)) it discards the correct sum and returns p.

4 files changed, 32 insertions(+), 2 deletions(-)

lib/std/crypto/pcurves/p256.zig+1-1
...@@ -215,7 +215,7 @@ pub const P256 = struct {...@@ -215,7 +215,7 @@ pub const P256 = struct {
215 .y = Y3,215 .y = Y3,
216 .z = Z3,216 .z = Z3,
217 };217 };
218 ret.cMov(p, @intFromBool(q.x.isZero()));218 ret.cMov(p, @intFromBool(q.x.isZero() and q.y.isZero()));
219 return ret;219 return ret;
220 }220 }
221221
lib/std/crypto/pcurves/p384.zig+1-1
...@@ -215,7 +215,7 @@ pub const P384 = struct {...@@ -215,7 +215,7 @@ pub const P384 = struct {
215 .y = Y3,215 .y = Y3,
216 .z = Z3,216 .z = Z3,
217 };217 };
218 ret.cMov(p, @intFromBool(q.x.isZero()));218 ret.cMov(p, @intFromBool(q.x.isZero() and q.y.isZero()));
219 return ret;219 return ret;
220 }220 }
221221
lib/std/crypto/pcurves/tests/p256.zig+15
...@@ -145,3 +145,18 @@ test "p256 scalar parity" {...@@ -145,3 +145,18 @@ test "p256 scalar parity" {
145 try std.testing.expect(P256.scalar.Scalar.one.isOdd());145 try std.testing.expect(P256.scalar.Scalar.one.isOdd());
146 try std.testing.expect(P256.scalar.Scalar.one.dbl().isOdd() == false);146 try std.testing.expect(P256.scalar.Scalar.one.dbl().isOdd() == false);
147}147}
148
149test "p256 with x = 0 point" {
150 const yh = "66485c780e2f83d72433bd5d84a06bb6541c2af31dae871728bf856a174f93f4";
151 var ys: [32]u8 = undefined;
152 _ = try fmt.hexToBytes(&ys, yh);
153 const y = try P256.Fe.fromBytes(ys, .big);
154
155 const p = P256.basePoint.dbl();
156 const q = try P256.fromAffineCoordinates(.{ .x = .zero, .y = y });
157 const want = p.add(q);
158 const got = p.addMixed(.{ .x = .zero, .y = y });
159
160 try std.testing.expect(!got.equivalent(p));
161 try std.testing.expect(got.equivalent(want));
162}
lib/std/crypto/pcurves/tests/p384.zig+15
...@@ -155,3 +155,18 @@ test "p384 scalar parity" {...@@ -155,3 +155,18 @@ test "p384 scalar parity" {
155 try std.testing.expect(P384.scalar.Scalar.one.isOdd());155 try std.testing.expect(P384.scalar.Scalar.one.isOdd());
156 try std.testing.expect(P384.scalar.Scalar.one.dbl().isOdd() == false);156 try std.testing.expect(P384.scalar.Scalar.one.dbl().isOdd() == false);
157}157}
158
159test "p384 with x = 0 point" {
160 const yh = "c306610fb0ae5a159cf45c06069f22a6c5eb3641c602d42dea2c4b4f75550793406d80d2b91ad54f9048bd487af1ade1";
161 var ys: [48]u8 = undefined;
162 _ = try fmt.hexToBytes(&ys, yh);
163 const y = try P384.Fe.fromBytes(ys, .big);
164
165 const p = P384.basePoint.dbl();
166 const q = try P384.fromAffineCoordinates(.{ .x = .zero, .y = y });
167 const want = p.add(q);
168 const got = p.addMixed(.{ .x = .zero, .y = y });
169
170 try std.testing.expect(!got.equivalent(p));
171 try std.testing.expect(got.equivalent(want));
172}