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.Writer.Error;
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 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
42test 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
50pub 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
78test 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
88test "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
96const 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
105const 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
116pub const asn1_tag = asn1.Tag.init(.oid, false, .universal);
117
118pub 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
126pub fn encodeDer(self: Oid, encoder: *der.Encoder) !void {
127 try encoder.tagBytes(asn1_tag, self.encoded);
128}
129
130fn 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.
137fn 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
144test encodeComptime {
145 try std.testing.expectEqual(
146 hexToBytes("2b0601040182371514"),
147 comptime encodeComptime("1.3.6.1.4.1.311.21.20"),
148 );
149}
150
151pub 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
159pub 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.
208fn 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
214const std = @import("std");
215const Oid = @This();
216const Arc = u32;
217const encoding_base = 128;
218const Allocator = std.mem.Allocator;
219const der = @import("der.zig");
220const asn1 = @import("../asn1.zig");