lib/sys/src/tls.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const builtin = @import("builtin");
3 const capabilities = @import("capabilities.zig");
4 const fd = @import("fd.zig");
5 const linux = @import("linux.zig");
6 const time = @import("time.zig");
7
8 const posix = std.posix;
9 const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n");
10
11 pub const required_capabilities = capabilities.noLibc(&.{ .descriptors, .filesystem, .random, .time, .tls });
12
13 pub const Client = std.crypto.tls.Client;
14 pub const Certificate = std.crypto.Certificate;
15 pub const Bundle = Certificate.Bundle;
16 pub const BundleLock = std.Io.RwLock;
17 pub const Authority = @FieldType(Client.Options, "ca");
18
19 pub const EntropyError = error{Unavailable};
20
21 const BundleLocalError = error{
22 Unavailable,
23 FileNotFound,
24 CertificateAuthorityBundleTooBig,
25 MissingEndCertificateMarker,
26 };
27
28 pub const BundleError =
29 BundleLocalError ||
30 std.mem.Allocator.Error ||
31 std.base64.Error ||
32 Bundle.ParseCertError;
33
34 const certificate_bundle_max_bytes = 16 * 1024 * 1024;
35
36 pub fn bundleAuthority(allocator: std.mem.Allocator, lock: *BundleLock, bundle: *Bundle) Authority {
37 return .{ .bundle = .{
38 .gpa = allocator,
39 .io = std.Options.debug_io,
40 .lock = lock,
41 .bundle = bundle,
42 } };
43 }
44
45 pub fn fillEntropy(buffer: []u8) EntropyError!void {
46 switch (builtin.os.tag) {
47 .linux => return fillLinuxEntropy(buffer),
48 else => {
49 std.Options.debug_io.random(buffer);
50 return;
51 },
52 }
53 }
54
55 pub fn realtimeNow() std.Io.Timestamp {
56 const nanoseconds = time.realNanoseconds() orelse
57 return std.Io.Clock.real.now(std.Options.debug_io);
58 return std.Io.Timestamp.fromNanoseconds(std.math.cast(i96, nanoseconds) orelse saturatedNanoseconds(nanoseconds));
59 }
60
61 pub fn loadSystemBundle(bundle: *Bundle, allocator: std.mem.Allocator) BundleError!void {
62 switch (builtin.os.tag) {
63 .linux => return loadLinuxSystemBundle(bundle, allocator),
64 else => {
65 bundle.rescan(allocator, std.Options.debug_io, realtimeNow()) catch return error.Unavailable;
66 return;
67 },
68 }
69 }
70
71 fn fillLinuxEntropy(buffer: []u8) EntropyError!void {
72 var filled: usize = 0;
73 while (filled < buffer.len) {
74 const rc = linux.getRandom(buffer[filled..]);
75 switch (linux.errno(rc)) {
76 .success => {
77 if (rc == 0) return error.Unavailable;
78 filled += rc;
79 },
80 .intr => {},
81 else => return error.Unavailable,
82 }
83 }
84 }
85
86 fn saturatedNanoseconds(nanoseconds: i128) i96 {
87 if (nanoseconds < 0) return std.math.minInt(i96);
88 return std.math.maxInt(i96);
89 }
90
91 fn loadLinuxSystemBundle(bundle: *Bundle, allocator: std.mem.Allocator) BundleError!void {
92 const cert_file_paths = [_][]const u8{
93 "/etc/ssl/certs/ca-certificates.crt",
94 "/etc/pki/tls/certs/ca-bundle.crt",
95 "/etc/ssl/ca-bundle.pem",
96 "/etc/pki/tls/cacert.pem",
97 "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
98 "/etc/ssl/cert.pem",
99 };
100
101 bundle.bytes.clearRetainingCapacity();
102 bundle.map.clearRetainingCapacity();
103
104 const now = realtimeNow().toSeconds();
105 for (cert_file_paths) |path| {
106 loadBundleFromPath(bundle, allocator, path, now) catch |err| switch (err) {
107 error.FileNotFound => continue,
108 else => |other| return other,
109 };
110 bundle.bytes.shrinkAndFree(allocator, bundle.bytes.items.len);
111 return;
112 }
113
114 return error.FileNotFound;
115 }
116
117 fn loadBundleFromPath(bundle: *Bundle, allocator: std.mem.Allocator, path: []const u8, now_seconds: i64) BundleError!void {
118 const pem = try readFileAlloc(allocator, path);
119 defer allocator.free(pem);
120 try loadBundleFromPem(bundle, allocator, pem, now_seconds);
121 }
122
123 fn readFileAlloc(allocator: std.mem.Allocator, path: []const u8) BundleError![]u8 {
124 const descriptor = posix.openat(posix.AT.FDCWD, path, .{ .CLOEXEC = true }, 0) catch |err| switch (err) {
125 error.FileNotFound => return error.FileNotFound,
126 else => return error.Unavailable,
127 };
128 defer fd.close(descriptor);
129
130 var bytes = std.ArrayListUnmanaged(u8).empty;
131 errdefer bytes.deinit(allocator);
132
133 var buffer: [8192]u8 = undefined;
134 while (true) {
135 const count = fd.read(descriptor, &buffer) catch return error.Unavailable;
136 if (count == 0) break;
137 if (bytes.items.len + count > certificate_bundle_max_bytes) return error.CertificateAuthorityBundleTooBig;
138 try bytes.appendSlice(allocator, buffer[0..count]);
139 }
140
141 return bytes.toOwnedSlice(allocator);
142 }
143
144 fn loadBundleFromPem(bundle: *Bundle, allocator: std.mem.Allocator, pem: []const u8, now_seconds: i64) BundleError!void {
145 const begin_marker = "-----BEGIN CERTIFICATE-----";
146 const end_marker = "-----END CERTIFICATE-----";
147
148 var start_index: usize = 0;
149 while (std.mem.indexOfPos(u8, pem, start_index, begin_marker)) |begin_marker_start| {
150 const cert_start = begin_marker_start + begin_marker.len;
151 const cert_end = std.mem.indexOfPos(u8, pem, cert_start, end_marker) orelse
152 return error.MissingEndCertificateMarker;
153 start_index = cert_end + end_marker.len;
154 const encoded_cert = std.mem.trim(u8, pem[cert_start..cert_end], " \t\r\n");
155 const decoded_start: u32 = @intCast(bundle.bytes.items.len);
156 const decoded_size_upper_bound = encoded_cert.len / 4 * 3 + 3;
157 const needed_capacity = std.math.cast(u32, decoded_size_upper_bound) orelse
158 return error.CertificateAuthorityBundleTooBig;
159 try bundle.bytes.ensureUnusedCapacity(allocator, needed_capacity);
160 const dest = bundle.bytes.allocatedSlice()[decoded_start..][0..decoded_size_upper_bound];
161 bundle.bytes.items.len += try base64.decode(dest, encoded_cert);
162 try bundle.parseCert(allocator, decoded_start, now_seconds);
163 }
164 }
165
166 test "TLS entropy source fills caller buffer" {
167 var entropy: [Client.Options.entropy_len]u8 = undefined;
168 try fillEntropy(&entropy);
169 }
170
171 test "system certificate bundle loader is callable" {
172 var bundle: Bundle = .empty;
173 defer bundle.deinit(std.testing.allocator);
174
175 loadSystemBundle(&bundle, std.testing.allocator) catch |err| switch (err) {
176 error.FileNotFound, error.Unavailable => return error.SkipZigTest,
177 else => |other| return other,
178 };
179 try std.testing.expect(bundle.bytes.items.len != 0);
180 }