authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-22 20:21:09-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-02-22 20:21:09-05:00
loged7004a2ee3b984a17b3fbd468d73a3875f209eb
tree1edb372aca4159479ab8e3e9222771f7d29b7613
parent6769806213ab28ed629221085325d8f143e515b0
parenta51bc1d1d11717e9aab72121918ddb09fb3333f7
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #19976 from clickingbuttons/asn1

std.crypto: Add ASN1 module with OIDs and DER

10 files changed, 1141 insertions(+), 0 deletions(-)

lib/std/crypto.zig+2
...@@ -220,6 +220,7 @@ pub const errors = @import("crypto/errors.zig");...@@ -220,6 +220,7 @@ pub const errors = @import("crypto/errors.zig");
220220
221pub const tls = @import("crypto/tls.zig");221pub const tls = @import("crypto/tls.zig");
222pub const Certificate = @import("crypto/Certificate.zig");222pub const Certificate = @import("crypto/Certificate.zig");
223pub const asn1 = @import("crypto/asn1.zig");
223224
224/// Side-channels mitigations.225/// Side-channels mitigations.
225pub const SideChannelsMitigations = enum {226pub const SideChannelsMitigations = enum {
...@@ -334,6 +335,7 @@ test {...@@ -334,6 +335,7 @@ test {
334 _ = errors;335 _ = errors;
335 _ = tls;336 _ = tls;
336 _ = Certificate;337 _ = Certificate;
338 _ = asn1;
337}339}
338340
339test "CSPRNG" {341test "CSPRNG" {
lib/std/crypto/asn1.zig created+359
...@@ -0,0 +1,359 @@
1//! ASN.1 types for public consumption.
2const std = @import("std");
3pub const der = @import("./asn1/der.zig");
4pub const Oid = @import("./asn1/Oid.zig");
5
6pub const Index = u32;
7
8pub const Tag = struct {
9 number: Number,
10 /// Whether this ASN.1 type contains other ASN.1 types.
11 constructed: bool,
12 class: Class,
13
14 /// These values apply to class == .universal.
15 pub const Number = enum(u16) {
16 // 0 is reserved by spec
17 boolean = 1,
18 integer = 2,
19 bitstring = 3,
20 octetstring = 4,
21 null = 5,
22 oid = 6,
23 object_descriptor = 7,
24 real = 9,
25 enumerated = 10,
26 embedded = 11,
27 string_utf8 = 12,
28 oid_relative = 13,
29 time = 14,
30 // 15 is reserved to mean that the tag is >= 32
31 sequence = 16,
32 /// Elements may appear in any order.
33 sequence_of = 17,
34 string_numeric = 18,
35 string_printable = 19,
36 string_teletex = 20,
37 string_videotex = 21,
38 string_ia5 = 22,
39 utc_time = 23,
40 generalized_time = 24,
41 string_graphic = 25,
42 string_visible = 26,
43 string_general = 27,
44 string_universal = 28,
45 string_char = 29,
46 string_bmp = 30,
47 date = 31,
48 time_of_day = 32,
49 date_time = 33,
50 duration = 34,
51 /// IRI = Internationalized Resource Identifier
52 oid_iri = 35,
53 oid_iri_relative = 36,
54 _,
55 };
56
57 pub const Class = enum(u2) {
58 universal,
59 application,
60 context_specific,
61 private,
62 };
63
64 pub fn init(number: Tag.Number, constructed: bool, class: Tag.Class) Tag {
65 return .{ .number = number, .constructed = constructed, .class = class };
66 }
67
68 pub fn universal(number: Tag.Number, constructed: bool) Tag {
69 return .{ .number = number, .constructed = constructed, .class = .universal };
70 }
71
72 pub fn decode(reader: anytype) !Tag {
73 const tag1: FirstTag = @bitCast(try reader.readByte());
74 var number: u14 = tag1.number;
75
76 if (tag1.number == 15) {
77 const tag2: NextTag = @bitCast(try reader.readByte());
78 number = tag2.number;
79 if (tag2.continues) {
80 const tag3: NextTag = @bitCast(try reader.readByte());
81 number = (number << 7) + tag3.number;
82 if (tag3.continues) return error.InvalidLength;
83 }
84 }
85
86 return Tag{
87 .number = @enumFromInt(number),
88 .constructed = tag1.constructed,
89 .class = tag1.class,
90 };
91 }
92
93 pub fn encode(self: Tag, writer: anytype) @TypeOf(writer).Error!void {
94 var tag1 = FirstTag{
95 .number = undefined,
96 .constructed = self.constructed,
97 .class = self.class,
98 };
99
100 var buffer: [3]u8 = undefined;
101 var stream = std.io.fixedBufferStream(&buffer);
102 var writer2 = stream.writer();
103
104 switch (@intFromEnum(self.number)) {
105 0...std.math.maxInt(u5) => |n| {
106 tag1.number = @intCast(n);
107 writer2.writeByte(@bitCast(tag1)) catch unreachable;
108 },
109 std.math.maxInt(u5) + 1...std.math.maxInt(u7) => |n| {
110 tag1.number = 15;
111 const tag2 = NextTag{ .number = @intCast(n), .continues = false };
112 writer2.writeByte(@bitCast(tag1)) catch unreachable;
113 writer2.writeByte(@bitCast(tag2)) catch unreachable;
114 },
115 else => |n| {
116 tag1.number = 15;
117 const tag2 = NextTag{ .number = @intCast(n >> 7), .continues = true };
118 const tag3 = NextTag{ .number = @truncate(n), .continues = false };
119 writer2.writeByte(@bitCast(tag1)) catch unreachable;
120 writer2.writeByte(@bitCast(tag2)) catch unreachable;
121 writer2.writeByte(@bitCast(tag3)) catch unreachable;
122 },
123 }
124
125 _ = try writer.write(stream.getWritten());
126 }
127
128 const FirstTag = packed struct(u8) { number: u5, constructed: bool, class: Tag.Class };
129 const NextTag = packed struct(u8) { number: u7, continues: bool };
130
131 pub fn toExpected(self: Tag) ExpectedTag {
132 return ExpectedTag{
133 .number = self.number,
134 .constructed = self.constructed,
135 .class = self.class,
136 };
137 }
138
139 pub fn fromZig(comptime T: type) Tag {
140 switch (@typeInfo(T)) {
141 .Struct, .Enum, .Union => {
142 if (@hasDecl(T, "asn1_tag")) return T.asn1_tag;
143 },
144 else => {},
145 }
146
147 switch (@typeInfo(T)) {
148 .Struct, .Union => return universal(.sequence, true),
149 .Bool => return universal(.boolean, false),
150 .Int => return universal(.integer, false),
151 .Enum => |e| {
152 if (@hasDecl(T, "oids")) return Oid.asn1_tag;
153 return universal(if (e.is_exhaustive) .enumerated else .integer, false);
154 },
155 .Optional => |o| return fromZig(o.child),
156 .Null => return universal(.null, false),
157 else => @compileError("cannot map Zig type to asn1_tag " ++ @typeName(T)),
158 }
159 }
160};
161
162test Tag {
163 const buf = [_]u8{0xa3};
164 var stream = std.io.fixedBufferStream(&buf);
165 const t = Tag.decode(stream.reader());
166 try std.testing.expectEqual(Tag.init(@enumFromInt(3), true, .context_specific), t);
167}
168
169/// A decoded view.
170pub const Element = struct {
171 tag: Tag,
172 slice: Slice,
173
174 pub const Slice = struct {
175 start: Index,
176 end: Index,
177
178 pub fn len(self: Slice) Index {
179 return self.end - self.start;
180 }
181
182 pub fn view(self: Slice, bytes: []const u8) []const u8 {
183 return bytes[self.start..self.end];
184 }
185 };
186
187 pub const DecodeError = error{ InvalidLength, EndOfStream };
188
189 /// Safely decode a DER/BER/CER element at `index`:
190 /// - Ensures length uses shortest form
191 /// - Ensures length is within `bytes`
192 /// - Ensures length is less than `std.math.maxInt(Index)`
193 pub fn decode(bytes: []const u8, index: Index) DecodeError!Element {
194 var stream = std.io.fixedBufferStream(bytes[index..]);
195 var reader = stream.reader();
196
197 const tag = try Tag.decode(reader);
198 const size_or_len_size = try reader.readByte();
199
200 var start = index + 2;
201 var end = start + size_or_len_size;
202 // short form between 0-127
203 if (size_or_len_size < 128) {
204 if (end > bytes.len) return error.InvalidLength;
205 } else {
206 // long form between 0 and std.math.maxInt(u1024)
207 const len_size: u7 = @truncate(size_or_len_size);
208 start += len_size;
209 if (len_size > @sizeOf(Index)) return error.InvalidLength;
210
211 const len = try reader.readVarInt(Index, .big, len_size);
212 if (len < 128) return error.InvalidLength; // should have used short form
213
214 end = std.math.add(Index, start, len) catch return error.InvalidLength;
215 if (end > bytes.len) return error.InvalidLength;
216 }
217
218 return Element{ .tag = tag, .slice = Slice{ .start = start, .end = end } };
219 }
220};
221
222test Element {
223 const short_form = [_]u8{ 0x30, 0x03, 0x02, 0x01, 0x09 };
224 try std.testing.expectEqual(Element{
225 .tag = Tag.universal(.sequence, true),
226 .slice = Element.Slice{ .start = 2, .end = short_form.len },
227 }, Element.decode(&short_form, 0));
228
229 const long_form = [_]u8{ 0x30, 129, 129 } ++ [_]u8{0} ** 129;
230 try std.testing.expectEqual(Element{
231 .tag = Tag.universal(.sequence, true),
232 .slice = Element.Slice{ .start = 3, .end = long_form.len },
233 }, Element.decode(&long_form, 0));
234}
235
236/// For decoding.
237pub const ExpectedTag = struct {
238 number: ?Tag.Number = null,
239 constructed: ?bool = null,
240 class: ?Tag.Class = null,
241
242 pub fn init(number: ?Tag.Number, constructed: ?bool, class: ?Tag.Class) ExpectedTag {
243 return .{ .number = number, .constructed = constructed, .class = class };
244 }
245
246 pub fn primitive(number: ?Tag.Number) ExpectedTag {
247 return .{ .number = number, .constructed = false, .class = .universal };
248 }
249
250 pub fn match(self: ExpectedTag, tag: Tag) bool {
251 if (self.number) |e| {
252 if (tag.number != e) return false;
253 }
254 if (self.constructed) |e| {
255 if (tag.constructed != e) return false;
256 }
257 if (self.class) |e| {
258 if (tag.class != e) return false;
259 }
260 return true;
261 }
262};
263
264pub const FieldTag = struct {
265 number: std.meta.Tag(Tag.Number),
266 class: Tag.Class,
267 explicit: bool = true,
268
269 pub fn explicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag {
270 return FieldTag{ .number = number, .class = class, .explicit = true };
271 }
272
273 pub fn implicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag {
274 return FieldTag{ .number = number, .class = class, .explicit = false };
275 }
276
277 pub fn fromContainer(comptime Container: type, comptime field_name: []const u8) ?FieldTag {
278 if (@hasDecl(Container, "asn1_tags") and @hasField(@TypeOf(Container.asn1_tags), field_name)) {
279 return @field(Container.asn1_tags, field_name);
280 }
281
282 return null;
283 }
284
285 pub fn toTag(self: FieldTag) Tag {
286 return Tag.init(@enumFromInt(self.number), self.explicit, self.class);
287 }
288};
289
290pub const BitString = struct {
291 /// Number of bits in rightmost byte that are unused.
292 right_padding: u3 = 0,
293 bytes: []const u8,
294
295 pub fn bitLen(self: BitString) usize {
296 return self.bytes.len * 8 - self.right_padding;
297 }
298
299 const asn1_tag = Tag.universal(.bitstring, false);
300
301 pub fn decodeDer(decoder: *der.Decoder) !BitString {
302 const ele = try decoder.element(asn1_tag.toExpected());
303 const bytes = decoder.view(ele);
304
305 if (bytes.len < 1) return error.InvalidBitString;
306 const padding = bytes[0];
307 if (padding >= 8) return error.InvalidBitString;
308 const right_padding: u3 = @intCast(padding);
309
310 // DER requires that unused bits be zero.
311 if (@ctz(bytes[bytes.len - 1]) < right_padding) return error.InvalidBitString;
312
313 return BitString{ .bytes = bytes[1..], .right_padding = right_padding };
314 }
315
316 pub fn encodeDer(self: BitString, encoder: *der.Encoder) !void {
317 try encoder.writer().writeAll(self.bytes);
318 try encoder.writer().writeByte(self.right_padding);
319 try encoder.length(self.bytes.len + 1);
320 try encoder.tag(asn1_tag);
321 }
322};
323
324pub fn Opaque(comptime tag: Tag) type {
325 return struct {
326 bytes: []const u8,
327
328 pub fn decodeDer(decoder: *der.Decoder) !@This() {
329 const ele = try decoder.element(tag.toExpected());
330 if (tag.constructed) decoder.index = ele.slice.end;
331 return .{ .bytes = decoder.view(ele) };
332 }
333
334 pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void {
335 try encoder.tagBytes(tag, self.bytes);
336 }
337 };
338}
339
340/// Use sparingly.
341pub const Any = struct {
342 tag: Tag,
343 bytes: []const u8,
344
345 pub fn decodeDer(decoder: *der.Decoder) !@This() {
346 const ele = try decoder.element(ExpectedTag{});
347 return .{ .tag = ele.tag, .bytes = decoder.view(ele) };
348 }
349
350 pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void {
351 try encoder.tagBytes(self.tag, self.bytes);
352 }
353};
354
355test {
356 _ = der;
357 _ = Oid;
358 _ = @import("asn1/test.zig");
359}
lib/std/crypto/asn1/Oid.zig created+210
...@@ -0,0 +1,210 @@
1//! Globally unique hierarchical identifier made of a sequence of integers.
2//!
3//! Commonly used to identify standards, algorithms, certificate extensions,
4//! organizations, or policy documents.
5encoded: []const u8,
6
7pub const InitError = std.fmt.ParseIntError || error{MissingPrefix} || std.io.FixedBufferStream(u8).WriteError;
8
9pub fn fromDot(dot_notation: []const u8, out: []u8) InitError!Oid {
10 var split = std.mem.splitScalar(u8, dot_notation, '.');
11 const first_str = split.next() orelse return error.MissingPrefix;
12 const second_str = split.next() orelse return error.MissingPrefix;
13
14 const first = try std.fmt.parseInt(u8, first_str, 10);
15 const second = try std.fmt.parseInt(u8, second_str, 10);
16
17 var stream = std.io.fixedBufferStream(out);
18 var writer = stream.writer();
19
20 try writer.writeByte(first * 40 + second);
21
22 var i: usize = 1;
23 while (split.next()) |s| {
24 var parsed = try std.fmt.parseUnsigned(Arc, s, 10);
25 const n_bytes = if (parsed == 0) 0 else std.math.log(Arc, encoding_base, parsed);
26
27 for (0..n_bytes) |j| {
28 const place = std.math.pow(Arc, encoding_base, n_bytes - @as(Arc, @intCast(j)));
29 const digit: u8 = @intCast(@divFloor(parsed, place));
30
31 try writer.writeByte(digit | 0x80);
32 parsed -= digit * place;
33
34 i += 1;
35 }
36 try writer.writeByte(@intCast(parsed));
37 i += 1;
38 }
39
40 return .{ .encoded = stream.getWritten() };
41}
42
43test fromDot {
44 var buf: [256]u8 = undefined;
45 for (test_cases) |t| {
46 const actual = try fromDot(t.dot_notation, &buf);
47 try std.testing.expectEqualSlices(u8, t.encoded, actual.encoded);
48 }
49}
50
51pub fn toDot(self: Oid, writer: anytype) @TypeOf(writer).Error!void {
52 const encoded = self.encoded;
53 const first = @divTrunc(encoded[0], 40);
54 const second = encoded[0] - first * 40;
55 try writer.print("{d}.{d}", .{ first, second });
56
57 var i: usize = 1;
58 while (i != encoded.len) {
59 const n_bytes: usize = brk: {
60 var res: usize = 1;
61 var j: usize = i;
62 while (encoded[j] & 0x80 != 0) {
63 res += 1;
64 j += 1;
65 }
66 break :brk res;
67 };
68
69 var n: usize = 0;
70 for (0..n_bytes) |j| {
71 const place = std.math.pow(usize, encoding_base, n_bytes - j - 1);
72 n += place * (encoded[i] & 0b01111111);
73 i += 1;
74 }
75 try writer.print(".{d}", .{n});
76 }
77}
78
79test toDot {
80 var buf: [256]u8 = undefined;
81
82 for (test_cases) |t| {
83 var stream = std.io.fixedBufferStream(&buf);
84 try toDot(Oid{ .encoded = t.encoded }, stream.writer());
85 try std.testing.expectEqualStrings(t.dot_notation, stream.getWritten());
86 }
87}
88
89const TestCase = struct {
90 encoded: []const u8,
91 dot_notation: []const u8,
92
93 pub fn init(comptime hex: []const u8, dot_notation: []const u8) TestCase {
94 return .{ .encoded = &hexToBytes(hex), .dot_notation = dot_notation };
95 }
96};
97
98const test_cases = [_]TestCase{
99 // https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier
100 TestCase.init("2b0601040182371514", "1.3.6.1.4.1.311.21.20"),
101 // https://luca.ntop.org/Teaching/Appunti/asn1.html
102 TestCase.init("2a864886f70d", "1.2.840.113549"),
103 // https://www.sysadmins.lv/blog-en/how-to-encode-object-identifier-to-an-asn1-der-encoded-string.aspx
104 TestCase.init("2a868d20", "1.2.100000"),
105 TestCase.init("2a864886f70d01010b", "1.2.840.113549.1.1.11"),
106 TestCase.init("2b6570", "1.3.101.112"),
107};
108
109pub const asn1_tag = asn1.Tag.init(.oid, false, .universal);
110
111pub fn decodeDer(decoder: *der.Decoder) !Oid {
112 const ele = try decoder.element(asn1_tag.toExpected());
113 return Oid{ .encoded = decoder.view(ele) };
114}
115
116pub fn encodeDer(self: Oid, encoder: *der.Encoder) !void {
117 try encoder.tagBytes(asn1_tag, self.encoded);
118}
119
120fn encodedLen(dot_notation: []const u8) usize {
121 var buf: [256]u8 = undefined;
122 const oid = fromDot(dot_notation, &buf) catch unreachable;
123 return oid.encoded.len;
124}
125
126/// Returns encoded bytes of OID.
127fn encodeComptime(comptime dot_notation: []const u8) [encodedLen(dot_notation)]u8 {
128 @setEvalBranchQuota(4000);
129 comptime var buf: [256]u8 = undefined;
130 const oid = comptime fromDot(dot_notation, &buf) catch unreachable;
131 return oid.encoded[0..oid.encoded.len].*;
132}
133
134test encodeComptime {
135 try std.testing.expectEqual(
136 hexToBytes("2b0601040182371514"),
137 comptime encodeComptime("1.3.6.1.4.1.311.21.20"),
138 );
139}
140
141pub fn fromDotComptime(comptime dot_notation: []const u8) Oid {
142 const tmp = comptime encodeComptime(dot_notation);
143 return Oid{ .encoded = &tmp };
144}
145
146/// Maps of:
147/// - Oid -> enum
148/// - Enum -> oid
149pub fn StaticMap(comptime Enum: type) type {
150 const enum_info = @typeInfo(Enum).Enum;
151 const EnumToOid = std.EnumArray(Enum, []const u8);
152 const ReturnType = struct {
153 oid_to_enum: std.StaticStringMap(Enum),
154 enum_to_oid: EnumToOid,
155
156 pub fn oidToEnum(self: @This(), encoded: []const u8) ?Enum {
157 return self.oid_to_enum.get(encoded);
158 }
159
160 pub fn enumToOid(self: @This(), value: Enum) Oid {
161 const bytes = self.enum_to_oid.get(value);
162 return .{ .encoded = bytes };
163 }
164 };
165
166 return struct {
167 pub fn initComptime(comptime key_pairs: anytype) ReturnType {
168 const struct_info = @typeInfo(@TypeOf(key_pairs)).Struct;
169 const error_msg = "Each field of '" ++ @typeName(Enum) ++ "' must map to exactly one OID";
170 if (!enum_info.is_exhaustive or enum_info.fields.len != struct_info.fields.len) {
171 @compileError(error_msg);
172 }
173
174 comptime var enum_to_oid = EnumToOid.initUndefined();
175
176 const KeyPair = struct { []const u8, Enum };
177 comptime var static_key_pairs: [enum_info.fields.len]KeyPair = undefined;
178
179 comptime for (enum_info.fields, 0..) |f, i| {
180 if (!@hasField(@TypeOf(key_pairs), f.name)) {
181 @compileError("Field '" ++ f.name ++ "' missing Oid.StaticMap entry");
182 }
183 const encoded = &encodeComptime(@field(key_pairs, f.name));
184 const tag: Enum = @enumFromInt(f.value);
185 static_key_pairs[i] = .{ encoded, tag };
186 enum_to_oid.set(tag, encoded);
187 };
188
189 const oid_to_enum = std.StaticStringMap(Enum).initComptime(static_key_pairs);
190 if (oid_to_enum.values().len != enum_info.fields.len) @compileError(error_msg);
191
192 return ReturnType{ .oid_to_enum = oid_to_enum, .enum_to_oid = enum_to_oid };
193 }
194 };
195}
196
197/// Strictly for testing.
198fn hexToBytes(comptime hex: []const u8) [hex.len / 2]u8 {
199 var res: [hex.len / 2]u8 = undefined;
200 _ = std.fmt.hexToBytes(&res, hex) catch unreachable;
201 return res;
202}
203
204const std = @import("std");
205const Oid = @This();
206const Arc = u32;
207const encoding_base = 128;
208const Allocator = std.mem.Allocator;
209const der = @import("der.zig");
210const asn1 = @import("../asn1.zig");
lib/std/crypto/asn1/der.zig created+55
...@@ -0,0 +1,55 @@
1//! Distinguised Encoding Rules as defined in X.690 and X.691.
2//!
3//! Subset of Basic Encoding Rules (BER) which eliminates flexibility in
4//! an effort to acheive normality. Used in PKI.
5const std = @import("std");
6const asn1 = @import("../asn1.zig");
7
8pub const Decoder = @import("der/Decoder.zig");
9pub const Encoder = @import("der/Encoder.zig");
10
11pub fn decode(comptime T: type, encoded: []const u8) !T {
12 var decoder = Decoder{ .bytes = encoded };
13 const res = try decoder.any(T);
14 std.debug.assert(decoder.index == encoded.len);
15 return res;
16}
17
18/// Caller owns returned memory.
19pub fn encode(allocator: std.mem.Allocator, value: anytype) ![]u8 {
20 var encoder = Encoder.init(allocator);
21 defer encoder.deinit();
22 try encoder.any(value);
23 return try encoder.buffer.toOwnedSlice();
24}
25
26test encode {
27 // https://lapo.it/asn1js/#MAgGAyoDBAIBBA
28 const Value = struct { a: asn1.Oid, b: i32 };
29 const test_case = .{
30 .value = Value{ .a = asn1.Oid.fromDotComptime("1.2.3.4"), .b = 4 },
31 .encoded = &[_]u8{ 0x30, 0x08, 0x06, 0x03, 0x2A, 0x03, 0x04, 0x02, 0x01, 0x04 },
32 };
33 const allocator = std.testing.allocator;
34 const actual = try encode(allocator, test_case.value);
35 defer allocator.free(actual);
36
37 try std.testing.expectEqualSlices(u8, test_case.encoded, actual);
38}
39
40test decode {
41 // https://lapo.it/asn1js/#MAgGAyoDBAIBBA
42 const Value = struct { a: asn1.Oid, b: i32 };
43 const test_case = .{
44 .value = Value{ .a = asn1.Oid.fromDotComptime("1.2.3.4"), .b = 4 },
45 .encoded = &[_]u8{ 0x30, 0x08, 0x06, 0x03, 0x2A, 0x03, 0x04, 0x02, 0x01, 0x04 },
46 };
47 const decoded = try decode(Value, test_case.encoded);
48
49 try std.testing.expectEqualDeep(test_case.value, decoded);
50}
51
52test {
53 _ = Decoder;
54 _ = Encoder;
55}
lib/std/crypto/asn1/der/ArrayListReverse.zig created+97
...@@ -0,0 +1,97 @@
1//! An ArrayList that grows backwards. Counts nested prefix length fields
2//! in O(n) instead of O(n^depth) at the cost of extra buffering.
3//!
4//! Laid out in memory like:
5//! capacity |--------------------------|
6//! data |-------------|
7data: []u8,
8capacity: usize,
9allocator: Allocator,
10
11const ArrayListReverse = @This();
12const Error = Allocator.Error;
13
14pub fn init(allocator: Allocator) ArrayListReverse {
15 return .{ .data = &.{}, .capacity = 0, .allocator = allocator };
16}
17
18pub fn deinit(self: *ArrayListReverse) void {
19 self.allocator.free(self.allocatedSlice());
20}
21
22pub fn ensureCapacity(self: *ArrayListReverse, new_capacity: usize) Error!void {
23 if (self.capacity >= new_capacity) return;
24
25 const old_memory = self.allocatedSlice();
26 // Just make a new allocation to not worry about aliasing.
27 const new_memory = try self.allocator.alloc(u8, new_capacity);
28 @memcpy(new_memory[new_capacity - self.data.len ..], self.data);
29 self.allocator.free(old_memory);
30 self.data.ptr = new_memory.ptr + new_capacity - self.data.len;
31 self.capacity = new_memory.len;
32}
33
34pub fn prependSlice(self: *ArrayListReverse, data: []const u8) Error!void {
35 try self.ensureCapacity(self.data.len + data.len);
36 const old_len = self.data.len;
37 const new_len = old_len + data.len;
38 assert(new_len <= self.capacity);
39 self.data.len = new_len;
40
41 const end = self.data.ptr;
42 const begin = end - data.len;
43 const slice = begin[0..data.len];
44 @memcpy(slice, data);
45 self.data.ptr = begin;
46}
47
48pub const Writer = std.io.Writer(*ArrayListReverse, Error, prependSliceSize);
49/// Warning: This writer writes backwards. `fn print` will NOT work as expected.
50pub fn writer(self: *ArrayListReverse) Writer {
51 return .{ .context = self };
52}
53
54fn prependSliceSize(self: *ArrayListReverse, data: []const u8) Error!usize {
55 try self.prependSlice(data);
56 return data.len;
57}
58
59fn allocatedSlice(self: *ArrayListReverse) []u8 {
60 return (self.data.ptr + self.data.len - self.capacity)[0..self.capacity];
61}
62
63/// Invalidates all element pointers.
64pub fn clearAndFree(self: *ArrayListReverse) void {
65 self.allocator.free(self.allocatedSlice());
66 self.data.len = 0;
67 self.capacity = 0;
68}
69
70/// The caller owns the returned memory.
71/// Capacity is cleared, making deinit() safe but unnecessary to call.
72pub fn toOwnedSlice(self: *ArrayListReverse) Error![]u8 {
73 const new_memory = try self.allocator.alloc(u8, self.data.len);
74 @memcpy(new_memory, self.data);
75 @memset(self.data, undefined);
76 self.clearAndFree();
77 return new_memory;
78}
79
80const std = @import("std");
81const Allocator = std.mem.Allocator;
82const assert = std.debug.assert;
83const testing = std.testing;
84
85test ArrayListReverse {
86 var b = ArrayListReverse.init(testing.allocator);
87 defer b.deinit();
88 const data: []const u8 = &.{ 4, 5, 6 };
89 try b.prependSlice(data);
90 try testing.expectEqual(data.len, b.data.len);
91 try testing.expectEqualSlices(u8, data, b.data);
92
93 const data2: []const u8 = &.{ 1, 2, 3 };
94 try b.prependSlice(data2);
95 try testing.expectEqual(data.len + data2.len, b.data.len);
96 try testing.expectEqualSlices(u8, data2 ++ data, b.data);
97}
lib/std/crypto/asn1/der/Decoder.zig created+172
...@@ -0,0 +1,172 @@
1//! A secure DER parser that:
2//! - Prefers calling `fn decodeDer(self: @This(), decoder: *der.Decoder)`
3//! - Does NOT allocate. If you wish to parse lists you can do so lazily
4//! with an opaque type.
5//! - Does NOT read memory outside `bytes`.
6//! - Does NOT return elements with slices outside `bytes`.
7//! - Errors on values that do NOT follow DER rules:
8//! - Lengths that could be represented in a shorter form.
9//! - Booleans that are not 0xff or 0x00.
10bytes: []const u8,
11index: Index = 0,
12/// The field tag of the most recently visited field.
13/// This is needed because we might visit an implicitly tagged container with a `fn decodeDer`.
14field_tag: ?FieldTag = null,
15
16/// Expect a value.
17pub fn any(self: *Decoder, comptime T: type) !T {
18 if (std.meta.hasFn(T, "decodeDer")) return try T.decodeDer(self);
19
20 const tag = Tag.fromZig(T).toExpected();
21 switch (@typeInfo(T)) {
22 .Struct => {
23 const ele = try self.element(tag);
24 defer self.index = ele.slice.end; // don't force parsing all fields
25
26 var res: T = undefined;
27
28 inline for (std.meta.fields(T)) |f| {
29 self.field_tag = FieldTag.fromContainer(T, f.name);
30
31 if (self.field_tag) |ft| {
32 if (ft.explicit) {
33 const seq = try self.element(ft.toTag().toExpected());
34 self.index = seq.slice.start;
35 self.field_tag = null;
36 }
37 }
38
39 @field(res, f.name) = self.any(f.type) catch |err| brk: {
40 if (f.default_value) |d| {
41 break :brk @as(*const f.type, @alignCast(@ptrCast(d))).*;
42 }
43 return err;
44 };
45 // DER encodes null values by skipping them.
46 if (@typeInfo(f.type) == .Optional and @field(res, f.name) == null) {
47 if (f.default_value) |d| {
48 @field(res, f.name) = @as(*const f.type, @alignCast(@ptrCast(d))).*;
49 }
50 }
51 }
52
53 return res;
54 },
55 .Bool => {
56 const ele = try self.element(tag);
57 const bytes = self.view(ele);
58 if (bytes.len != 1) return error.InvalidBool;
59
60 return switch (bytes[0]) {
61 0x00 => false,
62 0xff => true,
63 else => error.InvalidBool,
64 };
65 },
66 .Int => {
67 const ele = try self.element(tag);
68 const bytes = self.view(ele);
69 return try int(T, bytes);
70 },
71 .Enum => |e| {
72 const ele = try self.element(tag);
73 const bytes = self.view(ele);
74 if (@hasDecl(T, "oids")) {
75 return T.oids.oidToEnum(bytes) orelse return error.UnknownOid;
76 }
77 return @enumFromInt(try int(e.tag_type, bytes));
78 },
79 .Optional => |o| return self.any(o.child) catch return null,
80 else => @compileError("cannot decode type " ++ @typeName(T)),
81 }
82}
83
84//// Expect a sequence.
85pub fn sequence(self: *Decoder) !Element {
86 return try self.element(ExpectedTag.init(.sequence, true, .universal));
87}
88
89//// Expect an element.
90pub fn element(
91 self: *Decoder,
92 expected: ExpectedTag,
93) (error{ EndOfStream, UnexpectedElement } || Element.DecodeError)!Element {
94 if (self.index >= self.bytes.len) return error.EndOfStream;
95
96 const res = try Element.decode(self.bytes, self.index);
97 var e = expected;
98 if (self.field_tag) |ft| {
99 e.number = @enumFromInt(ft.number);
100 e.class = ft.class;
101 }
102 if (!e.match(res.tag)) {
103 return error.UnexpectedElement;
104 }
105
106 self.index = if (res.tag.constructed) res.slice.start else res.slice.end;
107 return res;
108}
109
110/// View of element bytes.
111pub fn view(self: Decoder, elem: Element) []const u8 {
112 return elem.slice.view(self.bytes);
113}
114
115fn int(comptime T: type, value: []const u8) error{ NonCanonical, LargeValue }!T {
116 if (@typeInfo(T).Int.bits % 8 != 0) @compileError("T must be byte aligned");
117
118 var bytes = value;
119 if (bytes.len >= 2) {
120 if (bytes[0] == 0) {
121 if (@clz(bytes[1]) > 0) return error.NonCanonical;
122 bytes.ptr += 1;
123 }
124 if (bytes[0] == 0xff and @clz(bytes[1]) == 0) return error.NonCanonical;
125 }
126
127 if (bytes.len > @sizeOf(T)) return error.LargeValue;
128 if (@sizeOf(T) == 1) return @bitCast(bytes[0]);
129
130 return std.mem.readVarInt(T, bytes, .big);
131}
132
133test int {
134 try expectEqual(@as(u8, 1), try int(u8, &[_]u8{1}));
135 try expectError(error.NonCanonical, int(u8, &[_]u8{ 0, 1 }));
136 try expectError(error.NonCanonical, int(u8, &[_]u8{ 0xff, 0xff }));
137
138 const big = [_]u8{ 0xef, 0xff };
139 try expectError(error.LargeValue, int(u8, &big));
140 try expectEqual(0xefff, int(u16, &big));
141}
142
143test Decoder {
144 var parser = Decoder{ .bytes = @embedFile("./testdata/id_ecc.pub.der") };
145 const seq = try parser.sequence();
146
147 {
148 const seq2 = try parser.sequence();
149 _ = try parser.element(ExpectedTag.init(.oid, false, .universal));
150 _ = try parser.element(ExpectedTag.init(.oid, false, .universal));
151
152 try std.testing.expectEqual(parser.index, seq2.slice.end);
153 }
154 _ = try parser.element(ExpectedTag.init(.bitstring, false, .universal));
155
156 try std.testing.expectEqual(parser.index, seq.slice.end);
157 try std.testing.expectEqual(parser.index, parser.bytes.len);
158}
159
160const std = @import("std");
161const builtin = @import("builtin");
162const asn1 = @import("../../asn1.zig");
163const Oid = @import("../Oid.zig");
164
165const expectEqual = std.testing.expectEqual;
166const expectError = std.testing.expectError;
167const Decoder = @This();
168const Index = asn1.Index;
169const Tag = asn1.Tag;
170const FieldTag = asn1.FieldTag;
171const ExpectedTag = asn1.ExpectedTag;
172const Element = asn1.Element;
lib/std/crypto/asn1/der/Encoder.zig created+166
...@@ -0,0 +1,166 @@
1//! A buffered DER encoder.
2//!
3//! Prefers calling container's `fn encodeDer(self: @This(), encoder: *der.Encoder)`.
4//! That function should encode values, lengths, then tags.
5buffer: ArrayListReverse,
6/// The field tag set by a parent container.
7/// This is needed because we might visit an implicitly tagged container with a `fn encodeDer`.
8field_tag: ?FieldTag = null,
9
10pub fn init(allocator: std.mem.Allocator) Encoder {
11 return Encoder{ .buffer = ArrayListReverse.init(allocator) };
12}
13
14pub fn deinit(self: *Encoder) void {
15 self.buffer.deinit();
16}
17
18/// Encode any value.
19pub fn any(self: *Encoder, val: anytype) !void {
20 const T = @TypeOf(val);
21 try self.anyTag(Tag.fromZig(T), val);
22}
23
24fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void {
25 const T = @TypeOf(val);
26 if (std.meta.hasFn(T, "encodeDer")) return try val.encodeDer(self);
27 const start = self.buffer.data.len;
28 const merged_tag = self.mergedTag(tag_);
29
30 switch (@typeInfo(T)) {
31 .Struct => |info| {
32 inline for (0..info.fields.len) |i| {
33 const f = info.fields[info.fields.len - i - 1];
34 const field_val = @field(val, f.name);
35 const field_tag = FieldTag.fromContainer(T, f.name);
36
37 // > The encoding of a set value or sequence value shall not include an encoding for any
38 // > component value which is equal to its default value.
39 const is_default = if (f.is_comptime) false else if (f.default_value) |v| brk: {
40 const default_val: *const f.type = @alignCast(@ptrCast(v));
41 break :brk std.mem.eql(u8, std.mem.asBytes(default_val), std.mem.asBytes(&field_val));
42 } else false;
43
44 if (!is_default) {
45 const start2 = self.buffer.data.len;
46 self.field_tag = field_tag;
47 // will merge with self.field_tag.
48 // may mutate self.field_tag.
49 try self.anyTag(Tag.fromZig(f.type), field_val);
50 if (field_tag) |ft| {
51 if (ft.explicit) {
52 try self.length(self.buffer.data.len - start2);
53 try self.tag(ft.toTag());
54 self.field_tag = null;
55 }
56 }
57 }
58 }
59 },
60 .Bool => try self.buffer.prependSlice(&[_]u8{if (val) 0xff else 0}),
61 .Int => try self.int(T, val),
62 .Enum => |e| {
63 if (@hasDecl(T, "oids")) {
64 return self.any(T.oids.enumToOid(val));
65 } else {
66 try self.int(e.tag_type, @intFromEnum(val));
67 }
68 },
69 .Optional => if (val) |v| return try self.anyTag(tag_, v),
70 .Null => {},
71 else => @compileError("cannot encode type " ++ @typeName(T)),
72 }
73
74 try self.length(self.buffer.data.len - start);
75 try self.tag(merged_tag);
76}
77
78/// Encode a tag.
79pub fn tag(self: *Encoder, tag_: Tag) !void {
80 const t = self.mergedTag(tag_);
81 try t.encode(self.writer());
82}
83
84fn mergedTag(self: *Encoder, tag_: Tag) Tag {
85 var res = tag_;
86 if (self.field_tag) |ft| {
87 if (!ft.explicit) {
88 res.number = @enumFromInt(ft.number);
89 res.class = ft.class;
90 }
91 }
92 return res;
93}
94
95/// Encode a length.
96pub fn length(self: *Encoder, len: usize) !void {
97 const writer_ = self.writer();
98 if (len < 128) {
99 try writer_.writeInt(u8, @intCast(len), .big);
100 return;
101 }
102 inline for ([_]type{ u8, u16, u32 }) |T| {
103 if (len < std.math.maxInt(T)) {
104 try writer_.writeInt(T, @intCast(len), .big);
105 try writer_.writeInt(u8, @sizeOf(T) | 0x80, .big);
106 return;
107 }
108 }
109 return error.InvalidLength;
110}
111
112/// Encode a tag and length-prefixed bytes.
113pub fn tagBytes(self: *Encoder, tag_: Tag, bytes: []const u8) !void {
114 try self.buffer.prependSlice(bytes);
115 try self.length(bytes.len);
116 try self.tag(tag_);
117}
118
119/// Warning: This writer writes backwards. `fn print` will NOT work as expected.
120pub fn writer(self: *Encoder) ArrayListReverse.Writer {
121 return self.buffer.writer();
122}
123
124fn int(self: *Encoder, comptime T: type, value: T) !void {
125 const big = std.mem.nativeTo(T, value, .big);
126 const big_bytes = std.mem.asBytes(&big);
127
128 const bits_needed = @bitSizeOf(T) - @clz(value);
129 const needs_padding: u1 = if (value == 0)
130 1
131 else if (bits_needed > 8) brk: {
132 const RightShift = std.meta.Int(.unsigned, @bitSizeOf(@TypeOf(bits_needed)) - 1);
133 const right_shift: RightShift = @intCast(bits_needed - 9);
134 break :brk if (value >> right_shift == 0x1ff) 1 else 0;
135 } else 0;
136 const bytes_needed = try std.math.divCeil(usize, bits_needed, 8) + needs_padding;
137
138 const writer_ = self.writer();
139 for (0..bytes_needed - needs_padding) |i| try writer_.writeByte(big_bytes[big_bytes.len - i - 1]);
140 if (needs_padding == 1) try writer_.writeByte(0);
141}
142
143test int {
144 const allocator = std.testing.allocator;
145 var encoder = Encoder.init(allocator);
146 defer encoder.deinit();
147
148 try encoder.int(u8, 0);
149 try std.testing.expectEqualSlices(u8, &[_]u8{0}, encoder.buffer.data);
150
151 encoder.buffer.clearAndFree();
152 try encoder.int(u16, 0x00ff);
153 try std.testing.expectEqualSlices(u8, &[_]u8{0xff}, encoder.buffer.data);
154
155 encoder.buffer.clearAndFree();
156 try encoder.int(u32, 0xffff);
157 try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0xff, 0xff }, encoder.buffer.data);
158}
159
160const std = @import("std");
161const Oid = @import("../Oid.zig");
162const asn1 = @import("../../asn1.zig");
163const ArrayListReverse = @import("./ArrayListReverse.zig");
164const Tag = asn1.Tag;
165const FieldTag = asn1.FieldTag;
166const Encoder = @This();
lib/std/crypto/asn1/der/testdata/all_types.der created
Binary files /dev/null and b/lib/std/crypto/asn1/der/testdata/all_types.der differ
lib/std/crypto/asn1/der/testdata/id_ecc.pub.der created
Binary files /dev/null and b/lib/std/crypto/asn1/der/testdata/id_ecc.pub.der differ
lib/std/crypto/asn1/test.zig created+80
...@@ -0,0 +1,80 @@
1const std = @import("std");
2const asn1 = @import("../asn1.zig");
3
4const der = asn1.der;
5const Tag = asn1.Tag;
6const FieldTag = asn1.FieldTag;
7
8/// An example that uses all ASN1 types and available implementation features.
9const AllTypes = struct {
10 a: u8 = 0,
11 b: asn1.BitString,
12 c: C,
13 d: asn1.Opaque(Tag.universal(.string_utf8, false)),
14 e: asn1.Opaque(Tag.universal(.octetstring, false)),
15 f: ?u16,
16 g: ?Nested,
17 h: asn1.Any,
18
19 pub const asn1_tags = .{
20 .a = FieldTag.explicit(0, .context_specific),
21 .b = FieldTag.explicit(1, .context_specific),
22 .c = FieldTag.implicit(2, .context_specific),
23 .g = FieldTag.implicit(3, .context_specific),
24 };
25
26 const C = enum {
27 a,
28 b,
29
30 pub const oids = asn1.Oid.StaticMap(@This()).initComptime(.{
31 .a = "1.2.3.4",
32 .b = "1.2.3.5",
33 });
34 };
35
36 const Nested = struct {
37 inner: Asn1T,
38 sum: i16,
39
40 const Asn1T = struct { a: u8, b: i16 };
41
42 pub fn decodeDer(decoder: *der.Decoder) !Nested {
43 const inner = try decoder.any(Asn1T);
44 return Nested{ .inner = inner, .sum = inner.a + inner.b };
45 }
46
47 pub fn encodeDer(self: Nested, encoder: *der.Encoder) !void {
48 try encoder.any(self.inner);
49 }
50 };
51};
52
53test AllTypes {
54 const expected = AllTypes{
55 .a = 2,
56 .b = asn1.BitString{ .bytes = &[_]u8{ 0x04, 0xa0 } },
57 .c = .a,
58 .d = .{ .bytes = "asdf" },
59 .e = .{ .bytes = "fdsa" },
60 .f = (1 << 8) + 1,
61 .g = .{ .inner = .{ .a = 4, .b = 5 }, .sum = 9 },
62 .h = .{ .tag = Tag.init(.string_ia5, false, .universal), .bytes = "asdf" },
63 };
64 // https://lapo.it/asn1js/#MC-gAwIBAqEFAwMABKCCAyoDBAwEYXNkZgQEZmRzYQICAQGjBgIBBAIBBRYEYXNkZg
65 const path = "./der/testdata/all_types.der";
66 const encoded = @embedFile(path);
67 const actual = try asn1.der.decode(AllTypes, encoded);
68 try std.testing.expectEqualDeep(expected, actual);
69
70 const allocator = std.testing.allocator;
71 const buf = try asn1.der.encode(allocator, expected);
72 defer allocator.free(buf);
73 try std.testing.expectEqualSlices(u8, encoded, buf);
74
75 // Use this to update test file.
76 // const dir = try std.fs.cwd().openDir("lib/std/crypto/asn1", .{});
77 // var file = try dir.createFile(path, .{});
78 // defer file.close();
79 // try file.writeAll(buf);
80}