Skip to content

Commit e45b471

Browse files
Find system-installed root SSL certificates on macOS (#14325)
1 parent b42bd75 commit e45b471

File tree

4 files changed

+173
-36
lines changed

4 files changed

+173
-36
lines changed

lib/std/crypto/Certificate/Bundle.zig

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

68+
pub const rescanMac = @import("Bundle/macos.zig").rescanMac;
69+
7070
pub 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+
202+
pub 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

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
const std = @import("std");
2+
const assert = std.debug.assert;
3+
const mem = std.mem;
4+
const fs = std.fs;
5+
const Allocator = std.mem.Allocator;
6+
const Bundle = @import("../Bundle.zig");
7+
8+
pub 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+
68+
const 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+
76+
const ApplDbSchema = extern struct {
77+
schema_size: u32,
78+
table_count: u32,
79+
};
80+
81+
const 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+
91+
const 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+
120+
const 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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const math = std.math;
33
const assert = std.debug.assert;
44
const mem = std.mem;
55
const testing = std.testing;
6+
const native_endian = @import("builtin").target.cpu.arch.endian();
67

78
pub 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

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
const std = @import("std");
2-
const builtin = @import("builtin");
32
const log = std.log.scoped(.archive);
43
const macho = std.macho;
54
const mem = std.mem;
6-
const native_endian = builtin.target.cpu.arch.endian();
75

86
pub 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

22-
fn 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-
3220
pub 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) {

0 commit comments

Comments
 (0)