mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 05:44:20 +00:00
357 lines
11 KiB
Zig
357 lines
11 KiB
Zig
//! ASN.1 types for public consumption.
|
|
const std = @import("std");
|
|
pub const der = @import("./asn1/der.zig");
|
|
pub const Oid = @import("./asn1/Oid.zig");
|
|
|
|
pub const Index = u32;
|
|
|
|
pub const Tag = struct {
|
|
number: Number,
|
|
/// Whether this ASN.1 type contains other ASN.1 types.
|
|
constructed: bool,
|
|
class: Class,
|
|
|
|
/// These values apply to class == .universal.
|
|
pub const Number = enum(u16) {
|
|
// 0 is reserved by spec
|
|
boolean = 1,
|
|
integer = 2,
|
|
bitstring = 3,
|
|
octetstring = 4,
|
|
null = 5,
|
|
oid = 6,
|
|
object_descriptor = 7,
|
|
real = 9,
|
|
enumerated = 10,
|
|
embedded = 11,
|
|
string_utf8 = 12,
|
|
oid_relative = 13,
|
|
time = 14,
|
|
// 15 is reserved to mean that the tag is >= 32
|
|
sequence = 16,
|
|
/// Elements may appear in any order.
|
|
sequence_of = 17,
|
|
string_numeric = 18,
|
|
string_printable = 19,
|
|
string_teletex = 20,
|
|
string_videotex = 21,
|
|
string_ia5 = 22,
|
|
utc_time = 23,
|
|
generalized_time = 24,
|
|
string_graphic = 25,
|
|
string_visible = 26,
|
|
string_general = 27,
|
|
string_universal = 28,
|
|
string_char = 29,
|
|
string_bmp = 30,
|
|
date = 31,
|
|
time_of_day = 32,
|
|
date_time = 33,
|
|
duration = 34,
|
|
/// IRI = Internationalized Resource Identifier
|
|
oid_iri = 35,
|
|
oid_iri_relative = 36,
|
|
_,
|
|
};
|
|
|
|
pub const Class = enum(u2) {
|
|
universal,
|
|
application,
|
|
context_specific,
|
|
private,
|
|
};
|
|
|
|
pub fn init(number: Tag.Number, constructed: bool, class: Tag.Class) Tag {
|
|
return .{ .number = number, .constructed = constructed, .class = class };
|
|
}
|
|
|
|
pub fn universal(number: Tag.Number, constructed: bool) Tag {
|
|
return .{ .number = number, .constructed = constructed, .class = .universal };
|
|
}
|
|
|
|
pub fn decode(reader: *std.Io.Reader) !Tag {
|
|
const tag1: FirstTag = @bitCast(try reader.takeByte());
|
|
var number: u14 = tag1.number;
|
|
|
|
if (tag1.number == 15) {
|
|
const tag2: NextTag = @bitCast(try reader.takeByte());
|
|
number = tag2.number;
|
|
if (tag2.continues) {
|
|
const tag3: NextTag = @bitCast(try reader.takeByte());
|
|
number = (number << 7) + tag3.number;
|
|
if (tag3.continues) return error.InvalidLength;
|
|
}
|
|
}
|
|
|
|
return Tag{
|
|
.number = @enumFromInt(number),
|
|
.constructed = tag1.constructed,
|
|
.class = tag1.class,
|
|
};
|
|
}
|
|
|
|
pub fn encode(self: Tag, writer: *std.Io.Writer) @TypeOf(writer).Error!void {
|
|
var tag1 = FirstTag{
|
|
.number = undefined,
|
|
.constructed = self.constructed,
|
|
.class = self.class,
|
|
};
|
|
|
|
var buffer: [3]u8 = undefined;
|
|
var writer2: std.Io.Writer = .init(&buffer);
|
|
|
|
switch (@intFromEnum(self.number)) {
|
|
0...std.math.maxInt(u5) => |n| {
|
|
tag1.number = @intCast(n);
|
|
writer2.writeByte(@bitCast(tag1)) catch unreachable;
|
|
},
|
|
std.math.maxInt(u5) + 1...std.math.maxInt(u7) => |n| {
|
|
tag1.number = 15;
|
|
const tag2 = NextTag{ .number = @intCast(n), .continues = false };
|
|
writer2.writeByte(@bitCast(tag1)) catch unreachable;
|
|
writer2.writeByte(@bitCast(tag2)) catch unreachable;
|
|
},
|
|
else => |n| {
|
|
tag1.number = 15;
|
|
const tag2 = NextTag{ .number = @intCast(n >> 7), .continues = true };
|
|
const tag3 = NextTag{ .number = @truncate(n), .continues = false };
|
|
writer2.writeByte(@bitCast(tag1)) catch unreachable;
|
|
writer2.writeByte(@bitCast(tag2)) catch unreachable;
|
|
writer2.writeByte(@bitCast(tag3)) catch unreachable;
|
|
},
|
|
}
|
|
|
|
_ = try writer.write(writer2.buffered());
|
|
}
|
|
|
|
const FirstTag = packed struct(u8) { number: u5, constructed: bool, class: Tag.Class };
|
|
const NextTag = packed struct(u8) { number: u7, continues: bool };
|
|
|
|
pub fn toExpected(self: Tag) ExpectedTag {
|
|
return ExpectedTag{
|
|
.number = self.number,
|
|
.constructed = self.constructed,
|
|
.class = self.class,
|
|
};
|
|
}
|
|
|
|
pub fn fromZig(comptime T: type) Tag {
|
|
switch (@typeInfo(T)) {
|
|
.@"struct", .@"enum", .@"union" => {
|
|
if (@hasDecl(T, "asn1_tag")) return T.asn1_tag;
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
switch (@typeInfo(T)) {
|
|
.@"struct", .@"union" => return universal(.sequence, true),
|
|
.bool => return universal(.boolean, false),
|
|
.int => return universal(.integer, false),
|
|
.@"enum" => |e| {
|
|
if (@hasDecl(T, "oids")) return Oid.asn1_tag;
|
|
return universal(if (e.is_exhaustive) .enumerated else .integer, false);
|
|
},
|
|
.optional => |o| return fromZig(o.child),
|
|
.null => return universal(.null, false),
|
|
else => @compileError("cannot map Zig type to asn1_tag " ++ @typeName(T)),
|
|
}
|
|
}
|
|
};
|
|
|
|
test Tag {
|
|
const buf = [_]u8{0xa3};
|
|
var reader: std.Io.Reader = .fixed(&buf);
|
|
const t = Tag.decode(&reader);
|
|
try std.testing.expectEqual(Tag.init(@enumFromInt(3), true, .context_specific), t);
|
|
}
|
|
|
|
/// A decoded view.
|
|
pub const Element = struct {
|
|
tag: Tag,
|
|
slice: Slice,
|
|
|
|
pub const Slice = struct {
|
|
start: Index,
|
|
end: Index,
|
|
|
|
pub fn len(self: Slice) Index {
|
|
return self.end - self.start;
|
|
}
|
|
|
|
pub fn view(self: Slice, bytes: []const u8) []const u8 {
|
|
return bytes[self.start..self.end];
|
|
}
|
|
};
|
|
|
|
pub const DecodeError = error{ InvalidLength, EndOfStream };
|
|
|
|
/// Safely decode a DER/BER/CER element at `index`:
|
|
/// - Ensures length uses shortest form
|
|
/// - Ensures length is within `bytes`
|
|
/// - Ensures length is less than `std.math.maxInt(Index)`
|
|
pub fn decode(bytes: []const u8, index: Index) DecodeError!Element {
|
|
var reader: std.Io.Reader = .fixed(bytes[index..]);
|
|
|
|
const tag = try Tag.decode(&reader);
|
|
const size_or_len_size = try reader.takeByte();
|
|
|
|
var start = index + 2;
|
|
var end = start + size_or_len_size;
|
|
// short form between 0-127
|
|
if (size_or_len_size < 128) {
|
|
if (end > bytes.len) return error.InvalidLength;
|
|
} else {
|
|
// long form between 0 and std.math.maxInt(u1024)
|
|
const len_size: u7 = @truncate(size_or_len_size);
|
|
start += len_size;
|
|
if (len_size > @sizeOf(Index)) return error.InvalidLength;
|
|
|
|
const len = try reader.takeVarInt(Index, .big, len_size);
|
|
if (len < 128) return error.InvalidLength; // should have used short form
|
|
|
|
end = std.math.add(Index, start, len) catch return error.InvalidLength;
|
|
if (end > bytes.len) return error.InvalidLength;
|
|
}
|
|
|
|
return Element{ .tag = tag, .slice = Slice{ .start = start, .end = end } };
|
|
}
|
|
};
|
|
|
|
test Element {
|
|
const short_form = [_]u8{ 0x30, 0x03, 0x02, 0x01, 0x09 };
|
|
try std.testing.expectEqual(Element{
|
|
.tag = Tag.universal(.sequence, true),
|
|
.slice = Element.Slice{ .start = 2, .end = short_form.len },
|
|
}, Element.decode(&short_form, 0));
|
|
|
|
const long_form = [_]u8{ 0x30, 129, 129 } ++ [_]u8{0} ** 129;
|
|
try std.testing.expectEqual(Element{
|
|
.tag = Tag.universal(.sequence, true),
|
|
.slice = Element.Slice{ .start = 3, .end = long_form.len },
|
|
}, Element.decode(&long_form, 0));
|
|
}
|
|
|
|
/// For decoding.
|
|
pub const ExpectedTag = struct {
|
|
number: ?Tag.Number = null,
|
|
constructed: ?bool = null,
|
|
class: ?Tag.Class = null,
|
|
|
|
pub fn init(number: ?Tag.Number, constructed: ?bool, class: ?Tag.Class) ExpectedTag {
|
|
return .{ .number = number, .constructed = constructed, .class = class };
|
|
}
|
|
|
|
pub fn primitive(number: ?Tag.Number) ExpectedTag {
|
|
return .{ .number = number, .constructed = false, .class = .universal };
|
|
}
|
|
|
|
pub fn match(self: ExpectedTag, tag: Tag) bool {
|
|
if (self.number) |e| {
|
|
if (tag.number != e) return false;
|
|
}
|
|
if (self.constructed) |e| {
|
|
if (tag.constructed != e) return false;
|
|
}
|
|
if (self.class) |e| {
|
|
if (tag.class != e) return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
pub const FieldTag = struct {
|
|
number: std.meta.Tag(Tag.Number),
|
|
class: Tag.Class,
|
|
explicit: bool = true,
|
|
|
|
pub fn initExplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag {
|
|
return .{ .number = number, .class = class, .explicit = true };
|
|
}
|
|
|
|
pub fn initImplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag {
|
|
return .{ .number = number, .class = class, .explicit = false };
|
|
}
|
|
|
|
pub fn fromContainer(comptime Container: type, comptime field_name: []const u8) ?FieldTag {
|
|
if (@hasDecl(Container, "asn1_tags") and @hasField(@TypeOf(Container.asn1_tags), field_name)) {
|
|
return @field(Container.asn1_tags, field_name);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn toTag(self: FieldTag) Tag {
|
|
return Tag.init(@enumFromInt(self.number), self.explicit, self.class);
|
|
}
|
|
};
|
|
|
|
pub const BitString = struct {
|
|
/// Number of bits in rightmost byte that are unused.
|
|
right_padding: u3 = 0,
|
|
bytes: []const u8,
|
|
|
|
pub fn bitLen(self: BitString) usize {
|
|
return self.bytes.len * 8 - self.right_padding;
|
|
}
|
|
|
|
const asn1_tag = Tag.universal(.bitstring, false);
|
|
|
|
pub fn decodeDer(decoder: *der.Decoder) !BitString {
|
|
const ele = try decoder.element(asn1_tag.toExpected());
|
|
const bytes = decoder.view(ele);
|
|
|
|
if (bytes.len < 1) return error.InvalidBitString;
|
|
const padding = bytes[0];
|
|
if (padding >= 8) return error.InvalidBitString;
|
|
const right_padding: u3 = @intCast(padding);
|
|
|
|
// DER requires that unused bits be zero.
|
|
if (@ctz(bytes[bytes.len - 1]) < right_padding) return error.InvalidBitString;
|
|
|
|
return BitString{ .bytes = bytes[1..], .right_padding = right_padding };
|
|
}
|
|
|
|
pub fn encodeDer(self: BitString, encoder: *der.Encoder) !void {
|
|
try encoder.writer().writeAll(self.bytes);
|
|
try encoder.writer().writeByte(self.right_padding);
|
|
try encoder.length(self.bytes.len + 1);
|
|
try encoder.tag(asn1_tag);
|
|
}
|
|
};
|
|
|
|
pub fn Opaque(comptime tag: Tag) type {
|
|
return struct {
|
|
bytes: []const u8,
|
|
|
|
pub fn decodeDer(decoder: *der.Decoder) !@This() {
|
|
const ele = try decoder.element(tag.toExpected());
|
|
if (tag.constructed) decoder.index = ele.slice.end;
|
|
return .{ .bytes = decoder.view(ele) };
|
|
}
|
|
|
|
pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void {
|
|
try encoder.tagBytes(tag, self.bytes);
|
|
}
|
|
};
|
|
}
|
|
|
|
/// Use sparingly.
|
|
pub const Any = struct {
|
|
tag: Tag,
|
|
bytes: []const u8,
|
|
|
|
pub fn decodeDer(decoder: *der.Decoder) !@This() {
|
|
const ele = try decoder.element(ExpectedTag{});
|
|
return .{ .tag = ele.tag, .bytes = decoder.view(ele) };
|
|
}
|
|
|
|
pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void {
|
|
try encoder.tagBytes(self.tag, self.bytes);
|
|
}
|
|
};
|
|
|
|
test {
|
|
_ = der;
|
|
_ = Oid;
|
|
_ = @import("asn1/test.zig");
|
|
}
|