diff --git a/.gitattributes b/.gitattributes index 24579dc16c259d532b8f671c06ea4e6c061f3837..25b2900b3d6b8b339c4280d72d49efe7284e5754 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,7 @@ langref.html.in text eol=lf lib/std/compress/testdata/** binary lib/std/compress/deflate/testdata/** binary lib/std/compress/flate/testdata/** binary +lib/std/crypto/codecs/asn1/der/testdata/** binary lib/include/** linguist-vendored lib/libc/** linguist-vendored diff --git a/lib/std/crypto.zig b/lib/std/crypto.zig index a444b41cc3af5a342843a6ddbd4be4ed64aa5824..8482502828d47a68856089ae860b702bec99d618 100644 --- a/lib/std/crypto.zig +++ b/lib/std/crypto.zig @@ -214,13 +214,15 @@ pub const ff = @import("crypto/ff.zig"); /// This is a thread-local, cryptographically secure pseudo random number generator. pub const random = @import("crypto/tlcsprng.zig").interface; +/// Encoding and decoding +pub const codecs = @import("crypto/codecs.zig"); + const std = @import("std.zig"); pub const errors = @import("crypto/errors.zig"); pub const tls = @import("crypto/tls.zig"); pub const Certificate = @import("crypto/Certificate.zig"); -pub const asn1 = @import("crypto/asn1.zig"); /// Side-channels mitigations. pub const SideChannelsMitigations = enum { @@ -335,7 +337,7 @@ test { _ = errors; _ = tls; _ = Certificate; - _ = asn1; + _ = codecs; } test "CSPRNG" { diff --git a/lib/std/crypto/asn1.zig b/lib/std/crypto/asn1.zig deleted file mode 100644 index 7921c701183730d7f2ec44477fb44aa45204cefc..0000000000000000000000000000000000000000 --- a/lib/std/crypto/asn1.zig +++ /dev/null @@ -1,359 +0,0 @@ -//! ASN.1 types for public consumption. -const std = @import("std"); -pub const der = @import("./asn1/der.zig"); -pub const Oid = @import("./asn1/Oid.zig"); - -pub const Index = u32; - -pub const Tag = struct { - number: Number, - /// Whether this ASN.1 type contains other ASN.1 types. - constructed: bool, - class: Class, - - /// These values apply to class == .universal. - pub const Number = enum(u16) { - // 0 is reserved by spec - boolean = 1, - integer = 2, - bitstring = 3, - octetstring = 4, - null = 5, - oid = 6, - object_descriptor = 7, - real = 9, - enumerated = 10, - embedded = 11, - string_utf8 = 12, - oid_relative = 13, - time = 14, - // 15 is reserved to mean that the tag is >= 32 - sequence = 16, - /// Elements may appear in any order. - sequence_of = 17, - string_numeric = 18, - string_printable = 19, - string_teletex = 20, - string_videotex = 21, - string_ia5 = 22, - utc_time = 23, - generalized_time = 24, - string_graphic = 25, - string_visible = 26, - string_general = 27, - string_universal = 28, - string_char = 29, - string_bmp = 30, - date = 31, - time_of_day = 32, - date_time = 33, - duration = 34, - /// IRI = Internationalized Resource Identifier - oid_iri = 35, - oid_iri_relative = 36, - _, - }; - - pub const Class = enum(u2) { - universal, - application, - context_specific, - private, - }; - - pub fn init(number: Tag.Number, constructed: bool, class: Tag.Class) Tag { - return .{ .number = number, .constructed = constructed, .class = class }; - } - - pub fn universal(number: Tag.Number, constructed: bool) Tag { - return .{ .number = number, .constructed = constructed, .class = .universal }; - } - - pub fn decode(reader: anytype) !Tag { - const tag1: FirstTag = @bitCast(try reader.readByte()); - var number: u14 = tag1.number; - - if (tag1.number == 15) { - const tag2: NextTag = @bitCast(try reader.readByte()); - number = tag2.number; - if (tag2.continues) { - const tag3: NextTag = @bitCast(try reader.readByte()); - number = (number << 7) + tag3.number; - if (tag3.continues) return error.InvalidLength; - } - } - - return Tag{ - .number = @enumFromInt(number), - .constructed = tag1.constructed, - .class = tag1.class, - }; - } - - pub fn encode(self: Tag, writer: anytype) @TypeOf(writer).Error!void { - var tag1 = FirstTag{ - .number = undefined, - .constructed = self.constructed, - .class = self.class, - }; - - var buffer: [3]u8 = undefined; - var stream = std.io.fixedBufferStream(&buffer); - var writer2 = stream.writer(); - - 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; - }, - } - - _ = try writer.write(stream.getWritten()); - } - - const FirstTag = packed struct(u8) { number: u5, constructed: bool, class: Tag.Class }; - const NextTag = packed struct(u8) { number: u7, continues: bool }; - - pub fn toExpected(self: Tag) ExpectedTag { - return ExpectedTag{ - .number = self.number, - .constructed = self.constructed, - .class = self.class, - }; - } - - pub fn fromZig(comptime T: type) Tag { - switch (@typeInfo(T)) { - .@"struct", .@"enum", .@"union" => { - if (@hasDecl(T, "asn1_tag")) return T.asn1_tag; - }, - else => {}, - } - - switch (@typeInfo(T)) { - .@"struct", .@"union" => return universal(.sequence, true), - .bool => return universal(.boolean, false), - .int => return universal(.integer, false), - .@"enum" => |e| { - if (@hasDecl(T, "oids")) return Oid.asn1_tag; - return universal(if (e.is_exhaustive) .enumerated else .integer, false); - }, - .optional => |o| return fromZig(o.child), - .null => return universal(.null, false), - else => @compileError("cannot map Zig type to asn1_tag " ++ @typeName(T)), - } - } -}; - -test Tag { - const buf = [_]u8{0xa3}; - var stream = std.io.fixedBufferStream(&buf); - const t = Tag.decode(stream.reader()); - try std.testing.expectEqual(Tag.init(@enumFromInt(3), true, .context_specific), t); -} - -/// A decoded view. -pub const Element = struct { - tag: Tag, - slice: Slice, - - pub const Slice = struct { - start: Index, - end: Index, - - pub fn len(self: Slice) Index { - return self.end - self.start; - } - - pub fn view(self: Slice, bytes: []const u8) []const u8 { - return bytes[self.start..self.end]; - } - }; - - pub const DecodeError = error{ InvalidLength, EndOfStream }; - - /// 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 { - var stream = std.io.fixedBufferStream(bytes[index..]); - var reader = stream.reader(); - - const tag = try Tag.decode(reader); - const size_or_len_size = try reader.readByte(); - - var start = index + 2; - var end = start + size_or_len_size; - // short form between 0-127 - if (size_or_len_size < 128) { - if (end > bytes.len) return error.InvalidLength; - } else { - // long form between 0 and std.math.maxInt(u1024) - const len_size: u7 = @truncate(size_or_len_size); - start += len_size; - if (len_size > @sizeOf(Index)) return error.InvalidLength; - - const len = try reader.readVarInt(Index, .big, len_size); - if (len < 128) return error.InvalidLength; // should have used short form - - end = std.math.add(Index, start, len) catch return error.InvalidLength; - if (end > bytes.len) return error.InvalidLength; - } - - return Element{ .tag = tag, .slice = Slice{ .start = start, .end = end } }; - } -}; - -test Element { - const short_form = [_]u8{ 0x30, 0x03, 0x02, 0x01, 0x09 }; - try std.testing.expectEqual(Element{ - .tag = Tag.universal(.sequence, true), - .slice = Element.Slice{ .start = 2, .end = short_form.len }, - }, Element.decode(&short_form, 0)); - - const long_form = [_]u8{ 0x30, 129, 129 } ++ [_]u8{0} ** 129; - try std.testing.expectEqual(Element{ - .tag = Tag.universal(.sequence, true), - .slice = Element.Slice{ .start = 3, .end = long_form.len }, - }, Element.decode(&long_form, 0)); -} - -/// For decoding. -pub const ExpectedTag = struct { - number: ?Tag.Number = null, - constructed: ?bool = null, - class: ?Tag.Class = null, - - pub fn init(number: ?Tag.Number, constructed: ?bool, class: ?Tag.Class) ExpectedTag { - return .{ .number = number, .constructed = constructed, .class = class }; - } - - pub fn primitive(number: ?Tag.Number) ExpectedTag { - return .{ .number = number, .constructed = false, .class = .universal }; - } - - pub fn match(self: ExpectedTag, tag: Tag) bool { - if (self.number) |e| { - if (tag.number != e) return false; - } - if (self.constructed) |e| { - if (tag.constructed != e) return false; - } - if (self.class) |e| { - if (tag.class != e) return false; - } - return true; - } -}; - -pub const FieldTag = struct { - number: std.meta.Tag(Tag.Number), - class: Tag.Class, - explicit: bool = true, - - pub fn initExplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { - return .{ .number = number, .class = class, .explicit = true }; - } - - pub fn initImplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { - return .{ .number = number, .class = class, .explicit = false }; - } - - pub fn fromContainer(comptime Container: type, comptime field_name: []const u8) ?FieldTag { - if (@hasDecl(Container, "asn1_tags") and @hasField(@TypeOf(Container.asn1_tags), field_name)) { - return @field(Container.asn1_tags, field_name); - } - - return null; - } - - pub fn toTag(self: FieldTag) Tag { - return Tag.init(@enumFromInt(self.number), self.explicit, self.class); - } -}; - -pub const BitString = struct { - /// Number of bits in rightmost byte that are unused. - right_padding: u3 = 0, - bytes: []const u8, - - pub fn bitLen(self: BitString) usize { - return self.bytes.len * 8 - self.right_padding; - } - - const asn1_tag = Tag.universal(.bitstring, false); - - pub fn decodeDer(decoder: *der.Decoder) !BitString { - const ele = try decoder.element(asn1_tag.toExpected()); - const bytes = decoder.view(ele); - - if (bytes.len < 1) return error.InvalidBitString; - const padding = bytes[0]; - if (padding >= 8) return error.InvalidBitString; - const right_padding: u3 = @intCast(padding); - - // DER requires that unused bits be zero. - if (@ctz(bytes[bytes.len - 1]) < right_padding) return error.InvalidBitString; - - return BitString{ .bytes = bytes[1..], .right_padding = right_padding }; - } - - pub fn encodeDer(self: BitString, encoder: *der.Encoder) !void { - try encoder.writer().writeAll(self.bytes); - try encoder.writer().writeByte(self.right_padding); - try encoder.length(self.bytes.len + 1); - try encoder.tag(asn1_tag); - } -}; - -pub fn Opaque(comptime tag: Tag) type { - return struct { - bytes: []const u8, - - pub fn decodeDer(decoder: *der.Decoder) !@This() { - const ele = try decoder.element(tag.toExpected()); - if (tag.constructed) decoder.index = ele.slice.end; - return .{ .bytes = decoder.view(ele) }; - } - - pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { - try encoder.tagBytes(tag, self.bytes); - } - }; -} - -/// Use sparingly. -pub const Any = struct { - tag: Tag, - bytes: []const u8, - - pub fn decodeDer(decoder: *der.Decoder) !@This() { - const ele = try decoder.element(ExpectedTag{}); - return .{ .tag = ele.tag, .bytes = decoder.view(ele) }; - } - - pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { - try encoder.tagBytes(self.tag, self.bytes); - } -}; - -test { - _ = der; - _ = Oid; - _ = @import("asn1/test.zig"); -} diff --git a/lib/std/crypto/asn1/Oid.zig b/lib/std/crypto/asn1/Oid.zig deleted file mode 100644 index edca55205058db83f952044743caabe28a0d0c8e..0000000000000000000000000000000000000000 --- a/lib/std/crypto/asn1/Oid.zig +++ /dev/null @@ -1,210 +0,0 @@ -//! Globally unique hierarchical identifier made of a sequence of integers. -//! -//! Commonly used to identify standards, algorithms, certificate extensions, -//! organizations, or policy documents. -encoded: []const u8, - -pub const InitError = std.fmt.ParseIntError || error{MissingPrefix} || std.io.FixedBufferStream(u8).WriteError; - -pub fn fromDot(dot_notation: []const u8, out: []u8) InitError!Oid { - var split = std.mem.splitScalar(u8, dot_notation, '.'); - const first_str = split.next() orelse return error.MissingPrefix; - const second_str = split.next() orelse return error.MissingPrefix; - - const first = try std.fmt.parseInt(u8, first_str, 10); - const second = try std.fmt.parseInt(u8, second_str, 10); - - var stream = std.io.fixedBufferStream(out); - var writer = stream.writer(); - - try writer.writeByte(first * 40 + second); - - var i: usize = 1; - while (split.next()) |s| { - var parsed = try std.fmt.parseUnsigned(Arc, s, 10); - const n_bytes = if (parsed == 0) 0 else std.math.log(Arc, encoding_base, parsed); - - for (0..n_bytes) |j| { - const place = std.math.pow(Arc, encoding_base, n_bytes - @as(Arc, @intCast(j))); - const digit: u8 = @intCast(@divFloor(parsed, place)); - - try writer.writeByte(digit | 0x80); - parsed -= digit * place; - - i += 1; - } - try writer.writeByte(@intCast(parsed)); - i += 1; - } - - return .{ .encoded = stream.getWritten() }; -} - -test fromDot { - var buf: [256]u8 = undefined; - for (test_cases) |t| { - const actual = try fromDot(t.dot_notation, &buf); - try std.testing.expectEqualSlices(u8, t.encoded, actual.encoded); - } -} - -pub fn toDot(self: Oid, writer: anytype) @TypeOf(writer).Error!void { - const encoded = self.encoded; - const first = @divTrunc(encoded[0], 40); - const second = encoded[0] - first * 40; - try writer.print("{d}.{d}", .{ first, second }); - - var i: usize = 1; - while (i != encoded.len) { - const n_bytes: usize = brk: { - var res: usize = 1; - var j: usize = i; - while (encoded[j] & 0x80 != 0) { - res += 1; - j += 1; - } - break :brk res; - }; - - var n: usize = 0; - for (0..n_bytes) |j| { - const place = std.math.pow(usize, encoding_base, n_bytes - j - 1); - n += place * (encoded[i] & 0b01111111); - i += 1; - } - try writer.print(".{d}", .{n}); - } -} - -test toDot { - var buf: [256]u8 = undefined; - - for (test_cases) |t| { - var stream = std.io.fixedBufferStream(&buf); - try toDot(Oid{ .encoded = t.encoded }, stream.writer()); - try std.testing.expectEqualStrings(t.dot_notation, stream.getWritten()); - } -} - -const TestCase = struct { - encoded: []const u8, - dot_notation: []const u8, - - pub fn init(comptime hex: []const u8, dot_notation: []const u8) TestCase { - return .{ .encoded = &hexToBytes(hex), .dot_notation = dot_notation }; - } -}; - -const test_cases = [_]TestCase{ - // https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier - TestCase.init("2b0601040182371514", "1.3.6.1.4.1.311.21.20"), - // https://luca.ntop.org/Teaching/Appunti/asn1.html - TestCase.init("2a864886f70d", "1.2.840.113549"), - // https://www.sysadmins.lv/blog-en/how-to-encode-object-identifier-to-an-asn1-der-encoded-string.aspx - TestCase.init("2a868d20", "1.2.100000"), - TestCase.init("2a864886f70d01010b", "1.2.840.113549.1.1.11"), - TestCase.init("2b6570", "1.3.101.112"), -}; - -pub const asn1_tag = asn1.Tag.init(.oid, false, .universal); - -pub fn decodeDer(decoder: *der.Decoder) !Oid { - const ele = try decoder.element(asn1_tag.toExpected()); - return Oid{ .encoded = decoder.view(ele) }; -} - -pub fn encodeDer(self: Oid, encoder: *der.Encoder) !void { - try encoder.tagBytes(asn1_tag, self.encoded); -} - -fn encodedLen(dot_notation: []const u8) usize { - var buf: [256]u8 = undefined; - const oid = fromDot(dot_notation, &buf) catch unreachable; - return oid.encoded.len; -} - -/// Returns encoded bytes of OID. -fn encodeComptime(comptime dot_notation: []const u8) [encodedLen(dot_notation)]u8 { - @setEvalBranchQuota(4000); - comptime var buf: [256]u8 = undefined; - const oid = comptime fromDot(dot_notation, &buf) catch unreachable; - return oid.encoded[0..oid.encoded.len].*; -} - -test encodeComptime { - try std.testing.expectEqual( - hexToBytes("2b0601040182371514"), - comptime encodeComptime("1.3.6.1.4.1.311.21.20"), - ); -} - -pub fn fromDotComptime(comptime dot_notation: []const u8) Oid { - const tmp = comptime encodeComptime(dot_notation); - return Oid{ .encoded = &tmp }; -} - -/// Maps of: -/// - Oid -> enum -/// - Enum -> oid -pub fn StaticMap(comptime Enum: type) type { - const enum_info = @typeInfo(Enum).@"enum"; - const EnumToOid = std.EnumArray(Enum, []const u8); - const ReturnType = struct { - oid_to_enum: std.StaticStringMap(Enum), - enum_to_oid: EnumToOid, - - pub fn oidToEnum(self: @This(), encoded: []const u8) ?Enum { - return self.oid_to_enum.get(encoded); - } - - pub fn enumToOid(self: @This(), value: Enum) Oid { - const bytes = self.enum_to_oid.get(value); - return .{ .encoded = bytes }; - } - }; - - return struct { - pub fn initComptime(comptime key_pairs: anytype) ReturnType { - const struct_info = @typeInfo(@TypeOf(key_pairs)).@"struct"; - const error_msg = "Each field of '" ++ @typeName(Enum) ++ "' must map to exactly one OID"; - if (!enum_info.is_exhaustive or enum_info.fields.len != struct_info.fields.len) { - @compileError(error_msg); - } - - comptime var enum_to_oid = EnumToOid.initUndefined(); - - const KeyPair = struct { []const u8, Enum }; - comptime var static_key_pairs: [enum_info.fields.len]KeyPair = undefined; - - comptime for (enum_info.fields, 0..) |f, i| { - if (!@hasField(@TypeOf(key_pairs), f.name)) { - @compileError("Field '" ++ f.name ++ "' missing Oid.StaticMap entry"); - } - const encoded = &encodeComptime(@field(key_pairs, f.name)); - const tag: Enum = @enumFromInt(f.value); - static_key_pairs[i] = .{ encoded, tag }; - enum_to_oid.set(tag, encoded); - }; - - const oid_to_enum = std.StaticStringMap(Enum).initComptime(static_key_pairs); - if (oid_to_enum.values().len != enum_info.fields.len) @compileError(error_msg); - - return ReturnType{ .oid_to_enum = oid_to_enum, .enum_to_oid = enum_to_oid }; - } - }; -} - -/// Strictly for testing. -fn hexToBytes(comptime hex: []const u8) [hex.len / 2]u8 { - var res: [hex.len / 2]u8 = undefined; - _ = std.fmt.hexToBytes(&res, hex) catch unreachable; - return res; -} - -const std = @import("std"); -const Oid = @This(); -const Arc = u32; -const encoding_base = 128; -const Allocator = std.mem.Allocator; -const der = @import("der.zig"); -const asn1 = @import("../asn1.zig"); diff --git a/lib/std/crypto/asn1/der.zig b/lib/std/crypto/asn1/der.zig deleted file mode 100644 index 4395f9f3b61b8856209198a79733d2d48ebbb5f5..0000000000000000000000000000000000000000 --- a/lib/std/crypto/asn1/der.zig +++ /dev/null @@ -1,55 +0,0 @@ -//! Distinguised Encoding Rules as defined in X.690 and X.691. -//! -//! Subset of Basic Encoding Rules (BER) which eliminates flexibility in -//! an effort to acheive normality. Used in PKI. -const std = @import("std"); -const asn1 = @import("../asn1.zig"); - -pub const Decoder = @import("der/Decoder.zig"); -pub const Encoder = @import("der/Encoder.zig"); - -pub fn decode(comptime T: type, encoded: []const u8) !T { - var decoder = Decoder{ .bytes = encoded }; - const res = try decoder.any(T); - std.debug.assert(decoder.index == encoded.len); - return res; -} - -/// Caller owns returned memory. -pub fn encode(allocator: std.mem.Allocator, value: anytype) ![]u8 { - var encoder = Encoder.init(allocator); - defer encoder.deinit(); - try encoder.any(value); - return try encoder.buffer.toOwnedSlice(); -} - -test encode { - // https://lapo.it/asn1js/#MAgGAyoDBAIBBA - const Value = struct { a: asn1.Oid, b: i32 }; - const test_case = .{ - .value = Value{ .a = asn1.Oid.fromDotComptime("1.2.3.4"), .b = 4 }, - .encoded = &[_]u8{ 0x30, 0x08, 0x06, 0x03, 0x2A, 0x03, 0x04, 0x02, 0x01, 0x04 }, - }; - const allocator = std.testing.allocator; - const actual = try encode(allocator, test_case.value); - defer allocator.free(actual); - - try std.testing.expectEqualSlices(u8, test_case.encoded, actual); -} - -test decode { - // https://lapo.it/asn1js/#MAgGAyoDBAIBBA - const Value = struct { a: asn1.Oid, b: i32 }; - const test_case = .{ - .value = Value{ .a = asn1.Oid.fromDotComptime("1.2.3.4"), .b = 4 }, - .encoded = &[_]u8{ 0x30, 0x08, 0x06, 0x03, 0x2A, 0x03, 0x04, 0x02, 0x01, 0x04 }, - }; - const decoded = try decode(Value, test_case.encoded); - - try std.testing.expectEqualDeep(test_case.value, decoded); -} - -test { - _ = Decoder; - _ = Encoder; -} diff --git a/lib/std/crypto/asn1/der/ArrayListReverse.zig b/lib/std/crypto/asn1/der/ArrayListReverse.zig deleted file mode 100644 index f580c54546dd69a36ffda14bc256332b0dcdee91..0000000000000000000000000000000000000000 --- a/lib/std/crypto/asn1/der/ArrayListReverse.zig +++ /dev/null @@ -1,97 +0,0 @@ -//! An ArrayList that grows backwards. Counts nested prefix length fields -//! in O(n) instead of O(n^depth) at the cost of extra buffering. -//! -//! Laid out in memory like: -//! capacity |--------------------------| -//! data |-------------| -data: []u8, -capacity: usize, -allocator: Allocator, - -const ArrayListReverse = @This(); -const Error = Allocator.Error; - -pub fn init(allocator: Allocator) ArrayListReverse { - return .{ .data = &.{}, .capacity = 0, .allocator = allocator }; -} - -pub fn deinit(self: *ArrayListReverse) void { - self.allocator.free(self.allocatedSlice()); -} - -pub fn ensureCapacity(self: *ArrayListReverse, new_capacity: usize) Error!void { - if (self.capacity >= new_capacity) return; - - const old_memory = self.allocatedSlice(); - // Just make a new allocation to not worry about aliasing. - const new_memory = try self.allocator.alloc(u8, new_capacity); - @memcpy(new_memory[new_capacity - self.data.len ..], self.data); - self.allocator.free(old_memory); - self.data.ptr = new_memory.ptr + new_capacity - self.data.len; - self.capacity = new_memory.len; -} - -pub fn prependSlice(self: *ArrayListReverse, data: []const u8) Error!void { - try self.ensureCapacity(self.data.len + data.len); - const old_len = self.data.len; - const new_len = old_len + data.len; - assert(new_len <= self.capacity); - self.data.len = new_len; - - const end = self.data.ptr; - const begin = end - data.len; - const slice = begin[0..data.len]; - @memcpy(slice, data); - self.data.ptr = begin; -} - -pub const Writer = std.io.Writer(*ArrayListReverse, Error, prependSliceSize); -/// Warning: This writer writes backwards. `fn print` will NOT work as expected. -pub fn writer(self: *ArrayListReverse) Writer { - return .{ .context = self }; -} - -fn prependSliceSize(self: *ArrayListReverse, data: []const u8) Error!usize { - try self.prependSlice(data); - return data.len; -} - -fn allocatedSlice(self: *ArrayListReverse) []u8 { - return (self.data.ptr + self.data.len - self.capacity)[0..self.capacity]; -} - -/// Invalidates all element pointers. -pub fn clearAndFree(self: *ArrayListReverse) void { - self.allocator.free(self.allocatedSlice()); - self.data.len = 0; - self.capacity = 0; -} - -/// The caller owns the returned memory. -/// Capacity is cleared, making deinit() safe but unnecessary to call. -pub fn toOwnedSlice(self: *ArrayListReverse) Error![]u8 { - const new_memory = try self.allocator.alloc(u8, self.data.len); - @memcpy(new_memory, self.data); - @memset(self.data, undefined); - self.clearAndFree(); - return new_memory; -} - -const std = @import("std"); -const Allocator = std.mem.Allocator; -const assert = std.debug.assert; -const testing = std.testing; - -test ArrayListReverse { - var b = ArrayListReverse.init(testing.allocator); - defer b.deinit(); - const data: []const u8 = &.{ 4, 5, 6 }; - try b.prependSlice(data); - try testing.expectEqual(data.len, b.data.len); - try testing.expectEqualSlices(u8, data, b.data); - - const data2: []const u8 = &.{ 1, 2, 3 }; - try b.prependSlice(data2); - try testing.expectEqual(data.len + data2.len, b.data.len); - try testing.expectEqualSlices(u8, data2 ++ data, b.data); -} diff --git a/lib/std/crypto/asn1/der/Decoder.zig b/lib/std/crypto/asn1/der/Decoder.zig deleted file mode 100644 index 333e52cdf38fb6d696f6004179d1b3947e56c792..0000000000000000000000000000000000000000 --- a/lib/std/crypto/asn1/der/Decoder.zig +++ /dev/null @@ -1,170 +0,0 @@ -//! A secure DER parser that: -//! - Prefers calling `fn decodeDer(self: @This(), decoder: *der.Decoder)` -//! - Does NOT allocate. If you wish to parse lists you can do so lazily -//! with an opaque type. -//! - Does NOT read memory outside `bytes`. -//! - Does NOT return elements with slices outside `bytes`. -//! - Errors on values that do NOT follow DER rules: -//! - Lengths that could be represented in a shorter form. -//! - Booleans that are not 0xff or 0x00. -bytes: []const u8, -index: Index = 0, -/// The field tag of the most recently visited field. -/// This is needed because we might visit an implicitly tagged container with a `fn decodeDer`. -field_tag: ?FieldTag = null, - -/// Expect a value. -pub fn any(self: *Decoder, comptime T: type) !T { - if (std.meta.hasFn(T, "decodeDer")) return try T.decodeDer(self); - - const tag = Tag.fromZig(T).toExpected(); - switch (@typeInfo(T)) { - .@"struct" => { - const ele = try self.element(tag); - defer self.index = ele.slice.end; // don't force parsing all fields - - var res: T = undefined; - - inline for (std.meta.fields(T)) |f| { - self.field_tag = FieldTag.fromContainer(T, f.name); - - if (self.field_tag) |ft| { - if (ft.explicit) { - const seq = try self.element(ft.toTag().toExpected()); - self.index = seq.slice.start; - self.field_tag = null; - } - } - - @field(res, f.name) = self.any(f.type) catch |err| brk: { - if (f.defaultValue()) |d| { - break :brk d; - } - return err; - }; - // DER encodes null values by skipping them. - if (@typeInfo(f.type) == .optional and @field(res, f.name) == null) { - if (f.defaultValue()) |d| @field(res, f.name) = d; - } - } - - return res; - }, - .bool => { - const ele = try self.element(tag); - const bytes = self.view(ele); - if (bytes.len != 1) return error.InvalidBool; - - return switch (bytes[0]) { - 0x00 => false, - 0xff => true, - else => error.InvalidBool, - }; - }, - .int => { - const ele = try self.element(tag); - const bytes = self.view(ele); - return try int(T, bytes); - }, - .@"enum" => |e| { - const ele = try self.element(tag); - const bytes = self.view(ele); - if (@hasDecl(T, "oids")) { - return T.oids.oidToEnum(bytes) orelse return error.UnknownOid; - } - return @enumFromInt(try int(e.tag_type, bytes)); - }, - .optional => |o| return self.any(o.child) catch return null, - else => @compileError("cannot decode type " ++ @typeName(T)), - } -} - -//// Expect a sequence. -pub fn sequence(self: *Decoder) !Element { - return try self.element(ExpectedTag.init(.sequence, true, .universal)); -} - -//// Expect an element. -pub fn element( - self: *Decoder, - expected: ExpectedTag, -) (error{ EndOfStream, UnexpectedElement } || Element.DecodeError)!Element { - if (self.index >= self.bytes.len) return error.EndOfStream; - - const res = try Element.decode(self.bytes, self.index); - var e = expected; - if (self.field_tag) |ft| { - e.number = @enumFromInt(ft.number); - e.class = ft.class; - } - if (!e.match(res.tag)) { - return error.UnexpectedElement; - } - - self.index = if (res.tag.constructed) res.slice.start else res.slice.end; - return res; -} - -/// View of element bytes. -pub fn view(self: Decoder, elem: Element) []const u8 { - return elem.slice.view(self.bytes); -} - -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"); - - 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 (bytes.len > @sizeOf(T)) return error.LargeValue; - if (@sizeOf(T) == 1) return @bitCast(bytes[0]); - - return std.mem.readVarInt(T, bytes, .big); -} - -test int { - try expectEqual(@as(u8, 1), try int(u8, &[_]u8{1})); - try expectError(error.NonCanonical, int(u8, &[_]u8{ 0, 1 })); - try expectError(error.NonCanonical, int(u8, &[_]u8{ 0xff, 0xff })); - - const big = [_]u8{ 0xef, 0xff }; - try expectError(error.LargeValue, int(u8, &big)); - try expectEqual(0xefff, int(u16, &big)); -} - -test Decoder { - var parser = Decoder{ .bytes = @embedFile("./testdata/id_ecc.pub.der") }; - const seq = try parser.sequence(); - - { - const seq2 = try parser.sequence(); - _ = try parser.element(ExpectedTag.init(.oid, false, .universal)); - _ = try parser.element(ExpectedTag.init(.oid, false, .universal)); - - try std.testing.expectEqual(parser.index, seq2.slice.end); - } - _ = try parser.element(ExpectedTag.init(.bitstring, false, .universal)); - - try std.testing.expectEqual(parser.index, seq.slice.end); - try std.testing.expectEqual(parser.index, parser.bytes.len); -} - -const std = @import("std"); -const builtin = @import("builtin"); -const asn1 = @import("../../asn1.zig"); -const Oid = @import("../Oid.zig"); - -const expectEqual = std.testing.expectEqual; -const expectError = std.testing.expectError; -const Decoder = @This(); -const Index = asn1.Index; -const Tag = asn1.Tag; -const FieldTag = asn1.FieldTag; -const ExpectedTag = asn1.ExpectedTag; -const Element = asn1.Element; diff --git a/lib/std/crypto/asn1/der/Encoder.zig b/lib/std/crypto/asn1/der/Encoder.zig deleted file mode 100644 index da861e3d7d5a06f9352856949e1c6f06804b2fa2..0000000000000000000000000000000000000000 --- a/lib/std/crypto/asn1/der/Encoder.zig +++ /dev/null @@ -1,166 +0,0 @@ -//! A buffered DER encoder. -//! -//! Prefers calling container's `fn encodeDer(self: @This(), encoder: *der.Encoder)`. -//! That function should encode values, lengths, then tags. -buffer: ArrayListReverse, -/// The field tag set by a parent container. -/// This is needed because we might visit an implicitly tagged container with a `fn encodeDer`. -field_tag: ?FieldTag = null, - -pub fn init(allocator: std.mem.Allocator) Encoder { - return Encoder{ .buffer = ArrayListReverse.init(allocator) }; -} - -pub fn deinit(self: *Encoder) void { - self.buffer.deinit(); -} - -/// Encode any value. -pub fn any(self: *Encoder, val: anytype) !void { - const T = @TypeOf(val); - try self.anyTag(Tag.fromZig(T), val); -} - -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 start = self.buffer.data.len; - const merged_tag = self.mergedTag(tag_); - - switch (@typeInfo(T)) { - .@"struct" => |info| { - inline for (0..info.fields.len) |i| { - const f = info.fields[info.fields.len - i - 1]; - const field_val = @field(val, f.name); - const field_tag = FieldTag.fromContainer(T, f.name); - - // > The encoding of a set value or sequence value shall not include an encoding for any - // > component value which is equal to its default value. - const is_default = if (f.is_comptime) false else if (f.default_value_ptr) |v| brk: { - const default_val: *const f.type = @alignCast(@ptrCast(v)); - break :brk std.mem.eql(u8, std.mem.asBytes(default_val), std.mem.asBytes(&field_val)); - } else false; - - if (!is_default) { - const start2 = self.buffer.data.len; - self.field_tag = field_tag; - // will merge with self.field_tag. - // may mutate self.field_tag. - try self.anyTag(Tag.fromZig(f.type), field_val); - if (field_tag) |ft| { - if (ft.explicit) { - try self.length(self.buffer.data.len - start2); - try self.tag(ft.toTag()); - self.field_tag = null; - } - } - } - } - }, - .bool => try self.buffer.prependSlice(&[_]u8{if (val) 0xff else 0}), - .int => try self.int(T, val), - .@"enum" => |e| { - if (@hasDecl(T, "oids")) { - return self.any(T.oids.enumToOid(val)); - } else { - try self.int(e.tag_type, @intFromEnum(val)); - } - }, - .optional => if (val) |v| return try self.anyTag(tag_, v), - .null => {}, - else => @compileError("cannot encode type " ++ @typeName(T)), - } - - try self.length(self.buffer.data.len - start); - try self.tag(merged_tag); -} - -/// Encode a tag. -pub fn tag(self: *Encoder, tag_: Tag) !void { - const t = self.mergedTag(tag_); - try t.encode(self.writer()); -} - -fn mergedTag(self: *Encoder, tag_: Tag) Tag { - var res = tag_; - if (self.field_tag) |ft| { - if (!ft.explicit) { - res.number = @enumFromInt(ft.number); - res.class = ft.class; - } - } - return res; -} - -/// 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; -} - -/// Encode a tag and length-prefixed bytes. -pub fn tagBytes(self: *Encoder, tag_: Tag, bytes: []const u8) !void { - try self.buffer.prependSlice(bytes); - try self.length(bytes.len); - 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(); -} - -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 bits_needed = @bitSizeOf(T) - @clz(value); - const needs_padding: u1 = if (value == 0) - 1 - else if (bits_needed > 8) brk: { - const RightShift = std.meta.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); -} - -test int { - const allocator = std.testing.allocator; - var encoder = Encoder.init(allocator); - defer encoder.deinit(); - - try encoder.int(u8, 0); - try std.testing.expectEqualSlices(u8, &[_]u8{0}, encoder.buffer.data); - - encoder.buffer.clearAndFree(); - try encoder.int(u16, 0x00ff); - try std.testing.expectEqualSlices(u8, &[_]u8{0xff}, encoder.buffer.data); - - encoder.buffer.clearAndFree(); - try encoder.int(u32, 0xffff); - try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0xff, 0xff }, encoder.buffer.data); -} - -const std = @import("std"); -const Oid = @import("../Oid.zig"); -const asn1 = @import("../../asn1.zig"); -const ArrayListReverse = @import("./ArrayListReverse.zig"); -const Tag = asn1.Tag; -const FieldTag = asn1.FieldTag; -const Encoder = @This(); diff --git a/lib/std/crypto/asn1/der/testdata/all_types.der b/lib/std/crypto/asn1/der/testdata/all_types.der deleted file mode 100644 index a4f784938b54d2f3aef4d4049a02c35342f106ea..0000000000000000000000000000000000000000 Binary files a/lib/std/crypto/asn1/der/testdata/all_types.der and /dev/null differ diff --git a/lib/std/crypto/asn1/der/testdata/id_ecc.pub.der b/lib/std/crypto/asn1/der/testdata/id_ecc.pub.der deleted file mode 100644 index 3964a8f0fcaa5710ea9b8f198b1dfb7ee9ced25e..0000000000000000000000000000000000000000 Binary files a/lib/std/crypto/asn1/der/testdata/id_ecc.pub.der and /dev/null differ diff --git a/lib/std/crypto/asn1/test.zig b/lib/std/crypto/asn1/test.zig deleted file mode 100644 index fe12cba81985254bcb08ad53216f201271e3293e..0000000000000000000000000000000000000000 --- a/lib/std/crypto/asn1/test.zig +++ /dev/null @@ -1,80 +0,0 @@ -const std = @import("std"); -const asn1 = @import("../asn1.zig"); - -const der = asn1.der; -const Tag = asn1.Tag; -const FieldTag = asn1.FieldTag; - -/// An example that uses all ASN1 types and available implementation features. -const AllTypes = struct { - a: u8 = 0, - b: asn1.BitString, - c: C, - d: asn1.Opaque(Tag.universal(.string_utf8, false)), - e: asn1.Opaque(Tag.universal(.octetstring, false)), - f: ?u16, - g: ?Nested, - h: asn1.Any, - - pub const asn1_tags = .{ - .a = FieldTag.initExplicit(0, .context_specific), - .b = FieldTag.initExplicit(1, .context_specific), - .c = FieldTag.initImplicit(2, .context_specific), - .g = FieldTag.initImplicit(3, .context_specific), - }; - - const C = enum { - a, - b, - - pub const oids = asn1.Oid.StaticMap(@This()).initComptime(.{ - .a = "1.2.3.4", - .b = "1.2.3.5", - }); - }; - - const Nested = struct { - inner: Asn1T, - sum: i16, - - const Asn1T = struct { a: u8, b: i16 }; - - pub fn decodeDer(decoder: *der.Decoder) !Nested { - const inner = try decoder.any(Asn1T); - return Nested{ .inner = inner, .sum = inner.a + inner.b }; - } - - pub fn encodeDer(self: Nested, encoder: *der.Encoder) !void { - try encoder.any(self.inner); - } - }; -}; - -test AllTypes { - const expected = AllTypes{ - .a = 2, - .b = asn1.BitString{ .bytes = &[_]u8{ 0x04, 0xa0 } }, - .c = .a, - .d = .{ .bytes = "asdf" }, - .e = .{ .bytes = "fdsa" }, - .f = (1 << 8) + 1, - .g = .{ .inner = .{ .a = 4, .b = 5 }, .sum = 9 }, - .h = .{ .tag = Tag.init(.string_ia5, false, .universal), .bytes = "asdf" }, - }; - // https://lapo.it/asn1js/#MC-gAwIBAqEFAwMABKCCAyoDBAwEYXNkZgQEZmRzYQICAQGjBgIBBAIBBRYEYXNkZg - const path = "./der/testdata/all_types.der"; - const encoded = @embedFile(path); - const actual = try asn1.der.decode(AllTypes, encoded); - try std.testing.expectEqualDeep(expected, actual); - - const allocator = std.testing.allocator; - const buf = try asn1.der.encode(allocator, expected); - defer allocator.free(buf); - try std.testing.expectEqualSlices(u8, encoded, buf); - - // Use this to update test file. - // const dir = try std.fs.cwd().openDir("lib/std/crypto/asn1", .{}); - // var file = try dir.createFile(path, .{}); - // defer file.close(); - // try file.writeAll(buf); -} diff --git a/lib/std/crypto/codecs.zig b/lib/std/crypto/codecs.zig new file mode 100644 index 0000000000000000000000000000000000000000..78a6da1284db9db39774d58f3708739b7f90c3d0 --- /dev/null +++ b/lib/std/crypto/codecs.zig @@ -0,0 +1,3 @@ +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; diff --git a/lib/std/crypto/codecs/asn1.zig b/lib/std/crypto/codecs/asn1.zig new file mode 100644 index 0000000000000000000000000000000000000000..7921c701183730d7f2ec44477fb44aa45204cefc --- /dev/null +++ b/lib/std/crypto/codecs/asn1.zig @@ -0,0 +1,359 @@ +//! ASN.1 types for public consumption. +const std = @import("std"); +pub const der = @import("./asn1/der.zig"); +pub const Oid = @import("./asn1/Oid.zig"); + +pub const Index = u32; + +pub const Tag = struct { + number: Number, + /// Whether this ASN.1 type contains other ASN.1 types. + constructed: bool, + class: Class, + + /// These values apply to class == .universal. + pub const Number = enum(u16) { + // 0 is reserved by spec + boolean = 1, + integer = 2, + bitstring = 3, + octetstring = 4, + null = 5, + oid = 6, + object_descriptor = 7, + real = 9, + enumerated = 10, + embedded = 11, + string_utf8 = 12, + oid_relative = 13, + time = 14, + // 15 is reserved to mean that the tag is >= 32 + sequence = 16, + /// Elements may appear in any order. + sequence_of = 17, + string_numeric = 18, + string_printable = 19, + string_teletex = 20, + string_videotex = 21, + string_ia5 = 22, + utc_time = 23, + generalized_time = 24, + string_graphic = 25, + string_visible = 26, + string_general = 27, + string_universal = 28, + string_char = 29, + string_bmp = 30, + date = 31, + time_of_day = 32, + date_time = 33, + duration = 34, + /// IRI = Internationalized Resource Identifier + oid_iri = 35, + oid_iri_relative = 36, + _, + }; + + pub const Class = enum(u2) { + universal, + application, + context_specific, + private, + }; + + pub fn init(number: Tag.Number, constructed: bool, class: Tag.Class) Tag { + return .{ .number = number, .constructed = constructed, .class = class }; + } + + pub fn universal(number: Tag.Number, constructed: bool) Tag { + return .{ .number = number, .constructed = constructed, .class = .universal }; + } + + pub fn decode(reader: anytype) !Tag { + const tag1: FirstTag = @bitCast(try reader.readByte()); + var number: u14 = tag1.number; + + if (tag1.number == 15) { + const tag2: NextTag = @bitCast(try reader.readByte()); + number = tag2.number; + if (tag2.continues) { + const tag3: NextTag = @bitCast(try reader.readByte()); + number = (number << 7) + tag3.number; + if (tag3.continues) return error.InvalidLength; + } + } + + return Tag{ + .number = @enumFromInt(number), + .constructed = tag1.constructed, + .class = tag1.class, + }; + } + + pub fn encode(self: Tag, writer: anytype) @TypeOf(writer).Error!void { + var tag1 = FirstTag{ + .number = undefined, + .constructed = self.constructed, + .class = self.class, + }; + + var buffer: [3]u8 = undefined; + var stream = std.io.fixedBufferStream(&buffer); + var writer2 = stream.writer(); + + 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; + }, + } + + _ = try writer.write(stream.getWritten()); + } + + const FirstTag = packed struct(u8) { number: u5, constructed: bool, class: Tag.Class }; + const NextTag = packed struct(u8) { number: u7, continues: bool }; + + pub fn toExpected(self: Tag) ExpectedTag { + return ExpectedTag{ + .number = self.number, + .constructed = self.constructed, + .class = self.class, + }; + } + + pub fn fromZig(comptime T: type) Tag { + switch (@typeInfo(T)) { + .@"struct", .@"enum", .@"union" => { + if (@hasDecl(T, "asn1_tag")) return T.asn1_tag; + }, + else => {}, + } + + switch (@typeInfo(T)) { + .@"struct", .@"union" => return universal(.sequence, true), + .bool => return universal(.boolean, false), + .int => return universal(.integer, false), + .@"enum" => |e| { + if (@hasDecl(T, "oids")) return Oid.asn1_tag; + return universal(if (e.is_exhaustive) .enumerated else .integer, false); + }, + .optional => |o| return fromZig(o.child), + .null => return universal(.null, false), + else => @compileError("cannot map Zig type to asn1_tag " ++ @typeName(T)), + } + } +}; + +test Tag { + const buf = [_]u8{0xa3}; + var stream = std.io.fixedBufferStream(&buf); + const t = Tag.decode(stream.reader()); + try std.testing.expectEqual(Tag.init(@enumFromInt(3), true, .context_specific), t); +} + +/// A decoded view. +pub const Element = struct { + tag: Tag, + slice: Slice, + + pub const Slice = struct { + start: Index, + end: Index, + + pub fn len(self: Slice) Index { + return self.end - self.start; + } + + pub fn view(self: Slice, bytes: []const u8) []const u8 { + return bytes[self.start..self.end]; + } + }; + + pub const DecodeError = error{ InvalidLength, EndOfStream }; + + /// 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 { + var stream = std.io.fixedBufferStream(bytes[index..]); + var reader = stream.reader(); + + const tag = try Tag.decode(reader); + const size_or_len_size = try reader.readByte(); + + var start = index + 2; + var end = start + size_or_len_size; + // short form between 0-127 + if (size_or_len_size < 128) { + if (end > bytes.len) return error.InvalidLength; + } else { + // long form between 0 and std.math.maxInt(u1024) + const len_size: u7 = @truncate(size_or_len_size); + start += len_size; + if (len_size > @sizeOf(Index)) return error.InvalidLength; + + const len = try reader.readVarInt(Index, .big, len_size); + if (len < 128) return error.InvalidLength; // should have used short form + + end = std.math.add(Index, start, len) catch return error.InvalidLength; + if (end > bytes.len) return error.InvalidLength; + } + + return Element{ .tag = tag, .slice = Slice{ .start = start, .end = end } }; + } +}; + +test Element { + const short_form = [_]u8{ 0x30, 0x03, 0x02, 0x01, 0x09 }; + try std.testing.expectEqual(Element{ + .tag = Tag.universal(.sequence, true), + .slice = Element.Slice{ .start = 2, .end = short_form.len }, + }, Element.decode(&short_form, 0)); + + const long_form = [_]u8{ 0x30, 129, 129 } ++ [_]u8{0} ** 129; + try std.testing.expectEqual(Element{ + .tag = Tag.universal(.sequence, true), + .slice = Element.Slice{ .start = 3, .end = long_form.len }, + }, Element.decode(&long_form, 0)); +} + +/// For decoding. +pub const ExpectedTag = struct { + number: ?Tag.Number = null, + constructed: ?bool = null, + class: ?Tag.Class = null, + + pub fn init(number: ?Tag.Number, constructed: ?bool, class: ?Tag.Class) ExpectedTag { + return .{ .number = number, .constructed = constructed, .class = class }; + } + + pub fn primitive(number: ?Tag.Number) ExpectedTag { + return .{ .number = number, .constructed = false, .class = .universal }; + } + + pub fn match(self: ExpectedTag, tag: Tag) bool { + if (self.number) |e| { + if (tag.number != e) return false; + } + if (self.constructed) |e| { + if (tag.constructed != e) return false; + } + if (self.class) |e| { + if (tag.class != e) return false; + } + return true; + } +}; + +pub const FieldTag = struct { + number: std.meta.Tag(Tag.Number), + class: Tag.Class, + explicit: bool = true, + + pub fn initExplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { + return .{ .number = number, .class = class, .explicit = true }; + } + + pub fn initImplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { + return .{ .number = number, .class = class, .explicit = false }; + } + + pub fn fromContainer(comptime Container: type, comptime field_name: []const u8) ?FieldTag { + if (@hasDecl(Container, "asn1_tags") and @hasField(@TypeOf(Container.asn1_tags), field_name)) { + return @field(Container.asn1_tags, field_name); + } + + return null; + } + + pub fn toTag(self: FieldTag) Tag { + return Tag.init(@enumFromInt(self.number), self.explicit, self.class); + } +}; + +pub const BitString = struct { + /// Number of bits in rightmost byte that are unused. + right_padding: u3 = 0, + bytes: []const u8, + + pub fn bitLen(self: BitString) usize { + return self.bytes.len * 8 - self.right_padding; + } + + const asn1_tag = Tag.universal(.bitstring, false); + + pub fn decodeDer(decoder: *der.Decoder) !BitString { + const ele = try decoder.element(asn1_tag.toExpected()); + const bytes = decoder.view(ele); + + if (bytes.len < 1) return error.InvalidBitString; + const padding = bytes[0]; + if (padding >= 8) return error.InvalidBitString; + const right_padding: u3 = @intCast(padding); + + // DER requires that unused bits be zero. + if (@ctz(bytes[bytes.len - 1]) < right_padding) return error.InvalidBitString; + + return BitString{ .bytes = bytes[1..], .right_padding = right_padding }; + } + + pub fn encodeDer(self: BitString, encoder: *der.Encoder) !void { + try encoder.writer().writeAll(self.bytes); + try encoder.writer().writeByte(self.right_padding); + try encoder.length(self.bytes.len + 1); + try encoder.tag(asn1_tag); + } +}; + +pub fn Opaque(comptime tag: Tag) type { + return struct { + bytes: []const u8, + + pub fn decodeDer(decoder: *der.Decoder) !@This() { + const ele = try decoder.element(tag.toExpected()); + if (tag.constructed) decoder.index = ele.slice.end; + return .{ .bytes = decoder.view(ele) }; + } + + pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { + try encoder.tagBytes(tag, self.bytes); + } + }; +} + +/// Use sparingly. +pub const Any = struct { + tag: Tag, + bytes: []const u8, + + pub fn decodeDer(decoder: *der.Decoder) !@This() { + const ele = try decoder.element(ExpectedTag{}); + return .{ .tag = ele.tag, .bytes = decoder.view(ele) }; + } + + pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { + try encoder.tagBytes(self.tag, self.bytes); + } +}; + +test { + _ = der; + _ = Oid; + _ = @import("asn1/test.zig"); +} diff --git a/lib/std/crypto/codecs/asn1/Oid.zig b/lib/std/crypto/codecs/asn1/Oid.zig new file mode 100644 index 0000000000000000000000000000000000000000..edca55205058db83f952044743caabe28a0d0c8e --- /dev/null +++ b/lib/std/crypto/codecs/asn1/Oid.zig @@ -0,0 +1,210 @@ +//! Globally unique hierarchical identifier made of a sequence of integers. +//! +//! Commonly used to identify standards, algorithms, certificate extensions, +//! organizations, or policy documents. +encoded: []const u8, + +pub const InitError = std.fmt.ParseIntError || error{MissingPrefix} || std.io.FixedBufferStream(u8).WriteError; + +pub fn fromDot(dot_notation: []const u8, out: []u8) InitError!Oid { + var split = std.mem.splitScalar(u8, dot_notation, '.'); + const first_str = split.next() orelse return error.MissingPrefix; + const second_str = split.next() orelse return error.MissingPrefix; + + const first = try std.fmt.parseInt(u8, first_str, 10); + const second = try std.fmt.parseInt(u8, second_str, 10); + + var stream = std.io.fixedBufferStream(out); + var writer = stream.writer(); + + try writer.writeByte(first * 40 + second); + + var i: usize = 1; + while (split.next()) |s| { + var parsed = try std.fmt.parseUnsigned(Arc, s, 10); + const n_bytes = if (parsed == 0) 0 else std.math.log(Arc, encoding_base, parsed); + + for (0..n_bytes) |j| { + const place = std.math.pow(Arc, encoding_base, n_bytes - @as(Arc, @intCast(j))); + const digit: u8 = @intCast(@divFloor(parsed, place)); + + try writer.writeByte(digit | 0x80); + parsed -= digit * place; + + i += 1; + } + try writer.writeByte(@intCast(parsed)); + i += 1; + } + + return .{ .encoded = stream.getWritten() }; +} + +test fromDot { + var buf: [256]u8 = undefined; + for (test_cases) |t| { + const actual = try fromDot(t.dot_notation, &buf); + try std.testing.expectEqualSlices(u8, t.encoded, actual.encoded); + } +} + +pub fn toDot(self: Oid, writer: anytype) @TypeOf(writer).Error!void { + const encoded = self.encoded; + const first = @divTrunc(encoded[0], 40); + const second = encoded[0] - first * 40; + try writer.print("{d}.{d}", .{ first, second }); + + var i: usize = 1; + while (i != encoded.len) { + const n_bytes: usize = brk: { + var res: usize = 1; + var j: usize = i; + while (encoded[j] & 0x80 != 0) { + res += 1; + j += 1; + } + break :brk res; + }; + + var n: usize = 0; + for (0..n_bytes) |j| { + const place = std.math.pow(usize, encoding_base, n_bytes - j - 1); + n += place * (encoded[i] & 0b01111111); + i += 1; + } + try writer.print(".{d}", .{n}); + } +} + +test toDot { + var buf: [256]u8 = undefined; + + for (test_cases) |t| { + var stream = std.io.fixedBufferStream(&buf); + try toDot(Oid{ .encoded = t.encoded }, stream.writer()); + try std.testing.expectEqualStrings(t.dot_notation, stream.getWritten()); + } +} + +const TestCase = struct { + encoded: []const u8, + dot_notation: []const u8, + + pub fn init(comptime hex: []const u8, dot_notation: []const u8) TestCase { + return .{ .encoded = &hexToBytes(hex), .dot_notation = dot_notation }; + } +}; + +const test_cases = [_]TestCase{ + // https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier + TestCase.init("2b0601040182371514", "1.3.6.1.4.1.311.21.20"), + // https://luca.ntop.org/Teaching/Appunti/asn1.html + TestCase.init("2a864886f70d", "1.2.840.113549"), + // https://www.sysadmins.lv/blog-en/how-to-encode-object-identifier-to-an-asn1-der-encoded-string.aspx + TestCase.init("2a868d20", "1.2.100000"), + TestCase.init("2a864886f70d01010b", "1.2.840.113549.1.1.11"), + TestCase.init("2b6570", "1.3.101.112"), +}; + +pub const asn1_tag = asn1.Tag.init(.oid, false, .universal); + +pub fn decodeDer(decoder: *der.Decoder) !Oid { + const ele = try decoder.element(asn1_tag.toExpected()); + return Oid{ .encoded = decoder.view(ele) }; +} + +pub fn encodeDer(self: Oid, encoder: *der.Encoder) !void { + try encoder.tagBytes(asn1_tag, self.encoded); +} + +fn encodedLen(dot_notation: []const u8) usize { + var buf: [256]u8 = undefined; + const oid = fromDot(dot_notation, &buf) catch unreachable; + return oid.encoded.len; +} + +/// Returns encoded bytes of OID. +fn encodeComptime(comptime dot_notation: []const u8) [encodedLen(dot_notation)]u8 { + @setEvalBranchQuota(4000); + comptime var buf: [256]u8 = undefined; + const oid = comptime fromDot(dot_notation, &buf) catch unreachable; + return oid.encoded[0..oid.encoded.len].*; +} + +test encodeComptime { + try std.testing.expectEqual( + hexToBytes("2b0601040182371514"), + comptime encodeComptime("1.3.6.1.4.1.311.21.20"), + ); +} + +pub fn fromDotComptime(comptime dot_notation: []const u8) Oid { + const tmp = comptime encodeComptime(dot_notation); + return Oid{ .encoded = &tmp }; +} + +/// Maps of: +/// - Oid -> enum +/// - Enum -> oid +pub fn StaticMap(comptime Enum: type) type { + const enum_info = @typeInfo(Enum).@"enum"; + const EnumToOid = std.EnumArray(Enum, []const u8); + const ReturnType = struct { + oid_to_enum: std.StaticStringMap(Enum), + enum_to_oid: EnumToOid, + + pub fn oidToEnum(self: @This(), encoded: []const u8) ?Enum { + return self.oid_to_enum.get(encoded); + } + + pub fn enumToOid(self: @This(), value: Enum) Oid { + const bytes = self.enum_to_oid.get(value); + return .{ .encoded = bytes }; + } + }; + + return struct { + pub fn initComptime(comptime key_pairs: anytype) ReturnType { + const struct_info = @typeInfo(@TypeOf(key_pairs)).@"struct"; + const error_msg = "Each field of '" ++ @typeName(Enum) ++ "' must map to exactly one OID"; + if (!enum_info.is_exhaustive or enum_info.fields.len != struct_info.fields.len) { + @compileError(error_msg); + } + + comptime var enum_to_oid = EnumToOid.initUndefined(); + + const KeyPair = struct { []const u8, Enum }; + comptime var static_key_pairs: [enum_info.fields.len]KeyPair = undefined; + + comptime for (enum_info.fields, 0..) |f, i| { + if (!@hasField(@TypeOf(key_pairs), f.name)) { + @compileError("Field '" ++ f.name ++ "' missing Oid.StaticMap entry"); + } + const encoded = &encodeComptime(@field(key_pairs, f.name)); + const tag: Enum = @enumFromInt(f.value); + static_key_pairs[i] = .{ encoded, tag }; + enum_to_oid.set(tag, encoded); + }; + + const oid_to_enum = std.StaticStringMap(Enum).initComptime(static_key_pairs); + if (oid_to_enum.values().len != enum_info.fields.len) @compileError(error_msg); + + return ReturnType{ .oid_to_enum = oid_to_enum, .enum_to_oid = enum_to_oid }; + } + }; +} + +/// Strictly for testing. +fn hexToBytes(comptime hex: []const u8) [hex.len / 2]u8 { + var res: [hex.len / 2]u8 = undefined; + _ = std.fmt.hexToBytes(&res, hex) catch unreachable; + return res; +} + +const std = @import("std"); +const Oid = @This(); +const Arc = u32; +const encoding_base = 128; +const Allocator = std.mem.Allocator; +const der = @import("der.zig"); +const asn1 = @import("../asn1.zig"); diff --git a/lib/std/crypto/codecs/asn1/der.zig b/lib/std/crypto/codecs/asn1/der.zig new file mode 100644 index 0000000000000000000000000000000000000000..4395f9f3b61b8856209198a79733d2d48ebbb5f5 --- /dev/null +++ b/lib/std/crypto/codecs/asn1/der.zig @@ -0,0 +1,55 @@ +//! Distinguised Encoding Rules as defined in X.690 and X.691. +//! +//! Subset of Basic Encoding Rules (BER) which eliminates flexibility in +//! an effort to acheive normality. Used in PKI. +const std = @import("std"); +const asn1 = @import("../asn1.zig"); + +pub const Decoder = @import("der/Decoder.zig"); +pub const Encoder = @import("der/Encoder.zig"); + +pub fn decode(comptime T: type, encoded: []const u8) !T { + var decoder = Decoder{ .bytes = encoded }; + const res = try decoder.any(T); + std.debug.assert(decoder.index == encoded.len); + return res; +} + +/// Caller owns returned memory. +pub fn encode(allocator: std.mem.Allocator, value: anytype) ![]u8 { + var encoder = Encoder.init(allocator); + defer encoder.deinit(); + try encoder.any(value); + return try encoder.buffer.toOwnedSlice(); +} + +test encode { + // https://lapo.it/asn1js/#MAgGAyoDBAIBBA + const Value = struct { a: asn1.Oid, b: i32 }; + const test_case = .{ + .value = Value{ .a = asn1.Oid.fromDotComptime("1.2.3.4"), .b = 4 }, + .encoded = &[_]u8{ 0x30, 0x08, 0x06, 0x03, 0x2A, 0x03, 0x04, 0x02, 0x01, 0x04 }, + }; + const allocator = std.testing.allocator; + const actual = try encode(allocator, test_case.value); + defer allocator.free(actual); + + try std.testing.expectEqualSlices(u8, test_case.encoded, actual); +} + +test decode { + // https://lapo.it/asn1js/#MAgGAyoDBAIBBA + const Value = struct { a: asn1.Oid, b: i32 }; + const test_case = .{ + .value = Value{ .a = asn1.Oid.fromDotComptime("1.2.3.4"), .b = 4 }, + .encoded = &[_]u8{ 0x30, 0x08, 0x06, 0x03, 0x2A, 0x03, 0x04, 0x02, 0x01, 0x04 }, + }; + const decoded = try decode(Value, test_case.encoded); + + try std.testing.expectEqualDeep(test_case.value, decoded); +} + +test { + _ = Decoder; + _ = Encoder; +} diff --git a/lib/std/crypto/codecs/asn1/der/ArrayListReverse.zig b/lib/std/crypto/codecs/asn1/der/ArrayListReverse.zig new file mode 100644 index 0000000000000000000000000000000000000000..f580c54546dd69a36ffda14bc256332b0dcdee91 --- /dev/null +++ b/lib/std/crypto/codecs/asn1/der/ArrayListReverse.zig @@ -0,0 +1,97 @@ +//! An ArrayList that grows backwards. Counts nested prefix length fields +//! in O(n) instead of O(n^depth) at the cost of extra buffering. +//! +//! Laid out in memory like: +//! capacity |--------------------------| +//! data |-------------| +data: []u8, +capacity: usize, +allocator: Allocator, + +const ArrayListReverse = @This(); +const Error = Allocator.Error; + +pub fn init(allocator: Allocator) ArrayListReverse { + return .{ .data = &.{}, .capacity = 0, .allocator = allocator }; +} + +pub fn deinit(self: *ArrayListReverse) void { + self.allocator.free(self.allocatedSlice()); +} + +pub fn ensureCapacity(self: *ArrayListReverse, new_capacity: usize) Error!void { + if (self.capacity >= new_capacity) return; + + const old_memory = self.allocatedSlice(); + // Just make a new allocation to not worry about aliasing. + const new_memory = try self.allocator.alloc(u8, new_capacity); + @memcpy(new_memory[new_capacity - self.data.len ..], self.data); + self.allocator.free(old_memory); + self.data.ptr = new_memory.ptr + new_capacity - self.data.len; + self.capacity = new_memory.len; +} + +pub fn prependSlice(self: *ArrayListReverse, data: []const u8) Error!void { + try self.ensureCapacity(self.data.len + data.len); + const old_len = self.data.len; + const new_len = old_len + data.len; + assert(new_len <= self.capacity); + self.data.len = new_len; + + const end = self.data.ptr; + const begin = end - data.len; + const slice = begin[0..data.len]; + @memcpy(slice, data); + self.data.ptr = begin; +} + +pub const Writer = std.io.Writer(*ArrayListReverse, Error, prependSliceSize); +/// Warning: This writer writes backwards. `fn print` will NOT work as expected. +pub fn writer(self: *ArrayListReverse) Writer { + return .{ .context = self }; +} + +fn prependSliceSize(self: *ArrayListReverse, data: []const u8) Error!usize { + try self.prependSlice(data); + return data.len; +} + +fn allocatedSlice(self: *ArrayListReverse) []u8 { + return (self.data.ptr + self.data.len - self.capacity)[0..self.capacity]; +} + +/// Invalidates all element pointers. +pub fn clearAndFree(self: *ArrayListReverse) void { + self.allocator.free(self.allocatedSlice()); + self.data.len = 0; + self.capacity = 0; +} + +/// The caller owns the returned memory. +/// Capacity is cleared, making deinit() safe but unnecessary to call. +pub fn toOwnedSlice(self: *ArrayListReverse) Error![]u8 { + const new_memory = try self.allocator.alloc(u8, self.data.len); + @memcpy(new_memory, self.data); + @memset(self.data, undefined); + self.clearAndFree(); + return new_memory; +} + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const testing = std.testing; + +test ArrayListReverse { + var b = ArrayListReverse.init(testing.allocator); + defer b.deinit(); + const data: []const u8 = &.{ 4, 5, 6 }; + try b.prependSlice(data); + try testing.expectEqual(data.len, b.data.len); + try testing.expectEqualSlices(u8, data, b.data); + + const data2: []const u8 = &.{ 1, 2, 3 }; + try b.prependSlice(data2); + try testing.expectEqual(data.len + data2.len, b.data.len); + try testing.expectEqualSlices(u8, data2 ++ data, b.data); +} diff --git a/lib/std/crypto/codecs/asn1/der/Decoder.zig b/lib/std/crypto/codecs/asn1/der/Decoder.zig new file mode 100644 index 0000000000000000000000000000000000000000..333e52cdf38fb6d696f6004179d1b3947e56c792 --- /dev/null +++ b/lib/std/crypto/codecs/asn1/der/Decoder.zig @@ -0,0 +1,170 @@ +//! A secure DER parser that: +//! - Prefers calling `fn decodeDer(self: @This(), decoder: *der.Decoder)` +//! - Does NOT allocate. If you wish to parse lists you can do so lazily +//! with an opaque type. +//! - Does NOT read memory outside `bytes`. +//! - Does NOT return elements with slices outside `bytes`. +//! - Errors on values that do NOT follow DER rules: +//! - Lengths that could be represented in a shorter form. +//! - Booleans that are not 0xff or 0x00. +bytes: []const u8, +index: Index = 0, +/// The field tag of the most recently visited field. +/// This is needed because we might visit an implicitly tagged container with a `fn decodeDer`. +field_tag: ?FieldTag = null, + +/// Expect a value. +pub fn any(self: *Decoder, comptime T: type) !T { + if (std.meta.hasFn(T, "decodeDer")) return try T.decodeDer(self); + + const tag = Tag.fromZig(T).toExpected(); + switch (@typeInfo(T)) { + .@"struct" => { + const ele = try self.element(tag); + defer self.index = ele.slice.end; // don't force parsing all fields + + var res: T = undefined; + + inline for (std.meta.fields(T)) |f| { + self.field_tag = FieldTag.fromContainer(T, f.name); + + if (self.field_tag) |ft| { + if (ft.explicit) { + const seq = try self.element(ft.toTag().toExpected()); + self.index = seq.slice.start; + self.field_tag = null; + } + } + + @field(res, f.name) = self.any(f.type) catch |err| brk: { + if (f.defaultValue()) |d| { + break :brk d; + } + return err; + }; + // DER encodes null values by skipping them. + if (@typeInfo(f.type) == .optional and @field(res, f.name) == null) { + if (f.defaultValue()) |d| @field(res, f.name) = d; + } + } + + return res; + }, + .bool => { + const ele = try self.element(tag); + const bytes = self.view(ele); + if (bytes.len != 1) return error.InvalidBool; + + return switch (bytes[0]) { + 0x00 => false, + 0xff => true, + else => error.InvalidBool, + }; + }, + .int => { + const ele = try self.element(tag); + const bytes = self.view(ele); + return try int(T, bytes); + }, + .@"enum" => |e| { + const ele = try self.element(tag); + const bytes = self.view(ele); + if (@hasDecl(T, "oids")) { + return T.oids.oidToEnum(bytes) orelse return error.UnknownOid; + } + return @enumFromInt(try int(e.tag_type, bytes)); + }, + .optional => |o| return self.any(o.child) catch return null, + else => @compileError("cannot decode type " ++ @typeName(T)), + } +} + +//// Expect a sequence. +pub fn sequence(self: *Decoder) !Element { + return try self.element(ExpectedTag.init(.sequence, true, .universal)); +} + +//// Expect an element. +pub fn element( + self: *Decoder, + expected: ExpectedTag, +) (error{ EndOfStream, UnexpectedElement } || Element.DecodeError)!Element { + if (self.index >= self.bytes.len) return error.EndOfStream; + + const res = try Element.decode(self.bytes, self.index); + var e = expected; + if (self.field_tag) |ft| { + e.number = @enumFromInt(ft.number); + e.class = ft.class; + } + if (!e.match(res.tag)) { + return error.UnexpectedElement; + } + + self.index = if (res.tag.constructed) res.slice.start else res.slice.end; + return res; +} + +/// View of element bytes. +pub fn view(self: Decoder, elem: Element) []const u8 { + return elem.slice.view(self.bytes); +} + +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"); + + 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 (bytes.len > @sizeOf(T)) return error.LargeValue; + if (@sizeOf(T) == 1) return @bitCast(bytes[0]); + + return std.mem.readVarInt(T, bytes, .big); +} + +test int { + try expectEqual(@as(u8, 1), try int(u8, &[_]u8{1})); + try expectError(error.NonCanonical, int(u8, &[_]u8{ 0, 1 })); + try expectError(error.NonCanonical, int(u8, &[_]u8{ 0xff, 0xff })); + + const big = [_]u8{ 0xef, 0xff }; + try expectError(error.LargeValue, int(u8, &big)); + try expectEqual(0xefff, int(u16, &big)); +} + +test Decoder { + var parser = Decoder{ .bytes = @embedFile("./testdata/id_ecc.pub.der") }; + const seq = try parser.sequence(); + + { + const seq2 = try parser.sequence(); + _ = try parser.element(ExpectedTag.init(.oid, false, .universal)); + _ = try parser.element(ExpectedTag.init(.oid, false, .universal)); + + try std.testing.expectEqual(parser.index, seq2.slice.end); + } + _ = try parser.element(ExpectedTag.init(.bitstring, false, .universal)); + + try std.testing.expectEqual(parser.index, seq.slice.end); + try std.testing.expectEqual(parser.index, parser.bytes.len); +} + +const std = @import("std"); +const builtin = @import("builtin"); +const asn1 = @import("../../asn1.zig"); +const Oid = @import("../Oid.zig"); + +const expectEqual = std.testing.expectEqual; +const expectError = std.testing.expectError; +const Decoder = @This(); +const Index = asn1.Index; +const Tag = asn1.Tag; +const FieldTag = asn1.FieldTag; +const ExpectedTag = asn1.ExpectedTag; +const Element = asn1.Element; diff --git a/lib/std/crypto/codecs/asn1/der/Encoder.zig b/lib/std/crypto/codecs/asn1/der/Encoder.zig new file mode 100644 index 0000000000000000000000000000000000000000..da861e3d7d5a06f9352856949e1c6f06804b2fa2 --- /dev/null +++ b/lib/std/crypto/codecs/asn1/der/Encoder.zig @@ -0,0 +1,166 @@ +//! A buffered DER encoder. +//! +//! Prefers calling container's `fn encodeDer(self: @This(), encoder: *der.Encoder)`. +//! That function should encode values, lengths, then tags. +buffer: ArrayListReverse, +/// The field tag set by a parent container. +/// This is needed because we might visit an implicitly tagged container with a `fn encodeDer`. +field_tag: ?FieldTag = null, + +pub fn init(allocator: std.mem.Allocator) Encoder { + return Encoder{ .buffer = ArrayListReverse.init(allocator) }; +} + +pub fn deinit(self: *Encoder) void { + self.buffer.deinit(); +} + +/// Encode any value. +pub fn any(self: *Encoder, val: anytype) !void { + const T = @TypeOf(val); + try self.anyTag(Tag.fromZig(T), val); +} + +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 start = self.buffer.data.len; + const merged_tag = self.mergedTag(tag_); + + switch (@typeInfo(T)) { + .@"struct" => |info| { + inline for (0..info.fields.len) |i| { + const f = info.fields[info.fields.len - i - 1]; + const field_val = @field(val, f.name); + const field_tag = FieldTag.fromContainer(T, f.name); + + // > The encoding of a set value or sequence value shall not include an encoding for any + // > component value which is equal to its default value. + const is_default = if (f.is_comptime) false else if (f.default_value_ptr) |v| brk: { + const default_val: *const f.type = @alignCast(@ptrCast(v)); + break :brk std.mem.eql(u8, std.mem.asBytes(default_val), std.mem.asBytes(&field_val)); + } else false; + + if (!is_default) { + const start2 = self.buffer.data.len; + self.field_tag = field_tag; + // will merge with self.field_tag. + // may mutate self.field_tag. + try self.anyTag(Tag.fromZig(f.type), field_val); + if (field_tag) |ft| { + if (ft.explicit) { + try self.length(self.buffer.data.len - start2); + try self.tag(ft.toTag()); + self.field_tag = null; + } + } + } + } + }, + .bool => try self.buffer.prependSlice(&[_]u8{if (val) 0xff else 0}), + .int => try self.int(T, val), + .@"enum" => |e| { + if (@hasDecl(T, "oids")) { + return self.any(T.oids.enumToOid(val)); + } else { + try self.int(e.tag_type, @intFromEnum(val)); + } + }, + .optional => if (val) |v| return try self.anyTag(tag_, v), + .null => {}, + else => @compileError("cannot encode type " ++ @typeName(T)), + } + + try self.length(self.buffer.data.len - start); + try self.tag(merged_tag); +} + +/// Encode a tag. +pub fn tag(self: *Encoder, tag_: Tag) !void { + const t = self.mergedTag(tag_); + try t.encode(self.writer()); +} + +fn mergedTag(self: *Encoder, tag_: Tag) Tag { + var res = tag_; + if (self.field_tag) |ft| { + if (!ft.explicit) { + res.number = @enumFromInt(ft.number); + res.class = ft.class; + } + } + return res; +} + +/// 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; +} + +/// Encode a tag and length-prefixed bytes. +pub fn tagBytes(self: *Encoder, tag_: Tag, bytes: []const u8) !void { + try self.buffer.prependSlice(bytes); + try self.length(bytes.len); + 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(); +} + +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 bits_needed = @bitSizeOf(T) - @clz(value); + const needs_padding: u1 = if (value == 0) + 1 + else if (bits_needed > 8) brk: { + const RightShift = std.meta.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); +} + +test int { + const allocator = std.testing.allocator; + var encoder = Encoder.init(allocator); + defer encoder.deinit(); + + try encoder.int(u8, 0); + try std.testing.expectEqualSlices(u8, &[_]u8{0}, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u16, 0x00ff); + try std.testing.expectEqualSlices(u8, &[_]u8{0xff}, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u32, 0xffff); + try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0xff, 0xff }, encoder.buffer.data); +} + +const std = @import("std"); +const Oid = @import("../Oid.zig"); +const asn1 = @import("../../asn1.zig"); +const ArrayListReverse = @import("./ArrayListReverse.zig"); +const Tag = asn1.Tag; +const FieldTag = asn1.FieldTag; +const Encoder = @This(); diff --git a/lib/std/crypto/codecs/asn1/der/testdata/all_types.der b/lib/std/crypto/codecs/asn1/der/testdata/all_types.der new file mode 100644 index 0000000000000000000000000000000000000000..a4f784938b54d2f3aef4d4049a02c35342f106ea Binary files /dev/null and b/lib/std/crypto/codecs/asn1/der/testdata/all_types.der differ diff --git a/lib/std/crypto/codecs/asn1/der/testdata/id_ecc.pub.der b/lib/std/crypto/codecs/asn1/der/testdata/id_ecc.pub.der new file mode 100644 index 0000000000000000000000000000000000000000..3964a8f0fcaa5710ea9b8f198b1dfb7ee9ced25e Binary files /dev/null and b/lib/std/crypto/codecs/asn1/der/testdata/id_ecc.pub.der differ diff --git a/lib/std/crypto/codecs/asn1/test.zig b/lib/std/crypto/codecs/asn1/test.zig new file mode 100644 index 0000000000000000000000000000000000000000..fe12cba81985254bcb08ad53216f201271e3293e --- /dev/null +++ b/lib/std/crypto/codecs/asn1/test.zig @@ -0,0 +1,80 @@ +const std = @import("std"); +const asn1 = @import("../asn1.zig"); + +const der = asn1.der; +const Tag = asn1.Tag; +const FieldTag = asn1.FieldTag; + +/// An example that uses all ASN1 types and available implementation features. +const AllTypes = struct { + a: u8 = 0, + b: asn1.BitString, + c: C, + d: asn1.Opaque(Tag.universal(.string_utf8, false)), + e: asn1.Opaque(Tag.universal(.octetstring, false)), + f: ?u16, + g: ?Nested, + h: asn1.Any, + + pub const asn1_tags = .{ + .a = FieldTag.initExplicit(0, .context_specific), + .b = FieldTag.initExplicit(1, .context_specific), + .c = FieldTag.initImplicit(2, .context_specific), + .g = FieldTag.initImplicit(3, .context_specific), + }; + + const C = enum { + a, + b, + + pub const oids = asn1.Oid.StaticMap(@This()).initComptime(.{ + .a = "1.2.3.4", + .b = "1.2.3.5", + }); + }; + + const Nested = struct { + inner: Asn1T, + sum: i16, + + const Asn1T = struct { a: u8, b: i16 }; + + pub fn decodeDer(decoder: *der.Decoder) !Nested { + const inner = try decoder.any(Asn1T); + return Nested{ .inner = inner, .sum = inner.a + inner.b }; + } + + pub fn encodeDer(self: Nested, encoder: *der.Encoder) !void { + try encoder.any(self.inner); + } + }; +}; + +test AllTypes { + const expected = AllTypes{ + .a = 2, + .b = asn1.BitString{ .bytes = &[_]u8{ 0x04, 0xa0 } }, + .c = .a, + .d = .{ .bytes = "asdf" }, + .e = .{ .bytes = "fdsa" }, + .f = (1 << 8) + 1, + .g = .{ .inner = .{ .a = 4, .b = 5 }, .sum = 9 }, + .h = .{ .tag = Tag.init(.string_ia5, false, .universal), .bytes = "asdf" }, + }; + // https://lapo.it/asn1js/#MC-gAwIBAqEFAwMABKCCAyoDBAwEYXNkZgQEZmRzYQICAQGjBgIBBAIBBRYEYXNkZg + const path = "./der/testdata/all_types.der"; + const encoded = @embedFile(path); + const actual = try asn1.der.decode(AllTypes, encoded); + try std.testing.expectEqualDeep(expected, actual); + + const allocator = std.testing.allocator; + const buf = try asn1.der.encode(allocator, expected); + defer allocator.free(buf); + try std.testing.expectEqualSlices(u8, encoded, buf); + + // Use this to update test file. + // const dir = try std.fs.cwd().openDir("lib/std/crypto/asn1", .{}); + // var file = try dir.createFile(path, .{}); + // defer file.close(); + // try file.writeAll(buf); +} diff --git a/lib/std/crypto/codecs/base64_hex_ct.zig b/lib/std/crypto/codecs/base64_hex_ct.zig new file mode 100644 index 0000000000000000000000000000000000000000..2a2a3c3005e0d174ca6be67956b00f37d63d99cf --- /dev/null +++ b/lib/std/crypto/codecs/base64_hex_ct.zig @@ -0,0 +1,463 @@ +//! Hexadecimal and Base64 codecs designed for cryptographic use. +//! This file provides (best-effort) constant-time encoding and decoding functions for hexadecimal and Base64 formats. +//! This is designed to be used in cryptographic applications where timing attacks are a concern. +const std = @import("std"); +const testing = std.testing; +const StaticBitSet = std.StaticBitSet; + +pub const Error = error{ + /// An invalid character was found in the input. + InvalidCharacter, + /// The input is not properly padded. + InvalidPadding, + /// The input buffer is too small to hold the output. + NoSpaceLeft, + /// The input and output buffers are not the same size. + SizeMismatch, +}; + +/// (best-effort) constant time hexadecimal encoding and decoding. +pub const hex = struct { + /// Encodes a binary buffer into a hexadecimal string. + /// The output buffer must be twice the size of the input buffer. + pub fn encode(encoded: []u8, bin: []const u8, comptime case: std.fmt.Case) error{SizeMismatch}!void { + if (encoded.len / 2 != bin.len) { + return error.SizeMismatch; + } + for (bin, 0..) |v, i| { + const b: u16 = v >> 4; + const c: u16 = v & 0xf; + const off = if (case == .upper) 32 else 0; + const x = + ((87 - off + c + (((c -% 10) >> 8) & ~@as(u16, 38 - off))) & 0xff) << 8 | + ((87 - off + b + (((b -% 10) >> 8) & ~@as(u16, 38 - off))) & 0xff); + encoded[i * 2] = @truncate(x); + encoded[i * 2 + 1] = @truncate(x >> 8); + } + } + + /// Decodes a hexadecimal string into a binary buffer. + /// The output buffer must be half the size of the input buffer. + pub fn decode(bin: []u8, encoded: []const u8) error{ SizeMismatch, InvalidCharacter, InvalidPadding }!void { + if (encoded.len % 2 != 0) { + return error.InvalidPadding; + } + if (bin.len < encoded.len / 2) { + return error.SizeMismatch; + } + _ = decodeAny(bin, encoded, null) catch |err| { + switch (err) { + error.InvalidCharacter => return error.InvalidCharacter, + error.InvalidPadding => return error.InvalidPadding, + else => unreachable, + } + }; + } + + /// A decoder that ignores certain characters. + /// The decoder will skip any characters that are in the ignore list. + pub const DecoderWithIgnore = struct { + /// The characters to ignore. + ignored_chars: StaticBitSet(256) = undefined, + + /// Decodes a hexadecimal string into a binary buffer. + /// The output buffer must be half the size of the input buffer. + pub fn decode( + self: DecoderWithIgnore, + bin: []u8, + encoded: []const u8, + ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 { + return decodeAny(bin, encoded, self.ignored_chars); + } + + /// Returns the decoded length of a hexadecimal string, ignoring any characters in the ignore list. + /// This operation does not run in constant time, but it aims to avoid leaking information about the underlying hexadecimal string. + pub fn decodedLenForSlice(decoder: DecoderWithIgnore, encoded: []const u8) !usize { + var hex_len = encoded.len; + for (encoded) |c| { + if (decoder.ignored_chars.isSet(c)) hex_len -= 1; + } + if (hex_len % 2 != 0) { + return error.InvalidPadding; + } + return hex_len / 2; + } + + /// Returns the maximum possible decoded size for a given input length after skipping ignored characters. + pub fn decodedLenUpperBound(hex_len: usize) usize { + return hex_len / 2; + } + }; + + /// Creates a new decoder that ignores certain characters. + /// The decoder will skip any characters that are in the ignore list. + /// The ignore list must not contain any valid hexadecimal characters. + pub fn decoderWithIgnore(ignore_chars: []const u8) error{InvalidCharacter}!DecoderWithIgnore { + var ignored_chars = StaticBitSet(256).initEmpty(); + for (ignore_chars) |c| { + switch (c) { + '0'...'9', 'a'...'f', 'A'...'F' => return error.InvalidCharacter, + else => if (ignored_chars.isSet(c)) return error.InvalidCharacter, + } + ignored_chars.set(c); + } + return DecoderWithIgnore{ .ignored_chars = ignored_chars }; + } + + fn decodeAny( + bin: []u8, + encoded: []const u8, + ignored_chars: ?StaticBitSet(256), + ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 { + var bin_pos: usize = 0; + var state: bool = false; + var c_acc: u8 = 0; + for (encoded) |c| { + const c_num = c ^ 48; + const c_num0: u8 = @truncate((@as(u16, c_num) -% 10) >> 8); + const c_alpha: u8 = (c & ~@as(u8, 32)) -% 55; + const c_alpha0: u8 = @truncate(((@as(u16, c_alpha) -% 10) ^ (@as(u16, c_alpha) -% 16)) >> 8); + if ((c_num0 | c_alpha0) == 0) { + if (ignored_chars) |set| { + if (set.isSet(c)) { + continue; + } + } + return error.InvalidCharacter; + } + const c_val = (c_num0 & c_num) | (c_alpha0 & c_alpha); + if (bin_pos >= bin.len) { + return error.NoSpaceLeft; + } + if (!state) { + c_acc = c_val << 4; + } else { + bin[bin_pos] = c_acc | c_val; + bin_pos += 1; + } + state = !state; + } + if (state) { + return error.InvalidPadding; + } + return bin[0..bin_pos]; + } +}; + +/// (best-effort) constant time base64 encoding and decoding. +pub const base64 = struct { + /// The base64 variant to use. + pub const Variant = packed struct { + /// Use the URL-safe alphabet instead of the standard alphabet. + urlsafe_alphabet: bool = false, + /// Enable padding with '=' characters. + padding: bool = true, + + /// The standard base64 variant. + pub const standard: Variant = .{ .urlsafe_alphabet = false, .padding = true }; + /// The URL-safe base64 variant. + pub const urlsafe: Variant = .{ .urlsafe_alphabet = true, .padding = true }; + /// The standard base64 variant without padding. + pub const standard_nopad: Variant = .{ .urlsafe_alphabet = false, .padding = false }; + /// The URL-safe base64 variant without padding. + pub const urlsafe_nopad: Variant = .{ .urlsafe_alphabet = true, .padding = false }; + }; + + /// Returns the length of the encoded base64 string for a given length. + pub fn encodedLen(bin_len: usize, variant: Variant) usize { + if (variant.padding) { + return (bin_len + 2) / 3 * 4; + } else { + const leftover = bin_len % 3; + return bin_len / 3 * 4 + (leftover * 4 + 2) / 3; + } + } + + /// Returns the maximum possible decoded size for a given input length - The actual length may be less if the input includes padding. + /// `InvalidPadding` is returned if the input length is not valid. + pub fn decodedLen(b64_len: usize, variant: Variant) !usize { + var result = b64_len / 4 * 3; + const leftover = b64_len % 4; + if (variant.padding) { + if (leftover % 4 != 0) return error.InvalidPadding; + } else { + if (leftover % 4 == 1) return error.InvalidPadding; + result += leftover * 3 / 4; + } + return result; + } + + /// Encodes a binary buffer into a base64 string. + /// The output buffer must be at least `encodedLen(bin.len)` bytes long. + pub fn encode(encoded: []u8, bin: []const u8, comptime variant: Variant) error{NoSpaceLeft}![]const u8 { + var acc_len: u4 = 0; + var b64_pos: usize = 0; + var acc: u16 = 0; + const nibbles = bin.len / 3; + const remainder = bin.len - 3 * nibbles; + var b64_len = nibbles * 4; + if (remainder != 0) { + b64_len += if (variant.padding) 4 else 2 + (remainder >> 1); + } + if (encoded.len < b64_len) { + return error.NoSpaceLeft; + } + const urlsafe = variant.urlsafe_alphabet; + for (bin) |v| { + acc = (acc << 8) + v; + acc_len += 8; + while (acc_len >= 6) { + acc_len -= 6; + encoded[b64_pos] = charFromByte(@as(u6, @truncate(acc >> acc_len)), urlsafe); + b64_pos += 1; + } + } + if (acc_len > 0) { + encoded[b64_pos] = charFromByte(@as(u6, @truncate(acc << (6 - acc_len))), urlsafe); + b64_pos += 1; + } + while (b64_pos < b64_len) { + encoded[b64_pos] = '='; + b64_pos += 1; + } + return encoded[0..b64_pos]; + } + + /// Decodes a base64 string into a binary buffer. + /// The output buffer must be at least `decodedLenUpperBound(encoded.len)` bytes long. + pub fn decode(bin: []u8, encoded: []const u8, comptime variant: Variant) error{ InvalidCharacter, InvalidPadding }![]const u8 { + return decodeAny(bin, encoded, variant, null) catch |err| { + switch (err) { + error.InvalidCharacter => return error.InvalidCharacter, + error.InvalidPadding => return error.InvalidPadding, + else => unreachable, + } + }; + } + + //// A decoder that ignores certain characters. + pub const DecoderWithIgnore = struct { + /// The characters to ignore. + ignored_chars: StaticBitSet(256) = undefined, + + /// Decodes a base64 string into a binary buffer. + /// The output buffer must be at least `decodedLenUpperBound(encoded.len)` bytes long. + pub fn decode( + self: DecoderWithIgnore, + bin: []u8, + encoded: []const u8, + comptime variant: Variant, + ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 { + return decodeAny(bin, encoded, variant, self.ignored_chars); + } + + /// Returns the decoded length of a base64 string, ignoring any characters in the ignore list. + /// This operation does not run in constant time, but it aims to avoid leaking information about the underlying base64 string. + pub fn decodedLenForSlice(decoder: DecoderWithIgnore, encoded: []const u8, variant: Variant) !usize { + var b64_len = encoded.len; + for (encoded) |c| { + if (decoder.ignored_chars.isSet(c)) b64_len -= 1; + } + return base64.decodedLen(b64_len, variant); + } + + /// Returns the maximum possible decoded size for a given input length after skipping ignored characters. + pub fn decodedLenUpperBound(b64_len: usize) usize { + return b64_len / 3 * 4; + } + }; + + /// Creates a new decoder that ignores certain characters. + pub fn decoderWithIgnore(ignore_chars: []const u8) error{InvalidCharacter}!DecoderWithIgnore { + var ignored_chars = StaticBitSet(256).initEmpty(); + for (ignore_chars) |c| { + switch (c) { + 'A'...'Z', 'a'...'z', '0'...'9' => return error.InvalidCharacter, + else => if (ignored_chars.isSet(c)) return error.InvalidCharacter, + } + ignored_chars.set(c); + } + return DecoderWithIgnore{ .ignored_chars = ignored_chars }; + } + + inline fn eq(x: u8, y: u8) u8 { + return ~@as(u8, @truncate((0 -% (@as(u16, x) ^ @as(u16, y))) >> 8)); + } + + inline fn gt(x: u8, y: u8) u8 { + return @truncate((@as(u16, y) -% @as(u16, x)) >> 8); + } + + inline fn ge(x: u8, y: u8) u8 { + return ~gt(y, x); + } + + inline fn lt(x: u8, y: u8) u8 { + return gt(y, x); + } + + inline fn le(x: u8, y: u8) u8 { + return ge(y, x); + } + + inline fn charFromByte(x: u8, comptime urlsafe: bool) u8 { + return (lt(x, 26) & (x +% 'A')) | + (ge(x, 26) & lt(x, 52) & (x +% 'a' -% 26)) | + (ge(x, 52) & lt(x, 62) & (x +% '0' -% 52)) | + (eq(x, 62) & '+') | (eq(x, 63) & if (urlsafe) '_' else '/'); + } + + inline fn byteFromChar(c: u8, comptime urlsafe: bool) u8 { + const x = + (ge(c, 'A') & le(c, 'Z') & (c -% 'A')) | + (ge(c, 'a') & le(c, 'z') & (c -% 'a' +% 26)) | + (ge(c, '0') & le(c, '9') & (c -% '0' +% 52)) | + (eq(c, '+') & 62) | (eq(c, if (urlsafe) '_' else '/') & 63); + return x | (eq(x, 0) & ~eq(c, 'A')); + } + + fn skipPadding( + encoded: []const u8, + padding_len: usize, + ignored_chars: ?StaticBitSet(256), + ) error{InvalidPadding}![]const u8 { + var b64_pos: usize = 0; + var i = padding_len; + while (i > 0) { + if (b64_pos >= encoded.len) { + return error.InvalidPadding; + } + const c = encoded[b64_pos]; + if (c == '=') { + i -= 1; + } else if (ignored_chars) |set| { + if (!set.isSet(c)) { + return error.InvalidPadding; + } + } + b64_pos += 1; + } + return encoded[b64_pos..]; + } + + fn decodeAny( + bin: []u8, + encoded: []const u8, + comptime variant: Variant, + ignored_chars: ?StaticBitSet(256), + ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 { + var acc: u16 = 0; + var acc_len: u4 = 0; + var bin_pos: usize = 0; + var premature_end: ?usize = null; + const urlsafe = variant.urlsafe_alphabet; + for (encoded, 0..) |c, b64_pos| { + const d = byteFromChar(c, urlsafe); + if (d == 0xff) { + if (ignored_chars) |set| { + if (set.isSet(c)) continue; + } + premature_end = b64_pos; + break; + } + acc = (acc << 6) + d; + acc_len += 6; + if (acc_len >= 8) { + acc_len -= 8; + if (bin_pos >= bin.len) { + return error.NoSpaceLeft; + } + bin[bin_pos] = @truncate(acc >> acc_len); + bin_pos += 1; + } + } + if (acc_len > 4 or (acc & ((@as(u16, 1) << acc_len) -% 1)) != 0) { + return error.InvalidCharacter; + } + const padding_len = acc_len / 2; + if (premature_end) |pos| { + const remaining = + if (variant.padding) + try skipPadding(encoded[pos..], padding_len, ignored_chars) + else + encoded[pos..]; + if (ignored_chars) |set| { + for (remaining) |c| { + if (!set.isSet(c)) { + return error.InvalidCharacter; + } + } + } else if (remaining.len != 0) { + return error.InvalidCharacter; + } + } else if (variant.padding and padding_len != 0) { + return error.InvalidPadding; + } + return bin[0..bin_pos]; + } +}; + +test "hex" { + var default_rng = std.Random.DefaultPrng.init(testing.random_seed); + var rng = default_rng.random(); + var bin_buf: [1000]u8 = undefined; + rng.bytes(&bin_buf); + var bin2_buf: [bin_buf.len]u8 = undefined; + var hex_buf: [bin_buf.len * 2]u8 = undefined; + for (0..1000) |_| { + const bin_len = rng.intRangeAtMost(usize, 0, bin_buf.len); + const bin = bin_buf[0..bin_len]; + const bin2 = bin2_buf[0..bin_len]; + inline for (.{ .lower, .upper }) |case| { + const hex_len = bin_len * 2; + const encoded = hex_buf[0..hex_len]; + try hex.encode(encoded, bin, case); + try hex.decode(bin2, encoded); + try testing.expectEqualSlices(u8, bin, bin2); + } + } +} + +test "base64" { + var default_rng = std.Random.DefaultPrng.init(testing.random_seed); + var rng = default_rng.random(); + var bin_buf: [1000]u8 = undefined; + rng.bytes(&bin_buf); + var bin2_buf: [bin_buf.len]u8 = undefined; + var b64_buf: [(bin_buf.len + 3) / 3 * 4]u8 = undefined; + for (0..1000) |_| { + const bin_len = rng.intRangeAtMost(usize, 0, bin_buf.len); + const bin = bin_buf[0..bin_len]; + const bin2 = bin2_buf[0..bin_len]; + inline for ([_]base64.Variant{ + .standard, + .standard_nopad, + .urlsafe, + .urlsafe_nopad, + }) |variant| { + const b64_len = base64.encodedLen(bin_len, variant); + const encoded_buf = b64_buf[0..b64_len]; + const encoded = try base64.encode(encoded_buf, bin, variant); + const decoded = try base64.decode(bin2, encoded, variant); + try testing.expectEqualSlices(u8, bin, decoded); + } + } +} + +test "hex with ignored chars" { + const encoded = "01020304050607\n08090A0B0C0D0E0F\n"; + const expected = [_]u8{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }; + var bin_buf: [encoded.len / 2]u8 = undefined; + try testing.expectError(error.InvalidCharacter, hex.decode(&bin_buf, encoded)); + const bin = try (try hex.decoderWithIgnore("\r\n")).decode(&bin_buf, encoded); + try testing.expectEqualSlices(u8, &expected, bin); +} + +test "base64 with ignored chars" { + const encoded = "dGVzdCBi\r\nYXNlNjQ=\n"; + const expected = "test base64"; + var bin_buf: [base64.DecoderWithIgnore.decodedLenUpperBound(encoded.len)]u8 = undefined; + try testing.expectError(error.InvalidCharacter, base64.decode(&bin_buf, encoded, .standard)); + const bin = try (try base64.decoderWithIgnore("\r\n")).decode(&bin_buf, encoded, .standard); + try testing.expectEqualSlices(u8, expected, bin); +}