From 6f4477ffdc658a19ad83e192e2c8ea6362032a59 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 4 Oct 2026 12:03:06 +0200 Subject: [PATCH] crypto.ecdsa: fix edge cases in DER encoding/decoding An all-zero r or s component would try to encode an empty slice, and panic. Not great. When decoding, we accept integers shorter than the encoding and pad them with zeros. Problem it that to check the sign, we were always looking at the first byte, which in that case was padding instead of the actual sign byte. This is now fixed. And also reject integers with unnecessary leading zeros. For example 02 02 00 01 was accepted, but the canonical representation is 02 01 01. We should be strict and only accept the canonical representation. --- lib/std/crypto/ecdsa.zig | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/std/crypto/ecdsa.zig b/lib/std/crypto/ecdsa.zig index a7aa961375efe3e56971e8287d9499e955c8477b..2a11869998e731d791937e7898e6e233f800671d 100644 --- a/lib/std/crypto/ecdsa.zig +++ b/lib/std/crypto/ecdsa.zig @@ -138,8 +138,10 @@ pub fn Ecdsa(comptime C: type, comptime H: 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 sig_r = mem.trimStart(u8, &sig.r, &.{0}); - const sig_s = mem.trimStart(u8, &sig.s, &.{0}); + const trimmed_r = mem.trimStart(u8, &sig.r, &.{0}); + const trimmed_s = mem.trimStart(u8, &sig.s, &.{0}); + const sig_r: []const u8 = if (trimmed_r.len == 0) &.{0} else trimmed_r; + const sig_s: []const u8 = if (trimmed_s.len == 0) &.{0} else trimmed_s; 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)); @@ -171,7 +173,8 @@ pub fn Ecdsa(comptime C: type, comptime H: type) type { } const out_slice = out[out.len - expected_len ..]; reader.readSliceAll(out_slice) catch return error.InvalidEncoding; - if (@intFromBool(has_top_bit) != out[0] >> 7) return error.InvalidEncoding; + if (@intFromBool(has_top_bit) != out_slice[0] >> 7) return error.InvalidEncoding; + if (expected_len > 1 and out_slice[0] == 0 and out_slice[1] >> 7 == 0) return error.InvalidEncoding; } /// Create a signature from a DER representation. @@ -534,6 +537,23 @@ test "Prehashed message operations" { try sig.verify(msg, kp.public_key); } +test "DER decoding edge cases" { + const Scheme = EcdsaP256Sha256; + const negative_r = [_]u8{ 0x30, 0x06, 0x02, 0x01, 0x80, 0x02, 0x01, 0x01 }; + const padded_r = [_]u8{ 0x30, 0x07, 0x02, 0x02, 0x00, 0x01, 0x02, 0x01, 0x01 }; + try testing.expectError(error.InvalidEncoding, Scheme.Signature.fromDer(&negative_r)); + try testing.expectError(error.InvalidEncoding, Scheme.Signature.fromDer(&padded_r)); + + const raw: [Scheme.Signature.encoded_length]u8 = @splat(0); + const sig = Scheme.Signature.fromBytes(raw); + var der: [Scheme.Signature.der_encoded_length_max]u8 = undefined; + try testing.expectEqualSlices( + u8, + &.{ 0x30, 0x06, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00 }, + sig.toDer(&der), + ); +} + const TestVector = struct { key: []const u8, msg: []const u8, -- 2.54.0