From c41b9d75083dc80b56c45673dab167be0f2727e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Ani=C4=87?= Date: Mon, 8 Sep 2025 22:53:03 +0200 Subject: [PATCH] ECDSA signature der encoding should produce smallest number of octets (#25177) I noticed this by stress testing my tls server implementation. From time to time curl (and other tools: ab, vegeta) will report invalid signature. I trace the problem to the way how std lib is encoding raw signature into der format. Using raw signature I got in some cases different encoding using std and openssl. Std is not producing minimal der when signature `r` or `s` integers has leading zero(es). Here is an example to illustrate difference. Notice leading 00 in `s` integer which is removed in openssl encoding but not in std encoding. ```Zig const std = @import("std"); test "ecdsa signature to der" { // raw signature r and s bytes const raw = hexToBytes( \\ 49 63 0c 94 95 2e ff 4b 02 bf 35 c4 97 9e a7 24 \\ 20 dc 94 de aa 1b 17 ff e1 49 25 3e 34 ef e8 d0 \\ c4 43 aa 7b a9 f3 9c b9 f8 72 7d d7 0c 9a 13 1e \\ \\ 00 56 85 43 d3 d4 05 62 a1 1d d8 a1 45 44 b5 dd \\ 62 9f d1 e0 ab f1 cd 4a 85 d0 1f 5d 11 d9 f8 89 \\ 89 d4 59 0c b0 6e ea 3c 19 6a f7 0b 1a 4a ce f1 ); // encoded by openssl const expected = hexToBytes( \\ 30 63 02 30 \\ 49 63 0c 94 95 2e ff 4b 02 bf 35 c4 97 9e a7 24 \\ 20 dc 94 de aa 1b 17 ff e1 49 25 3e 34 ef e8 d0 \\ c4 43 aa 7b a9 f3 9c b9 f8 72 7d d7 0c 9a 13 1e \\ \\ 02 2f \\ 56 85 43 d3 d4 05 62 a1 1d d8 a1 45 44 b5 dd \\ 62 9f d1 e0 ab f1 cd 4a 85 d0 1f 5d 11 d9 f8 89 \\ 89 d4 59 0c b0 6e ea 3c 19 6a f7 0b 1a 4a ce f1 ); // encoded by std const actual = hexToBytes( \\ 30 64 02 30 \\ 49 63 0c 94 95 2e ff 4b 02 bf 35 c4 97 9e a7 24 \\ 20 dc 94 de aa 1b 17 ff e1 49 25 3e 34 ef e8 d0 \\ c4 43 aa 7b a9 f3 9c b9 f8 72 7d d7 0c 9a 13 1e \\ \\ 02 30 \\ 00 56 85 43 d3 d4 05 62 a1 1d d8 a1 45 44 b5 dd \\ 62 9f d1 e0 ab f1 cd 4a 85 d0 1f 5d 11 d9 f8 89 \\ 89 d4 59 0c b0 6e ea 3c 19 6a f7 0b 1a 4a ce f1 ); _ = actual; const Ecdsa = std.crypto.sign.ecdsa.EcdsaP384Sha384; const sig = Ecdsa.Signature.fromBytes(raw); var buf: [Ecdsa.Signature.der_encoded_length_max]u8 = undefined; const encoded = sig.toDer(&buf); try std.testing.expectEqualSlices(u8, &expected, encoded); } pub fn hexToBytes(comptime hex: []const u8) [removeNonHex(hex).len / 2]u8 { @setEvalBranchQuota(1000 * 100); const hex2 = comptime removeNonHex(hex); comptime var res: [hex2.len / 2]u8 = undefined; _ = comptime std.fmt.hexToBytes(&res, hex2) catch unreachable; return res; } fn removeNonHex(comptime hex: []const u8) []const u8 { @setEvalBranchQuota(1000 * 100); var res: [hex.len]u8 = undefined; var i: usize = 0; for (hex) |c| { if (std.ascii.isHex(c)) { res[i] = c; i += 1; } } return res[0..i]; } ``` Trimming leading zeroes from signature integers fixes encoding. --- lib/std/crypto/ecdsa.zig | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/std/crypto/ecdsa.zig b/lib/std/crypto/ecdsa.zig index 4935278da5..130c47d10d 100644 --- a/lib/std/crypto/ecdsa.zig +++ b/lib/std/crypto/ecdsa.zig @@ -135,20 +135,22 @@ pub fn Ecdsa(comptime Curve: type, comptime Hash: type) type { /// The function returns a slice, that can be shorter than der_encoded_length_max. pub fn toDer(sig: Signature, buf: *[der_encoded_length_max]u8) []u8 { var w: std.Io.Writer = .fixed(buf); - const r_len = @as(u8, @intCast(sig.r.len + (sig.r[0] >> 7))); - const s_len = @as(u8, @intCast(sig.s.len + (sig.s[0] >> 7))); + const sig_r = mem.trimLeft(u8, &sig.r, &.{0}); + const sig_s = mem.trimLeft(u8, &sig.s, &.{0}); + const r_len = @as(u8, @intCast(sig_r.len + (sig_r[0] >> 7))); + const s_len = @as(u8, @intCast(sig_s.len + (sig_s[0] >> 7))); const seq_len = @as(u8, @intCast(2 + r_len + 2 + s_len)); w.writeAll(&[_]u8{ 0x30, seq_len }) catch unreachable; w.writeAll(&[_]u8{ 0x02, r_len }) catch unreachable; - if (sig.r[0] >> 7 != 0) { + if (sig_r[0] >> 7 != 0) { w.writeByte(0x00) catch unreachable; } - w.writeAll(&sig.r) catch unreachable; + w.writeAll(sig_r) catch unreachable; w.writeAll(&[_]u8{ 0x02, s_len }) catch unreachable; - if (sig.s[0] >> 7 != 0) { + if (sig_s[0] >> 7 != 0) { w.writeByte(0x00) catch unreachable; } - w.writeAll(&sig.s) catch unreachable; + w.writeAll(sig_s) catch unreachable; return w.buffered(); }