authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-04-12 20:13:45+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-04-12 20:13:45+02:00
loga7122b73231808a8a07b79c84e1eaac9cf4c28aa
treeedafeb2bbf26110b704a23167f9cb08008460b4b
parent9352f379e8a08bcc5a3bfc851bfb6c6a662000af
signaturebadge-check Signed by PGP key B5690EEEBB952194

std.crypto: add constant-time codecs (#23420)

std.crypto: add constant-time codecs Add constant-time hex/base64 codecs designed to process cryptographic secrets, adapted from libsodium's implementations. Introduce a `crypto.codecs` namespace for crypto-related encoders and decoders. Move ASN.1 codecs to this namespace. This will also naturally accommodate the proposed PEM codecs.

22 files changed, 1608 insertions(+), 1139 deletions(-)

.gitattributes+1
...@@ -5,6 +5,7 @@ langref.html.in text eol=lf...@@ -5,6 +5,7 @@ langref.html.in text eol=lf
5lib/std/compress/testdata/** binary5lib/std/compress/testdata/** binary
6lib/std/compress/deflate/testdata/** binary6lib/std/compress/deflate/testdata/** binary
7lib/std/compress/flate/testdata/** binary7lib/std/compress/flate/testdata/** binary
8lib/std/crypto/codecs/asn1/der/testdata/** binary
89
9lib/include/** linguist-vendored10lib/include/** linguist-vendored
10lib/libc/** linguist-vendored11lib/libc/** linguist-vendored
lib/std/crypto.zig+4-2
...@@ -214,13 +214,15 @@ pub const ff = @import("crypto/ff.zig");...@@ -214,13 +214,15 @@ pub const ff = @import("crypto/ff.zig");
214/// This is a thread-local, cryptographically secure pseudo random number generator.214/// This is a thread-local, cryptographically secure pseudo random number generator.
215pub const random = @import("crypto/tlcsprng.zig").interface;215pub const random = @import("crypto/tlcsprng.zig").interface;
216216
217/// Encoding and decoding
218pub const codecs = @import("crypto/codecs.zig");
219
217const std = @import("std.zig");220const std = @import("std.zig");
218221
219pub const errors = @import("crypto/errors.zig");222pub const errors = @import("crypto/errors.zig");
220223
221pub const tls = @import("crypto/tls.zig");224pub const tls = @import("crypto/tls.zig");
222pub const Certificate = @import("crypto/Certificate.zig");225pub const Certificate = @import("crypto/Certificate.zig");
223pub const asn1 = @import("crypto/asn1.zig");
224226
225/// Side-channels mitigations.227/// Side-channels mitigations.
226pub const SideChannelsMitigations = enum {228pub const SideChannelsMitigations = enum {
...@@ -335,7 +337,7 @@ test {...@@ -335,7 +337,7 @@ test {
335 _ = errors;337 _ = errors;
336 _ = tls;338 _ = tls;
337 _ = Certificate;339 _ = Certificate;
338 _ = asn1;340 _ = codecs;
339}341}
340342
341test "CSPRNG" {343test "CSPRNG" {
lib/std/crypto/asn1.zig deleted-359
...@@ -1,359 +0,0 @@
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 initExplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag {
270 return .{ .number = number, .class = class, .explicit = true };
271 }
272
273 pub fn initImplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag {
274 return .{ .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 deleted-210
...@@ -1,210 +0,0 @@
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 deleted-55
...@@ -1,55 +0,0 @@
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 deleted-97
...@@ -1,97 +0,0 @@
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 deleted-170
...@@ -1,170 +0,0 @@
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.defaultValue()) |d| {
41 break :brk 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.defaultValue()) |d| @field(res, f.name) = d;
48 }
49 }
50
51 return res;
52 },
53 .bool => {
54 const ele = try self.element(tag);
55 const bytes = self.view(ele);
56 if (bytes.len != 1) return error.InvalidBool;
57
58 return switch (bytes[0]) {
59 0x00 => false,
60 0xff => true,
61 else => error.InvalidBool,
62 };
63 },
64 .int => {
65 const ele = try self.element(tag);
66 const bytes = self.view(ele);
67 return try int(T, bytes);
68 },
69 .@"enum" => |e| {
70 const ele = try self.element(tag);
71 const bytes = self.view(ele);
72 if (@hasDecl(T, "oids")) {
73 return T.oids.oidToEnum(bytes) orelse return error.UnknownOid;
74 }
75 return @enumFromInt(try int(e.tag_type, bytes));
76 },
77 .optional => |o| return self.any(o.child) catch return null,
78 else => @compileError("cannot decode type " ++ @typeName(T)),
79 }
80}
81
82//// Expect a sequence.
83pub fn sequence(self: *Decoder) !Element {
84 return try self.element(ExpectedTag.init(.sequence, true, .universal));
85}
86
87//// Expect an element.
88pub fn element(
89 self: *Decoder,
90 expected: ExpectedTag,
91) (error{ EndOfStream, UnexpectedElement } || Element.DecodeError)!Element {
92 if (self.index >= self.bytes.len) return error.EndOfStream;
93
94 const res = try Element.decode(self.bytes, self.index);
95 var e = expected;
96 if (self.field_tag) |ft| {
97 e.number = @enumFromInt(ft.number);
98 e.class = ft.class;
99 }
100 if (!e.match(res.tag)) {
101 return error.UnexpectedElement;
102 }
103
104 self.index = if (res.tag.constructed) res.slice.start else res.slice.end;
105 return res;
106}
107
108/// View of element bytes.
109pub fn view(self: Decoder, elem: Element) []const u8 {
110 return elem.slice.view(self.bytes);
111}
112
113fn int(comptime T: type, value: []const u8) error{ NonCanonical, LargeValue }!T {
114 if (@typeInfo(T).int.bits % 8 != 0) @compileError("T must be byte aligned");
115
116 var bytes = value;
117 if (bytes.len >= 2) {
118 if (bytes[0] == 0) {
119 if (@clz(bytes[1]) > 0) return error.NonCanonical;
120 bytes.ptr += 1;
121 }
122 if (bytes[0] == 0xff and @clz(bytes[1]) == 0) return error.NonCanonical;
123 }
124
125 if (bytes.len > @sizeOf(T)) return error.LargeValue;
126 if (@sizeOf(T) == 1) return @bitCast(bytes[0]);
127
128 return std.mem.readVarInt(T, bytes, .big);
129}
130
131test int {
132 try expectEqual(@as(u8, 1), try int(u8, &[_]u8{1}));
133 try expectError(error.NonCanonical, int(u8, &[_]u8{ 0, 1 }));
134 try expectError(error.NonCanonical, int(u8, &[_]u8{ 0xff, 0xff }));
135
136 const big = [_]u8{ 0xef, 0xff };
137 try expectError(error.LargeValue, int(u8, &big));
138 try expectEqual(0xefff, int(u16, &big));
139}
140
141test Decoder {
142 var parser = Decoder{ .bytes = @embedFile("./testdata/id_ecc.pub.der") };
143 const seq = try parser.sequence();
144
145 {
146 const seq2 = try parser.sequence();
147 _ = try parser.element(ExpectedTag.init(.oid, false, .universal));
148 _ = try parser.element(ExpectedTag.init(.oid, false, .universal));
149
150 try std.testing.expectEqual(parser.index, seq2.slice.end);
151 }
152 _ = try parser.element(ExpectedTag.init(.bitstring, false, .universal));
153
154 try std.testing.expectEqual(parser.index, seq.slice.end);
155 try std.testing.expectEqual(parser.index, parser.bytes.len);
156}
157
158const std = @import("std");
159const builtin = @import("builtin");
160const asn1 = @import("../../asn1.zig");
161const Oid = @import("../Oid.zig");
162
163const expectEqual = std.testing.expectEqual;
164const expectError = std.testing.expectError;
165const Decoder = @This();
166const Index = asn1.Index;
167const Tag = asn1.Tag;
168const FieldTag = asn1.FieldTag;
169const ExpectedTag = asn1.ExpectedTag;
170const Element = asn1.Element;
lib/std/crypto/asn1/der/Encoder.zig deleted-166
...@@ -1,166 +0,0 @@
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_ptr) |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 deleted
Binary files a/lib/std/crypto/asn1/der/testdata/all_types.der and /dev/null differ
lib/std/crypto/asn1/der/testdata/id_ecc.pub.der deleted
Binary files a/lib/std/crypto/asn1/der/testdata/id_ecc.pub.der and /dev/null differ
lib/std/crypto/asn1/test.zig deleted-80
...@@ -1,80 +0,0 @@
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.initExplicit(0, .context_specific),
21 .b = FieldTag.initExplicit(1, .context_specific),
22 .c = FieldTag.initImplicit(2, .context_specific),
23 .g = FieldTag.initImplicit(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}
lib/std/crypto/codecs.zig created+3
...@@ -0,0 +1,3 @@
1pub const asn1 = @import("codecs/asn1.zig");
2pub const Base64 = @import("codecs/base64_hex_ct.zig").Base64;
3pub const Hex = @import("codecs/base64_hex_ct.zig").Hex;
lib/std/crypto/codecs/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 initExplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag {
270 return .{ .number = number, .class = class, .explicit = true };
271 }
272
273 pub fn initImplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag {
274 return .{ .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/codecs/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/codecs/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/codecs/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/codecs/asn1/der/Decoder.zig created+170
...@@ -0,0 +1,170 @@
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.defaultValue()) |d| {
41 break :brk 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.defaultValue()) |d| @field(res, f.name) = d;
48 }
49 }
50
51 return res;
52 },
53 .bool => {
54 const ele = try self.element(tag);
55 const bytes = self.view(ele);
56 if (bytes.len != 1) return error.InvalidBool;
57
58 return switch (bytes[0]) {
59 0x00 => false,
60 0xff => true,
61 else => error.InvalidBool,
62 };
63 },
64 .int => {
65 const ele = try self.element(tag);
66 const bytes = self.view(ele);
67 return try int(T, bytes);
68 },
69 .@"enum" => |e| {
70 const ele = try self.element(tag);
71 const bytes = self.view(ele);
72 if (@hasDecl(T, "oids")) {
73 return T.oids.oidToEnum(bytes) orelse return error.UnknownOid;
74 }
75 return @enumFromInt(try int(e.tag_type, bytes));
76 },
77 .optional => |o| return self.any(o.child) catch return null,
78 else => @compileError("cannot decode type " ++ @typeName(T)),
79 }
80}
81
82//// Expect a sequence.
83pub fn sequence(self: *Decoder) !Element {
84 return try self.element(ExpectedTag.init(.sequence, true, .universal));
85}
86
87//// Expect an element.
88pub fn element(
89 self: *Decoder,
90 expected: ExpectedTag,
91) (error{ EndOfStream, UnexpectedElement } || Element.DecodeError)!Element {
92 if (self.index >= self.bytes.len) return error.EndOfStream;
93
94 const res = try Element.decode(self.bytes, self.index);
95 var e = expected;
96 if (self.field_tag) |ft| {
97 e.number = @enumFromInt(ft.number);
98 e.class = ft.class;
99 }
100 if (!e.match(res.tag)) {
101 return error.UnexpectedElement;
102 }
103
104 self.index = if (res.tag.constructed) res.slice.start else res.slice.end;
105 return res;
106}
107
108/// View of element bytes.
109pub fn view(self: Decoder, elem: Element) []const u8 {
110 return elem.slice.view(self.bytes);
111}
112
113fn int(comptime T: type, value: []const u8) error{ NonCanonical, LargeValue }!T {
114 if (@typeInfo(T).int.bits % 8 != 0) @compileError("T must be byte aligned");
115
116 var bytes = value;
117 if (bytes.len >= 2) {
118 if (bytes[0] == 0) {
119 if (@clz(bytes[1]) > 0) return error.NonCanonical;
120 bytes.ptr += 1;
121 }
122 if (bytes[0] == 0xff and @clz(bytes[1]) == 0) return error.NonCanonical;
123 }
124
125 if (bytes.len > @sizeOf(T)) return error.LargeValue;
126 if (@sizeOf(T) == 1) return @bitCast(bytes[0]);
127
128 return std.mem.readVarInt(T, bytes, .big);
129}
130
131test int {
132 try expectEqual(@as(u8, 1), try int(u8, &[_]u8{1}));
133 try expectError(error.NonCanonical, int(u8, &[_]u8{ 0, 1 }));
134 try expectError(error.NonCanonical, int(u8, &[_]u8{ 0xff, 0xff }));
135
136 const big = [_]u8{ 0xef, 0xff };
137 try expectError(error.LargeValue, int(u8, &big));
138 try expectEqual(0xefff, int(u16, &big));
139}
140
141test Decoder {
142 var parser = Decoder{ .bytes = @embedFile("./testdata/id_ecc.pub.der") };
143 const seq = try parser.sequence();
144
145 {
146 const seq2 = try parser.sequence();
147 _ = try parser.element(ExpectedTag.init(.oid, false, .universal));
148 _ = try parser.element(ExpectedTag.init(.oid, false, .universal));
149
150 try std.testing.expectEqual(parser.index, seq2.slice.end);
151 }
152 _ = try parser.element(ExpectedTag.init(.bitstring, false, .universal));
153
154 try std.testing.expectEqual(parser.index, seq.slice.end);
155 try std.testing.expectEqual(parser.index, parser.bytes.len);
156}
157
158const std = @import("std");
159const builtin = @import("builtin");
160const asn1 = @import("../../asn1.zig");
161const Oid = @import("../Oid.zig");
162
163const expectEqual = std.testing.expectEqual;
164const expectError = std.testing.expectError;
165const Decoder = @This();
166const Index = asn1.Index;
167const Tag = asn1.Tag;
168const FieldTag = asn1.FieldTag;
169const ExpectedTag = asn1.ExpectedTag;
170const Element = asn1.Element;
lib/std/crypto/codecs/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_ptr) |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/codecs/asn1/der/testdata/all_types.der created
Binary files /dev/null and b/lib/std/crypto/codecs/asn1/der/testdata/all_types.der differ
lib/std/crypto/codecs/asn1/der/testdata/id_ecc.pub.der created
Binary files /dev/null and b/lib/std/crypto/codecs/asn1/der/testdata/id_ecc.pub.der differ
lib/std/crypto/codecs/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.initExplicit(0, .context_specific),
21 .b = FieldTag.initExplicit(1, .context_specific),
22 .c = FieldTag.initImplicit(2, .context_specific),
23 .g = FieldTag.initImplicit(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}
lib/std/crypto/codecs/base64_hex_ct.zig created+463
...@@ -0,0 +1,463 @@
1//! Hexadecimal and Base64 codecs designed for cryptographic use.
2//! This file provides (best-effort) constant-time encoding and decoding functions for hexadecimal and Base64 formats.
3//! This is designed to be used in cryptographic applications where timing attacks are a concern.
4const std = @import("std");
5const testing = std.testing;
6const StaticBitSet = std.StaticBitSet;
7
8pub const Error = error{
9 /// An invalid character was found in the input.
10 InvalidCharacter,
11 /// The input is not properly padded.
12 InvalidPadding,
13 /// The input buffer is too small to hold the output.
14 NoSpaceLeft,
15 /// The input and output buffers are not the same size.
16 SizeMismatch,
17};
18
19/// (best-effort) constant time hexadecimal encoding and decoding.
20pub const hex = struct {
21 /// Encodes a binary buffer into a hexadecimal string.
22 /// The output buffer must be twice the size of the input buffer.
23 pub fn encode(encoded: []u8, bin: []const u8, comptime case: std.fmt.Case) error{SizeMismatch}!void {
24 if (encoded.len / 2 != bin.len) {
25 return error.SizeMismatch;
26 }
27 for (bin, 0..) |v, i| {
28 const b: u16 = v >> 4;
29 const c: u16 = v & 0xf;
30 const off = if (case == .upper) 32 else 0;
31 const x =
32 ((87 - off + c + (((c -% 10) >> 8) & ~@as(u16, 38 - off))) & 0xff) << 8 |
33 ((87 - off + b + (((b -% 10) >> 8) & ~@as(u16, 38 - off))) & 0xff);
34 encoded[i * 2] = @truncate(x);
35 encoded[i * 2 + 1] = @truncate(x >> 8);
36 }
37 }
38
39 /// Decodes a hexadecimal string into a binary buffer.
40 /// The output buffer must be half the size of the input buffer.
41 pub fn decode(bin: []u8, encoded: []const u8) error{ SizeMismatch, InvalidCharacter, InvalidPadding }!void {
42 if (encoded.len % 2 != 0) {
43 return error.InvalidPadding;
44 }
45 if (bin.len < encoded.len / 2) {
46 return error.SizeMismatch;
47 }
48 _ = decodeAny(bin, encoded, null) catch |err| {
49 switch (err) {
50 error.InvalidCharacter => return error.InvalidCharacter,
51 error.InvalidPadding => return error.InvalidPadding,
52 else => unreachable,
53 }
54 };
55 }
56
57 /// A decoder that ignores certain characters.
58 /// The decoder will skip any characters that are in the ignore list.
59 pub const DecoderWithIgnore = struct {
60 /// The characters to ignore.
61 ignored_chars: StaticBitSet(256) = undefined,
62
63 /// Decodes a hexadecimal string into a binary buffer.
64 /// The output buffer must be half the size of the input buffer.
65 pub fn decode(
66 self: DecoderWithIgnore,
67 bin: []u8,
68 encoded: []const u8,
69 ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 {
70 return decodeAny(bin, encoded, self.ignored_chars);
71 }
72
73 /// Returns the decoded length of a hexadecimal string, ignoring any characters in the ignore list.
74 /// This operation does not run in constant time, but it aims to avoid leaking information about the underlying hexadecimal string.
75 pub fn decodedLenForSlice(decoder: DecoderWithIgnore, encoded: []const u8) !usize {
76 var hex_len = encoded.len;
77 for (encoded) |c| {
78 if (decoder.ignored_chars.isSet(c)) hex_len -= 1;
79 }
80 if (hex_len % 2 != 0) {
81 return error.InvalidPadding;
82 }
83 return hex_len / 2;
84 }
85
86 /// Returns the maximum possible decoded size for a given input length after skipping ignored characters.
87 pub fn decodedLenUpperBound(hex_len: usize) usize {
88 return hex_len / 2;
89 }
90 };
91
92 /// Creates a new decoder that ignores certain characters.
93 /// The decoder will skip any characters that are in the ignore list.
94 /// The ignore list must not contain any valid hexadecimal characters.
95 pub fn decoderWithIgnore(ignore_chars: []const u8) error{InvalidCharacter}!DecoderWithIgnore {
96 var ignored_chars = StaticBitSet(256).initEmpty();
97 for (ignore_chars) |c| {
98 switch (c) {
99 '0'...'9', 'a'...'f', 'A'...'F' => return error.InvalidCharacter,
100 else => if (ignored_chars.isSet(c)) return error.InvalidCharacter,
101 }
102 ignored_chars.set(c);
103 }
104 return DecoderWithIgnore{ .ignored_chars = ignored_chars };
105 }
106
107 fn decodeAny(
108 bin: []u8,
109 encoded: []const u8,
110 ignored_chars: ?StaticBitSet(256),
111 ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 {
112 var bin_pos: usize = 0;
113 var state: bool = false;
114 var c_acc: u8 = 0;
115 for (encoded) |c| {
116 const c_num = c ^ 48;
117 const c_num0: u8 = @truncate((@as(u16, c_num) -% 10) >> 8);
118 const c_alpha: u8 = (c & ~@as(u8, 32)) -% 55;
119 const c_alpha0: u8 = @truncate(((@as(u16, c_alpha) -% 10) ^ (@as(u16, c_alpha) -% 16)) >> 8);
120 if ((c_num0 | c_alpha0) == 0) {
121 if (ignored_chars) |set| {
122 if (set.isSet(c)) {
123 continue;
124 }
125 }
126 return error.InvalidCharacter;
127 }
128 const c_val = (c_num0 & c_num) | (c_alpha0 & c_alpha);
129 if (bin_pos >= bin.len) {
130 return error.NoSpaceLeft;
131 }
132 if (!state) {
133 c_acc = c_val << 4;
134 } else {
135 bin[bin_pos] = c_acc | c_val;
136 bin_pos += 1;
137 }
138 state = !state;
139 }
140 if (state) {
141 return error.InvalidPadding;
142 }
143 return bin[0..bin_pos];
144 }
145};
146
147/// (best-effort) constant time base64 encoding and decoding.
148pub const base64 = struct {
149 /// The base64 variant to use.
150 pub const Variant = packed struct {
151 /// Use the URL-safe alphabet instead of the standard alphabet.
152 urlsafe_alphabet: bool = false,
153 /// Enable padding with '=' characters.
154 padding: bool = true,
155
156 /// The standard base64 variant.
157 pub const standard: Variant = .{ .urlsafe_alphabet = false, .padding = true };
158 /// The URL-safe base64 variant.
159 pub const urlsafe: Variant = .{ .urlsafe_alphabet = true, .padding = true };
160 /// The standard base64 variant without padding.
161 pub const standard_nopad: Variant = .{ .urlsafe_alphabet = false, .padding = false };
162 /// The URL-safe base64 variant without padding.
163 pub const urlsafe_nopad: Variant = .{ .urlsafe_alphabet = true, .padding = false };
164 };
165
166 /// Returns the length of the encoded base64 string for a given length.
167 pub fn encodedLen(bin_len: usize, variant: Variant) usize {
168 if (variant.padding) {
169 return (bin_len + 2) / 3 * 4;
170 } else {
171 const leftover = bin_len % 3;
172 return bin_len / 3 * 4 + (leftover * 4 + 2) / 3;
173 }
174 }
175
176 /// Returns the maximum possible decoded size for a given input length - The actual length may be less if the input includes padding.
177 /// `InvalidPadding` is returned if the input length is not valid.
178 pub fn decodedLen(b64_len: usize, variant: Variant) !usize {
179 var result = b64_len / 4 * 3;
180 const leftover = b64_len % 4;
181 if (variant.padding) {
182 if (leftover % 4 != 0) return error.InvalidPadding;
183 } else {
184 if (leftover % 4 == 1) return error.InvalidPadding;
185 result += leftover * 3 / 4;
186 }
187 return result;
188 }
189
190 /// Encodes a binary buffer into a base64 string.
191 /// The output buffer must be at least `encodedLen(bin.len)` bytes long.
192 pub fn encode(encoded: []u8, bin: []const u8, comptime variant: Variant) error{NoSpaceLeft}![]const u8 {
193 var acc_len: u4 = 0;
194 var b64_pos: usize = 0;
195 var acc: u16 = 0;
196 const nibbles = bin.len / 3;
197 const remainder = bin.len - 3 * nibbles;
198 var b64_len = nibbles * 4;
199 if (remainder != 0) {
200 b64_len += if (variant.padding) 4 else 2 + (remainder >> 1);
201 }
202 if (encoded.len < b64_len) {
203 return error.NoSpaceLeft;
204 }
205 const urlsafe = variant.urlsafe_alphabet;
206 for (bin) |v| {
207 acc = (acc << 8) + v;
208 acc_len += 8;
209 while (acc_len >= 6) {
210 acc_len -= 6;
211 encoded[b64_pos] = charFromByte(@as(u6, @truncate(acc >> acc_len)), urlsafe);
212 b64_pos += 1;
213 }
214 }
215 if (acc_len > 0) {
216 encoded[b64_pos] = charFromByte(@as(u6, @truncate(acc << (6 - acc_len))), urlsafe);
217 b64_pos += 1;
218 }
219 while (b64_pos < b64_len) {
220 encoded[b64_pos] = '=';
221 b64_pos += 1;
222 }
223 return encoded[0..b64_pos];
224 }
225
226 /// Decodes a base64 string into a binary buffer.
227 /// The output buffer must be at least `decodedLenUpperBound(encoded.len)` bytes long.
228 pub fn decode(bin: []u8, encoded: []const u8, comptime variant: Variant) error{ InvalidCharacter, InvalidPadding }![]const u8 {
229 return decodeAny(bin, encoded, variant, null) catch |err| {
230 switch (err) {
231 error.InvalidCharacter => return error.InvalidCharacter,
232 error.InvalidPadding => return error.InvalidPadding,
233 else => unreachable,
234 }
235 };
236 }
237
238 //// A decoder that ignores certain characters.
239 pub const DecoderWithIgnore = struct {
240 /// The characters to ignore.
241 ignored_chars: StaticBitSet(256) = undefined,
242
243 /// Decodes a base64 string into a binary buffer.
244 /// The output buffer must be at least `decodedLenUpperBound(encoded.len)` bytes long.
245 pub fn decode(
246 self: DecoderWithIgnore,
247 bin: []u8,
248 encoded: []const u8,
249 comptime variant: Variant,
250 ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 {
251 return decodeAny(bin, encoded, variant, self.ignored_chars);
252 }
253
254 /// Returns the decoded length of a base64 string, ignoring any characters in the ignore list.
255 /// This operation does not run in constant time, but it aims to avoid leaking information about the underlying base64 string.
256 pub fn decodedLenForSlice(decoder: DecoderWithIgnore, encoded: []const u8, variant: Variant) !usize {
257 var b64_len = encoded.len;
258 for (encoded) |c| {
259 if (decoder.ignored_chars.isSet(c)) b64_len -= 1;
260 }
261 return base64.decodedLen(b64_len, variant);
262 }
263
264 /// Returns the maximum possible decoded size for a given input length after skipping ignored characters.
265 pub fn decodedLenUpperBound(b64_len: usize) usize {
266 return b64_len / 3 * 4;
267 }
268 };
269
270 /// Creates a new decoder that ignores certain characters.
271 pub fn decoderWithIgnore(ignore_chars: []const u8) error{InvalidCharacter}!DecoderWithIgnore {
272 var ignored_chars = StaticBitSet(256).initEmpty();
273 for (ignore_chars) |c| {
274 switch (c) {
275 'A'...'Z', 'a'...'z', '0'...'9' => return error.InvalidCharacter,
276 else => if (ignored_chars.isSet(c)) return error.InvalidCharacter,
277 }
278 ignored_chars.set(c);
279 }
280 return DecoderWithIgnore{ .ignored_chars = ignored_chars };
281 }
282
283 inline fn eq(x: u8, y: u8) u8 {
284 return ~@as(u8, @truncate((0 -% (@as(u16, x) ^ @as(u16, y))) >> 8));
285 }
286
287 inline fn gt(x: u8, y: u8) u8 {
288 return @truncate((@as(u16, y) -% @as(u16, x)) >> 8);
289 }
290
291 inline fn ge(x: u8, y: u8) u8 {
292 return ~gt(y, x);
293 }
294
295 inline fn lt(x: u8, y: u8) u8 {
296 return gt(y, x);
297 }
298
299 inline fn le(x: u8, y: u8) u8 {
300 return ge(y, x);
301 }
302
303 inline fn charFromByte(x: u8, comptime urlsafe: bool) u8 {
304 return (lt(x, 26) & (x +% 'A')) |
305 (ge(x, 26) & lt(x, 52) & (x +% 'a' -% 26)) |
306 (ge(x, 52) & lt(x, 62) & (x +% '0' -% 52)) |
307 (eq(x, 62) & '+') | (eq(x, 63) & if (urlsafe) '_' else '/');
308 }
309
310 inline fn byteFromChar(c: u8, comptime urlsafe: bool) u8 {
311 const x =
312 (ge(c, 'A') & le(c, 'Z') & (c -% 'A')) |
313 (ge(c, 'a') & le(c, 'z') & (c -% 'a' +% 26)) |
314 (ge(c, '0') & le(c, '9') & (c -% '0' +% 52)) |
315 (eq(c, '+') & 62) | (eq(c, if (urlsafe) '_' else '/') & 63);
316 return x | (eq(x, 0) & ~eq(c, 'A'));
317 }
318
319 fn skipPadding(
320 encoded: []const u8,
321 padding_len: usize,
322 ignored_chars: ?StaticBitSet(256),
323 ) error{InvalidPadding}![]const u8 {
324 var b64_pos: usize = 0;
325 var i = padding_len;
326 while (i > 0) {
327 if (b64_pos >= encoded.len) {
328 return error.InvalidPadding;
329 }
330 const c = encoded[b64_pos];
331 if (c == '=') {
332 i -= 1;
333 } else if (ignored_chars) |set| {
334 if (!set.isSet(c)) {
335 return error.InvalidPadding;
336 }
337 }
338 b64_pos += 1;
339 }
340 return encoded[b64_pos..];
341 }
342
343 fn decodeAny(
344 bin: []u8,
345 encoded: []const u8,
346 comptime variant: Variant,
347 ignored_chars: ?StaticBitSet(256),
348 ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 {
349 var acc: u16 = 0;
350 var acc_len: u4 = 0;
351 var bin_pos: usize = 0;
352 var premature_end: ?usize = null;
353 const urlsafe = variant.urlsafe_alphabet;
354 for (encoded, 0..) |c, b64_pos| {
355 const d = byteFromChar(c, urlsafe);
356 if (d == 0xff) {
357 if (ignored_chars) |set| {
358 if (set.isSet(c)) continue;
359 }
360 premature_end = b64_pos;
361 break;
362 }
363 acc = (acc << 6) + d;
364 acc_len += 6;
365 if (acc_len >= 8) {
366 acc_len -= 8;
367 if (bin_pos >= bin.len) {
368 return error.NoSpaceLeft;
369 }
370 bin[bin_pos] = @truncate(acc >> acc_len);
371 bin_pos += 1;
372 }
373 }
374 if (acc_len > 4 or (acc & ((@as(u16, 1) << acc_len) -% 1)) != 0) {
375 return error.InvalidCharacter;
376 }
377 const padding_len = acc_len / 2;
378 if (premature_end) |pos| {
379 const remaining =
380 if (variant.padding)
381 try skipPadding(encoded[pos..], padding_len, ignored_chars)
382 else
383 encoded[pos..];
384 if (ignored_chars) |set| {
385 for (remaining) |c| {
386 if (!set.isSet(c)) {
387 return error.InvalidCharacter;
388 }
389 }
390 } else if (remaining.len != 0) {
391 return error.InvalidCharacter;
392 }
393 } else if (variant.padding and padding_len != 0) {
394 return error.InvalidPadding;
395 }
396 return bin[0..bin_pos];
397 }
398};
399
400test "hex" {
401 var default_rng = std.Random.DefaultPrng.init(testing.random_seed);
402 var rng = default_rng.random();
403 var bin_buf: [1000]u8 = undefined;
404 rng.bytes(&bin_buf);
405 var bin2_buf: [bin_buf.len]u8 = undefined;
406 var hex_buf: [bin_buf.len * 2]u8 = undefined;
407 for (0..1000) |_| {
408 const bin_len = rng.intRangeAtMost(usize, 0, bin_buf.len);
409 const bin = bin_buf[0..bin_len];
410 const bin2 = bin2_buf[0..bin_len];
411 inline for (.{ .lower, .upper }) |case| {
412 const hex_len = bin_len * 2;
413 const encoded = hex_buf[0..hex_len];
414 try hex.encode(encoded, bin, case);
415 try hex.decode(bin2, encoded);
416 try testing.expectEqualSlices(u8, bin, bin2);
417 }
418 }
419}
420
421test "base64" {
422 var default_rng = std.Random.DefaultPrng.init(testing.random_seed);
423 var rng = default_rng.random();
424 var bin_buf: [1000]u8 = undefined;
425 rng.bytes(&bin_buf);
426 var bin2_buf: [bin_buf.len]u8 = undefined;
427 var b64_buf: [(bin_buf.len + 3) / 3 * 4]u8 = undefined;
428 for (0..1000) |_| {
429 const bin_len = rng.intRangeAtMost(usize, 0, bin_buf.len);
430 const bin = bin_buf[0..bin_len];
431 const bin2 = bin2_buf[0..bin_len];
432 inline for ([_]base64.Variant{
433 .standard,
434 .standard_nopad,
435 .urlsafe,
436 .urlsafe_nopad,
437 }) |variant| {
438 const b64_len = base64.encodedLen(bin_len, variant);
439 const encoded_buf = b64_buf[0..b64_len];
440 const encoded = try base64.encode(encoded_buf, bin, variant);
441 const decoded = try base64.decode(bin2, encoded, variant);
442 try testing.expectEqualSlices(u8, bin, decoded);
443 }
444 }
445}
446
447test "hex with ignored chars" {
448 const encoded = "01020304050607\n08090A0B0C0D0E0F\n";
449 const expected = [_]u8{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
450 var bin_buf: [encoded.len / 2]u8 = undefined;
451 try testing.expectError(error.InvalidCharacter, hex.decode(&bin_buf, encoded));
452 const bin = try (try hex.decoderWithIgnore("\r\n")).decode(&bin_buf, encoded);
453 try testing.expectEqualSlices(u8, &expected, bin);
454}
455
456test "base64 with ignored chars" {
457 const encoded = "dGVzdCBi\r\nYXNlNjQ=\n";
458 const expected = "test base64";
459 var bin_buf: [base64.DecoderWithIgnore.decodedLenUpperBound(encoded.len)]u8 = undefined;
460 try testing.expectError(error.InvalidCharacter, base64.decode(&bin_buf, encoded, .standard));
461 const bin = try (try base64.decoderWithIgnore("\r\n")).decode(&bin_buf, encoded, .standard);
462 try testing.expectEqualSlices(u8, expected, bin);
463}