authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-05-03 17:30:21+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-05-28 16:28:27+02:00
logf6deade4e0033031d2161049dc62ab94c27209f4
treeab48e72918d7195db1dbab5526b0bd4880dceb21
parentaf1f91cadf0a2b04ea099334fe2008e526d4080b

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

1 files changed, 67 insertions(+), 35 deletions(-)

lib/std/crypto/codecs/asn1.zig+67-35
......@@ -71,16 +71,18 @@ pub const Tag = struct {
7171
7272 pub fn decode(reader: *std.Io.Reader) !Tag {
7373 const tag1: FirstTag = @bitCast(try reader.takeByte());
74 var number: u14 = tag1.number;
75
76 if (tag1.number == 31) {
77 const tag2: NextTag = @bitCast(try reader.takeByte());
78 number = tag2.number;
79 if (tag2.continues) {
80 const tag3: NextTag = @bitCast(try reader.takeByte());
81 number = (number << 7) + tag3.number;
82 if (tag3.continues) return error.EndOfStream;
83 }
74 var number: std.meta.Tag(Tag.Number) = tag1.number;
75
76 if (tag1.number == high_tag_marker) {
77 number = 0;
78 for (0..max_continuations) |i| {
79 const next: NextTag = @bitCast(try reader.takeByte());
80 if (i == 0 and next.number == 0) return error.InvalidEncoding;
81 number = std.math.shlExact(@TypeOf(number), number, 7) catch return error.InvalidEncoding;
82 number |= next.number;
83 if (!next.continues) break;
84 } else return error.InvalidEncoding;
85 if (number < high_tag_marker) return error.InvalidEncoding;
8486 }
8587
8688 return Tag{
......@@ -90,40 +92,51 @@ pub const Tag = struct {
9092 };
9193 }
9294
93 pub fn encode(self: Tag, writer: *std.Io.Writer) @TypeOf(writer).Error!void {
94 var tag1 = FirstTag{
95 pub fn encodeToSlice(self: Tag, buf: *[max_encoded_len]u8) []const u8 {
96 const n = @intFromEnum(self.number);
97 var tag1: FirstTag = .{
9598 .number = undefined,
9699 .constructed = self.constructed,
97100 .class = self.class,
98101 };
99102
100 var buffer: [3]u8 = undefined;
101 var writer2: std.Io.Writer = .init(&buffer);
103 if (n < high_tag_marker) {
104 tag1.number = @intCast(n);
105 buf[0] = @bitCast(tag1);
106 return buf[0..1];
107 }
102108
103 switch (@intFromEnum(self.number)) {
104 0...std.math.maxInt(u5) => |n| {
105 tag1.number = @intCast(n);
106 writer2.writeByte(@bitCast(tag1)) catch unreachable;
107 },
108 std.math.maxInt(u5) + 1...std.math.maxInt(u7) => |n| {
109 tag1.number = 15;
110 const tag2 = NextTag{ .number = @intCast(n), .continues = false };
111 writer2.writeByte(@bitCast(tag1)) catch unreachable;
112 writer2.writeByte(@bitCast(tag2)) catch unreachable;
113 },
114 else => |n| {
115 tag1.number = 15;
116 const tag2 = NextTag{ .number = @intCast(n >> 7), .continues = true };
117 const tag3 = NextTag{ .number = @truncate(n), .continues = false };
118 writer2.writeByte(@bitCast(tag1)) catch unreachable;
119 writer2.writeByte(@bitCast(tag2)) catch unreachable;
120 writer2.writeByte(@bitCast(tag3)) catch unreachable;
121 },
109 tag1.number = high_tag_marker;
110 buf[0] = @bitCast(tag1);
111
112 const bits_used = @bitSizeOf(@TypeOf(n)) - @clz(n);
113 const len = std.math.divCeil(usize, bits_used, 7) catch unreachable;
114
115 var remaining = n;
116 var i = len;
117 while (i > 0) : (i -= 1) {
118 buf[i] = @bitCast(NextTag{
119 .number = @truncate(remaining),
120 .continues = i != len,
121 });
122 remaining >>= 7;
122123 }
124 return buf[0 .. 1 + len];
125 }
123126
124 _ = try writer.write(writer2.buffered());
127 pub fn encode(self: Tag, writer: *std.Io.Writer) std.Io.Writer.Error!void {
128 var buf: [max_encoded_len]u8 = undefined;
129 try writer.writeAll(self.encodeToSlice(&buf));
125130 }
126131
132 pub const max_encoded_len = 1 + (std.math.divCeil(
133 comptime_int,
134 @bitSizeOf(std.meta.Tag(Tag.Number)),
135 7,
136 ) catch unreachable);
137 const max_continuations = max_encoded_len - 1;
138 const high_tag_marker = std.math.maxInt(u5);
139
127140 const FirstTag = packed struct(u8) { number: u5, constructed: bool, class: Tag.Class };
128141 const NextTag = packed struct(u8) { number: u7, continues: bool };
129142
......@@ -165,6 +178,24 @@ test Tag {
165178 try std.testing.expectEqual(Tag.init(@enumFromInt(3), true, .context_specific), t);
166179}
167180
181test "Tag.encode/decode round trip" {
182 for ([_]u16{ 0, 30, 31, 32, 127, 128, 16383, 16384, 65535 }) |n| {
183 const tag = Tag.init(@enumFromInt(n), false, .universal);
184 var buf: [Tag.max_encoded_len]u8 = undefined;
185 const encoded = tag.encodeToSlice(&buf);
186 var reader: std.Io.Reader = .fixed(encoded);
187 try std.testing.expectEqual(tag, try Tag.decode(&reader));
188 try std.testing.expectEqual(encoded.len, reader.seek);
189 }
190}
191
192test "Tag.decode rejects non-minimal high-tag form" {
193 for ([_][]const u8{ &.{ 0x1f, 0x1e }, &.{ 0x1f, 0x80, 0x01 } }) |bytes| {
194 var reader: std.Io.Reader = .fixed(bytes);
195 try std.testing.expectError(error.InvalidEncoding, Tag.decode(&reader));
196 }
197}
198
168199/// A decoded view.
169200pub const Element = struct {
170201 tag: Tag,
......@@ -183,13 +214,14 @@ pub const Element = struct {
183214 }
184215 };
185216
186 pub const DecodeError = error{EndOfStream};
217 pub const DecodeError = error{ EndOfStream, InvalidEncoding };
187218
188219 /// Safely decode a DER/BER/CER element at `index`:
189220 /// - Ensures length uses shortest form
190221 /// - Ensures length is within `bytes`
191222 /// - Ensures length is less than `std.math.maxInt(Index)`
192223 pub fn decode(bytes: []const u8, index: Index) DecodeError!Element {
224 if (index > bytes.len) return error.EndOfStream;
193225 var reader: std.Io.Reader = .fixed(bytes[index..]);
194226
195227 const tag = Tag.decode(&reader) catch |err| switch (err) {