authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-01 12:06:49+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-01 12:06:49+02:00
log7e35037eac561dc0b22545269075ae548c16a667
treec0884de18e6e739143e9fffe5a867e130bca34b6
parentb4ee9c1fee470aa8fb05610803bccc5075256571
parenta14e2cda6388e4b3eb2098d01208636b0a1165da

Merge pull request 'crypto.secp256k1: use the endomorphism in public multiplications' (#36992) from jedisct1/zig:secp256k1opt into master

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

2 files changed, 93 insertions(+), 136 deletions(-)

lib/std/crypto/pcurves/secp256k1.zig+58-136
......@@ -35,15 +35,17 @@ pub const Secp256k1 = struct {
3535
3636 pub const B = Fe.fromInt(7) catch unreachable;
3737
38 pub const Endormorphism = struct {
39 const lambda: u256 = 37718080363155996902926221483475020450927657555482586988616620542887997980018;
40 const beta: u256 = 55594575648329892869085402983802832744385952214688224221778511981742606582254;
38 pub const Endomorphism = struct {
39 const lambda = scalarFromInt(37718080363155996902926221483475020450927657555482586988616620542887997980018);
40 const beta = Fe.fromInt(55594575648329892869085402983802832744385952214688224221778511981742606582254) catch unreachable;
41 const b1_neg = scalarFromInt(303414439467246543595250775667605759171);
42 const b2_neg = scalarFromInt(scalar.field_order - 64502973549206556628585045361533709077);
4143
42 const lambda_s = s: {
44 fn scalarFromInt(x: u256) scalar.Scalar {
4345 var buf: [32]u8 = undefined;
44 mem.writeInt(u256, &buf, Endormorphism.lambda, .little);
45 break :s buf;
46 };
46 mem.writeInt(u256, &buf, x, .little);
47 return scalar.Scalar.fromBytes(buf, .little) catch unreachable;
48 }
4749
4850 pub const SplitScalar = struct {
4951 r1: [32]u8,
......@@ -51,39 +53,21 @@ pub const Secp256k1 = struct {
5153 };
5254
5355 /// Compute r1 and r2 so that k = r1 + r2*lambda (mod L).
56 /// Both outputs use the requested byte order.
5457 pub fn splitScalar(s: [32]u8, endian: std.lang.Endian) NonCanonicalError!SplitScalar {
55 const b1_neg_s = comptime s: {
56 var buf: [32]u8 = undefined;
57 mem.writeInt(u256, &buf, 303414439467246543595250775667605759171, .little);
58 break :s buf;
59 };
60 const b2_neg_s = comptime s: {
61 var buf: [32]u8 = undefined;
62 mem.writeInt(u256, &buf, scalar.field_order - 64502973549206556628585045361533709077, .little);
63 break :s buf;
64 };
65 const k = mem.readInt(u256, &s, endian);
66
67 const t1 = math.mulWide(u256, k, 21949224512762693861512883645436906316123769664773102907882521278123970637873);
68 const t2 = math.mulWide(u256, k, 103246583619904461035481197785446227098457807945486720222659797044629401272177);
58 const k = try scalar.Scalar.fromBytes(s, endian);
59 const k_int = mem.readInt(u256, &s, endian);
60
61 const t1 = math.mulWide(u256, k_int, 21949224512762693861512883645436906316123769664773102907882521278123970637873);
62 const t2 = math.mulWide(u256, k_int, 103246583619904461035481197785446227098457807945486720222659797044629401272177);
6963
7064 const c1 = @as(u128, @truncate(t1 >> 384)) + @as(u1, @truncate(t1 >> 383));
7165 const c2 = @as(u128, @truncate(t2 >> 384)) + @as(u1, @truncate(t2 >> 383));
7266
73 var buf: [32]u8 = undefined;
74
75 mem.writeInt(u256, &buf, c1, .little);
76 const c1x = try scalar.mul(buf, b1_neg_s, .little);
77
78 mem.writeInt(u256, &buf, c2, .little);
79 const c2x = try scalar.mul(buf, b2_neg_s, .little);
67 const r2 = scalarFromInt(c1).mul(b1_neg).add(scalarFromInt(c2).mul(b2_neg));
68 const r1 = k.sub(r2.mul(lambda));
8069
81 const r2 = try scalar.add(c1x, c2x, .little);
82
83 var r1 = try scalar.mul(r2, lambda_s, .little);
84 r1 = try scalar.sub(s, r1, .little);
85
86 return SplitScalar{ .r1 = r1, .r2 = r2 };
70 return .{ .r1 = r1.toBytes(endian), .r2 = r2.toBytes(endian) };
8771 }
8872 };
8973
......@@ -367,17 +351,43 @@ pub const Secp256k1 = struct {
367351 return e;
368352 }
369353
370 fn pcMul(pc: *const [9]Secp256k1, s: [32]u8, comptime vartime: bool) IdentityElementError!Secp256k1 {
371 std.debug.assert(vartime);
372 const e = slide(s);
354 // Compute the sum of s[i]*p[i] *IN VARIABLE TIME*. Scalars can be larger than the group order.
355 fn mulMultiPublic(comptime n: usize, p: [n]Secp256k1, s: [n][32]u8, endian: std.lang.Endian) IdentityElementError!Secp256k1 {
356 var pcs: [2 * n][9]Secp256k1 = undefined;
357 var es: [2 * n][2 * 32 + 1]i8 = undefined;
358 for (p, s, 0..) |point, k, i| {
359 pcs[2 * i] = if (point.is_base) basePointPc[0..9].* else precompute(point, 8);
360 // lambda*P
361 for (&pcs[2 * i + 1], pcs[2 * i]) |*pc_lambda, pc| {
362 pc_lambda.* = .{ .x = pc.x.mul(Endomorphism.beta), .y = pc.y, .z = pc.z };
363 }
364 var s48: [48]u8 = undefined;
365 mem.writeInt(u384, &s48, mem.readInt(u256, &k, endian), .little);
366 const split = Endomorphism.splitScalar(scalar.reduce48(s48, .little), .little) catch unreachable;
367
368 for ([_][32]u8{ split.r1, split.r2 }, es[2 * i ..][0..2]) |r, *e| {
369 const x = mem.readInt(u256, &r, .little);
370 const negate = x > scalar.field_order / 2;
371 var r_abs: [32]u8 = undefined;
372 mem.writeInt(u256, &r_abs, if (negate) scalar.field_order - x else x, .little);
373 e.* = slide(r_abs);
374 if (negate) {
375 for (e) |*d| d.* = -d.*;
376 }
377 }
378 }
379
380 var pos: usize = 0;
381 for (&es) |*e| pos = @max(pos, mem.findLastNone(i8, e, &.{0}) orelse 0);
373382 var q = Secp256k1.identityElement;
374 var pos = e.len - 1;
375383 while (true) : (pos -= 1) {
376 const slot = e[pos];
377 if (slot > 0) {
378 q = q.add(pc[@as(usize, @intCast(slot))]);
379 } else if (slot < 0) {
380 q = q.sub(pc[@as(usize, @intCast(-slot))]);
384 for (&pcs, &es) |*pc, *e| {
385 const slot = e[pos];
386 if (slot > 0) {
387 q = q.add(pc[@as(usize, @intCast(slot))]);
388 } else if (slot < 0) {
389 q = q.sub(pc[@as(usize, @intCast(-slot))]);
390 }
381391 }
382392 if (pos == 0) break;
383393 q = q.dbl().dbl().dbl().dbl();
......@@ -435,105 +445,17 @@ pub const Secp256k1 = struct {
435445
436446 /// Multiply an elliptic curve point by a *PUBLIC* scalar *IN VARIABLE TIME*
437447 /// This can be used for signature verification.
438 pub fn mulPublic(p: Secp256k1, s_: [32]u8, endian: std.lang.Endian) (IdentityElementError || NonCanonicalError)!Secp256k1 {
439 const s = if (endian == .little) s_ else Fe.orderSwap(s_);
440 const zero = comptime scalar.Scalar.zero.toBytes(.little);
441 if (mem.eql(u8, &zero, &s)) {
442 return error.IdentityElement;
443 }
444 const pc = precompute(p, 8);
445 var lambda_p = try pcMul(&pc, Endormorphism.lambda_s, true);
446 var split_scalar = try Endormorphism.splitScalar(s, .little);
447 var px = p;
448
449 // If a key is negative, flip the sign to keep it half-sized,
450 // and flip the sign of the Y point coordinate to compensate.
451 if (split_scalar.r1[split_scalar.r1.len / 2] != 0) {
452 split_scalar.r1 = scalar.neg(split_scalar.r1, .little) catch zero;
453 px = px.neg();
454 }
455 if (split_scalar.r2[split_scalar.r2.len / 2] != 0) {
456 split_scalar.r2 = scalar.neg(split_scalar.r2, .little) catch zero;
457 lambda_p = lambda_p.neg();
458 }
459 return mulDoubleBasePublicEndo(px, split_scalar.r1, lambda_p, split_scalar.r2);
460 }
461
462 // Half-size double-base public multiplication when using the curve endomorphism.
463 // Scalars must be in little-endian.
464 // The second point is unlikely to be the generator, so don't even try to use the comptime table for it.
465 fn mulDoubleBasePublicEndo(p1: Secp256k1, s1: [32]u8, p2: Secp256k1, s2: [32]u8) IdentityElementError!Secp256k1 {
466 var pc1_array: [9]Secp256k1 = undefined;
467 const pc1 = if (p1.is_base) basePointPc[0..9] else pc: {
468 pc1_array = precompute(p1, 8);
469 break :pc &pc1_array;
470 };
471 const pc2 = precompute(p2, 8);
472 std.debug.assert(s1[s1.len / 2] == 0);
473 std.debug.assert(s2[s2.len / 2] == 0);
474 const e1 = slide(s1);
475 const e2 = slide(s2);
476 var q = Secp256k1.identityElement;
477 var pos: usize = 2 * 32 / 2; // second half is all zero
478 while (true) : (pos -= 1) {
479 const slot1 = e1[pos];
480 if (slot1 > 0) {
481 q = q.add(pc1[@as(usize, @intCast(slot1))]);
482 } else if (slot1 < 0) {
483 q = q.sub(pc1[@as(usize, @intCast(-slot1))]);
484 }
485 const slot2 = e2[pos];
486 if (slot2 > 0) {
487 q = q.add(pc2[@as(usize, @intCast(slot2))]);
488 } else if (slot2 < 0) {
489 q = q.sub(pc2[@as(usize, @intCast(-slot2))]);
490 }
491 if (pos == 0) break;
492 q = q.dbl().dbl().dbl().dbl();
493 }
494 try q.rejectIdentity();
495 return q;
448 pub fn mulPublic(p: Secp256k1, s: [32]u8, endian: std.lang.Endian) (IdentityElementError || NonCanonicalError)!Secp256k1 {
449 try scalar.rejectNonCanonical(s, endian);
450 return mulMultiPublic(1, .{p}, .{s}, endian);
496451 }
497452
498453 /// Double-base multiplication of public parameters - Compute (p1*s1)+(p2*s2) *IN VARIABLE TIME*
499454 /// This can be used for signature verification.
500 pub fn mulDoubleBasePublic(p1: Secp256k1, s1_: [32]u8, p2: Secp256k1, s2_: [32]u8, endian: std.lang.Endian) IdentityElementError!Secp256k1 {
501 const s1 = if (endian == .little) s1_ else Fe.orderSwap(s1_);
502 const s2 = if (endian == .little) s2_ else Fe.orderSwap(s2_);
455 pub fn mulDoubleBasePublic(p1: Secp256k1, s1: [32]u8, p2: Secp256k1, s2: [32]u8, endian: std.lang.Endian) IdentityElementError!Secp256k1 {
503456 try p1.rejectIdentity();
504 var pc1_array: [9]Secp256k1 = undefined;
505 const pc1 = if (p1.is_base) basePointPc[0..9] else pc: {
506 pc1_array = precompute(p1, 8);
507 break :pc &pc1_array;
508 };
509457 try p2.rejectIdentity();
510 var pc2_array: [9]Secp256k1 = undefined;
511 const pc2 = if (p2.is_base) basePointPc[0..9] else pc: {
512 pc2_array = precompute(p2, 8);
513 break :pc &pc2_array;
514 };
515 const e1 = slide(s1);
516 const e2 = slide(s2);
517 var q = Secp256k1.identityElement;
518 var pos: usize = 2 * 32;
519 while (true) : (pos -= 1) {
520 const slot1 = e1[pos];
521 if (slot1 > 0) {
522 q = q.add(pc1[@as(usize, @intCast(slot1))]);
523 } else if (slot1 < 0) {
524 q = q.sub(pc1[@as(usize, @intCast(-slot1))]);
525 }
526 const slot2 = e2[pos];
527 if (slot2 > 0) {
528 q = q.add(pc2[@as(usize, @intCast(slot2))]);
529 } else if (slot2 < 0) {
530 q = q.sub(pc2[@as(usize, @intCast(-slot2))]);
531 }
532 if (pos == 0) break;
533 q = q.dbl().dbl().dbl().dbl();
534 }
535 try q.rejectIdentity();
536 return q;
458 return mulMultiPublic(2, .{ p1, p2 }, .{ s1, s2 }, endian);
537459 }
538460};
539461
lib/std/crypto/pcurves/tests/secp256k1.zig+35
......@@ -4,6 +4,8 @@ const testing = std.testing;
44
55const Secp256k1 = @import("../secp256k1.zig").Secp256k1;
66
7const lambda: u256 = 0x5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72;
8
79test "secp256k1 ECDH key exchange" {
810 const io = testing.io;
911 const dha = Secp256k1.scalar.random(io, .little);
......@@ -138,6 +140,39 @@ test "secp256k1 double base multiplication" {
138140 try testing.expect(pr1.equivalent(pr2));
139141}
140142
143test "secp256k1 public multiplication" {
144 const io = testing.io;
145 const n = Secp256k1.scalar.field_order;
146 const p = Secp256k1.random(io);
147 for ([_]u256{ 1, 2, n - 1, lambda, n - lambda, (1 << 128) - 1 }) |x| {
148 var s: [32]u8 = undefined;
149 std.mem.writeInt(u256, &s, x, .big);
150 for ([_]Secp256k1{ p, Secp256k1.basePoint }) |q| {
151 try testing.expect((try q.mulPublic(s, .big)).equivalent(try q.mul(s, .big)));
152 }
153 }
154
155 var s_n: [32]u8 = undefined;
156 std.mem.writeInt(u256, &s_n, n, .little);
157 try testing.expectError(error.NonCanonical, p.mulPublic(s_n, .little));
158}
159
160test "secp256k1 scalar split" {
161 const io = testing.io;
162 const n = Secp256k1.scalar.field_order;
163 inline for (.{ .little, .big }) |endian| {
164 var lambda_s: [32]u8 = undefined;
165 std.mem.writeInt(u256, &lambda_s, lambda, endian);
166 const k = Secp256k1.scalar.random(io, endian);
167 const split = try Secp256k1.Endomorphism.splitScalar(k, endian);
168 try testing.expectEqual(k, try Secp256k1.scalar.mulAdd(split.r2, lambda_s, split.r1, endian));
169 for ([_][32]u8{ split.r1, split.r2 }) |r_s| {
170 const r = std.mem.readInt(u256, &r_s, endian);
171 try testing.expect(@min(r, n - r) < 1 << 128);
172 }
173 }
174}
175
141176test "secp256k1 scalar inverse" {
142177 const expected = "08d0684a0fe8ea978b68a29e4b4ffdbd19eeb59db25301cf23ecbe568e1f9822";
143178 var out: [32]u8 = undefined;