| 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 | set = 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: *std.Io.Reader) !Tag { |
| 73 | const tag1: FirstTag = @bitCast(try reader.takeByte()); |
| 74 | var number: std.meta.Tag(Tag.Number) = tag1.number; |
| 75 | |
| 76 | if (tag1.number == high_tag_marker) { |
| 77 | number = 0; |
| 78 | for (0..max_continuations) |i| { |
| 79 | const next: NextTag = @bitCast(try reader.takeByte()); |
| 80 | if (i == 0 and next.number == 0) return error.InvalidEncoding; |
| 81 | number = std.math.shlExact(@TypeOf(number), number, 7) catch return error.InvalidEncoding; |
| 82 | number |= next.number; |
| 83 | if (!next.continues) break; |
| 84 | } else return error.InvalidEncoding; |
| 85 | if (number < high_tag_marker) return error.InvalidEncoding; |
| 86 | } |
| 87 | |
| 88 | return Tag{ |
| 89 | .number = @fromBackingInt(@intCast(number)), |
| 90 | .constructed = tag1.constructed, |
| 91 | .class = tag1.class, |
| 92 | }; |
| 93 | } |
| 94 | |
| 95 | pub fn encodeToSlice(self: Tag, buf: *[max_encoded_len]u8) []const u8 { |
| 96 | const n = @backingInt(self.number); |
| 97 | var tag1: FirstTag = .{ |
| 98 | .number = undefined, |
| 99 | .constructed = self.constructed, |
| 100 | .class = self.class, |
| 101 | }; |
| 102 | |
| 103 | if (n < high_tag_marker) { |
| 104 | tag1.number = @intCast(n); |
| 105 | buf[0] = @bitCast(tag1); |
| 106 | return buf[0..1]; |
| 107 | } |
| 108 | |
| 109 | tag1.number = high_tag_marker; |
| 110 | buf[0] = @bitCast(tag1); |
| 111 | |
| 112 | const bits_used = @bitSizeOf(@TypeOf(n)) - @clz(n); |
| 113 | const len = std.math.divCeil(usize, bits_used, 7) catch unreachable; |
| 114 | |
| 115 | var remaining = n; |
| 116 | var i = len; |
| 117 | while (i > 0) : (i -= 1) { |
| 118 | buf[i] = @bitCast(NextTag{ |
| 119 | .number = @truncate(remaining), |
| 120 | .continues = i != len, |
| 121 | }); |
| 122 | remaining >>= 7; |
| 123 | } |
| 124 | return buf[0 .. 1 + len]; |
| 125 | } |
| 126 | |
| 127 | pub fn encode(self: Tag, writer: *std.Io.Writer) std.Io.Writer.Error!void { |
| 128 | var buf: [max_encoded_len]u8 = undefined; |
| 129 | try writer.writeAll(self.encodeToSlice(&buf)); |
| 130 | } |
| 131 | |
| 132 | pub const max_encoded_len = 1 + (std.math.divCeil( |
| 133 | comptime_int, |
| 134 | @bitSizeOf(std.meta.Tag(Tag.Number)), |
| 135 | 7, |
| 136 | ) catch unreachable); |
| 137 | const max_continuations = max_encoded_len - 1; |
| 138 | const high_tag_marker = std.math.maxInt(u5); |
| 139 | |
| 140 | const FirstTag = packed struct(u8) { number: u5, constructed: bool, class: Tag.Class }; |
| 141 | const NextTag = packed struct(u8) { number: u7, continues: bool }; |
| 142 | |
| 143 | pub fn toExpected(self: Tag) ExpectedTag { |
| 144 | return ExpectedTag{ |
| 145 | .number = self.number, |
| 146 | .constructed = self.constructed, |
| 147 | .class = self.class, |
| 148 | }; |
| 149 | } |
| 150 | |
| 151 | pub fn fromZig(comptime T: type) Tag { |
| 152 | switch (@typeInfo(T)) { |
| 153 | .@"struct", .@"enum", .@"union" => { |
| 154 | if (@hasDecl(T, "asn1_tag")) return T.asn1_tag; |
| 155 | }, |
| 156 | else => {}, |
| 157 | } |
| 158 | |
| 159 | switch (@typeInfo(T)) { |
| 160 | .@"struct", .@"union" => return universal(.sequence, true), |
| 161 | .bool => return universal(.boolean, false), |
| 162 | .int => return universal(.integer, false), |
| 163 | .@"enum" => |e| { |
| 164 | if (@hasDecl(T, "oids")) return Oid.asn1_tag; |
| 165 | return universal(if (e.mode == .exhaustive) .enumerated else .integer, false); |
| 166 | }, |
| 167 | .optional => |o| return fromZig(o.child), |
| 168 | .null => return universal(.null, false), |
| 169 | else => @compileError("cannot map Zig type to asn1_tag " ++ @typeName(T)), |
| 170 | } |
| 171 | } |
| 172 | }; |
| 173 | |
| 174 | test Tag { |
| 175 | const buf = [_]u8{0xa3}; |
| 176 | var reader: std.Io.Reader = .fixed(&buf); |
| 177 | const t = Tag.decode(&reader); |
| 178 | try std.testing.expectEqual(Tag.init(@fromBackingInt(@intCast(3)), true, .context_specific), t); |
| 179 | } |
| 180 | |
| 181 | test "Tag.encode produces the exact bytes from X.690" { |
| 182 | const cases = [_]struct { number: u16, expected: []const u8 }{ |
| 183 | .{ .number = 0, .expected = &.{0x00} }, |
| 184 | .{ .number = 30, .expected = &.{0x1e} }, |
| 185 | .{ .number = 31, .expected = &.{ 0x1f, 0x1f } }, |
| 186 | .{ .number = 127, .expected = &.{ 0x1f, 0x7f } }, |
| 187 | .{ .number = 128, .expected = &.{ 0x1f, 0x81, 0x00 } }, |
| 188 | .{ .number = 16383, .expected = &.{ 0x1f, 0xff, 0x7f } }, |
| 189 | .{ .number = 16384, .expected = &.{ 0x1f, 0x81, 0x80, 0x00 } }, |
| 190 | .{ .number = 65535, .expected = &.{ 0x1f, 0x83, 0xff, 0x7f } }, |
| 191 | }; |
| 192 | for (cases) |c| { |
| 193 | const tag = Tag.init(@fromBackingInt(@intCast(c.number)), false, .universal); |
| 194 | var buf: [Tag.max_encoded_len]u8 = undefined; |
| 195 | try std.testing.expectEqualSlices(u8, c.expected, tag.encodeToSlice(&buf)); |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | test "Tag.encode/decode round trip" { |
| 200 | for ([_]u16{ 0, 30, 31, 32, 127, 128, 16383, 16384, 65535 }) |n| { |
| 201 | const tag = Tag.init(@fromBackingInt(@intCast(n)), false, .universal); |
| 202 | var buf: [Tag.max_encoded_len]u8 = undefined; |
| 203 | const encoded = tag.encodeToSlice(&buf); |
| 204 | var reader: std.Io.Reader = .fixed(encoded); |
| 205 | try std.testing.expectEqual(tag, try Tag.decode(&reader)); |
| 206 | try std.testing.expectEqual(encoded.len, reader.seek); |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | test "Tag.decode rejects non-minimal high-tag form" { |
| 211 | for ([_][]const u8{ &.{ 0x1f, 0x1e }, &.{ 0x1f, 0x80, 0x01 } }) |bytes| { |
| 212 | var reader: std.Io.Reader = .fixed(bytes); |
| 213 | try std.testing.expectError(error.InvalidEncoding, Tag.decode(&reader)); |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | /// A decoded view. |
| 218 | pub const Element = struct { |
| 219 | tag: Tag, |
| 220 | slice: Slice, |
| 221 | |
| 222 | pub const Slice = struct { |
| 223 | start: Index, |
| 224 | end: Index, |
| 225 | |
| 226 | pub fn len(self: Slice) Index { |
| 227 | return self.end - self.start; |
| 228 | } |
| 229 | |
| 230 | pub fn view(self: Slice, bytes: []const u8) []const u8 { |
| 231 | return bytes[self.start..self.end]; |
| 232 | } |
| 233 | }; |
| 234 | |
| 235 | pub const DecodeError = error{ EndOfStream, InvalidEncoding }; |
| 236 | |
| 237 | /// Safely decode a DER/BER/CER element at `index`: |
| 238 | /// - Ensures length uses shortest form |
| 239 | /// - Ensures length is within `bytes` |
| 240 | /// - Ensures length is less than `std.math.maxInt(Index)` |
| 241 | pub fn decode(bytes: []const u8, index: Index) DecodeError!Element { |
| 242 | if (index > bytes.len) return error.EndOfStream; |
| 243 | var reader: std.Io.Reader = .fixed(bytes[index..]); |
| 244 | |
| 245 | const tag = Tag.decode(&reader) catch |err| switch (err) { |
| 246 | error.ReadFailed => unreachable, // it's all fixed buffers |
| 247 | else => |e| return e, |
| 248 | }; |
| 249 | const size_or_len_size = reader.takeByte() catch |err| switch (err) { |
| 250 | error.ReadFailed => unreachable, // it's all fixed buffers |
| 251 | else => |e| return e, |
| 252 | }; |
| 253 | |
| 254 | const len = if (size_or_len_size < 128) |
| 255 | // short form between 0-127 |
| 256 | size_or_len_size |
| 257 | else blk: { |
| 258 | // long form between 0 and std.math.maxInt(u1024) |
| 259 | const len_size: u7 = @truncate(size_or_len_size); |
| 260 | if (len_size > @sizeOf(Index)) return error.EndOfStream; |
| 261 | |
| 262 | const len = reader.takeVarInt(Index, .big, len_size) catch |err| switch (err) { |
| 263 | error.ReadFailed => unreachable, // it's all fixed buffers |
| 264 | else => |e| return e, |
| 265 | }; |
| 266 | if (len < 128) return error.EndOfStream; // should have used short form |
| 267 | |
| 268 | break :blk len; |
| 269 | }; |
| 270 | |
| 271 | const start = index + @as(Index, @intCast(reader.seek)); |
| 272 | const end = std.math.add(Index, start, len) catch return error.EndOfStream; |
| 273 | if (end > bytes.len) return error.EndOfStream; |
| 274 | |
| 275 | return Element{ .tag = tag, .slice = Slice{ .start = start, .end = end } }; |
| 276 | } |
| 277 | }; |
| 278 | |
| 279 | test Element { |
| 280 | const short_form = [_]u8{ 0x30, 0x03, 0x02, 0x01, 0x09 }; |
| 281 | try std.testing.expectEqual(Element{ |
| 282 | .tag = Tag.universal(.sequence, true), |
| 283 | .slice = Element.Slice{ .start = 2, .end = short_form.len }, |
| 284 | }, Element.decode(&short_form, 0)); |
| 285 | |
| 286 | const long_form = [_]u8{ 0x30, 129, 129 } ++ @as([129]u8, @splat(0)); |
| 287 | try std.testing.expectEqual(Element{ |
| 288 | .tag = Tag.universal(.sequence, true), |
| 289 | .slice = Element.Slice{ .start = 3, .end = long_form.len }, |
| 290 | }, Element.decode(&long_form, 0)); |
| 291 | |
| 292 | const multi_byte_tag = [_]u8{ 0x1F, 0x20, 0x08, 0x30, 0x36, 0x3A, 0x32, 0x37, 0x3A, 0x31, 0x35 }; |
| 293 | try std.testing.expectEqual(Element{ |
| 294 | .tag = Tag.universal(.time_of_day, false), |
| 295 | .slice = Element.Slice{ .start = 3, .end = multi_byte_tag.len }, |
| 296 | }, Element.decode(&multi_byte_tag, 0)); |
| 297 | } |
| 298 | |
| 299 | /// For decoding. |
| 300 | pub const ExpectedTag = struct { |
| 301 | number: ?Tag.Number = null, |
| 302 | constructed: ?bool = null, |
| 303 | class: ?Tag.Class = null, |
| 304 | |
| 305 | pub fn init(number: ?Tag.Number, constructed: ?bool, class: ?Tag.Class) ExpectedTag { |
| 306 | return .{ .number = number, .constructed = constructed, .class = class }; |
| 307 | } |
| 308 | |
| 309 | pub fn primitive(number: ?Tag.Number) ExpectedTag { |
| 310 | return .{ .number = number, .constructed = false, .class = .universal }; |
| 311 | } |
| 312 | |
| 313 | pub fn match(self: ExpectedTag, tag: Tag) bool { |
| 314 | if (self.number) |e| { |
| 315 | if (tag.number != e) return false; |
| 316 | } |
| 317 | if (self.constructed) |e| { |
| 318 | if (tag.constructed != e) return false; |
| 319 | } |
| 320 | if (self.class) |e| { |
| 321 | if (tag.class != e) return false; |
| 322 | } |
| 323 | return true; |
| 324 | } |
| 325 | }; |
| 326 | |
| 327 | pub const FieldTag = struct { |
| 328 | number: std.meta.Tag(Tag.Number), |
| 329 | class: Tag.Class, |
| 330 | explicit: bool = true, |
| 331 | |
| 332 | pub fn initExplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { |
| 333 | return .{ .number = number, .class = class, .explicit = true }; |
| 334 | } |
| 335 | |
| 336 | pub fn initImplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { |
| 337 | return .{ .number = number, .class = class, .explicit = false }; |
| 338 | } |
| 339 | |
| 340 | pub fn fromContainer(comptime Container: type, comptime field_name: []const u8) ?FieldTag { |
| 341 | if (@hasDecl(Container, "asn1_tags") and @hasField(@TypeOf(Container.asn1_tags), field_name)) { |
| 342 | return @field(Container.asn1_tags, field_name); |
| 343 | } |
| 344 | |
| 345 | return null; |
| 346 | } |
| 347 | |
| 348 | pub fn toTag(self: FieldTag) Tag { |
| 349 | return Tag.init(@fromBackingInt(@intCast(self.number)), self.explicit, self.class); |
| 350 | } |
| 351 | }; |
| 352 | |
| 353 | pub const BitString = struct { |
| 354 | /// Number of bits in rightmost byte that are unused. |
| 355 | right_padding: u3 = 0, |
| 356 | bytes: []const u8, |
| 357 | |
| 358 | pub fn bitLen(self: BitString) usize { |
| 359 | return self.bytes.len * 8 - self.right_padding; |
| 360 | } |
| 361 | |
| 362 | const asn1_tag = Tag.universal(.bitstring, false); |
| 363 | |
| 364 | pub fn decodeDer(decoder: *der.Decoder) !BitString { |
| 365 | const ele = try decoder.element(asn1_tag.toExpected()); |
| 366 | const bytes = decoder.view(ele); |
| 367 | |
| 368 | if (bytes.len < 1) return error.InvalidBitString; |
| 369 | const padding = bytes[0]; |
| 370 | if (padding >= 8) return error.InvalidBitString; |
| 371 | const right_padding: u3 = @intCast(padding); |
| 372 | |
| 373 | // DER requires that unused bits be zero. |
| 374 | if (@ctz(bytes[bytes.len - 1]) < right_padding) return error.InvalidBitString; |
| 375 | |
| 376 | return BitString{ .bytes = bytes[1..], .right_padding = right_padding }; |
| 377 | } |
| 378 | |
| 379 | pub fn encodeDer(self: BitString, encoder: *der.Encoder) !void { |
| 380 | try encoder.prependBytes(self.bytes); |
| 381 | try encoder.prependBytes(&.{self.right_padding}); |
| 382 | try encoder.length(self.bytes.len + 1); |
| 383 | try encoder.tag(asn1_tag); |
| 384 | } |
| 385 | }; |
| 386 | |
| 387 | test BitString { |
| 388 | const bs = BitString{ .bytes = &.{ 0x6e, 0x5d, 0xc0 }, .right_padding = 6 }; |
| 389 | const allocator = std.testing.allocator; |
| 390 | const buf = try der.encode(allocator, bs); |
| 391 | defer allocator.free(buf); |
| 392 | try std.testing.expectEqualSlices(u8, &.{ 0x03, 0x04, 0x06, 0x6e, 0x5d, 0xc0 }, buf); |
| 393 | try std.testing.expectEqualDeep(bs, try der.decode(BitString, buf)); |
| 394 | } |
| 395 | |
| 396 | pub fn Opaque(comptime tag: Tag) type { |
| 397 | return struct { |
| 398 | bytes: []const u8, |
| 399 | |
| 400 | pub fn decodeDer(decoder: *der.Decoder) !@This() { |
| 401 | const ele = try decoder.element(tag.toExpected()); |
| 402 | if (tag.constructed) decoder.index = ele.slice.end; |
| 403 | return .{ .bytes = decoder.view(ele) }; |
| 404 | } |
| 405 | |
| 406 | pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { |
| 407 | try encoder.tagBytes(tag, self.bytes); |
| 408 | } |
| 409 | }; |
| 410 | } |
| 411 | |
| 412 | /// Use sparingly. |
| 413 | pub const Any = struct { |
| 414 | tag: Tag, |
| 415 | bytes: []const u8, |
| 416 | |
| 417 | pub fn decodeDer(decoder: *der.Decoder) !@This() { |
| 418 | const ele = try decoder.element(ExpectedTag{}); |
| 419 | return .{ .tag = ele.tag, .bytes = decoder.view(ele) }; |
| 420 | } |
| 421 | |
| 422 | pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { |
| 423 | try encoder.tagBytes(self.tag, self.bytes); |
| 424 | } |
| 425 | }; |
| 426 | |
| 427 | test { |
| 428 | _ = der; |
| 429 | _ = Oid; |
| 430 | _ = @import("asn1/test.zig"); |
| 431 | } |