Skip to content

Commit 8664743

Browse files
committed
introduce std.crypto.CertificateBundle
for reading root certificate authority bundles from standard installation locations on the file system. So far only Linux logic is added.
1 parent 3089041 commit 8664743

File tree

6 files changed

+369
-173
lines changed

6 files changed

+369
-173
lines changed

lib/std/crypto.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ const std = @import("std.zig");
177177
pub const errors = @import("crypto/errors.zig");
178178

179179
pub const tls = @import("crypto/tls.zig");
180+
pub const Der = @import("crypto/Der.zig");
181+
pub const CertificateBundle = @import("crypto/CertificateBundle.zig");
180182

181183
test {
182184
_ = aead.aegis.Aegis128L;
@@ -266,6 +268,9 @@ test {
266268
_ = utils;
267269
_ = random;
268270
_ = errors;
271+
_ = tls;
272+
_ = Der;
273+
_ = CertificateBundle;
269274
}
270275

271276
test "CSPRNG" {
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
//! A set of certificates. Typically pre-installed on every operating system,
2+
//! these are "Certificate Authorities" used to validate SSL certificates.
3+
//! This data structure stores certificates in DER-encoded form, all of them
4+
//! concatenated together in the `bytes` array. The `map` field contains an
5+
//! index from the DER-encoded subject name to the index within `bytes`.
6+
7+
map: std.HashMapUnmanaged(Key, u32, MapContext, std.hash_map.default_max_load_percentage) = .{},
8+
bytes: std.ArrayListUnmanaged(u8) = .{},
9+
10+
pub const Key = struct {
11+
subject_start: u32,
12+
subject_end: u32,
13+
};
14+
15+
/// The returned bytes become invalid after calling any of the rescan functions
16+
/// or add functions.
17+
pub fn find(cb: CertificateBundle, subject_name: []const u8) ?[]const u8 {
18+
const Adapter = struct {
19+
cb: CertificateBundle,
20+
21+
pub fn hash(ctx: @This(), k: []const u8) u64 {
22+
_ = ctx;
23+
return std.hash_map.hashString(k);
24+
}
25+
26+
pub fn eql(ctx: @This(), a: []const u8, b_key: Key) bool {
27+
const b = ctx.cb.bytes.items[b_key.subject_start..b_key.subject_end];
28+
return mem.eql(u8, a, b);
29+
}
30+
};
31+
const index = cb.map.getAdapted(subject_name, Adapter{ .cb = cb }) orelse return null;
32+
return cb.bytes.items[index..];
33+
}
34+
35+
pub fn deinit(cb: *CertificateBundle, gpa: Allocator) void {
36+
cb.map.deinit(gpa);
37+
cb.bytes.deinit(gpa);
38+
cb.* = undefined;
39+
}
40+
41+
/// Empties the set of certificates and then scans the host operating system
42+
/// file system standard locations for certificates.
43+
pub fn rescan(cb: *CertificateBundle, gpa: Allocator) !void {
44+
switch (builtin.os.tag) {
45+
.linux => return rescanLinux(cb, gpa),
46+
else => @compileError("it is unknown where the root CA certificates live on this OS"),
47+
}
48+
}
49+
50+
pub fn rescanLinux(cb: *CertificateBundle, gpa: Allocator) !void {
51+
var dir = fs.openIterableDirAbsolute("/etc/ssl/certs", .{}) catch |err| switch (err) {
52+
error.FileNotFound => return,
53+
else => |e| return e,
54+
};
55+
defer dir.close();
56+
57+
cb.bytes.clearRetainingCapacity();
58+
cb.map.clearRetainingCapacity();
59+
60+
var it = dir.iterate();
61+
while (try it.next()) |entry| {
62+
switch (entry.kind) {
63+
.File, .SymLink => {},
64+
else => continue,
65+
}
66+
67+
try addCertsFromFile(cb, gpa, dir.dir, entry.name);
68+
}
69+
70+
cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
71+
}
72+
73+
pub fn addCertsFromFile(
74+
cb: *CertificateBundle,
75+
gpa: Allocator,
76+
dir: fs.Dir,
77+
sub_file_path: []const u8,
78+
) !void {
79+
var file = try dir.openFile(sub_file_path, .{});
80+
defer file.close();
81+
82+
const size = try file.getEndPos();
83+
84+
// We borrow `bytes` as a temporary buffer for the base64-encoded data.
85+
// This is possible by computing the decoded length and reserving the space
86+
// for the decoded bytes first.
87+
const decoded_size_upper_bound = size / 4 * 3;
88+
try cb.bytes.ensureUnusedCapacity(gpa, decoded_size_upper_bound + size);
89+
const end_reserved = cb.bytes.items.len + decoded_size_upper_bound;
90+
const buffer = cb.bytes.allocatedSlice()[end_reserved..];
91+
const end_index = try file.readAll(buffer);
92+
const encoded_bytes = buffer[0..end_index];
93+
94+
const begin_marker = "-----BEGIN CERTIFICATE-----";
95+
const end_marker = "-----END CERTIFICATE-----";
96+
97+
var start_index: usize = 0;
98+
while (mem.indexOfPos(u8, encoded_bytes, start_index, begin_marker)) |begin_marker_start| {
99+
const cert_start = begin_marker_start + begin_marker.len;
100+
const cert_end = mem.indexOfPos(u8, encoded_bytes, cert_start, end_marker) orelse
101+
return error.MissingEndCertificateMarker;
102+
start_index = cert_end + end_marker.len;
103+
const encoded_cert = mem.trim(u8, encoded_bytes[cert_start..cert_end], " \t\r\n");
104+
const decoded_start = @intCast(u32, cb.bytes.items.len);
105+
const dest_buf = cb.bytes.allocatedSlice()[decoded_start..];
106+
cb.bytes.items.len += try base64.decode(dest_buf, encoded_cert);
107+
const k = try key(cb, decoded_start);
108+
try cb.map.putContext(gpa, k, decoded_start, .{ .cb = cb });
109+
}
110+
}
111+
112+
pub fn key(cb: *CertificateBundle, bytes_index: u32) !Key {
113+
const bytes = cb.bytes.items;
114+
const certificate = try Der.parseElement(bytes, bytes_index);
115+
const tbs_certificate = try Der.parseElement(bytes, certificate.start);
116+
const version = try Der.parseElement(bytes, tbs_certificate.start);
117+
if (@bitCast(u8, version.identifier) != 0xa0 or
118+
!mem.eql(u8, bytes[version.start..version.end], "\x02\x01\x02"))
119+
{
120+
return error.UnsupportedCertificateVersion;
121+
}
122+
123+
const serial_number = try Der.parseElement(bytes, version.end);
124+
125+
// RFC 5280, section 4.1.2.3:
126+
// "This field MUST contain the same algorithm identifier as
127+
// the signatureAlgorithm field in the sequence Certificate."
128+
const signature = try Der.parseElement(bytes, serial_number.end);
129+
const issuer = try Der.parseElement(bytes, signature.end);
130+
const validity = try Der.parseElement(bytes, issuer.end);
131+
const subject = try Der.parseElement(bytes, validity.end);
132+
//const subject_pub_key = try Der.parseElement(bytes, subject.end);
133+
//const extensions = try Der.parseElement(bytes, subject_pub_key.end);
134+
135+
return .{
136+
.subject_start = subject.start,
137+
.subject_end = subject.end,
138+
};
139+
}
140+
141+
const builtin = @import("builtin");
142+
const std = @import("../std.zig");
143+
const fs = std.fs;
144+
const mem = std.mem;
145+
const Allocator = std.mem.Allocator;
146+
const Der = std.crypto.Der;
147+
const CertificateBundle = @This();
148+
149+
const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n");
150+
151+
const MapContext = struct {
152+
cb: *const CertificateBundle,
153+
154+
pub fn hash(ctx: MapContext, k: Key) u64 {
155+
return std.hash_map.hashString(ctx.cb.bytes.items[k.subject_start..k.subject_end]);
156+
}
157+
158+
pub fn eql(ctx: MapContext, a: Key, b: Key) bool {
159+
const bytes = ctx.cb.bytes.items;
160+
return mem.eql(
161+
u8,
162+
bytes[a.subject_start..a.subject_end],
163+
bytes[b.subject_start..b.subject_end],
164+
);
165+
}
166+
};
167+
168+
test {
169+
var bundle: CertificateBundle = .{};
170+
defer bundle.deinit(std.testing.allocator);
171+
172+
try bundle.rescan(std.testing.allocator);
173+
}

lib/std/crypto/Der.zig

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
pub const Class = enum(u2) {
2+
universal,
3+
application,
4+
context_specific,
5+
private,
6+
};
7+
8+
pub const PC = enum(u1) {
9+
primitive,
10+
constructed,
11+
};
12+
13+
pub const Identifier = packed struct(u8) {
14+
tag: Tag,
15+
pc: PC,
16+
class: Class,
17+
};
18+
19+
pub const Tag = enum(u5) {
20+
boolean = 1,
21+
integer = 2,
22+
bitstring = 3,
23+
null = 5,
24+
object_identifier = 6,
25+
sequence = 16,
26+
sequence_of = 17,
27+
_,
28+
};
29+
30+
pub const Oid = enum {
31+
rsadsi,
32+
pkcs,
33+
rsaEncryption,
34+
md2WithRSAEncryption,
35+
md5WithRSAEncryption,
36+
sha1WithRSAEncryption,
37+
sha256WithRSAEncryption,
38+
sha384WithRSAEncryption,
39+
sha512WithRSAEncryption,
40+
sha224WithRSAEncryption,
41+
pbeWithMD2AndDES_CBC,
42+
pbeWithMD5AndDES_CBC,
43+
pkcs9_emailAddress,
44+
md2,
45+
md5,
46+
rc4,
47+
ecdsa_with_Recommended,
48+
ecdsa_with_Specified,
49+
ecdsa_with_SHA224,
50+
ecdsa_with_SHA256,
51+
ecdsa_with_SHA384,
52+
ecdsa_with_SHA512,
53+
X500,
54+
X509,
55+
commonName,
56+
serialNumber,
57+
countryName,
58+
localityName,
59+
stateOrProvinceName,
60+
organizationName,
61+
organizationalUnitName,
62+
organizationIdentifier,
63+
64+
pub const map = std.ComptimeStringMap(Oid, .{
65+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D }, .rsadsi },
66+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01 }, .pkcs },
67+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 }, .rsaEncryption },
68+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x02 }, .md2WithRSAEncryption },
69+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x04 }, .md5WithRSAEncryption },
70+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x05 }, .sha1WithRSAEncryption },
71+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B }, .sha256WithRSAEncryption },
72+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0C }, .sha384WithRSAEncryption },
73+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0D }, .sha512WithRSAEncryption },
74+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0E }, .sha224WithRSAEncryption },
75+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x05, 0x01 }, .pbeWithMD2AndDES_CBC },
76+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x05, 0x03 }, .pbeWithMD5AndDES_CBC },
77+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x01 }, .pkcs9_emailAddress },
78+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x02, 0x02 }, .md2 },
79+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x02, 0x05 }, .md5 },
80+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x03, 0x04 }, .rc4 },
81+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x02 }, .ecdsa_with_Recommended },
82+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03 }, .ecdsa_with_Specified },
83+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x01 }, .ecdsa_with_SHA224 },
84+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02 }, .ecdsa_with_SHA256 },
85+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x03 }, .ecdsa_with_SHA384 },
86+
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x04 }, .ecdsa_with_SHA512 },
87+
.{ &[_]u8{0x55}, .X500 },
88+
.{ &[_]u8{ 0x55, 0x04 }, .X509 },
89+
.{ &[_]u8{ 0x55, 0x04, 0x03 }, .commonName },
90+
.{ &[_]u8{ 0x55, 0x04, 0x05 }, .serialNumber },
91+
.{ &[_]u8{ 0x55, 0x04, 0x06 }, .countryName },
92+
.{ &[_]u8{ 0x55, 0x04, 0x07 }, .localityName },
93+
.{ &[_]u8{ 0x55, 0x04, 0x08 }, .stateOrProvinceName },
94+
.{ &[_]u8{ 0x55, 0x04, 0x0A }, .organizationName },
95+
.{ &[_]u8{ 0x55, 0x04, 0x0B }, .organizationalUnitName },
96+
.{ &[_]u8{ 0x55, 0x04, 0x61 }, .organizationIdentifier },
97+
});
98+
};
99+
100+
pub const Element = struct {
101+
identifier: Identifier,
102+
start: u32,
103+
end: u32,
104+
};
105+
106+
pub const ParseElementError = error{CertificateHasFieldWithInvalidLength};
107+
108+
pub fn parseElement(bytes: []const u8, index: u32) ParseElementError!Element {
109+
var i = index;
110+
const identifier = @bitCast(Identifier, bytes[i]);
111+
i += 1;
112+
const size_byte = bytes[i];
113+
i += 1;
114+
if ((size_byte >> 7) == 0) {
115+
return .{
116+
.identifier = identifier,
117+
.start = i,
118+
.end = i + size_byte,
119+
};
120+
}
121+
122+
const len_size = @truncate(u7, size_byte);
123+
if (len_size > @sizeOf(u32)) {
124+
return error.CertificateHasFieldWithInvalidLength;
125+
}
126+
127+
const end_i = i + len_size;
128+
var long_form_size: u32 = 0;
129+
while (i < end_i) : (i += 1) {
130+
long_form_size = (long_form_size << 8) | bytes[i];
131+
}
132+
133+
return .{
134+
.identifier = identifier,
135+
.start = i,
136+
.end = i + long_form_size,
137+
};
138+
}
139+
140+
pub const ParseObjectIdError = error{
141+
CertificateHasUnrecognizedObjectId,
142+
CertificateFieldHasWrongDataType,
143+
} || ParseElementError;
144+
145+
pub fn parseObjectId(bytes: []const u8, element: Element) ParseObjectIdError!Oid {
146+
if (element.identifier.tag != .object_identifier)
147+
return error.CertificateFieldHasWrongDataType;
148+
return Oid.map.get(bytes[element.start..element.end]) orelse
149+
return error.CertificateHasUnrecognizedObjectId;
150+
}
151+
152+
const std = @import("../std.zig");
153+
const Der = @This();

0 commit comments

Comments
 (0)