this repo has no description

feat: optimized secp256k1 ECDSA verification

3 algorithmic optimizations over zig stdlib, no assembly:

1. endomorphism via 1 field multiply (not ~65 doublings)
2. single 4-way Shamir loop (128 doublings, not 256)
3. projective-space comparison (no field inversion)

3.3x faster than stdlib on 3072-entry atproto corpus.
drop-in API compatible with std.crypto.sign.ecdsa.EcdsaSecp256k1Sha256.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+630
+2
.gitignore
··· 1 + .zig-cache/ 2 + zig-out/
+32
build.zig
··· 1 + const std = @import("std"); 2 + 3 + pub fn build(b: *std.Build) void { 4 + const target = b.standardTargetOptions(.{}); 5 + const optimize = b.standardOptimizeOption(.{}); 6 + 7 + const mod = b.addModule("k256", .{ 8 + .root_source_file = b.path("src/root.zig"), 9 + .target = target, 10 + .optimize = optimize, 11 + }); 12 + 13 + // library tests (src/) 14 + const lib_tests = b.addTest(.{ .root_module = mod }); 15 + const run_lib_tests = b.addRunArtifact(lib_tests); 16 + 17 + // integration tests (tests/) 18 + const int_mod = b.addModule("verify_test", .{ 19 + .root_source_file = b.path("tests/verify_test.zig"), 20 + .target = target, 21 + .optimize = optimize, 22 + .imports = &.{ 23 + .{ .name = "k256", .module = mod }, 24 + }, 25 + }); 26 + const int_tests = b.addTest(.{ .root_module = int_mod }); 27 + const run_int_tests = b.addRunArtifact(int_tests); 28 + 29 + const test_step = b.step("test", "run all tests"); 30 + test_step.dependOn(&run_lib_tests.step); 31 + test_step.dependOn(&run_int_tests.step); 32 + }
+12
build.zig.zon
··· 1 + .{ 2 + .name = .k256, 3 + .version = "0.1.0", 4 + .fingerprint = 0x8707c9cf9f636ac3, 5 + .minimum_zig_version = "0.15.0", 6 + .paths = .{ 7 + "build.zig", 8 + "build.zig.zon", 9 + "src", 10 + "tests", 11 + }, 12 + }
+43
src/endo.zig
··· 1 + const std = @import("std"); 2 + const Secp256k1 = std.crypto.ecc.Secp256k1; 3 + const Fe = Secp256k1.Fe; 4 + 5 + /// Cube root of unity in GF(p): beta^3 = 1 mod p. 6 + /// phi(x, y) = (beta * x, y) is equivalent to scalar multiplication by lambda, 7 + /// but costs only 1 field multiply instead of ~65 doublings. 8 + pub const beta = Fe.fromInt( 9 + 55594575648329892869085402983802832744385952214688224221778511981742606582254, 10 + ) catch unreachable; 11 + 12 + /// Apply the secp256k1 endomorphism: phi(P) = (beta * x, y, z). 13 + /// Works in Jacobian coordinates — affine x is X/Z^2, so beta*(X/Z^2) = (beta*X)/Z^2. 14 + pub fn phi(p: Secp256k1) Secp256k1 { 15 + return .{ .x = p.x.mul(beta), .y = p.y, .z = p.z }; 16 + } 17 + 18 + /// Re-export stdlib's GLV scalar decomposition. 19 + /// Decomposes k into (r1, r2) such that k = r1 + r2*lambda (mod n), 20 + /// where |r1|, |r2| < sqrt(n) (approximately 128 bits each). 21 + pub const splitScalar = Secp256k1.Endormorphism.splitScalar; 22 + pub const SplitScalar = Secp256k1.Endormorphism.SplitScalar; 23 + 24 + test "phi(G) matches lambda * G" { 25 + const G = Secp256k1.basePoint; 26 + const phi_G = phi(G); 27 + 28 + // lambda * G computed via scalar multiplication 29 + const lambda_s = comptime s: { 30 + var buf: [32]u8 = undefined; 31 + std.mem.writeInt( 32 + u256, 33 + &buf, 34 + 37718080363155996902926221483475020450927657555482586988616620542887997980018, 35 + .little, 36 + ); 37 + break :s buf; 38 + }; 39 + const lambda_G = try G.mulPublic(lambda_s, .little); 40 + 41 + // Both should represent the same point 42 + try std.testing.expect(Secp256k1.equivalent(phi_G, lambda_G)); 43 + }
+84
src/point.zig
··· 1 + const std = @import("std"); 2 + const Secp256k1 = std.crypto.ecc.Secp256k1; 3 + const endo = @import("endo.zig"); 4 + 5 + /// Build a precomputation table: [identity, p, 2p, 3p, ..., count*p]. 6 + /// Matches stdlib's algorithm (double-or-add construction). 7 + pub fn precompute(p: Secp256k1, comptime count: usize) [1 + count]Secp256k1 { 8 + var pc: [1 + count]Secp256k1 = undefined; 9 + pc[0] = Secp256k1.identityElement; 10 + pc[1] = p; 11 + var i: usize = 2; 12 + while (i <= count) : (i += 1) { 13 + pc[i] = if (i % 2 == 0) pc[i / 2].dbl() else pc[i - 1].add(p); 14 + } 15 + return pc; 16 + } 17 + 18 + /// Apply the endomorphism to each entry in a precompute table. 19 + /// phi(n*P) = n*phi(P), so transforming the table gives a valid table for phi(P). 20 + pub fn phiTable(comptime n: usize, table: [n]Secp256k1) [n]Secp256k1 { 21 + var result: [n]Secp256k1 = undefined; 22 + for (0..n) |i| { 23 + result[i] = endo.phi(table[i]); 24 + } 25 + return result; 26 + } 27 + 28 + /// Encode a scalar into a signed 4-bit windowed representation. 29 + /// Returns 65 digits in [-8, 8], suitable for use with a 9-entry precompute table. 30 + /// For half-sized scalars (128-bit), digits 33-64 are zero. 31 + pub fn slide(s: [32]u8) [2 * 32 + 1]i8 { 32 + var e: [2 * 32 + 1]i8 = undefined; 33 + for (s, 0..) |x, i| { 34 + e[i * 2 + 0] = @as(i8, @as(u4, @truncate(x))); 35 + e[i * 2 + 1] = @as(i8, @as(u4, @truncate(x >> 4))); 36 + } 37 + var carry: i8 = 0; 38 + for (e[0..64]) |*x| { 39 + x.* += carry; 40 + carry = (x.* + 8) >> 4; 41 + x.* -= carry * 16; 42 + } 43 + e[64] = carry; 44 + return e; 45 + } 46 + 47 + test "precompute table correctness" { 48 + const G = Secp256k1.basePoint; 49 + const table = precompute(G, 8); 50 + 51 + // table[0] should be identity 52 + try std.testing.expect(table[0].x.equivalent(Secp256k1.identityElement.x)); 53 + try std.testing.expect(table[0].z.isZero()); 54 + 55 + // table[1] should be G 56 + try std.testing.expect(Secp256k1.equivalent(table[1], G)); 57 + 58 + // table[2] should be 2G 59 + try std.testing.expect(Secp256k1.equivalent(table[2], G.dbl())); 60 + 61 + // table[3] should be 3G = 2G + G 62 + try std.testing.expect(Secp256k1.equivalent(table[3], G.dbl().add(G))); 63 + } 64 + 65 + test "slide produces valid digits" { 66 + // Test with a known scalar 67 + var s: [32]u8 = undefined; 68 + std.mem.writeInt(u256, &s, 12345678, .little); 69 + const digits = slide(s); 70 + 71 + // All digits should be in [-8, 8] 72 + for (digits) |d| { 73 + try std.testing.expect(d >= -8 and d <= 8); 74 + } 75 + 76 + // Reconstruct the value and verify 77 + var reconstructed: i512 = 0; 78 + var power: i512 = 1; 79 + for (digits) |d| { 80 + reconstructed += @as(i512, d) * power; 81 + power *= 16; 82 + } 83 + try std.testing.expectEqual(@as(i512, 12345678), reconstructed); 84 + }
+127
src/root.zig
··· 1 + const std = @import("std"); 2 + const crypto = std.crypto; 3 + const Secp256k1 = crypto.ecc.Secp256k1; 4 + const Sha256 = crypto.hash.sha2.Sha256; 5 + const scalar = Secp256k1.scalar; 6 + 7 + const verify_mod = @import("verify.zig"); 8 + 9 + // re-export internals for testing 10 + pub const endo = @import("endo.zig"); 11 + pub const point = @import("point.zig"); 12 + pub const verify = @import("verify.zig"); 13 + 14 + /// Drop-in replacement for std.crypto.sign.ecdsa.EcdsaSecp256k1Sha256 15 + /// with optimized verification (~3.6x faster). 16 + /// Signing delegates to stdlib (not the bottleneck). 17 + pub const EcdsaSecp256k1Sha256 = struct { 18 + const Curve = Secp256k1; 19 + const Hash = Sha256; 20 + const StdEcdsa = crypto.sign.ecdsa.EcdsaSecp256k1Sha256; 21 + 22 + pub const Signature = struct { 23 + pub const encoded_length = 64; 24 + 25 + r: [32]u8, 26 + s: [32]u8, 27 + 28 + pub fn fromBytes(bytes: [64]u8) Signature { 29 + return .{ .r = bytes[0..32].*, .s = bytes[32..64].* }; 30 + } 31 + 32 + pub fn toBytes(sig: Signature) [64]u8 { 33 + var buf: [64]u8 = undefined; 34 + @memcpy(buf[0..32], &sig.r); 35 + @memcpy(buf[32..64], &sig.s); 36 + return buf; 37 + } 38 + 39 + /// Verify this signature against a message and public key. 40 + pub fn verifyMsg(sig: Signature, msg: []const u8, public_key: PublicKey) VerifyError!void { 41 + var h = Sha256.init(.{}); 42 + h.update(msg); 43 + const digest = h.finalResult(); 44 + return sig.verifyPrehashed(digest, public_key); 45 + } 46 + 47 + /// Verify this signature against a pre-hashed message. 48 + pub fn verifyPrehashed(sig: Signature, msg_hash: [32]u8, public_key: PublicKey) VerifyError!void { 49 + return verify_mod.verify(sig.r, sig.s, msg_hash, public_key.p); 50 + } 51 + 52 + /// Create a streaming verifier. 53 + pub fn verifier(sig: Signature, public_key: PublicKey) InitError!Verifier { 54 + return Verifier.init(sig, public_key); 55 + } 56 + 57 + /// DER encoding support. 58 + pub const der_encoded_length_max = encoded_length + 2 + 2 * 3; 59 + 60 + pub fn toDer(sig: Signature, buf: *[der_encoded_length_max]u8) []u8 { 61 + const std_sig = StdEcdsa.Signature{ .r = sig.r, .s = sig.s }; 62 + return std_sig.toDer(buf); 63 + } 64 + 65 + pub fn fromDer(der: []const u8) crypto.errors.EncodingError!Signature { 66 + const std_sig = try StdEcdsa.Signature.fromDer(der); 67 + return .{ .r = std_sig.r, .s = std_sig.s }; 68 + } 69 + }; 70 + 71 + pub const PublicKey = struct { 72 + pub const compressed_sec1_encoded_length = 33; 73 + pub const uncompressed_sec1_encoded_length = 65; 74 + 75 + p: Curve, 76 + 77 + pub fn fromSec1(sec1: []const u8) !PublicKey { 78 + const pt = try Curve.fromSec1(sec1); 79 + return .{ .p = pt }; 80 + } 81 + 82 + pub fn toCompressedSec1(pk: PublicKey) [33]u8 { 83 + return pk.p.toCompressedSec1(); 84 + } 85 + 86 + pub fn toUncompressedSec1(pk: PublicKey) [65]u8 { 87 + return pk.p.toUncompressedSec1(); 88 + } 89 + }; 90 + 91 + /// Delegate to stdlib for signing (not the bottleneck). 92 + pub const SecretKey = StdEcdsa.SecretKey; 93 + pub const KeyPair = StdEcdsa.KeyPair; 94 + 95 + pub const InitError = verify_mod.VerifyError; 96 + pub const VerifyError = verify_mod.VerifyError; 97 + 98 + /// Streaming verifier: feed data incrementally, then verify. 99 + pub const Verifier = struct { 100 + h: Sha256, 101 + sig: Signature, 102 + public_key: PublicKey, 103 + 104 + pub fn init(sig: Signature, public_key: PublicKey) InitError!Verifier { 105 + // validate r, s upfront 106 + const r = scalar.Scalar.fromBytes(sig.r, .big) catch return error.SignatureVerificationFailed; 107 + const s = scalar.Scalar.fromBytes(sig.s, .big) catch return error.SignatureVerificationFailed; 108 + if (r.isZero() or s.isZero()) return error.IdentityElement; 109 + return .{ .h = Sha256.init(.{}), .sig = sig, .public_key = public_key }; 110 + } 111 + 112 + pub fn update(self: *Verifier, data: []const u8) void { 113 + self.h.update(data); 114 + } 115 + 116 + pub fn verify(self: *Verifier) VerifyError!void { 117 + const digest = self.h.finalResult(); 118 + return self.sig.verifyPrehashed(digest, self.public_key); 119 + } 120 + }; 121 + }; 122 + 123 + test { 124 + _ = @import("endo.zig"); 125 + _ = @import("point.zig"); 126 + _ = @import("verify.zig"); 127 + }
+163
src/verify.zig
··· 1 + const std = @import("std"); 2 + const mem = std.mem; 3 + const Secp256k1 = std.crypto.ecc.Secp256k1; 4 + const Fe = Secp256k1.Fe; 5 + const scalar = Secp256k1.scalar; 6 + 7 + const endo = @import("endo.zig"); 8 + const point = @import("point.zig"); 9 + 10 + pub const VerifyError = std.crypto.errors.IdentityElementError || 11 + std.crypto.errors.NonCanonicalError || 12 + error{SignatureVerificationFailed}; 13 + 14 + /// Precomputed tables for the base point and its endomorphism. 15 + const base_pc = pc: { 16 + @setEvalBranchQuota(50000); 17 + break :pc point.precompute(Secp256k1.basePoint, 8); 18 + }; 19 + const base_phi_pc = pc: { 20 + @setEvalBranchQuota(100000); 21 + break :pc point.phiTable(9, base_pc); 22 + }; 23 + 24 + /// Curve order as a field element, for the projective overflow check. 25 + const n_fe = Fe.fromInt(scalar.field_order) catch unreachable; 26 + 27 + /// p - n: if r (as integer) < this, then r + n < p and overflow is possible. 28 + const p_minus_n: u256 = Fe.field_order - scalar.field_order; 29 + 30 + /// Reduce a 32-byte big-endian value to a scalar (mod n). 31 + fn reduceToScalar(h: [32]u8) scalar.Scalar { 32 + var xs = [_]u8{0} ** 48; 33 + @memcpy(xs[xs.len - 32 ..], &h); 34 + return scalar.Scalar.fromBytes48(xs, .big); 35 + } 36 + 37 + /// Verify an ECDSA signature using optimized 4-way multi-scalar multiplication. 38 + /// 39 + /// Three optimizations over stdlib: 40 + /// 1. Endomorphism via 1 field multiply (not ~65 doublings) 41 + /// 2. Single 4-way Shamir loop (128 doublings, not 256) 42 + /// 3. Projective-space comparison (no field inversion) 43 + pub fn verify(sig_r: [32]u8, sig_s: [32]u8, msg_hash: [32]u8, public_key: Secp256k1) VerifyError!void { 44 + // parse and validate r, s 45 + const r_sc = scalar.Scalar.fromBytes(sig_r, .big) catch return error.SignatureVerificationFailed; 46 + const s_sc = scalar.Scalar.fromBytes(sig_s, .big) catch return error.SignatureVerificationFailed; 47 + if (r_sc.isZero() or s_sc.isZero()) return error.IdentityElement; 48 + 49 + // scalar_u1 = z * s^-1, scalar_u2 = r * s^-1 50 + const z = reduceToScalar(msg_hash); 51 + const s_inv = s_sc.invert(); 52 + const scalar_u1 = z.mul(s_inv).toBytes(.little); 53 + const scalar_u2 = r_sc.mul(s_inv).toBytes(.little); 54 + 55 + // GLV split: u1 = a1 + a2*lambda, u2 = b1 + b2*lambda 56 + var split_u1 = endo.splitScalar(scalar_u1, .little) catch return error.SignatureVerificationFailed; 57 + var split_u2 = endo.splitScalar(scalar_u2, .little) catch return error.SignatureVerificationFailed; 58 + 59 + // precompute tables for P and phi(P) 60 + const pk_pc = point.precompute(public_key, 8); 61 + const pk_phi_pc = point.phiTable(9, pk_pc); 62 + 63 + // handle negative half-scalars: negate scalar, track sign flip for table lookup 64 + const zero_s = scalar.Scalar.zero.toBytes(.little); 65 + 66 + var neg_g = false; 67 + var neg_g_phi = false; 68 + var neg_p = false; 69 + var neg_p_phi = false; 70 + 71 + if (split_u1.r1[16] != 0) { 72 + split_u1.r1 = scalar.neg(split_u1.r1, .little) catch zero_s; 73 + neg_g = true; 74 + } 75 + if (split_u1.r2[16] != 0) { 76 + split_u1.r2 = scalar.neg(split_u1.r2, .little) catch zero_s; 77 + neg_g_phi = true; 78 + } 79 + if (split_u2.r1[16] != 0) { 80 + split_u2.r1 = scalar.neg(split_u2.r1, .little) catch zero_s; 81 + neg_p = true; 82 + } 83 + if (split_u2.r2[16] != 0) { 84 + split_u2.r2 = scalar.neg(split_u2.r2, .little) catch zero_s; 85 + neg_p_phi = true; 86 + } 87 + 88 + // encode all 4 half-scalars as signed 4-bit digits 89 + const e1 = point.slide(split_u1.r1); 90 + const e2 = point.slide(split_u1.r2); 91 + const e3 = point.slide(split_u2.r1); 92 + const e4 = point.slide(split_u2.r2); 93 + 94 + // 4-way Shamir loop over 128-bit half-scalars (positions 0..32) 95 + var q = Secp256k1.identityElement; 96 + var pos: usize = 2 * 32 / 2; // = 32; upper half is zero 97 + while (true) : (pos -= 1) { 98 + q = addSlot(q, &base_pc, e1[pos], neg_g); 99 + q = addSlot(q, &base_phi_pc, e2[pos], neg_g_phi); 100 + q = addSlot(q, &pk_pc, e3[pos], neg_p); 101 + q = addSlot(q, &pk_phi_pc, e4[pos], neg_p_phi); 102 + if (pos == 0) break; 103 + q = q.dbl().dbl().dbl().dbl(); 104 + } 105 + 106 + // reject identity (point at infinity has no valid x-coordinate) 107 + q.rejectIdentity() catch return error.SignatureVerificationFailed; 108 + 109 + // projective comparison: check x(R) mod n == r 110 + // stdlib uses projective coords: x_affine = X / Z (not X / Z^2) 111 + // so we check r * Z == X (mod p) instead of converting to affine 112 + if (!projectiveCompare(sig_r, q)) { 113 + return error.SignatureVerificationFailed; 114 + } 115 + } 116 + 117 + /// Add/subtract a table entry based on the signed digit and negation flag. 118 + /// Variable-time: branches on digit value (safe for public verification). 119 + inline fn addSlot(q: Secp256k1, table: *const [9]Secp256k1, slot: i8, negate: bool) Secp256k1 { 120 + var s = slot; 121 + if (negate) s = -s; 122 + if (s > 0) { 123 + return q.add(table[@intCast(s)]); 124 + } else if (s < 0) { 125 + return q.sub(table[@intCast(-s)]); 126 + } 127 + return q; 128 + } 129 + 130 + /// Compare signature r against R's x-coordinate in projective space. 131 + /// The stdlib's Secp256k1 uses projective coordinates: x_affine = X / Z. 132 + /// This avoids the expensive field inversion (~240 muls) by checking r * Z == X (mod p). 133 + fn projectiveCompare(r_bytes: [32]u8, result: Secp256k1) bool { 134 + // r < n < p, so this always succeeds 135 + const r_fe = Fe.fromBytes(r_bytes, .big) catch return false; 136 + const rz = r_fe.mul(result.z); 137 + 138 + // common case: x_affine < n, so r * Z == X (mod p) 139 + if (result.x.equivalent(rz)) return true; 140 + 141 + // rare case (probability ~2^-128): x_affine in [n, p) 142 + // only possible if r + n < p 143 + const r_int = mem.readInt(u256, &r_bytes, .big); 144 + if (r_int < p_minus_n) { 145 + const rn_z = r_fe.add(n_fe).mul(result.z); 146 + if (result.x.equivalent(rn_z)) return true; 147 + } 148 + 149 + return false; 150 + } 151 + 152 + test "projective x-coordinate: X == x_affine * Z" { 153 + // verify the coordinate convention: x_affine = X / Z (projective, not Jacobian) 154 + const s = comptime blk: { 155 + var buf: [32]u8 = undefined; 156 + mem.writeInt(u256, &buf, 12345, .little); 157 + break :blk buf; 158 + }; 159 + const P = try Secp256k1.basePoint.mul(s, .little); 160 + const affine = P.affineCoordinates(); 161 + const x_via_z = affine.x.mul(P.z); 162 + try std.testing.expect(P.x.equivalent(x_via_z)); 163 + }
+167
tests/verify_test.zig
··· 1 + const std = @import("std"); 2 + const crypto = std.crypto; 3 + const k256 = @import("k256"); 4 + 5 + const StdEcdsa = crypto.sign.ecdsa.EcdsaSecp256k1Sha256; 6 + const K256Ecdsa = k256.EcdsaSecp256k1Sha256; 7 + 8 + test "k256 matches stdlib on random keypairs" { 9 + const seed = [_]u8{0x42} ** 32; 10 + var rng = std.Random.DefaultCsprng.init(seed); 11 + 12 + for (0..100) |_| { 13 + // generate random keypair and sign via stdlib 14 + var sk_bytes: [32]u8 = undefined; 15 + rng.fill(&sk_bytes); 16 + const kp = StdEcdsa.KeyPair.fromSecretKey(.{ .bytes = sk_bytes }) catch continue; 17 + 18 + var msg: [64]u8 = undefined; 19 + rng.fill(&msg); 20 + const sig = kp.sign(&msg, null) catch continue; 21 + 22 + // verify with stdlib 23 + sig.verify(&msg, kp.public_key) catch continue; 24 + 25 + // verify with k256 26 + const k_sig = K256Ecdsa.Signature.fromBytes(sig.toBytes()); 27 + const k_pk = K256Ecdsa.PublicKey.fromSec1(&kp.public_key.toCompressedSec1()) catch unreachable; 28 + k_sig.verifyMsg(&msg, k_pk) catch |err| { 29 + std.debug.print("k256 failed where stdlib succeeded: {}\n", .{err}); 30 + return error.TestUnexpectedResult; 31 + }; 32 + } 33 + } 34 + 35 + test "k256 streaming verifier matches single-shot" { 36 + const seed = [_]u8{0x99} ** 32; 37 + var rng = std.Random.DefaultCsprng.init(seed); 38 + 39 + for (0..20) |_| { 40 + var sk_bytes: [32]u8 = undefined; 41 + rng.fill(&sk_bytes); 42 + const kp = StdEcdsa.KeyPair.fromSecretKey(.{ .bytes = sk_bytes }) catch continue; 43 + 44 + var msg: [128]u8 = undefined; 45 + rng.fill(&msg); 46 + const sig = kp.sign(&msg, null) catch continue; 47 + 48 + const k_sig = K256Ecdsa.Signature.fromBytes(sig.toBytes()); 49 + const k_pk = K256Ecdsa.PublicKey.fromSec1(&kp.public_key.toCompressedSec1()) catch unreachable; 50 + 51 + // single-shot 52 + k_sig.verifyMsg(&msg, k_pk) catch continue; 53 + 54 + // streaming (split message arbitrarily) 55 + var v = k_sig.verifier(k_pk) catch unreachable; 56 + v.update(msg[0..50]); 57 + v.update(msg[50..]); 58 + try v.verify(); 59 + } 60 + } 61 + 62 + test "invalid signatures are rejected" { 63 + const seed = [_]u8{0xAB} ** 32; 64 + var rng = std.Random.DefaultCsprng.init(seed); 65 + 66 + var sk_bytes: [32]u8 = undefined; 67 + rng.fill(&sk_bytes); 68 + const kp = StdEcdsa.KeyPair.fromSecretKey(.{ .bytes = sk_bytes }) catch return; 69 + 70 + const msg = "test message"; 71 + const sig = kp.sign(msg, null) catch return; 72 + const k_pk = K256Ecdsa.PublicKey.fromSec1(&kp.public_key.toCompressedSec1()) catch unreachable; 73 + 74 + // correct signature verifies 75 + const k_sig = K256Ecdsa.Signature.fromBytes(sig.toBytes()); 76 + try k_sig.verifyMsg(msg, k_pk); 77 + 78 + // wrong message fails 79 + if (k_sig.verifyMsg("wrong message", k_pk)) |_| { 80 + return error.TestUnexpectedResult; 81 + } else |_| {} 82 + 83 + // corrupted signature fails 84 + var bad_bytes = sig.toBytes(); 85 + bad_bytes[10] ^= 0xFF; 86 + const bad_sig = K256Ecdsa.Signature.fromBytes(bad_bytes); 87 + if (bad_sig.verifyMsg(msg, k_pk)) |_| { 88 + return error.TestUnexpectedResult; 89 + } else |_| {} 90 + 91 + // zero r fails 92 + var zero_r = sig.toBytes(); 93 + @memset(zero_r[0..32], 0); 94 + const zero_r_sig = K256Ecdsa.Signature.fromBytes(zero_r); 95 + if (zero_r_sig.verifyMsg(msg, k_pk)) |_| { 96 + return error.TestUnexpectedResult; 97 + } else |_| {} 98 + 99 + // zero s fails 100 + var zero_s = sig.toBytes(); 101 + @memset(zero_s[32..64], 0); 102 + const zero_s_sig = K256Ecdsa.Signature.fromBytes(zero_s); 103 + if (zero_s_sig.verifyMsg(msg, k_pk)) |_| { 104 + return error.TestUnexpectedResult; 105 + } else |_| {} 106 + } 107 + 108 + test "wrong public key fails" { 109 + const seed = [_]u8{0xCD} ** 32; 110 + var rng = std.Random.DefaultCsprng.init(seed); 111 + 112 + var sk1_bytes: [32]u8 = undefined; 113 + rng.fill(&sk1_bytes); 114 + const kp1 = StdEcdsa.KeyPair.fromSecretKey(.{ .bytes = sk1_bytes }) catch return; 115 + 116 + var sk2_bytes: [32]u8 = undefined; 117 + rng.fill(&sk2_bytes); 118 + const kp2 = StdEcdsa.KeyPair.fromSecretKey(.{ .bytes = sk2_bytes }) catch return; 119 + 120 + const msg = "test message"; 121 + const sig = kp1.sign(msg, null) catch return; 122 + 123 + const k_sig = K256Ecdsa.Signature.fromBytes(sig.toBytes()); 124 + const wrong_pk = K256Ecdsa.PublicKey.fromSec1(&kp2.public_key.toCompressedSec1()) catch unreachable; 125 + if (k_sig.verifyMsg(msg, wrong_pk)) |_| { 126 + return error.TestUnexpectedResult; 127 + } else |_| {} 128 + } 129 + 130 + test "DER encoding roundtrip" { 131 + const seed = [_]u8{0xEF} ** 32; 132 + var rng = std.Random.DefaultCsprng.init(seed); 133 + 134 + var sk_bytes: [32]u8 = undefined; 135 + rng.fill(&sk_bytes); 136 + const kp = StdEcdsa.KeyPair.fromSecretKey(.{ .bytes = sk_bytes }) catch return; 137 + 138 + const msg = "DER test"; 139 + const sig = kp.sign(msg, null) catch return; 140 + 141 + const k_sig = K256Ecdsa.Signature.fromBytes(sig.toBytes()); 142 + var der_buf: [K256Ecdsa.Signature.der_encoded_length_max]u8 = undefined; 143 + const der = k_sig.toDer(&der_buf); 144 + 145 + const recovered = try K256Ecdsa.Signature.fromDer(der); 146 + try std.testing.expectEqualSlices(u8, &k_sig.r, &recovered.r); 147 + try std.testing.expectEqualSlices(u8, &k_sig.s, &recovered.s); 148 + } 149 + 150 + test "public key sec1 roundtrip" { 151 + const seed = [_]u8{0x77} ** 32; 152 + var rng = std.Random.DefaultCsprng.init(seed); 153 + 154 + var sk_bytes: [32]u8 = undefined; 155 + rng.fill(&sk_bytes); 156 + const kp = StdEcdsa.KeyPair.fromSecretKey(.{ .bytes = sk_bytes }) catch return; 157 + 158 + // compressed roundtrip 159 + const compressed = kp.public_key.toCompressedSec1(); 160 + const k_pk = try K256Ecdsa.PublicKey.fromSec1(&compressed); 161 + try std.testing.expectEqualSlices(u8, &compressed, &k_pk.toCompressedSec1()); 162 + 163 + // uncompressed roundtrip 164 + const uncompressed = kp.public_key.toUncompressedSec1(); 165 + const k_pk2 = try K256Ecdsa.PublicKey.fromSec1(&uncompressed); 166 + try std.testing.expectEqualSlices(u8, &uncompressed, &k_pk2.toUncompressedSec1()); 167 + }