| 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.Writer.Error; |
| 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 writer: std.Io.Writer = .fixed(out); |
| 18 | |
| 19 | try writer.writeByte(first * 40 + second); |
| 20 | |
| 21 | var i: usize = 1; |
| 22 | while (split.next()) |s| { |
| 23 | var parsed = try std.fmt.parseUnsigned(Arc, s, 10); |
| 24 | const n_bytes = if (parsed == 0) 0 else std.math.log(Arc, encoding_base, parsed); |
| 25 | |
| 26 | for (0..n_bytes) |j| { |
| 27 | const place = std.math.pow(Arc, encoding_base, n_bytes - @as(Arc, @intCast(j))); |
| 28 | const digit: u8 = @intCast(@divFloor(parsed, place)); |
| 29 | |
| 30 | try writer.writeByte(digit | 0x80); |
| 31 | parsed -= digit * place; |
| 32 | |
| 33 | i += 1; |
| 34 | } |
| 35 | try writer.writeByte(@intCast(parsed)); |
| 36 | i += 1; |
| 37 | } |
| 38 | |
| 39 | return .{ .encoded = writer.buffered() }; |
| 40 | } |
| 41 | |
| 42 | test fromDot { |
| 43 | var buf: [256]u8 = undefined; |
| 44 | for (test_cases) |t| { |
| 45 | const actual = try fromDot(t.dot_notation, &buf); |
| 46 | try std.testing.expectEqualSlices(u8, t.encoded, actual.encoded); |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | pub fn toDot(self: Oid, writer: *std.Io.Writer) std.Io.Writer.Error!void { |
| 51 | const encoded = self.encoded; |
| 52 | const first = @divTrunc(encoded[0], 40); |
| 53 | const second = encoded[0] - first * 40; |
| 54 | try writer.print("{d}.{d}", .{ first, second }); |
| 55 | |
| 56 | var i: usize = 1; |
| 57 | while (i != encoded.len) { |
| 58 | const n_bytes: usize = brk: { |
| 59 | var res: usize = 1; |
| 60 | var j: usize = i; |
| 61 | while (encoded[j] & 0x80 != 0) { |
| 62 | res += 1; |
| 63 | j += 1; |
| 64 | } |
| 65 | break :brk res; |
| 66 | }; |
| 67 | |
| 68 | var n: usize = 0; |
| 69 | for (0..n_bytes) |j| { |
| 70 | const place = std.math.pow(usize, encoding_base, n_bytes - j - 1); |
| 71 | n += place * (encoded[i] & 0b01111111); |
| 72 | i += 1; |
| 73 | } |
| 74 | try writer.print(".{d}", .{n}); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | test toDot { |
| 79 | var buf: [256]u8 = undefined; |
| 80 | |
| 81 | for (test_cases) |t| { |
| 82 | var stream: std.Io.Writer = .fixed(&buf); |
| 83 | try toDot(Oid{ .encoded = t.encoded }, &stream); |
| 84 | try std.testing.expectEqualStrings(t.dot_notation, stream.buffered()); |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | test "malformed OID" { |
| 89 | var empty: der.Decoder = .{ .bytes = &.{ 0x06, 0x00 } }; |
| 90 | try std.testing.expectError(error.EndOfStream, decodeDer(&empty)); |
| 91 | |
| 92 | var truncated: der.Decoder = .{ .bytes = &.{ 0x06, 0x02, 0x2a, 0x80 } }; |
| 93 | try std.testing.expectError(error.InvalidEncoding, decodeDer(&truncated)); |
| 94 | } |
| 95 | |
| 96 | const TestCase = struct { |
| 97 | encoded: []const u8, |
| 98 | dot_notation: []const u8, |
| 99 | |
| 100 | pub fn init(comptime hex: []const u8, dot_notation: []const u8) TestCase { |
| 101 | return .{ .encoded = &hexToBytes(hex), .dot_notation = dot_notation }; |
| 102 | } |
| 103 | }; |
| 104 | |
| 105 | const test_cases = [_]TestCase{ |
| 106 | // https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier |
| 107 | TestCase.init("2b0601040182371514", "1.3.6.1.4.1.311.21.20"), |
| 108 | // https://luca.ntop.org/Teaching/Appunti/asn1.html |
| 109 | TestCase.init("2a864886f70d", "1.2.840.113549"), |
| 110 | // https://www.sysadmins.lv/blog-en/how-to-encode-object-identifier-to-an-asn1-der-encoded-string.aspx |
| 111 | TestCase.init("2a868d20", "1.2.100000"), |
| 112 | TestCase.init("2a864886f70d01010b", "1.2.840.113549.1.1.11"), |
| 113 | TestCase.init("2b6570", "1.3.101.112"), |
| 114 | }; |
| 115 | |
| 116 | pub const asn1_tag = asn1.Tag.init(.oid, false, .universal); |
| 117 | |
| 118 | pub fn decodeDer(decoder: *der.Decoder) !Oid { |
| 119 | const ele = try decoder.element(asn1_tag.toExpected()); |
| 120 | const encoded = decoder.view(ele); |
| 121 | if (encoded.len == 0) return error.EndOfStream; |
| 122 | if (encoded[encoded.len - 1] & 0x80 != 0) return error.InvalidEncoding; |
| 123 | return Oid{ .encoded = encoded }; |
| 124 | } |
| 125 | |
| 126 | pub fn encodeDer(self: Oid, encoder: *der.Encoder) !void { |
| 127 | try encoder.tagBytes(asn1_tag, self.encoded); |
| 128 | } |
| 129 | |
| 130 | fn encodedLen(dot_notation: []const u8) usize { |
| 131 | var buf: [256]u8 = undefined; |
| 132 | const oid = fromDot(dot_notation, &buf) catch unreachable; |
| 133 | return oid.encoded.len; |
| 134 | } |
| 135 | |
| 136 | /// Returns encoded bytes of OID. |
| 137 | fn encodeComptime(comptime dot_notation: []const u8) [encodedLen(dot_notation)]u8 { |
| 138 | @setEvalBranchQuota(4000); |
| 139 | comptime var buf: [256]u8 = undefined; |
| 140 | const oid = comptime fromDot(dot_notation, &buf) catch unreachable; |
| 141 | return oid.encoded[0..oid.encoded.len].*; |
| 142 | } |
| 143 | |
| 144 | test encodeComptime { |
| 145 | try std.testing.expectEqual( |
| 146 | hexToBytes("2b0601040182371514"), |
| 147 | comptime encodeComptime("1.3.6.1.4.1.311.21.20"), |
| 148 | ); |
| 149 | } |
| 150 | |
| 151 | pub fn fromDotComptime(comptime dot_notation: []const u8) Oid { |
| 152 | const tmp = comptime encodeComptime(dot_notation); |
| 153 | return Oid{ .encoded = &tmp }; |
| 154 | } |
| 155 | |
| 156 | /// Maps of: |
| 157 | /// - Oid -> enum |
| 158 | /// - Enum -> oid |
| 159 | pub fn StaticMap(comptime Enum: type) type { |
| 160 | const enum_info = @typeInfo(Enum).@"enum"; |
| 161 | const EnumToOid = std.EnumArray(Enum, []const u8); |
| 162 | const ReturnType = struct { |
| 163 | oid_to_enum: std.StaticStringMap(Enum), |
| 164 | enum_to_oid: EnumToOid, |
| 165 | |
| 166 | pub fn oidToEnum(self: @This(), encoded: []const u8) ?Enum { |
| 167 | return self.oid_to_enum.get(encoded); |
| 168 | } |
| 169 | |
| 170 | pub fn enumToOid(self: @This(), value: Enum) Oid { |
| 171 | const bytes = self.enum_to_oid.get(value); |
| 172 | return .{ .encoded = bytes }; |
| 173 | } |
| 174 | }; |
| 175 | |
| 176 | return struct { |
| 177 | pub fn initComptime(comptime key_pairs: anytype) ReturnType { |
| 178 | const struct_info = @typeInfo(@TypeOf(key_pairs)).@"struct"; |
| 179 | const error_msg = "Each field of '" ++ @typeName(Enum) ++ "' must map to exactly one OID"; |
| 180 | if (enum_info.mode == .nonexhaustive or enum_info.field_names.len != struct_info.field_names.len) { |
| 181 | @compileError(error_msg); |
| 182 | } |
| 183 | |
| 184 | comptime var enum_to_oid = EnumToOid.initUndefined(); |
| 185 | |
| 186 | const KeyPair = struct { []const u8, Enum }; |
| 187 | comptime var static_key_pairs: [enum_info.field_names.len]KeyPair = undefined; |
| 188 | |
| 189 | comptime for (enum_info.field_names, enum_info.field_values, 0..) |f_name, f_value, i| { |
| 190 | if (!@hasField(@TypeOf(key_pairs), f_name)) { |
| 191 | @compileError("Field '" ++ f_name ++ "' missing Oid.StaticMap entry"); |
| 192 | } |
| 193 | const encoded = &encodeComptime(@field(key_pairs, f_name)); |
| 194 | const tag: Enum = @fromBackingInt(@intCast(f_value)); |
| 195 | static_key_pairs[i] = .{ encoded, tag }; |
| 196 | enum_to_oid.set(tag, encoded); |
| 197 | }; |
| 198 | |
| 199 | const oid_to_enum = std.StaticStringMap(Enum).initComptime(static_key_pairs); |
| 200 | if (oid_to_enum.values().len != enum_info.field_names.len) @compileError(error_msg); |
| 201 | |
| 202 | return ReturnType{ .oid_to_enum = oid_to_enum, .enum_to_oid = enum_to_oid }; |
| 203 | } |
| 204 | }; |
| 205 | } |
| 206 | |
| 207 | /// Strictly for testing. |
| 208 | fn hexToBytes(comptime hex: []const u8) [hex.len / 2]u8 { |
| 209 | var res: [hex.len / 2]u8 = undefined; |
| 210 | _ = std.fmt.hexToBytes(&res, hex) catch unreachable; |
| 211 | return res; |
| 212 | } |
| 213 | |
| 214 | const std = @import("std"); |
| 215 | const Oid = @This(); |
| 216 | const Arc = u32; |
| 217 | const encoding_base = 128; |
| 218 | const Allocator = std.mem.Allocator; |
| 219 | const der = @import("der.zig"); |
| 220 | const asn1 = @import("../asn1.zig"); |