Skip to content

Commit ed7004a

Browse files
authored
Merge pull request ziglang#19976 from clickingbuttons/asn1
std.crypto: Add ASN1 module with OIDs and DER
2 parents 6769806 + a51bc1d commit ed7004a

File tree

10 files changed

+1141
-0
lines changed

10 files changed

+1141
-0
lines changed

lib/std/crypto.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ pub const errors = @import("crypto/errors.zig");
220220

221221
pub const tls = @import("crypto/tls.zig");
222222
pub const Certificate = @import("crypto/Certificate.zig");
223+
pub const asn1 = @import("crypto/asn1.zig");
223224

224225
/// Side-channels mitigations.
225226
pub const SideChannelsMitigations = enum {
@@ -334,6 +335,7 @@ test {
334335
_ = errors;
335336
_ = tls;
336337
_ = Certificate;
338+
_ = asn1;
337339
}
338340

339341
test "CSPRNG" {

lib/std/crypto/asn1.zig

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
//! ASN.1 types for public consumption.
2+
const std = @import("std");
3+
pub const der = @import("./asn1/der.zig");
4+
pub const Oid = @import("./asn1/Oid.zig");
5+
6+
pub const Index = u32;
7+
8+
pub 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+
162+
test 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.
170+
pub 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+
222+
test 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.
237+
pub 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+
264+
pub 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+
290+
pub 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+
324+
pub 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.
341+
pub 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+
355+
test {
356+
_ = der;
357+
_ = Oid;
358+
_ = @import("asn1/test.zig");
359+
}

0 commit comments

Comments
 (0)