From 87e0dc7dea6153d7b34c3b8d626316ca7d5ce787 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 3 May 2026 17:29:03 +0200 Subject: [PATCH 1/9] std.crypto.codecs.asn1.Oid: align toDot with the new Writer API Take a *std.Io.Writer instead of an anytype writer, and read the test output via stream.buffered() now that .written() is gone. --- lib/std/crypto/codecs/asn1/Oid.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/std/crypto/codecs/asn1/Oid.zig b/lib/std/crypto/codecs/asn1/Oid.zig index 3a8c45cb3e5892e1c79d2867a785a2d4208b5a34..2cc6dfc090edf767848ae55899a5ac46c23c1638 100644 --- a/lib/std/crypto/codecs/asn1/Oid.zig +++ b/lib/std/crypto/codecs/asn1/Oid.zig @@ -47,7 +47,7 @@ test fromDot { } } -pub fn toDot(self: Oid, writer: anytype) @TypeOf(writer).Error!void { +pub fn toDot(self: Oid, writer: *std.Io.Writer) std.Io.Writer.Error!void { const encoded = self.encoded; const first = @divTrunc(encoded[0], 40); const second = encoded[0] - first * 40; @@ -81,7 +81,7 @@ test toDot { for (test_cases) |t| { var stream: std.Io.Writer = .fixed(&buf); try toDot(Oid{ .encoded = t.encoded }, &stream); - try std.testing.expectEqualStrings(t.dot_notation, stream.written()); + try std.testing.expectEqualStrings(t.dot_notation, stream.buffered()); } } -- 2.54.0 From af1f91cadf0a2b04ea099334fe2008e526d4080b Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 3 May 2026 17:29:44 +0200 Subject: [PATCH 2/9] std.crypto.codecs.asn1.der.Encoder: rewrite with prependBytes Drop the writer wrapper since ArrayListReverse no longer exposes one, and use prependBytes consistently. Also rewrite length encoding so the high-bit length-of-length byte is placed last as expected by X.690. And always derive the leading-zero pad from the sign bit of the first encoded byte. --- lib/std/crypto/codecs/asn1.zig | 4 +- lib/std/crypto/codecs/asn1/der/Encoder.zig | 137 +++++++++++++++------ 2 files changed, 102 insertions(+), 39 deletions(-) diff --git a/lib/std/crypto/codecs/asn1.zig b/lib/std/crypto/codecs/asn1.zig index faa2ffb621e15b0012884836d93a457b87ff808e..0d6b4a8fca95a2e02be1af593ef9856b49e4a4f4 100644 --- a/lib/std/crypto/codecs/asn1.zig +++ b/lib/std/crypto/codecs/asn1.zig @@ -327,8 +327,8 @@ pub const BitString = struct { } pub fn encodeDer(self: BitString, encoder: *der.Encoder) !void { - try encoder.writer().writeAll(self.bytes); - try encoder.writer().writeByte(self.right_padding); + try encoder.prependBytes(self.bytes); + try encoder.prependBytes(&.{self.right_padding}); try encoder.length(self.bytes.len + 1); try encoder.tag(asn1_tag); } diff --git a/lib/std/crypto/codecs/asn1/der/Encoder.zig b/lib/std/crypto/codecs/asn1/der/Encoder.zig index f5a9553faad90aa5bc2723d753a837c440222b99..9a8b58d64a7e69ae5ef2803f63c4dfdb13ea8bf1 100644 --- a/lib/std/crypto/codecs/asn1/der/Encoder.zig +++ b/lib/std/crypto/codecs/asn1/der/Encoder.zig @@ -24,6 +24,7 @@ pub fn any(self: *Encoder, val: anytype) !void { fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { const T = @TypeOf(val); if (std.meta.hasFn(T, "encodeDer")) return try val.encodeDer(self); + const outer_field_tag = self.field_tag; const start = self.buffer.data.len; const merged_tag = self.mergedTag(tag_); @@ -42,8 +43,9 @@ fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { const is_default = if (f_attrs.@"comptime") false else if (f_attrs.defaultValue(f_type)) |default_val| brk: { break :brk std.mem.eql(u8, std.mem.asBytes(&default_val), std.mem.asBytes(&field_val)); } else false; + const is_null_optional = if (@typeInfo(f.type) == .optional) field_val == null else false; - if (!is_default) { + if (!is_default and !is_null_optional) { const start2 = self.buffer.data.len; self.field_tag = field_tag; // will merge with self.field_tag. @@ -58,6 +60,7 @@ fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { } } } + self.field_tag = outer_field_tag; }, .bool => try self.buffer.prependSlice(&[_]u8{if (val) 0xff else 0}), .int => try self.int(T, val), @@ -68,7 +71,7 @@ fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { try self.int(e.tag_type, @intFromEnum(val)); } }, - .optional => if (val) |v| return try self.anyTag(tag_, v), + .optional => if (val) |v| return try self.anyTag(tag_, v) else return, .null => {}, else => @compileError("cannot encode type " ++ @typeName(T)), } @@ -80,7 +83,8 @@ fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { /// Encode a tag. pub fn tag(self: *Encoder, tag_: Tag) !void { const t = self.mergedTag(tag_); - try t.encode(self.writer()); + var buf: [Tag.max_encoded_len]u8 = undefined; + try self.buffer.prependSlice(t.encodeToSlice(&buf)); } fn mergedTag(self: *Encoder, tag_: Tag) Tag { @@ -96,19 +100,14 @@ fn mergedTag(self: *Encoder, tag_: Tag) Tag { /// Encode a length. pub fn length(self: *Encoder, len: usize) !void { - const writer_ = self.writer(); - if (len < 128) { - try writer_.writeInt(u8, @intCast(len), .big); - return; - } - inline for ([_]type{ u8, u16, u32 }) |T| { - if (len < std.math.maxInt(T)) { - try writer_.writeInt(T, @intCast(len), .big); - try writer_.writeInt(u8, @sizeOf(T) | 0x80, .big); - return; - } - } - return error.InvalidLength; + if (len < 128) return self.buffer.prependSlice(&.{@intCast(len)}); + const len32 = std.math.cast(u32, len) orelse return error.InvalidLength; + var buf: [@sizeOf(u32) + 1]u8 = undefined; + std.mem.writeInt(u32, buf[1..], len32, .big); + var first: usize = 1; + while (buf[first] == 0) first += 1; + buf[first - 1] = @intCast((buf.len - first) | 0x80); + return self.buffer.prependSlice(buf[first - 1 ..]); } /// Encode a tag and length-prefixed bytes. @@ -118,28 +117,23 @@ pub fn tagBytes(self: *Encoder, tag_: Tag, bytes: []const u8) !void { try self.tag(tag_); } -/// Warning: This writer writes backwards. `fn print` will NOT work as expected. -pub fn writer(self: *Encoder) ArrayListReverse.Writer { - return self.buffer.writer(); +/// Write raw bytes. The encoder builds its output back-to-front, so chained +/// calls should be made in reverse of the desired on-wire order. +pub fn prependBytes(self: *Encoder, bytes: []const u8) !void { + return self.buffer.prependSlice(bytes); } fn int(self: *Encoder, comptime T: type, value: T) !void { - const big = std.mem.nativeTo(T, value, .big); - const big_bytes = std.mem.asBytes(&big); + const info = @typeInfo(T).int; + const Unsigned = @Int(.unsigned, info.bits); + const pad: u8 = if (info.signedness == .signed and value < 0) 0xff else 0; + var buf: [@sizeOf(Unsigned) + 1]u8 = undefined; + buf[0] = pad; + std.mem.writeInt(Unsigned, buf[1..], @bitCast(value), .big); - const bits_needed = @bitSizeOf(T) - @clz(value); - const needs_padding: u1 = if (value == 0) - 1 - else if (bits_needed > 8) brk: { - const RightShift = @Int(.unsigned, @bitSizeOf(@TypeOf(bits_needed)) - 1); - const right_shift: RightShift = @intCast(bits_needed - 9); - break :brk if (value >> right_shift == 0x1ff) 1 else 0; - } else 0; - const bytes_needed = try std.math.divCeil(usize, bits_needed, 8) + needs_padding; - - const writer_ = self.writer(); - for (0..bytes_needed - needs_padding) |i| try writer_.writeByte(big_bytes[big_bytes.len - i - 1]); - if (needs_padding == 1) try writer_.writeByte(0); + var first: usize = 0; + while (first + 1 < buf.len and buf[first] == pad and (buf[first + 1] ^ pad) & 0x80 == 0) first += 1; + try self.buffer.prependSlice(buf[first..]); } test int { @@ -148,15 +142,84 @@ test int { defer encoder.deinit(); try encoder.int(u8, 0); - try std.testing.expectEqualSlices(u8, &[_]u8{0}, encoder.buffer.data); + try std.testing.expectEqualSlices(u8, &.{0}, encoder.buffer.data); encoder.buffer.clearAndFree(); try encoder.int(u16, 0x00ff); - try std.testing.expectEqualSlices(u8, &[_]u8{0xff}, encoder.buffer.data); + try std.testing.expectEqualSlices(u8, &.{ 0, 0xff }, encoder.buffer.data); encoder.buffer.clearAndFree(); try encoder.int(u32, 0xffff); - try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0xff, 0xff }, encoder.buffer.data); + try std.testing.expectEqualSlices(u8, &.{ 0, 0xff, 0xff }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u32, 0x01020304); + try std.testing.expectEqualSlices(u8, &.{ 0x01, 0x02, 0x03, 0x04 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u8, 127); + try std.testing.expectEqualSlices(u8, &.{0x7f}, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u16, 128); + try std.testing.expectEqualSlices(u8, &.{ 0, 0x80 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u16, 256); + try std.testing.expectEqualSlices(u8, &.{ 0x01, 0x00 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u8, 128); + try std.testing.expectEqualSlices(u8, &.{ 0, 0x80 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u8, 255); + try std.testing.expectEqualSlices(u8, &.{ 0, 0xff }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u16, 0x8000); + try std.testing.expectEqualSlices(u8, &.{ 0, 0x80, 0 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(i8, -1); + try std.testing.expectEqualSlices(u8, &.{0xff}, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(i8, -128); + try std.testing.expectEqualSlices(u8, &.{0x80}, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(i16, -129); + try std.testing.expectEqualSlices(u8, &.{ 0xff, 0x7f }, encoder.buffer.data); +} + +test length { + const allocator = std.testing.allocator; + var encoder = Encoder.init(allocator); + defer encoder.deinit(); + + try encoder.length(127); + try std.testing.expectEqualSlices(u8, &.{0x7f}, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.length(128); + try std.testing.expectEqualSlices(u8, &.{ 0x81, 0x80 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.length(255); + try std.testing.expectEqualSlices(u8, &.{ 0x81, 0xff }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.length(256); + try std.testing.expectEqualSlices(u8, &.{ 0x82, 0x01, 0x00 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.length(65535); + try std.testing.expectEqualSlices(u8, &.{ 0x82, 0xff, 0xff }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.length(65536); + try std.testing.expectEqualSlices(u8, &.{ 0x83, 0x01, 0x00, 0x00 }, encoder.buffer.data); } const std = @import("std"); -- 2.54.0 From f6deade4e0033031d2161049dc62ab94c27209f4 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 3 May 2026 17:30:21 +0200 Subject: [PATCH 3/9] std.crypto.codecs.asn1: fix high-tag-number encoding The encoder was writing 15 in the low five bits to mark a high tag number insetad of 31. Emit the correct marker and as many continuation bytes as the tag number requires, and expose encodeToSlice so a Writer is not needed any more. And return proper errors. Fixes #32069 and supersedes #32139 --- lib/std/crypto/codecs/asn1.zig | 100 ++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/lib/std/crypto/codecs/asn1.zig b/lib/std/crypto/codecs/asn1.zig index 0d6b4a8fca95a2e02be1af593ef9856b49e4a4f4..78d9c6ce00046e6975a6fcdf3a3d6faf38247ea1 100644 --- a/lib/std/crypto/codecs/asn1.zig +++ b/lib/std/crypto/codecs/asn1.zig @@ -71,16 +71,18 @@ pub const Tag = struct { pub fn decode(reader: *std.Io.Reader) !Tag { const tag1: FirstTag = @bitCast(try reader.takeByte()); - var number: u14 = tag1.number; + var number: std.meta.Tag(Tag.Number) = tag1.number; - if (tag1.number == 31) { - const tag2: NextTag = @bitCast(try reader.takeByte()); - number = tag2.number; - if (tag2.continues) { - const tag3: NextTag = @bitCast(try reader.takeByte()); - number = (number << 7) + tag3.number; - if (tag3.continues) return error.EndOfStream; - } + if (tag1.number == high_tag_marker) { + number = 0; + for (0..max_continuations) |i| { + const next: NextTag = @bitCast(try reader.takeByte()); + if (i == 0 and next.number == 0) return error.InvalidEncoding; + number = std.math.shlExact(@TypeOf(number), number, 7) catch return error.InvalidEncoding; + number |= next.number; + if (!next.continues) break; + } else return error.InvalidEncoding; + if (number < high_tag_marker) return error.InvalidEncoding; } return Tag{ @@ -90,40 +92,51 @@ pub const Tag = struct { }; } - pub fn encode(self: Tag, writer: *std.Io.Writer) @TypeOf(writer).Error!void { - var tag1 = FirstTag{ + pub fn encodeToSlice(self: Tag, buf: *[max_encoded_len]u8) []const u8 { + const n = @intFromEnum(self.number); + var tag1: FirstTag = .{ .number = undefined, .constructed = self.constructed, .class = self.class, }; - var buffer: [3]u8 = undefined; - var writer2: std.Io.Writer = .init(&buffer); + if (n < high_tag_marker) { + tag1.number = @intCast(n); + buf[0] = @bitCast(tag1); + return buf[0..1]; + } + + tag1.number = high_tag_marker; + buf[0] = @bitCast(tag1); + + const bits_used = @bitSizeOf(@TypeOf(n)) - @clz(n); + const len = std.math.divCeil(usize, bits_used, 7) catch unreachable; - switch (@intFromEnum(self.number)) { - 0...std.math.maxInt(u5) => |n| { - tag1.number = @intCast(n); - writer2.writeByte(@bitCast(tag1)) catch unreachable; - }, - std.math.maxInt(u5) + 1...std.math.maxInt(u7) => |n| { - tag1.number = 15; - const tag2 = NextTag{ .number = @intCast(n), .continues = false }; - writer2.writeByte(@bitCast(tag1)) catch unreachable; - writer2.writeByte(@bitCast(tag2)) catch unreachable; - }, - else => |n| { - tag1.number = 15; - const tag2 = NextTag{ .number = @intCast(n >> 7), .continues = true }; - const tag3 = NextTag{ .number = @truncate(n), .continues = false }; - writer2.writeByte(@bitCast(tag1)) catch unreachable; - writer2.writeByte(@bitCast(tag2)) catch unreachable; - writer2.writeByte(@bitCast(tag3)) catch unreachable; - }, + var remaining = n; + var i = len; + while (i > 0) : (i -= 1) { + buf[i] = @bitCast(NextTag{ + .number = @truncate(remaining), + .continues = i != len, + }); + remaining >>= 7; } + return buf[0 .. 1 + len]; + } - _ = try writer.write(writer2.buffered()); + pub fn encode(self: Tag, writer: *std.Io.Writer) std.Io.Writer.Error!void { + var buf: [max_encoded_len]u8 = undefined; + try writer.writeAll(self.encodeToSlice(&buf)); } + pub const max_encoded_len = 1 + (std.math.divCeil( + comptime_int, + @bitSizeOf(std.meta.Tag(Tag.Number)), + 7, + ) catch unreachable); + const max_continuations = max_encoded_len - 1; + const high_tag_marker = std.math.maxInt(u5); + const FirstTag = packed struct(u8) { number: u5, constructed: bool, class: Tag.Class }; const NextTag = packed struct(u8) { number: u7, continues: bool }; @@ -165,6 +178,24 @@ test Tag { try std.testing.expectEqual(Tag.init(@enumFromInt(3), true, .context_specific), t); } +test "Tag.encode/decode round trip" { + for ([_]u16{ 0, 30, 31, 32, 127, 128, 16383, 16384, 65535 }) |n| { + const tag = Tag.init(@enumFromInt(n), false, .universal); + var buf: [Tag.max_encoded_len]u8 = undefined; + const encoded = tag.encodeToSlice(&buf); + var reader: std.Io.Reader = .fixed(encoded); + try std.testing.expectEqual(tag, try Tag.decode(&reader)); + try std.testing.expectEqual(encoded.len, reader.seek); + } +} + +test "Tag.decode rejects non-minimal high-tag form" { + for ([_][]const u8{ &.{ 0x1f, 0x1e }, &.{ 0x1f, 0x80, 0x01 } }) |bytes| { + var reader: std.Io.Reader = .fixed(bytes); + try std.testing.expectError(error.InvalidEncoding, Tag.decode(&reader)); + } +} + /// A decoded view. pub const Element = struct { tag: Tag, @@ -183,13 +214,14 @@ pub const Element = struct { } }; - pub const DecodeError = error{EndOfStream}; + pub const DecodeError = error{ EndOfStream, InvalidEncoding }; /// Safely decode a DER/BER/CER element at `index`: /// - Ensures length uses shortest form /// - Ensures length is within `bytes` /// - Ensures length is less than `std.math.maxInt(Index)` pub fn decode(bytes: []const u8, index: Index) DecodeError!Element { + if (index > bytes.len) return error.EndOfStream; var reader: std.Io.Reader = .fixed(bytes[index..]); const tag = Tag.decode(&reader) catch |err| switch (err) { -- 2.54.0 From 5ab3a21e390384672bc58171c9985d2d1d11b46e Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 3 May 2026 17:30:39 +0200 Subject: [PATCH 4/9] std.crypto.codecs: include asn1 tests Make sure the asn1 code compiles and runs. --- lib/std/crypto/codecs.zig | 6 ++++++ lib/std/crypto/codecs/asn1.zig | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/std/crypto/codecs.zig b/lib/std/crypto/codecs.zig index 7cf92b1117fff6da9a8eda3300ef746b206e9c44..5893f9068f0d8b870f8f3627adb3dbfab405cec7 100644 --- a/lib/std/crypto/codecs.zig +++ b/lib/std/crypto/codecs.zig @@ -1,3 +1,9 @@ pub const asn1 = @import("codecs/asn1.zig"); pub const base64 = @import("codecs/base64_hex_ct.zig").base64; pub const hex = @import("codecs/base64_hex_ct.zig").hex; + +test { + _ = asn1; + _ = base64; + _ = hex; +} diff --git a/lib/std/crypto/codecs/asn1.zig b/lib/std/crypto/codecs/asn1.zig index 78d9c6ce00046e6975a6fcdf3a3d6faf38247ea1..4d9c0dcf279830f851bdcff6945f7ce79f323073 100644 --- a/lib/std/crypto/codecs/asn1.zig +++ b/lib/std/crypto/codecs/asn1.zig @@ -366,6 +366,15 @@ pub const BitString = struct { } }; +test BitString { + const bs = BitString{ .bytes = &.{ 0x6e, 0x5d, 0xc0 }, .right_padding = 6 }; + const allocator = std.testing.allocator; + const buf = try der.encode(allocator, bs); + defer allocator.free(buf); + try std.testing.expectEqualSlices(u8, &.{ 0x03, 0x04, 0x06, 0x6e, 0x5d, 0xc0 }, buf); + try std.testing.expectEqualDeep(bs, try der.decode(BitString, buf)); +} + pub fn Opaque(comptime tag: Tag) type { return struct { bytes: []const u8, -- 2.54.0 From 761f8d2f8a244b01f303a0a03bfda9b56c06e18f Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 3 May 2026 17:48:20 +0200 Subject: [PATCH 5/9] der.Decoder: fix int slice and sign-extension --- lib/std/crypto/codecs/asn1/der.zig | 17 ++++++++++++ lib/std/crypto/codecs/asn1/der/Decoder.zig | 32 +++++++++++++++------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/lib/std/crypto/codecs/asn1/der.zig b/lib/std/crypto/codecs/asn1/der.zig index 4395f9f3b61b8856209198a79733d2d48ebbb5f5..41e1f769fd19fc3364bf14cf0d6b12cc10b7de6f 100644 --- a/lib/std/crypto/codecs/asn1/der.zig +++ b/lib/std/crypto/codecs/asn1/der.zig @@ -49,6 +49,23 @@ test decode { try std.testing.expectEqualDeep(test_case.value, decoded); } +test "integer round trip across signed and unsigned boundaries" { + const allocator = std.testing.allocator; + inline for (.{ u8, u16, u32, i8, i16, i32 }) |T| { + const cases = comptime blk: { + const min = std.math.minInt(T); + const max = std.math.maxInt(T); + break :blk [_]T{ 0, 1, max, min, @divTrunc(max, 2), @divTrunc(min, 2) }; + }; + for (cases) |value| { + const buf = try encode(allocator, value); + defer allocator.free(buf); + const decoded = try decode(T, buf); + try std.testing.expectEqual(value, decoded); + } + } +} + test { _ = Decoder; _ = Encoder; diff --git a/lib/std/crypto/codecs/asn1/der/Decoder.zig b/lib/std/crypto/codecs/asn1/der/Decoder.zig index 25a4139d6e687c4fbfa92a43a0cf3c126db2806c..3403c73047e21c6ee58c05741d2e1bbac49da3f6 100644 --- a/lib/std/crypto/codecs/asn1/der/Decoder.zig +++ b/lib/std/crypto/codecs/asn1/der/Decoder.zig @@ -111,21 +111,23 @@ pub fn view(self: Decoder, elem: Element) []const u8 { } fn int(comptime T: type, value: []const u8) error{ NonCanonical, LargeValue }!T { - if (@typeInfo(T).int.bits % 8 != 0) @compileError("T must be byte aligned"); + const info = @typeInfo(T).int; + if (info.bits % 8 != 0) @compileError("T must be byte aligned"); - var bytes = value; - if (bytes.len >= 2) { - if (bytes[0] == 0) { - if (@clz(bytes[1]) > 0) return error.NonCanonical; - bytes.ptr += 1; - } - if (bytes[0] == 0xff and @clz(bytes[1]) == 0) return error.NonCanonical; + if (value.len == 0) return error.NonCanonical; + if (value.len >= 2) { + if (value[0] == 0x00 and value[1] & 0x80 == 0) return error.NonCanonical; + if (value[0] == 0xff and value[1] & 0x80 != 0) return error.NonCanonical; } + const had_sign_byte = value.len >= 2 and value[0] == 0x00; + const bytes = if (had_sign_byte) value[1..] else value; if (bytes.len > @sizeOf(T)) return error.LargeValue; - if (@sizeOf(T) == 1) return @bitCast(bytes[0]); - return std.mem.readVarInt(T, bytes, .big); + const sign_extend = info.signedness == .signed and !had_sign_byte and bytes[0] & 0x80 != 0; + var buf: [@sizeOf(T)]u8 = @splat(if (sign_extend) 0xff else 0); + @memcpy(buf[buf.len - bytes.len ..], bytes); + return std.mem.readInt(T, &buf, .big); } test int { @@ -136,6 +138,16 @@ test int { const big = [_]u8{ 0xef, 0xff }; try expectError(error.LargeValue, int(u8, &big)); try expectEqual(0xefff, int(u16, &big)); + + try expectEqual(@as(u16, 255), try int(u16, &.{ 0x00, 0xff })); + try expectEqual(@as(u16, 0x8000), try int(u16, &.{ 0x00, 0x80, 0x00 })); + + try expectEqual(@as(i8, -1), try int(i8, &.{0xff})); + try expectEqual(@as(i16, -1), try int(i16, &.{0xff})); + try expectEqual(@as(i16, -128), try int(i16, &.{0x80})); + try expectEqual(@as(i16, -129), try int(i16, &.{ 0xff, 0x7f })); + try expectEqual(@as(i16, 255), try int(i16, &.{ 0x00, 0xff })); + try expectEqual(@as(i32, 0x7fffffff), try int(i32, &.{ 0x7f, 0xff, 0xff, 0xff })); } test Decoder { -- 2.54.0 From 3086628e7b4d8413fee6b85c886c21c95544d18d Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 3 May 2026 17:48:43 +0200 Subject: [PATCH 6/9] codecs.asn1: add tests against X.690 examples --- lib/std/crypto/codecs/asn1.zig | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/std/crypto/codecs/asn1.zig b/lib/std/crypto/codecs/asn1.zig index 4d9c0dcf279830f851bdcff6945f7ce79f323073..1b1a3ea3e3dddbf851387b9d3acbc1ac4616f3cd 100644 --- a/lib/std/crypto/codecs/asn1.zig +++ b/lib/std/crypto/codecs/asn1.zig @@ -178,6 +178,24 @@ test Tag { try std.testing.expectEqual(Tag.init(@enumFromInt(3), true, .context_specific), t); } +test "Tag.encode produces the exact bytes from X.690" { + const cases = [_]struct { number: u16, expected: []const u8 }{ + .{ .number = 0, .expected = &.{0x00} }, + .{ .number = 30, .expected = &.{0x1e} }, + .{ .number = 31, .expected = &.{ 0x1f, 0x1f } }, + .{ .number = 127, .expected = &.{ 0x1f, 0x7f } }, + .{ .number = 128, .expected = &.{ 0x1f, 0x81, 0x00 } }, + .{ .number = 16383, .expected = &.{ 0x1f, 0xff, 0x7f } }, + .{ .number = 16384, .expected = &.{ 0x1f, 0x81, 0x80, 0x00 } }, + .{ .number = 65535, .expected = &.{ 0x1f, 0x83, 0xff, 0x7f } }, + }; + for (cases) |c| { + const tag = Tag.init(@enumFromInt(c.number), false, .universal); + var buf: [Tag.max_encoded_len]u8 = undefined; + try std.testing.expectEqualSlices(u8, c.expected, tag.encodeToSlice(&buf)); + } +} + test "Tag.encode/decode round trip" { for ([_]u16{ 0, 30, 31, 32, 127, 128, 16383, 16384, 65535 }) |n| { const tag = Tag.init(@enumFromInt(n), false, .universal); -- 2.54.0 From 415c574b921a48f6cdcfac40fd3f38f236644f94 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 3 May 2026 18:36:20 +0200 Subject: [PATCH 7/9] der.Decoder: respect the bounds of the target type on int values --- lib/std/crypto/codecs/asn1/der/Decoder.zig | 27 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/std/crypto/codecs/asn1/der/Decoder.zig b/lib/std/crypto/codecs/asn1/der/Decoder.zig index 3403c73047e21c6ee58c05741d2e1bbac49da3f6..3b5cf70a436c6925c81947d7a3247cea3d799127 100644 --- a/lib/std/crypto/codecs/asn1/der/Decoder.zig +++ b/lib/std/crypto/codecs/asn1/der/Decoder.zig @@ -122,10 +122,20 @@ fn int(comptime T: type, value: []const u8) error{ NonCanonical, LargeValue }!T const had_sign_byte = value.len >= 2 and value[0] == 0x00; const bytes = if (had_sign_byte) value[1..] else value; - if (bytes.len > @sizeOf(T)) return error.LargeValue; + const der_negative = !had_sign_byte and bytes[0] & 0x80 != 0; - const sign_extend = info.signedness == .signed and !had_sign_byte and bytes[0] & 0x80 != 0; - var buf: [@sizeOf(T)]u8 = @splat(if (sign_extend) 0xff else 0); + switch (info.signedness) { + .unsigned => { + if (der_negative) return error.LargeValue; + if (bytes.len > @sizeOf(T)) return error.LargeValue; + }, + .signed => { + const max_len: usize = if (had_sign_byte) @sizeOf(T) - 1 else @sizeOf(T); + if (bytes.len > max_len) return error.LargeValue; + }, + } + + var buf: [@sizeOf(T)]u8 = @splat(if (der_negative) 0xff else 0); @memcpy(buf[buf.len - bytes.len ..], bytes); return std.mem.readInt(T, &buf, .big); } @@ -137,7 +147,8 @@ test int { const big = [_]u8{ 0xef, 0xff }; try expectError(error.LargeValue, int(u8, &big)); - try expectEqual(0xefff, int(u16, &big)); + try expectError(error.LargeValue, int(u16, &big)); + try expectEqual(@as(i16, -4097), try int(i16, &big)); try expectEqual(@as(u16, 255), try int(u16, &.{ 0x00, 0xff })); try expectEqual(@as(u16, 0x8000), try int(u16, &.{ 0x00, 0x80, 0x00 })); @@ -148,6 +159,14 @@ test int { try expectEqual(@as(i16, -129), try int(i16, &.{ 0xff, 0x7f })); try expectEqual(@as(i16, 255), try int(i16, &.{ 0x00, 0xff })); try expectEqual(@as(i32, 0x7fffffff), try int(i32, &.{ 0x7f, 0xff, 0xff, 0xff })); + + try expectError(error.LargeValue, int(i8, &.{ 0x00, 0xff })); + try expectError(error.LargeValue, int(i16, &.{ 0x00, 0x80, 0x00 })); + try expectError(error.LargeValue, int(i32, &.{ 0x00, 0x80, 0x00, 0x00, 0x00 })); + + try expectError(error.LargeValue, int(u8, &.{0xff})); + try expectError(error.LargeValue, int(u16, &.{0x80})); + try expectError(error.LargeValue, int(u32, &.{ 0x80, 0x00, 0x00, 0x00 })); } test Decoder { -- 2.54.0 From 944b55fb687d11ab3a1655849f56a651272cbe09 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 3 May 2026 19:10:27 +0200 Subject: [PATCH 8/9] der.Encoder: skip null optionals and restore outer field tag Null optional struct fields were emitted as empty TLVs. They should be omitted instead. The outer field tag should also be restored after encoding struct fields. --- lib/std/crypto/codecs/asn1/der.zig | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/std/crypto/codecs/asn1/der.zig b/lib/std/crypto/codecs/asn1/der.zig index 41e1f769fd19fc3364bf14cf0d6b12cc10b7de6f..128bb188c66255a0d0dc8755e340be2806047822 100644 --- a/lib/std/crypto/codecs/asn1/der.zig +++ b/lib/std/crypto/codecs/asn1/der.zig @@ -66,6 +66,31 @@ test "integer round trip across signed and unsigned boundaries" { } } +test "encode skips null optional fields" { + const Value = struct { a: ?u8, b: u8 }; + const allocator = std.testing.allocator; + const actual = try encode(allocator, Value{ .a = null, .b = 5 }); + defer allocator.free(actual); + + try std.testing.expectEqualSlices(u8, &.{ 0x30, 0x03, 0x02, 0x01, 0x05 }, actual); +} + +test "encode preserves outer sequence tag after implicit field tags" { + const Value = struct { + a: u8, + b: u8, + + pub const asn1_tags = .{ + .a = asn1.FieldTag.initImplicit(0, .context_specific), + }; + }; + const allocator = std.testing.allocator; + const actual = try encode(allocator, Value{ .a = 1, .b = 2 }); + defer allocator.free(actual); + + try std.testing.expectEqualSlices(u8, &.{ 0x30, 0x06, 0x80, 0x01, 0x01, 0x02, 0x01, 0x02 }, actual); +} + test { _ = Decoder; _ = Encoder; -- 2.54.0 From ff3fcb0290a1ce3f938d8cdaae459157af7d3e37 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Thu, 28 May 2026 16:42:21 +0200 Subject: [PATCH 9/9] Fix leftover from the older StructField --- lib/std/crypto/codecs/asn1/der/Encoder.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/crypto/codecs/asn1/der/Encoder.zig b/lib/std/crypto/codecs/asn1/der/Encoder.zig index 9a8b58d64a7e69ae5ef2803f63c4dfdb13ea8bf1..7f1913df628d7caace906fe905b423a60d9a8300 100644 --- a/lib/std/crypto/codecs/asn1/der/Encoder.zig +++ b/lib/std/crypto/codecs/asn1/der/Encoder.zig @@ -43,7 +43,7 @@ fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { const is_default = if (f_attrs.@"comptime") false else if (f_attrs.defaultValue(f_type)) |default_val| brk: { break :brk std.mem.eql(u8, std.mem.asBytes(&default_val), std.mem.asBytes(&field_val)); } else false; - const is_null_optional = if (@typeInfo(f.type) == .optional) field_val == null else false; + const is_null_optional = if (@typeInfo(f_type) == .optional) field_val == null else false; if (!is_default and !is_null_optional) { const start2 = self.buffer.data.len; -- 2.54.0