authorgravatar for fncontroloption@noreply.codeberg.orgFnControlOption <fncontroloption@noreply.codeberg.org> 2023-01-16 14:34:04-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-01-16 22:34:04+00:00
loge45b471ad35828c5a435b6df7b672f83e8e69b6a
tree3c3fdeae57c337db7b2b39fafb01bc2dce1568f4
parentb42bd759a7bdb6bc1d84c640d68e5384dc35776d
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Find system-installed root SSL certificates on macOS (#14325)


4 files changed, 173 insertions(+), 36 deletions(-)

lib/std/crypto/Certificate/Bundle.zig+26-22
......@@ -60,13 +60,13 @@ pub fn rescan(cb: *Bundle, gpa: Allocator) !void {
6060 .windows => {
6161 // TODO
6262 },
63 .macos => {
64 // TODO
65 },
63 .macos => return rescanMac(cb, gpa),
6664 else => {},
6765 }
6866}
6967
68pub const rescanMac = @import("Bundle/macos.zig").rescanMac;
69
7070pub fn rescanLinux(cb: *Bundle, gpa: Allocator) !void {
7171 // Possible certificate files; stop after finding one.
7272 const cert_file_paths = [_][]const u8{
......@@ -195,25 +195,29 @@ pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) !void {
195195 const decoded_start = @intCast(u32, cb.bytes.items.len);
196196 const dest_buf = cb.bytes.allocatedSlice()[decoded_start..];
197197 cb.bytes.items.len += try base64.decode(dest_buf, encoded_cert);
198 // Even though we could only partially parse the certificate to find
199 // the subject name, we pre-parse all of them to make sure and only
200 // include in the bundle ones that we know will parse. This way we can
201 // use `catch unreachable` later.
202 const parsed_cert = try Certificate.parse(.{
203 .buffer = cb.bytes.items,
204 .index = decoded_start,
205 });
206 if (now_sec > parsed_cert.validity.not_after) {
207 // Ignore expired cert.
208 cb.bytes.items.len = decoded_start;
209 continue;
210 }
211 const gop = try cb.map.getOrPutContext(gpa, parsed_cert.subject_slice, .{ .cb = cb });
212 if (gop.found_existing) {
213 cb.bytes.items.len = decoded_start;
214 } else {
215 gop.value_ptr.* = decoded_start;
216 }
198 try cb.parseCert(gpa, decoded_start, now_sec);
199 }
200}
201
202pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) !void {
203 // Even though we could only partially parse the certificate to find
204 // the subject name, we pre-parse all of them to make sure and only
205 // include in the bundle ones that we know will parse. This way we can
206 // use `catch unreachable` later.
207 const parsed_cert = try Certificate.parse(.{
208 .buffer = cb.bytes.items,
209 .index = decoded_start,
210 });
211 if (now_sec > parsed_cert.validity.not_after) {
212 // Ignore expired cert.
213 cb.bytes.items.len = decoded_start;
214 return;
215 }
216 const gop = try cb.map.getOrPutContext(gpa, parsed_cert.subject_slice, .{ .cb = cb });
217 if (gop.found_existing) {
218 cb.bytes.items.len = decoded_start;
219 } else {
220 gop.value_ptr.* = decoded_start;
217221 }
218222}
219223
lib/std/crypto/Certificate/Bundle/macos.zig created+136
......@@ -0,0 +1,136 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const mem = std.mem;
4const fs = std.fs;
5const Allocator = std.mem.Allocator;
6const Bundle = @import("../Bundle.zig");
7
8pub fn rescanMac(cb: *Bundle, gpa: Allocator) !void {
9 const file = try fs.openFileAbsolute("/System/Library/Keychains/SystemRootCertificates.keychain", .{});
10 defer file.close();
11
12 const bytes = try file.readToEndAlloc(gpa, std.math.maxInt(u32));
13 defer gpa.free(bytes);
14
15 var stream = std.io.fixedBufferStream(bytes);
16 const reader = stream.reader();
17
18 const db_header = try reader.readStructBig(ApplDbHeader);
19 assert(mem.eql(u8, "kych", &@bitCast([4]u8, db_header.signature)));
20
21 try stream.seekTo(db_header.schema_offset);
22
23 const db_schema = try reader.readStructBig(ApplDbSchema);
24
25 var table_list = try gpa.alloc(u32, db_schema.table_count);
26 defer gpa.free(table_list);
27
28 var table_idx: u32 = 0;
29 while (table_idx < table_list.len) : (table_idx += 1) {
30 table_list[table_idx] = try reader.readIntBig(u32);
31 }
32
33 const now_sec = std.time.timestamp();
34
35 for (table_list) |table_offset| {
36 try stream.seekTo(db_header.schema_offset + table_offset);
37
38 const table_header = try reader.readStructBig(TableHeader);
39
40 if (@intToEnum(TableId, table_header.table_id) != TableId.CSSM_DL_DB_RECORD_X509_CERTIFICATE) {
41 continue;
42 }
43
44 var record_list = try gpa.alloc(u32, table_header.record_count);
45 defer gpa.free(record_list);
46
47 var record_idx: u32 = 0;
48 while (record_idx < record_list.len) : (record_idx += 1) {
49 record_list[record_idx] = try reader.readIntBig(u32);
50 }
51
52 for (record_list) |record_offset| {
53 try stream.seekTo(db_header.schema_offset + table_offset + record_offset);
54
55 const cert_header = try reader.readStructBig(X509CertHeader);
56
57 try cb.bytes.ensureUnusedCapacity(gpa, cert_header.cert_size);
58
59 const cert_start = @intCast(u32, cb.bytes.items.len);
60 const dest_buf = cb.bytes.allocatedSlice()[cert_start..];
61 cb.bytes.items.len += try reader.readAtLeast(dest_buf, cert_header.cert_size);
62
63 try cb.parseCert(gpa, cert_start, now_sec);
64 }
65 }
66}
67
68const ApplDbHeader = extern struct {
69 signature: @Vector(4, u8),
70 version: u32,
71 header_size: u32,
72 schema_offset: u32,
73 auth_offset: u32,
74};
75
76const ApplDbSchema = extern struct {
77 schema_size: u32,
78 table_count: u32,
79};
80
81const TableHeader = extern struct {
82 table_size: u32,
83 table_id: u32,
84 record_count: u32,
85 records: u32,
86 indexes_offset: u32,
87 free_list_head: u32,
88 record_numbers_count: u32,
89};
90
91const TableId = enum(u32) {
92 CSSM_DL_DB_SCHEMA_INFO = 0x00000000,
93 CSSM_DL_DB_SCHEMA_INDEXES = 0x00000001,
94 CSSM_DL_DB_SCHEMA_ATTRIBUTES = 0x00000002,
95 CSSM_DL_DB_SCHEMA_PARSING_MODULE = 0x00000003,
96
97 CSSM_DL_DB_RECORD_ANY = 0x0000000a,
98 CSSM_DL_DB_RECORD_CERT = 0x0000000b,
99 CSSM_DL_DB_RECORD_CRL = 0x0000000c,
100 CSSM_DL_DB_RECORD_POLICY = 0x0000000d,
101 CSSM_DL_DB_RECORD_GENERIC = 0x0000000e,
102 CSSM_DL_DB_RECORD_PUBLIC_KEY = 0x0000000f,
103 CSSM_DL_DB_RECORD_PRIVATE_KEY = 0x00000010,
104 CSSM_DL_DB_RECORD_SYMMETRIC_KEY = 0x00000011,
105 CSSM_DL_DB_RECORD_ALL_KEYS = 0x00000012,
106
107 CSSM_DL_DB_RECORD_GENERIC_PASSWORD = 0x80000000,
108 CSSM_DL_DB_RECORD_INTERNET_PASSWORD = 0x80000001,
109 CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD = 0x80000002,
110 CSSM_DL_DB_RECORD_USER_TRUST = 0x80000003,
111 CSSM_DL_DB_RECORD_X509_CRL = 0x80000004,
112 CSSM_DL_DB_RECORD_UNLOCK_REFERRAL = 0x80000005,
113 CSSM_DL_DB_RECORD_EXTENDED_ATTRIBUTE = 0x80000006,
114 CSSM_DL_DB_RECORD_X509_CERTIFICATE = 0x80001000,
115 CSSM_DL_DB_RECORD_METADATA = 0x80008000,
116
117 _,
118};
119
120const X509CertHeader = extern struct {
121 record_size: u32,
122 record_number: u32,
123 unknown1: u32,
124 unknown2: u32,
125 cert_size: u32,
126 unknown3: u32,
127 cert_type: u32,
128 cert_encoding: u32,
129 print_name: u32,
130 alias: u32,
131 subject: u32,
132 issuer: u32,
133 serial_number: u32,
134 subject_key_identifier: u32,
135 public_key_hash: u32,
136};
lib/std/io/reader.zig+9
......@@ -3,6 +3,7 @@ const math = std.math;
33const assert = std.debug.assert;
44const mem = std.mem;
55const testing = std.testing;
6const native_endian = @import("builtin").target.cpu.arch.endian();
67
78pub fn Reader(
89 comptime Context: type,
......@@ -351,6 +352,14 @@ pub fn Reader(
351352 return res[0];
352353 }
353354
355 pub fn readStructBig(self: Self, comptime T: type) !T {
356 var res = try self.readStruct(T);
357 if (native_endian != std.builtin.Endian.Big) {
358 mem.byteSwapAllFields(T, &res);
359 }
360 return res;
361 }
362
354363 /// Reads an integer with the same size as the given enum's tag type. If the integer matches
355364 /// an enum tag, casts the integer to the enum tag and returns it. Otherwise, returns an error.
356365 /// TODO optimization taking advantage of most fields being in order
src/link/MachO/fat.zig+2-14
......@@ -1,9 +1,7 @@
11const std = @import("std");
2const builtin = @import("builtin");
32const log = std.log.scoped(.archive);
43const macho = std.macho;
54const mem = std.mem;
6const native_endian = builtin.target.cpu.arch.endian();
75
86pub fn decodeArch(cputype: macho.cpu_type_t, comptime logError: bool) !std.Target.Cpu.Arch {
97 const cpu_arch: std.Target.Cpu.Arch = switch (cputype) {
......@@ -19,23 +17,13 @@ pub fn decodeArch(cputype: macho.cpu_type_t, comptime logError: bool) !std.Targe
1917 return cpu_arch;
2018}
2119
22fn readFatStruct(reader: anytype, comptime T: type) !T {
23 // Fat structures (fat_header & fat_arch) are always written and read to/from
24 // disk in big endian order.
25 var res = try reader.readStruct(T);
26 if (native_endian != std.builtin.Endian.Big) {
27 mem.byteSwapAllFields(T, &res);
28 }
29 return res;
30}
31
3220pub fn getLibraryOffset(reader: anytype, cpu_arch: std.Target.Cpu.Arch) !u64 {
33 const fat_header = try readFatStruct(reader, macho.fat_header);
21 const fat_header = try reader.readStructBig(macho.fat_header);
3422 if (fat_header.magic != macho.FAT_MAGIC) return 0;
3523
3624 var fat_arch_index: u32 = 0;
3725 while (fat_arch_index < fat_header.nfat_arch) : (fat_arch_index += 1) {
38 const fat_arch = try readFatStruct(reader, macho.fat_arch);
26 const fat_arch = try reader.readStructBig(macho.fat_arch);
3927 // If we come across an architecture that we do not know how to handle, that's
4028 // fine because we can keep looking for one that might match.
4129 const lib_arch = decodeArch(fat_arch.cputype, false) catch |err| switch (err) {