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 }