authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-04 12:03:06+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-15 10:05:32+02:00
log6f4477ffdc658a19ad83e192e2c8ea6362032a59
treee31e8c6665f4e3a20c58128fc750b366c22c5bff
parent4b8b9cedb301a313cc3f1720ada31fd4f6ecb493

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.

1 files changed, 23 insertions(+), 3 deletions(-)

lib/std/crypto/ecdsa.zig+23-3
......@@ -138,8 +138,10 @@ pub fn Ecdsa(comptime C: type, comptime H: type) type {
138138 /// The function returns a slice, that can be shorter than der_encoded_length_max.
139139 pub fn toDer(sig: Signature, buf: *[der_encoded_length_max]u8) []u8 {
140140 var w: std.Io.Writer = .fixed(buf);
141 const sig_r = mem.trimStart(u8, &sig.r, &.{0});
142 const sig_s = mem.trimStart(u8, &sig.s, &.{0});
141 const trimmed_r = mem.trimStart(u8, &sig.r, &.{0});
142 const trimmed_s = mem.trimStart(u8, &sig.s, &.{0});
143 const sig_r: []const u8 = if (trimmed_r.len == 0) &.{0} else trimmed_r;
144 const sig_s: []const u8 = if (trimmed_s.len == 0) &.{0} else trimmed_s;
143145 const r_len = @as(u8, @intCast(sig_r.len + (sig_r[0] >> 7)));
144146 const s_len = @as(u8, @intCast(sig_s.len + (sig_s[0] >> 7)));
145147 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 {
171173 }
172174 const out_slice = out[out.len - expected_len ..];
173175 reader.readSliceAll(out_slice) catch return error.InvalidEncoding;
174 if (@intFromBool(has_top_bit) != out[0] >> 7) return error.InvalidEncoding;
176 if (@intFromBool(has_top_bit) != out_slice[0] >> 7) return error.InvalidEncoding;
177 if (expected_len > 1 and out_slice[0] == 0 and out_slice[1] >> 7 == 0) return error.InvalidEncoding;
175178 }
176179
177180 /// Create a signature from a DER representation.
......@@ -534,6 +537,23 @@ test "Prehashed message operations" {
534537 try sig.verify(msg, kp.public_key);
535538}
536539
540test "DER decoding edge cases" {
541 const Scheme = EcdsaP256Sha256;
542 const negative_r = [_]u8{ 0x30, 0x06, 0x02, 0x01, 0x80, 0x02, 0x01, 0x01 };
543 const padded_r = [_]u8{ 0x30, 0x07, 0x02, 0x02, 0x00, 0x01, 0x02, 0x01, 0x01 };
544 try testing.expectError(error.InvalidEncoding, Scheme.Signature.fromDer(&negative_r));
545 try testing.expectError(error.InvalidEncoding, Scheme.Signature.fromDer(&padded_r));
546
547 const raw: [Scheme.Signature.encoded_length]u8 = @splat(0);
548 const sig = Scheme.Signature.fromBytes(raw);
549 var der: [Scheme.Signature.der_encoded_length_max]u8 = undefined;
550 try testing.expectEqualSlices(
551 u8,
552 &.{ 0x30, 0x06, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00 },
553 sig.toDer(&der),
554 );
555}
556
537557const TestVector = struct {
538558 key: []const u8,
539559 msg: []const u8,