diff options
| author | fn ⌃ ⌥ <70830482+FnControlOption@users.noreply.github.com> | 2023-01-16 14:34:04 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-01-16 22:34:04 +0000 |
| commit | e45b471ad35828c5a435b6df7b672f83e8e69b6a (patch) | |
| tree | 3c3fdeae57c337db7b2b39fafb01bc2dce1568f4 /lib | |
| parent | b42bd759a7bdb6bc1d84c640d68e5384dc35776d (diff) | |
| download | zig-e45b471ad35828c5a435b6df7b672f83e8e69b6a.tar.gz zig-e45b471ad35828c5a435b6df7b672f83e8e69b6a.zip | |
Find system-installed root SSL certificates on macOS (#14325)
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/std/crypto/Certificate/Bundle.zig | 48 | ||||
| -rw-r--r-- | lib/std/crypto/Certificate/Bundle/macos.zig | 136 | ||||
| -rw-r--r-- | lib/std/io/reader.zig | 9 |
3 files changed, 171 insertions, 22 deletions
diff --git a/lib/std/crypto/Certificate/Bundle.zig b/lib/std/crypto/Certificate/Bundle.zig index 16b0329a2d..a8b3851b05 100644 --- a/lib/std/crypto/Certificate/Bundle.zig +++ b/lib/std/crypto/Certificate/Bundle.zig @@ -60,13 +60,13 @@ pub fn rescan(cb: *Bundle, gpa: Allocator) !void { .windows => { // TODO }, - .macos => { - // TODO - }, + .macos => return rescanMac(cb, gpa), else => {}, } } +pub const rescanMac = @import("Bundle/macos.zig").rescanMac; + pub fn rescanLinux(cb: *Bundle, gpa: Allocator) !void { // Possible certificate files; stop after finding one. const cert_file_paths = [_][]const u8{ @@ -195,25 +195,29 @@ pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) !void { const decoded_start = @intCast(u32, cb.bytes.items.len); const dest_buf = cb.bytes.allocatedSlice()[decoded_start..]; cb.bytes.items.len += try base64.decode(dest_buf, encoded_cert); - // Even though we could only partially parse the certificate to find - // the subject name, we pre-parse all of them to make sure and only - // include in the bundle ones that we know will parse. This way we can - // use `catch unreachable` later. - const parsed_cert = try Certificate.parse(.{ - .buffer = cb.bytes.items, - .index = decoded_start, - }); - if (now_sec > parsed_cert.validity.not_after) { - // Ignore expired cert. - cb.bytes.items.len = decoded_start; - continue; - } - const gop = try cb.map.getOrPutContext(gpa, parsed_cert.subject_slice, .{ .cb = cb }); - if (gop.found_existing) { - cb.bytes.items.len = decoded_start; - } else { - gop.value_ptr.* = decoded_start; - } + try cb.parseCert(gpa, decoded_start, now_sec); + } +} + +pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) !void { + // Even though we could only partially parse the certificate to find + // the subject name, we pre-parse all of them to make sure and only + // include in the bundle ones that we know will parse. This way we can + // use `catch unreachable` later. + const parsed_cert = try Certificate.parse(.{ + .buffer = cb.bytes.items, + .index = decoded_start, + }); + if (now_sec > parsed_cert.validity.not_after) { + // Ignore expired cert. + cb.bytes.items.len = decoded_start; + return; + } + const gop = try cb.map.getOrPutContext(gpa, parsed_cert.subject_slice, .{ .cb = cb }); + if (gop.found_existing) { + cb.bytes.items.len = decoded_start; + } else { + gop.value_ptr.* = decoded_start; } } diff --git a/lib/std/crypto/Certificate/Bundle/macos.zig b/lib/std/crypto/Certificate/Bundle/macos.zig new file mode 100644 index 0000000000..8268a67a48 --- /dev/null +++ b/lib/std/crypto/Certificate/Bundle/macos.zig @@ -0,0 +1,136 @@ +const std = @import("std"); +const assert = std.debug.assert; +const mem = std.mem; +const fs = std.fs; +const Allocator = std.mem.Allocator; +const Bundle = @import("../Bundle.zig"); + +pub fn rescanMac(cb: *Bundle, gpa: Allocator) !void { + const file = try fs.openFileAbsolute("/System/Library/Keychains/SystemRootCertificates.keychain", .{}); + defer file.close(); + + const bytes = try file.readToEndAlloc(gpa, std.math.maxInt(u32)); + defer gpa.free(bytes); + + var stream = std.io.fixedBufferStream(bytes); + const reader = stream.reader(); + + const db_header = try reader.readStructBig(ApplDbHeader); + assert(mem.eql(u8, "kych", &@bitCast([4]u8, db_header.signature))); + + try stream.seekTo(db_header.schema_offset); + + const db_schema = try reader.readStructBig(ApplDbSchema); + + var table_list = try gpa.alloc(u32, db_schema.table_count); + defer gpa.free(table_list); + + var table_idx: u32 = 0; + while (table_idx < table_list.len) : (table_idx += 1) { + table_list[table_idx] = try reader.readIntBig(u32); + } + + const now_sec = std.time.timestamp(); + + for (table_list) |table_offset| { + try stream.seekTo(db_header.schema_offset + table_offset); + + const table_header = try reader.readStructBig(TableHeader); + + if (@intToEnum(TableId, table_header.table_id) != TableId.CSSM_DL_DB_RECORD_X509_CERTIFICATE) { + continue; + } + + var record_list = try gpa.alloc(u32, table_header.record_count); + defer gpa.free(record_list); + + var record_idx: u32 = 0; + while (record_idx < record_list.len) : (record_idx += 1) { + record_list[record_idx] = try reader.readIntBig(u32); + } + + for (record_list) |record_offset| { + try stream.seekTo(db_header.schema_offset + table_offset + record_offset); + + const cert_header = try reader.readStructBig(X509CertHeader); + + try cb.bytes.ensureUnusedCapacity(gpa, cert_header.cert_size); + + const cert_start = @intCast(u32, cb.bytes.items.len); + const dest_buf = cb.bytes.allocatedSlice()[cert_start..]; + cb.bytes.items.len += try reader.readAtLeast(dest_buf, cert_header.cert_size); + + try cb.parseCert(gpa, cert_start, now_sec); + } + } +} + +const ApplDbHeader = extern struct { + signature: @Vector(4, u8), + version: u32, + header_size: u32, + schema_offset: u32, + auth_offset: u32, +}; + +const ApplDbSchema = extern struct { + schema_size: u32, + table_count: u32, +}; + +const TableHeader = extern struct { + table_size: u32, + table_id: u32, + record_count: u32, + records: u32, + indexes_offset: u32, + free_list_head: u32, + record_numbers_count: u32, +}; + +const TableId = enum(u32) { + CSSM_DL_DB_SCHEMA_INFO = 0x00000000, + CSSM_DL_DB_SCHEMA_INDEXES = 0x00000001, + CSSM_DL_DB_SCHEMA_ATTRIBUTES = 0x00000002, + CSSM_DL_DB_SCHEMA_PARSING_MODULE = 0x00000003, + + CSSM_DL_DB_RECORD_ANY = 0x0000000a, + CSSM_DL_DB_RECORD_CERT = 0x0000000b, + CSSM_DL_DB_RECORD_CRL = 0x0000000c, + CSSM_DL_DB_RECORD_POLICY = 0x0000000d, + CSSM_DL_DB_RECORD_GENERIC = 0x0000000e, + CSSM_DL_DB_RECORD_PUBLIC_KEY = 0x0000000f, + CSSM_DL_DB_RECORD_PRIVATE_KEY = 0x00000010, + CSSM_DL_DB_RECORD_SYMMETRIC_KEY = 0x00000011, + CSSM_DL_DB_RECORD_ALL_KEYS = 0x00000012, + + CSSM_DL_DB_RECORD_GENERIC_PASSWORD = 0x80000000, + CSSM_DL_DB_RECORD_INTERNET_PASSWORD = 0x80000001, + CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD = 0x80000002, + CSSM_DL_DB_RECORD_USER_TRUST = 0x80000003, + CSSM_DL_DB_RECORD_X509_CRL = 0x80000004, + CSSM_DL_DB_RECORD_UNLOCK_REFERRAL = 0x80000005, + CSSM_DL_DB_RECORD_EXTENDED_ATTRIBUTE = 0x80000006, + CSSM_DL_DB_RECORD_X509_CERTIFICATE = 0x80001000, + CSSM_DL_DB_RECORD_METADATA = 0x80008000, + + _, +}; + +const X509CertHeader = extern struct { + record_size: u32, + record_number: u32, + unknown1: u32, + unknown2: u32, + cert_size: u32, + unknown3: u32, + cert_type: u32, + cert_encoding: u32, + print_name: u32, + alias: u32, + subject: u32, + issuer: u32, + serial_number: u32, + subject_key_identifier: u32, + public_key_hash: u32, +}; diff --git a/lib/std/io/reader.zig b/lib/std/io/reader.zig index f9d3085a29..65b7a086c5 100644 --- a/lib/std/io/reader.zig +++ b/lib/std/io/reader.zig @@ -3,6 +3,7 @@ const math = std.math; const assert = std.debug.assert; const mem = std.mem; const testing = std.testing; +const native_endian = @import("builtin").target.cpu.arch.endian(); pub fn Reader( comptime Context: type, @@ -351,6 +352,14 @@ pub fn Reader( return res[0]; } + pub fn readStructBig(self: Self, comptime T: type) !T { + var res = try self.readStruct(T); + if (native_endian != std.builtin.Endian.Big) { + mem.byteSwapAllFields(T, &res); + } + return res; + } + /// Reads an integer with the same size as the given enum's tag type. If the integer matches /// an enum tag, casts the integer to the enum tag and returns it. Otherwise, returns an error. /// TODO optimization taking advantage of most fields being in order |
