| author | |
| committer | |
| log | ed7004a2ee3b984a17b3fbd468d73a3875f209eb |
| tree | 1edb372aca4159479ab8e3e9222771f7d29b7613 |
| parent | 6769806213ab28ed629221085325d8f143e515b0 |
| parent | a51bc1d1d11717e9aab72121918ddb09fb3333f7 |
| signature |
std.crypto: Add ASN1 module with OIDs and DER10 files changed, 1141 insertions(+), 0 deletions(-)
lib/std/crypto.zig+2| ... | ... | @@ -220,6 +220,7 @@ pub const errors = @import("crypto/errors.zig"); |
| 220 | 220 | |
| 221 | 221 | pub const tls = @import("crypto/tls.zig"); |
| 222 | 222 | pub const Certificate = @import("crypto/Certificate.zig"); |
| 223 | pub const asn1 = @import("crypto/asn1.zig"); | |
| 223 | 224 | |
| 224 | 225 | /// Side-channels mitigations. |
| 225 | 226 | pub const SideChannelsMitigations = enum { |
| ... | ... | @@ -334,6 +335,7 @@ test { |
| 334 | 335 | _ = errors; |
| 335 | 336 | _ = tls; |
| 336 | 337 | _ = Certificate; |
| 338 | _ = asn1; | |
| 337 | 339 | } |
| 338 | 340 | |
| 339 | 341 | test "CSPRNG" { |
lib/std/crypto/asn1.zig created+359| ... | ... | @@ -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 | } |
lib/std/crypto/asn1/Oid.zig created+210| ... | ... | @@ -0,0 +1,210 @@ |
| 1 | //! Globally unique hierarchical identifier made of a sequence of integers. | |
| 2 | //! | |
| 3 | //! Commonly used to identify standards, algorithms, certificate extensions, | |
| 4 | //! organizations, or policy documents. | |
| 5 | encoded: []const u8, | |
| 6 | ||
| 7 | pub const InitError = std.fmt.ParseIntError || error{MissingPrefix} || std.io.FixedBufferStream(u8).WriteError; | |
| 8 | ||
| 9 | pub 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 | ||
| 43 | test 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 | ||
| 51 | pub 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 | ||
| 79 | test 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 | ||
| 89 | const 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 | ||
| 98 | const 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 | ||
| 109 | pub const asn1_tag = asn1.Tag.init(.oid, false, .universal); | |
| 110 | ||
| 111 | pub fn decodeDer(decoder: *der.Decoder) !Oid { | |
| 112 | const ele = try decoder.element(asn1_tag.toExpected()); | |
| 113 | return Oid{ .encoded = decoder.view(ele) }; | |
| 114 | } | |
| 115 | ||
| 116 | pub fn encodeDer(self: Oid, encoder: *der.Encoder) !void { | |
| 117 | try encoder.tagBytes(asn1_tag, self.encoded); | |
| 118 | } | |
| 119 | ||
| 120 | fn 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. | |
| 127 | fn 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 | ||
| 134 | test encodeComptime { | |
| 135 | try std.testing.expectEqual( | |
| 136 | hexToBytes("2b0601040182371514"), | |
| 137 | comptime encodeComptime("1.3.6.1.4.1.311.21.20"), | |
| 138 | ); | |
| 139 | } | |
| 140 | ||
| 141 | pub 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 | |
| 149 | pub 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. | |
| 198 | fn 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 | ||
| 204 | const std = @import("std"); | |
| 205 | const Oid = @This(); | |
| 206 | const Arc = u32; | |
| 207 | const encoding_base = 128; | |
| 208 | const Allocator = std.mem.Allocator; | |
| 209 | const der = @import("der.zig"); | |
| 210 | const asn1 = @import("../asn1.zig"); |
lib/std/crypto/asn1/der.zig created+55| ... | ... | @@ -0,0 +1,55 @@ |
| 1 | //! Distinguised Encoding Rules as defined in X.690 and X.691. | |
| 2 | //! | |
| 3 | //! Subset of Basic Encoding Rules (BER) which eliminates flexibility in | |
| 4 | //! an effort to acheive normality. Used in PKI. | |
| 5 | const std = @import("std"); | |
| 6 | const asn1 = @import("../asn1.zig"); | |
| 7 | ||
| 8 | pub const Decoder = @import("der/Decoder.zig"); | |
| 9 | pub const Encoder = @import("der/Encoder.zig"); | |
| 10 | ||
| 11 | pub 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. | |
| 19 | pub 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 | ||
| 26 | test 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 | ||
| 40 | test 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 | ||
| 52 | test { | |
| 53 | _ = Decoder; | |
| 54 | _ = Encoder; | |
| 55 | } |
lib/std/crypto/asn1/der/ArrayListReverse.zig created+97| ... | ... | @@ -0,0 +1,97 @@ |
| 1 | //! An ArrayList that grows backwards. Counts nested prefix length fields | |
| 2 | //! in O(n) instead of O(n^depth) at the cost of extra buffering. | |
| 3 | //! | |
| 4 | //! Laid out in memory like: | |
| 5 | //! capacity |--------------------------| | |
| 6 | //! data |-------------| | |
| 7 | data: []u8, | |
| 8 | capacity: usize, | |
| 9 | allocator: Allocator, | |
| 10 | ||
| 11 | const ArrayListReverse = @This(); | |
| 12 | const Error = Allocator.Error; | |
| 13 | ||
| 14 | pub fn init(allocator: Allocator) ArrayListReverse { | |
| 15 | return .{ .data = &.{}, .capacity = 0, .allocator = allocator }; | |
| 16 | } | |
| 17 | ||
| 18 | pub fn deinit(self: *ArrayListReverse) void { | |
| 19 | self.allocator.free(self.allocatedSlice()); | |
| 20 | } | |
| 21 | ||
| 22 | pub 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 | ||
| 34 | pub 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 | ||
| 48 | pub const Writer = std.io.Writer(*ArrayListReverse, Error, prependSliceSize); | |
| 49 | /// Warning: This writer writes backwards. `fn print` will NOT work as expected. | |
| 50 | pub fn writer(self: *ArrayListReverse) Writer { | |
| 51 | return .{ .context = self }; | |
| 52 | } | |
| 53 | ||
| 54 | fn prependSliceSize(self: *ArrayListReverse, data: []const u8) Error!usize { | |
| 55 | try self.prependSlice(data); | |
| 56 | return data.len; | |
| 57 | } | |
| 58 | ||
| 59 | fn allocatedSlice(self: *ArrayListReverse) []u8 { | |
| 60 | return (self.data.ptr + self.data.len - self.capacity)[0..self.capacity]; | |
| 61 | } | |
| 62 | ||
| 63 | /// Invalidates all element pointers. | |
| 64 | pub 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. | |
| 72 | pub 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 | ||
| 80 | const std = @import("std"); | |
| 81 | const Allocator = std.mem.Allocator; | |
| 82 | const assert = std.debug.assert; | |
| 83 | const testing = std.testing; | |
| 84 | ||
| 85 | test ArrayListReverse { | |
| 86 | var b = ArrayListReverse.init(testing.allocator); | |
| 87 | defer b.deinit(); | |
| 88 | const data: []const u8 = &.{ 4, 5, 6 }; | |
| 89 | try b.prependSlice(data); | |
| 90 | try testing.expectEqual(data.len, b.data.len); | |
| 91 | try testing.expectEqualSlices(u8, data, b.data); | |
| 92 | ||
| 93 | const data2: []const u8 = &.{ 1, 2, 3 }; | |
| 94 | try b.prependSlice(data2); | |
| 95 | try testing.expectEqual(data.len + data2.len, b.data.len); | |
| 96 | try testing.expectEqualSlices(u8, data2 ++ data, b.data); | |
| 97 | } |
lib/std/crypto/asn1/der/Decoder.zig created+172| ... | ... | @@ -0,0 +1,172 @@ |
| 1 | //! A secure DER parser that: | |
| 2 | //! - Prefers calling `fn decodeDer(self: @This(), decoder: *der.Decoder)` | |
| 3 | //! - Does NOT allocate. If you wish to parse lists you can do so lazily | |
| 4 | //! with an opaque type. | |
| 5 | //! - Does NOT read memory outside `bytes`. | |
| 6 | //! - Does NOT return elements with slices outside `bytes`. | |
| 7 | //! - Errors on values that do NOT follow DER rules: | |
| 8 | //! - Lengths that could be represented in a shorter form. | |
| 9 | //! - Booleans that are not 0xff or 0x00. | |
| 10 | bytes: []const u8, | |
| 11 | index: 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`. | |
| 14 | field_tag: ?FieldTag = null, | |
| 15 | ||
| 16 | /// Expect a value. | |
| 17 | pub fn any(self: *Decoder, comptime T: type) !T { | |
| 18 | if (std.meta.hasFn(T, "decodeDer")) return try T.decodeDer(self); | |
| 19 | ||
| 20 | const tag = Tag.fromZig(T).toExpected(); | |
| 21 | switch (@typeInfo(T)) { | |
| 22 | .Struct => { | |
| 23 | const ele = try self.element(tag); | |
| 24 | defer self.index = ele.slice.end; // don't force parsing all fields | |
| 25 | ||
| 26 | var res: T = undefined; | |
| 27 | ||
| 28 | inline for (std.meta.fields(T)) |f| { | |
| 29 | self.field_tag = FieldTag.fromContainer(T, f.name); | |
| 30 | ||
| 31 | if (self.field_tag) |ft| { | |
| 32 | if (ft.explicit) { | |
| 33 | const seq = try self.element(ft.toTag().toExpected()); | |
| 34 | self.index = seq.slice.start; | |
| 35 | self.field_tag = null; | |
| 36 | } | |
| 37 | } | |
| 38 | ||
| 39 | @field(res, f.name) = self.any(f.type) catch |err| brk: { | |
| 40 | if (f.default_value) |d| { | |
| 41 | break :brk @as(*const f.type, @alignCast(@ptrCast(d))).*; | |
| 42 | } | |
| 43 | return err; | |
| 44 | }; | |
| 45 | // DER encodes null values by skipping them. | |
| 46 | if (@typeInfo(f.type) == .Optional and @field(res, f.name) == null) { | |
| 47 | if (f.default_value) |d| { | |
| 48 | @field(res, f.name) = @as(*const f.type, @alignCast(@ptrCast(d))).*; | |
| 49 | } | |
| 50 | } | |
| 51 | } | |
| 52 | ||
| 53 | return res; | |
| 54 | }, | |
| 55 | .Bool => { | |
| 56 | const ele = try self.element(tag); | |
| 57 | const bytes = self.view(ele); | |
| 58 | if (bytes.len != 1) return error.InvalidBool; | |
| 59 | ||
| 60 | return switch (bytes[0]) { | |
| 61 | 0x00 => false, | |
| 62 | 0xff => true, | |
| 63 | else => error.InvalidBool, | |
| 64 | }; | |
| 65 | }, | |
| 66 | .Int => { | |
| 67 | const ele = try self.element(tag); | |
| 68 | const bytes = self.view(ele); | |
| 69 | return try int(T, bytes); | |
| 70 | }, | |
| 71 | .Enum => |e| { | |
| 72 | const ele = try self.element(tag); | |
| 73 | const bytes = self.view(ele); | |
| 74 | if (@hasDecl(T, "oids")) { | |
| 75 | return T.oids.oidToEnum(bytes) orelse return error.UnknownOid; | |
| 76 | } | |
| 77 | return @enumFromInt(try int(e.tag_type, bytes)); | |
| 78 | }, | |
| 79 | .Optional => |o| return self.any(o.child) catch return null, | |
| 80 | else => @compileError("cannot decode type " ++ @typeName(T)), | |
| 81 | } | |
| 82 | } | |
| 83 | ||
| 84 | //// Expect a sequence. | |
| 85 | pub fn sequence(self: *Decoder) !Element { | |
| 86 | return try self.element(ExpectedTag.init(.sequence, true, .universal)); | |
| 87 | } | |
| 88 | ||
| 89 | //// Expect an element. | |
| 90 | pub fn element( | |
| 91 | self: *Decoder, | |
| 92 | expected: ExpectedTag, | |
| 93 | ) (error{ EndOfStream, UnexpectedElement } || Element.DecodeError)!Element { | |
| 94 | if (self.index >= self.bytes.len) return error.EndOfStream; | |
| 95 | ||
| 96 | const res = try Element.decode(self.bytes, self.index); | |
| 97 | var e = expected; | |
| 98 | if (self.field_tag) |ft| { | |
| 99 | e.number = @enumFromInt(ft.number); | |
| 100 | e.class = ft.class; | |
| 101 | } | |
| 102 | if (!e.match(res.tag)) { | |
| 103 | return error.UnexpectedElement; | |
| 104 | } | |
| 105 | ||
| 106 | self.index = if (res.tag.constructed) res.slice.start else res.slice.end; | |
| 107 | return res; | |
| 108 | } | |
| 109 | ||
| 110 | /// View of element bytes. | |
| 111 | pub fn view(self: Decoder, elem: Element) []const u8 { | |
| 112 | return elem.slice.view(self.bytes); | |
| 113 | } | |
| 114 | ||
| 115 | fn int(comptime T: type, value: []const u8) error{ NonCanonical, LargeValue }!T { | |
| 116 | if (@typeInfo(T).Int.bits % 8 != 0) @compileError("T must be byte aligned"); | |
| 117 | ||
| 118 | var bytes = value; | |
| 119 | if (bytes.len >= 2) { | |
| 120 | if (bytes[0] == 0) { | |
| 121 | if (@clz(bytes[1]) > 0) return error.NonCanonical; | |
| 122 | bytes.ptr += 1; | |
| 123 | } | |
| 124 | if (bytes[0] == 0xff and @clz(bytes[1]) == 0) return error.NonCanonical; | |
| 125 | } | |
| 126 | ||
| 127 | if (bytes.len > @sizeOf(T)) return error.LargeValue; | |
| 128 | if (@sizeOf(T) == 1) return @bitCast(bytes[0]); | |
| 129 | ||
| 130 | return std.mem.readVarInt(T, bytes, .big); | |
| 131 | } | |
| 132 | ||
| 133 | test int { | |
| 134 | try expectEqual(@as(u8, 1), try int(u8, &[_]u8{1})); | |
| 135 | try expectError(error.NonCanonical, int(u8, &[_]u8{ 0, 1 })); | |
| 136 | try expectError(error.NonCanonical, int(u8, &[_]u8{ 0xff, 0xff })); | |
| 137 | ||
| 138 | const big = [_]u8{ 0xef, 0xff }; | |
| 139 | try expectError(error.LargeValue, int(u8, &big)); | |
| 140 | try expectEqual(0xefff, int(u16, &big)); | |
| 141 | } | |
| 142 | ||
| 143 | test Decoder { | |
| 144 | var parser = Decoder{ .bytes = @embedFile("./testdata/id_ecc.pub.der") }; | |
| 145 | const seq = try parser.sequence(); | |
| 146 | ||
| 147 | { | |
| 148 | const seq2 = try parser.sequence(); | |
| 149 | _ = try parser.element(ExpectedTag.init(.oid, false, .universal)); | |
| 150 | _ = try parser.element(ExpectedTag.init(.oid, false, .universal)); | |
| 151 | ||
| 152 | try std.testing.expectEqual(parser.index, seq2.slice.end); | |
| 153 | } | |
| 154 | _ = try parser.element(ExpectedTag.init(.bitstring, false, .universal)); | |
| 155 | ||
| 156 | try std.testing.expectEqual(parser.index, seq.slice.end); | |
| 157 | try std.testing.expectEqual(parser.index, parser.bytes.len); | |
| 158 | } | |
| 159 | ||
| 160 | const std = @import("std"); | |
| 161 | const builtin = @import("builtin"); | |
| 162 | const asn1 = @import("../../asn1.zig"); | |
| 163 | const Oid = @import("../Oid.zig"); | |
| 164 | ||
| 165 | const expectEqual = std.testing.expectEqual; | |
| 166 | const expectError = std.testing.expectError; | |
| 167 | const Decoder = @This(); | |
| 168 | const Index = asn1.Index; | |
| 169 | const Tag = asn1.Tag; | |
| 170 | const FieldTag = asn1.FieldTag; | |
| 171 | const ExpectedTag = asn1.ExpectedTag; | |
| 172 | const Element = asn1.Element; |
lib/std/crypto/asn1/der/Encoder.zig created+166| ... | ... | @@ -0,0 +1,166 @@ |
| 1 | //! A buffered DER encoder. | |
| 2 | //! | |
| 3 | //! Prefers calling container's `fn encodeDer(self: @This(), encoder: *der.Encoder)`. | |
| 4 | //! That function should encode values, lengths, then tags. | |
| 5 | buffer: 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`. | |
| 8 | field_tag: ?FieldTag = null, | |
| 9 | ||
| 10 | pub fn init(allocator: std.mem.Allocator) Encoder { | |
| 11 | return Encoder{ .buffer = ArrayListReverse.init(allocator) }; | |
| 12 | } | |
| 13 | ||
| 14 | pub fn deinit(self: *Encoder) void { | |
| 15 | self.buffer.deinit(); | |
| 16 | } | |
| 17 | ||
| 18 | /// Encode any value. | |
| 19 | pub fn any(self: *Encoder, val: anytype) !void { | |
| 20 | const T = @TypeOf(val); | |
| 21 | try self.anyTag(Tag.fromZig(T), val); | |
| 22 | } | |
| 23 | ||
| 24 | fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { | |
| 25 | const T = @TypeOf(val); | |
| 26 | if (std.meta.hasFn(T, "encodeDer")) return try val.encodeDer(self); | |
| 27 | const start = self.buffer.data.len; | |
| 28 | const merged_tag = self.mergedTag(tag_); | |
| 29 | ||
| 30 | switch (@typeInfo(T)) { | |
| 31 | .Struct => |info| { | |
| 32 | inline for (0..info.fields.len) |i| { | |
| 33 | const f = info.fields[info.fields.len - i - 1]; | |
| 34 | const field_val = @field(val, f.name); | |
| 35 | const field_tag = FieldTag.fromContainer(T, f.name); | |
| 36 | ||
| 37 | // > The encoding of a set value or sequence value shall not include an encoding for any | |
| 38 | // > component value which is equal to its default value. | |
| 39 | const is_default = if (f.is_comptime) false else if (f.default_value) |v| brk: { | |
| 40 | const default_val: *const f.type = @alignCast(@ptrCast(v)); | |
| 41 | break :brk std.mem.eql(u8, std.mem.asBytes(default_val), std.mem.asBytes(&field_val)); | |
| 42 | } else false; | |
| 43 | ||
| 44 | if (!is_default) { | |
| 45 | const start2 = self.buffer.data.len; | |
| 46 | self.field_tag = field_tag; | |
| 47 | // will merge with self.field_tag. | |
| 48 | // may mutate self.field_tag. | |
| 49 | try self.anyTag(Tag.fromZig(f.type), field_val); | |
| 50 | if (field_tag) |ft| { | |
| 51 | if (ft.explicit) { | |
| 52 | try self.length(self.buffer.data.len - start2); | |
| 53 | try self.tag(ft.toTag()); | |
| 54 | self.field_tag = null; | |
| 55 | } | |
| 56 | } | |
| 57 | } | |
| 58 | } | |
| 59 | }, | |
| 60 | .Bool => try self.buffer.prependSlice(&[_]u8{if (val) 0xff else 0}), | |
| 61 | .Int => try self.int(T, val), | |
| 62 | .Enum => |e| { | |
| 63 | if (@hasDecl(T, "oids")) { | |
| 64 | return self.any(T.oids.enumToOid(val)); | |
| 65 | } else { | |
| 66 | try self.int(e.tag_type, @intFromEnum(val)); | |
| 67 | } | |
| 68 | }, | |
| 69 | .Optional => if (val) |v| return try self.anyTag(tag_, v), | |
| 70 | .Null => {}, | |
| 71 | else => @compileError("cannot encode type " ++ @typeName(T)), | |
| 72 | } | |
| 73 | ||
| 74 | try self.length(self.buffer.data.len - start); | |
| 75 | try self.tag(merged_tag); | |
| 76 | } | |
| 77 | ||
| 78 | /// Encode a tag. | |
| 79 | pub fn tag(self: *Encoder, tag_: Tag) !void { | |
| 80 | const t = self.mergedTag(tag_); | |
| 81 | try t.encode(self.writer()); | |
| 82 | } | |
| 83 | ||
| 84 | fn 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. | |
| 96 | pub 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. | |
| 113 | pub 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. | |
| 120 | pub fn writer(self: *Encoder) ArrayListReverse.Writer { | |
| 121 | return self.buffer.writer(); | |
| 122 | } | |
| 123 | ||
| 124 | fn 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 | ||
| 143 | test 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 | ||
| 160 | const std = @import("std"); | |
| 161 | const Oid = @import("../Oid.zig"); | |
| 162 | const asn1 = @import("../../asn1.zig"); | |
| 163 | const ArrayListReverse = @import("./ArrayListReverse.zig"); | |
| 164 | const Tag = asn1.Tag; | |
| 165 | const FieldTag = asn1.FieldTag; | |
| 166 | const Encoder = @This(); |
lib/std/crypto/asn1/der/testdata/all_types.der created| Binary files /dev/null and b/lib/std/crypto/asn1/der/testdata/all_types.der differ |
lib/std/crypto/asn1/der/testdata/id_ecc.pub.der created| Binary files /dev/null and b/lib/std/crypto/asn1/der/testdata/id_ecc.pub.der differ |
lib/std/crypto/asn1/test.zig created+80| ... | ... | @@ -0,0 +1,80 @@ |
| 1 | const std = @import("std"); | |
| 2 | const asn1 = @import("../asn1.zig"); | |
| 3 | ||
| 4 | const der = asn1.der; | |
| 5 | const Tag = asn1.Tag; | |
| 6 | const FieldTag = asn1.FieldTag; | |
| 7 | ||
| 8 | /// An example that uses all ASN1 types and available implementation features. | |
| 9 | const AllTypes = struct { | |
| 10 | a: u8 = 0, | |
| 11 | b: asn1.BitString, | |
| 12 | c: C, | |
| 13 | d: asn1.Opaque(Tag.universal(.string_utf8, false)), | |
| 14 | e: asn1.Opaque(Tag.universal(.octetstring, false)), | |
| 15 | f: ?u16, | |
| 16 | g: ?Nested, | |
| 17 | h: asn1.Any, | |
| 18 | ||
| 19 | pub const asn1_tags = .{ | |
| 20 | .a = FieldTag.explicit(0, .context_specific), | |
| 21 | .b = FieldTag.explicit(1, .context_specific), | |
| 22 | .c = FieldTag.implicit(2, .context_specific), | |
| 23 | .g = FieldTag.implicit(3, .context_specific), | |
| 24 | }; | |
| 25 | ||
| 26 | const C = enum { | |
| 27 | a, | |
| 28 | b, | |
| 29 | ||
| 30 | pub const oids = asn1.Oid.StaticMap(@This()).initComptime(.{ | |
| 31 | .a = "1.2.3.4", | |
| 32 | .b = "1.2.3.5", | |
| 33 | }); | |
| 34 | }; | |
| 35 | ||
| 36 | const Nested = struct { | |
| 37 | inner: Asn1T, | |
| 38 | sum: i16, | |
| 39 | ||
| 40 | const Asn1T = struct { a: u8, b: i16 }; | |
| 41 | ||
| 42 | pub fn decodeDer(decoder: *der.Decoder) !Nested { | |
| 43 | const inner = try decoder.any(Asn1T); | |
| 44 | return Nested{ .inner = inner, .sum = inner.a + inner.b }; | |
| 45 | } | |
| 46 | ||
| 47 | pub fn encodeDer(self: Nested, encoder: *der.Encoder) !void { | |
| 48 | try encoder.any(self.inner); | |
| 49 | } | |
| 50 | }; | |
| 51 | }; | |
| 52 | ||
| 53 | test 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 | } |