|
| 1 | +//! ASN.1 types for public consumption. |
| 2 | +const std = @import("std"); |
| 3 | +pub const der = @import("./asn1/der.zig"); |
| 4 | +pub const Oid = @import("./asn1/Oid.zig"); |
| 5 | + |
| 6 | +pub const Index = u32; |
| 7 | + |
| 8 | +pub const Tag = struct { |
| 9 | + number: Number, |
| 10 | + /// Whether this ASN.1 type contains other ASN.1 types. |
| 11 | + constructed: bool, |
| 12 | + class: Class, |
| 13 | + |
| 14 | + /// These values apply to class == .universal. |
| 15 | + pub const Number = enum(u16) { |
| 16 | + // 0 is reserved by spec |
| 17 | + boolean = 1, |
| 18 | + integer = 2, |
| 19 | + bitstring = 3, |
| 20 | + octetstring = 4, |
| 21 | + null = 5, |
| 22 | + oid = 6, |
| 23 | + object_descriptor = 7, |
| 24 | + real = 9, |
| 25 | + enumerated = 10, |
| 26 | + embedded = 11, |
| 27 | + string_utf8 = 12, |
| 28 | + oid_relative = 13, |
| 29 | + time = 14, |
| 30 | + // 15 is reserved to mean that the tag is >= 32 |
| 31 | + sequence = 16, |
| 32 | + /// Elements may appear in any order. |
| 33 | + sequence_of = 17, |
| 34 | + string_numeric = 18, |
| 35 | + string_printable = 19, |
| 36 | + string_teletex = 20, |
| 37 | + string_videotex = 21, |
| 38 | + string_ia5 = 22, |
| 39 | + utc_time = 23, |
| 40 | + generalized_time = 24, |
| 41 | + string_graphic = 25, |
| 42 | + string_visible = 26, |
| 43 | + string_general = 27, |
| 44 | + string_universal = 28, |
| 45 | + string_char = 29, |
| 46 | + string_bmp = 30, |
| 47 | + date = 31, |
| 48 | + time_of_day = 32, |
| 49 | + date_time = 33, |
| 50 | + duration = 34, |
| 51 | + /// IRI = Internationalized Resource Identifier |
| 52 | + oid_iri = 35, |
| 53 | + oid_iri_relative = 36, |
| 54 | + _, |
| 55 | + }; |
| 56 | + |
| 57 | + pub const Class = enum(u2) { |
| 58 | + universal, |
| 59 | + application, |
| 60 | + context_specific, |
| 61 | + private, |
| 62 | + }; |
| 63 | + |
| 64 | + pub fn init(number: Tag.Number, constructed: bool, class: Tag.Class) Tag { |
| 65 | + return .{ .number = number, .constructed = constructed, .class = class }; |
| 66 | + } |
| 67 | + |
| 68 | + pub fn universal(number: Tag.Number, constructed: bool) Tag { |
| 69 | + return .{ .number = number, .constructed = constructed, .class = .universal }; |
| 70 | + } |
| 71 | + |
| 72 | + pub fn decode(reader: anytype) !Tag { |
| 73 | + const tag1: FirstTag = @bitCast(try reader.readByte()); |
| 74 | + var number: u14 = tag1.number; |
| 75 | + |
| 76 | + if (tag1.number == 15) { |
| 77 | + const tag2: NextTag = @bitCast(try reader.readByte()); |
| 78 | + number = tag2.number; |
| 79 | + if (tag2.continues) { |
| 80 | + const tag3: NextTag = @bitCast(try reader.readByte()); |
| 81 | + number = (number << 7) + tag3.number; |
| 82 | + if (tag3.continues) return error.InvalidLength; |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + return Tag{ |
| 87 | + .number = @enumFromInt(number), |
| 88 | + .constructed = tag1.constructed, |
| 89 | + .class = tag1.class, |
| 90 | + }; |
| 91 | + } |
| 92 | + |
| 93 | + pub fn encode(self: Tag, writer: anytype) @TypeOf(writer).Error!void { |
| 94 | + var tag1 = FirstTag{ |
| 95 | + .number = undefined, |
| 96 | + .constructed = self.constructed, |
| 97 | + .class = self.class, |
| 98 | + }; |
| 99 | + |
| 100 | + var buffer: [3]u8 = undefined; |
| 101 | + var stream = std.io.fixedBufferStream(&buffer); |
| 102 | + var writer2 = stream.writer(); |
| 103 | + |
| 104 | + switch (@intFromEnum(self.number)) { |
| 105 | + 0...std.math.maxInt(u5) => |n| { |
| 106 | + tag1.number = @intCast(n); |
| 107 | + writer2.writeByte(@bitCast(tag1)) catch unreachable; |
| 108 | + }, |
| 109 | + std.math.maxInt(u5) + 1...std.math.maxInt(u7) => |n| { |
| 110 | + tag1.number = 15; |
| 111 | + const tag2 = NextTag{ .number = @intCast(n), .continues = false }; |
| 112 | + writer2.writeByte(@bitCast(tag1)) catch unreachable; |
| 113 | + writer2.writeByte(@bitCast(tag2)) catch unreachable; |
| 114 | + }, |
| 115 | + else => |n| { |
| 116 | + tag1.number = 15; |
| 117 | + const tag2 = NextTag{ .number = @intCast(n >> 7), .continues = true }; |
| 118 | + const tag3 = NextTag{ .number = @truncate(n), .continues = false }; |
| 119 | + writer2.writeByte(@bitCast(tag1)) catch unreachable; |
| 120 | + writer2.writeByte(@bitCast(tag2)) catch unreachable; |
| 121 | + writer2.writeByte(@bitCast(tag3)) catch unreachable; |
| 122 | + }, |
| 123 | + } |
| 124 | + |
| 125 | + _ = try writer.write(stream.getWritten()); |
| 126 | + } |
| 127 | + |
| 128 | + const FirstTag = packed struct(u8) { number: u5, constructed: bool, class: Tag.Class }; |
| 129 | + const NextTag = packed struct(u8) { number: u7, continues: bool }; |
| 130 | + |
| 131 | + pub fn toExpected(self: Tag) ExpectedTag { |
| 132 | + return ExpectedTag{ |
| 133 | + .number = self.number, |
| 134 | + .constructed = self.constructed, |
| 135 | + .class = self.class, |
| 136 | + }; |
| 137 | + } |
| 138 | + |
| 139 | + pub fn fromZig(comptime T: type) Tag { |
| 140 | + switch (@typeInfo(T)) { |
| 141 | + .Struct, .Enum, .Union => { |
| 142 | + if (@hasDecl(T, "asn1_tag")) return T.asn1_tag; |
| 143 | + }, |
| 144 | + else => {}, |
| 145 | + } |
| 146 | + |
| 147 | + switch (@typeInfo(T)) { |
| 148 | + .Struct, .Union => return universal(.sequence, true), |
| 149 | + .Bool => return universal(.boolean, false), |
| 150 | + .Int => return universal(.integer, false), |
| 151 | + .Enum => |e| { |
| 152 | + if (@hasDecl(T, "oids")) return Oid.asn1_tag; |
| 153 | + return universal(if (e.is_exhaustive) .enumerated else .integer, false); |
| 154 | + }, |
| 155 | + .Optional => |o| return fromZig(o.child), |
| 156 | + .Null => return universal(.null, false), |
| 157 | + else => @compileError("cannot map Zig type to asn1_tag " ++ @typeName(T)), |
| 158 | + } |
| 159 | + } |
| 160 | +}; |
| 161 | + |
| 162 | +test Tag { |
| 163 | + const buf = [_]u8{0xa3}; |
| 164 | + var stream = std.io.fixedBufferStream(&buf); |
| 165 | + const t = Tag.decode(stream.reader()); |
| 166 | + try std.testing.expectEqual(Tag.init(@enumFromInt(3), true, .context_specific), t); |
| 167 | +} |
| 168 | + |
| 169 | +/// A decoded view. |
| 170 | +pub const Element = struct { |
| 171 | + tag: Tag, |
| 172 | + slice: Slice, |
| 173 | + |
| 174 | + pub const Slice = struct { |
| 175 | + start: Index, |
| 176 | + end: Index, |
| 177 | + |
| 178 | + pub fn len(self: Slice) Index { |
| 179 | + return self.end - self.start; |
| 180 | + } |
| 181 | + |
| 182 | + pub fn view(self: Slice, bytes: []const u8) []const u8 { |
| 183 | + return bytes[self.start..self.end]; |
| 184 | + } |
| 185 | + }; |
| 186 | + |
| 187 | + pub const DecodeError = error{ InvalidLength, EndOfStream }; |
| 188 | + |
| 189 | + /// Safely decode a DER/BER/CER element at `index`: |
| 190 | + /// - Ensures length uses shortest form |
| 191 | + /// - Ensures length is within `bytes` |
| 192 | + /// - Ensures length is less than `std.math.maxInt(Index)` |
| 193 | + pub fn decode(bytes: []const u8, index: Index) DecodeError!Element { |
| 194 | + var stream = std.io.fixedBufferStream(bytes[index..]); |
| 195 | + var reader = stream.reader(); |
| 196 | + |
| 197 | + const tag = try Tag.decode(reader); |
| 198 | + const size_or_len_size = try reader.readByte(); |
| 199 | + |
| 200 | + var start = index + 2; |
| 201 | + var end = start + size_or_len_size; |
| 202 | + // short form between 0-127 |
| 203 | + if (size_or_len_size < 128) { |
| 204 | + if (end > bytes.len) return error.InvalidLength; |
| 205 | + } else { |
| 206 | + // long form between 0 and std.math.maxInt(u1024) |
| 207 | + const len_size: u7 = @truncate(size_or_len_size); |
| 208 | + start += len_size; |
| 209 | + if (len_size > @sizeOf(Index)) return error.InvalidLength; |
| 210 | + |
| 211 | + const len = try reader.readVarInt(Index, .big, len_size); |
| 212 | + if (len < 128) return error.InvalidLength; // should have used short form |
| 213 | + |
| 214 | + end = std.math.add(Index, start, len) catch return error.InvalidLength; |
| 215 | + if (end > bytes.len) return error.InvalidLength; |
| 216 | + } |
| 217 | + |
| 218 | + return Element{ .tag = tag, .slice = Slice{ .start = start, .end = end } }; |
| 219 | + } |
| 220 | +}; |
| 221 | + |
| 222 | +test Element { |
| 223 | + const short_form = [_]u8{ 0x30, 0x03, 0x02, 0x01, 0x09 }; |
| 224 | + try std.testing.expectEqual(Element{ |
| 225 | + .tag = Tag.universal(.sequence, true), |
| 226 | + .slice = Element.Slice{ .start = 2, .end = short_form.len }, |
| 227 | + }, Element.decode(&short_form, 0)); |
| 228 | + |
| 229 | + const long_form = [_]u8{ 0x30, 129, 129 } ++ [_]u8{0} ** 129; |
| 230 | + try std.testing.expectEqual(Element{ |
| 231 | + .tag = Tag.universal(.sequence, true), |
| 232 | + .slice = Element.Slice{ .start = 3, .end = long_form.len }, |
| 233 | + }, Element.decode(&long_form, 0)); |
| 234 | +} |
| 235 | + |
| 236 | +/// For decoding. |
| 237 | +pub const ExpectedTag = struct { |
| 238 | + number: ?Tag.Number = null, |
| 239 | + constructed: ?bool = null, |
| 240 | + class: ?Tag.Class = null, |
| 241 | + |
| 242 | + pub fn init(number: ?Tag.Number, constructed: ?bool, class: ?Tag.Class) ExpectedTag { |
| 243 | + return .{ .number = number, .constructed = constructed, .class = class }; |
| 244 | + } |
| 245 | + |
| 246 | + pub fn primitive(number: ?Tag.Number) ExpectedTag { |
| 247 | + return .{ .number = number, .constructed = false, .class = .universal }; |
| 248 | + } |
| 249 | + |
| 250 | + pub fn match(self: ExpectedTag, tag: Tag) bool { |
| 251 | + if (self.number) |e| { |
| 252 | + if (tag.number != e) return false; |
| 253 | + } |
| 254 | + if (self.constructed) |e| { |
| 255 | + if (tag.constructed != e) return false; |
| 256 | + } |
| 257 | + if (self.class) |e| { |
| 258 | + if (tag.class != e) return false; |
| 259 | + } |
| 260 | + return true; |
| 261 | + } |
| 262 | +}; |
| 263 | + |
| 264 | +pub const FieldTag = struct { |
| 265 | + number: std.meta.Tag(Tag.Number), |
| 266 | + class: Tag.Class, |
| 267 | + explicit: bool = true, |
| 268 | + |
| 269 | + pub fn explicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { |
| 270 | + return FieldTag{ .number = number, .class = class, .explicit = true }; |
| 271 | + } |
| 272 | + |
| 273 | + pub fn implicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { |
| 274 | + return FieldTag{ .number = number, .class = class, .explicit = false }; |
| 275 | + } |
| 276 | + |
| 277 | + pub fn fromContainer(comptime Container: type, comptime field_name: []const u8) ?FieldTag { |
| 278 | + if (@hasDecl(Container, "asn1_tags") and @hasField(@TypeOf(Container.asn1_tags), field_name)) { |
| 279 | + return @field(Container.asn1_tags, field_name); |
| 280 | + } |
| 281 | + |
| 282 | + return null; |
| 283 | + } |
| 284 | + |
| 285 | + pub fn toTag(self: FieldTag) Tag { |
| 286 | + return Tag.init(@enumFromInt(self.number), self.explicit, self.class); |
| 287 | + } |
| 288 | +}; |
| 289 | + |
| 290 | +pub const BitString = struct { |
| 291 | + /// Number of bits in rightmost byte that are unused. |
| 292 | + right_padding: u3 = 0, |
| 293 | + bytes: []const u8, |
| 294 | + |
| 295 | + pub fn bitLen(self: BitString) usize { |
| 296 | + return self.bytes.len * 8 - self.right_padding; |
| 297 | + } |
| 298 | + |
| 299 | + const asn1_tag = Tag.universal(.bitstring, false); |
| 300 | + |
| 301 | + pub fn decodeDer(decoder: *der.Decoder) !BitString { |
| 302 | + const ele = try decoder.element(asn1_tag.toExpected()); |
| 303 | + const bytes = decoder.view(ele); |
| 304 | + |
| 305 | + if (bytes.len < 1) return error.InvalidBitString; |
| 306 | + const padding = bytes[0]; |
| 307 | + if (padding >= 8) return error.InvalidBitString; |
| 308 | + const right_padding: u3 = @intCast(padding); |
| 309 | + |
| 310 | + // DER requires that unused bits be zero. |
| 311 | + if (@ctz(bytes[bytes.len - 1]) < right_padding) return error.InvalidBitString; |
| 312 | + |
| 313 | + return BitString{ .bytes = bytes[1..], .right_padding = right_padding }; |
| 314 | + } |
| 315 | + |
| 316 | + pub fn encodeDer(self: BitString, encoder: *der.Encoder) !void { |
| 317 | + try encoder.writer().writeAll(self.bytes); |
| 318 | + try encoder.writer().writeByte(self.right_padding); |
| 319 | + try encoder.length(self.bytes.len + 1); |
| 320 | + try encoder.tag(asn1_tag); |
| 321 | + } |
| 322 | +}; |
| 323 | + |
| 324 | +pub fn Opaque(comptime tag: Tag) type { |
| 325 | + return struct { |
| 326 | + bytes: []const u8, |
| 327 | + |
| 328 | + pub fn decodeDer(decoder: *der.Decoder) !@This() { |
| 329 | + const ele = try decoder.element(tag.toExpected()); |
| 330 | + if (tag.constructed) decoder.index = ele.slice.end; |
| 331 | + return .{ .bytes = decoder.view(ele) }; |
| 332 | + } |
| 333 | + |
| 334 | + pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { |
| 335 | + try encoder.tagBytes(tag, self.bytes); |
| 336 | + } |
| 337 | + }; |
| 338 | +} |
| 339 | + |
| 340 | +/// Use sparingly. |
| 341 | +pub const Any = struct { |
| 342 | + tag: Tag, |
| 343 | + bytes: []const u8, |
| 344 | + |
| 345 | + pub fn decodeDer(decoder: *der.Decoder) !@This() { |
| 346 | + const ele = try decoder.element(ExpectedTag{}); |
| 347 | + return .{ .tag = ele.tag, .bytes = decoder.view(ele) }; |
| 348 | + } |
| 349 | + |
| 350 | + pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { |
| 351 | + try encoder.tagBytes(self.tag, self.bytes); |
| 352 | + } |
| 353 | +}; |
| 354 | + |
| 355 | +test { |
| 356 | + _ = der; |
| 357 | + _ = Oid; |
| 358 | + _ = @import("asn1/test.zig"); |
| 359 | +} |
0 commit comments