commit f6deade4e0033031d2161049dc62ab94c27209f4 (tree)
parent af1f91cadf0a2b04ea099334fe2008e526d4080b
Author: Frank Denis <github@pureftpd.org>
Date: Sun, 3 May 2026 17:30:21 +0200
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
Diffstat:
1 file changed, 67 insertions(+), 35 deletions(-)
diff --git 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;
-
- 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;
- }
+ var number: std.meta.Tag(Tag.Number) = tag1.number;
+
+ 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];
+ }
- 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;
- },
+ 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;
+
+ 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) {