| ... | ... | @@ -4,6 +4,20 @@ |
| 4 | 4 | //! concatenated together in the `bytes` array. The `map` field contains an |
| 5 | 5 | //! index from the DER-encoded subject name to the index of the containing |
| 6 | 6 | //! certificate within `bytes`. |
| 7 | const Bundle = @This(); |
| 8 | const builtin = @import("builtin"); |
| 9 | |
| 10 | const std = @import("../../std.zig"); |
| 11 | const Io = std.Io; |
| 12 | const assert = std.debug.assert; |
| 13 | const fs = std.fs; |
| 14 | const mem = std.mem; |
| 15 | const crypto = std.crypto; |
| 16 | const Allocator = std.mem.Allocator; |
| 17 | const Certificate = std.crypto.Certificate; |
| 18 | const der = Certificate.der; |
| 19 | |
| 20 | const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n"); |
| 7 | 21 | |
| 8 | 22 | /// The key is the contents slice of the subject. |
| 9 | 23 | map: std.HashMapUnmanaged(der.Element.Slice, u32, MapContext, std.hash_map.default_max_load_percentage) = .empty, |
| ... | ... | @@ -56,17 +70,17 @@ pub const RescanError = RescanLinuxError || RescanMacError || RescanWithPathErro |
| 56 | 70 | /// file system standard locations for certificates. |
| 57 | 71 | /// For operating systems that do not have standard CA installations to be |
| 58 | 72 | /// found, this function clears the set of certificates. |
| 59 | | pub fn rescan(cb: *Bundle, gpa: Allocator) RescanError!void { |
| 73 | pub fn rescan(cb: *Bundle, gpa: Allocator, io: Io) RescanError!void { |
| 60 | 74 | switch (builtin.os.tag) { |
| 61 | | .linux => return rescanLinux(cb, gpa), |
| 75 | .linux => return rescanLinux(cb, gpa, io), |
| 62 | 76 | .macos => return rescanMac(cb, gpa), |
| 63 | | .freebsd, .openbsd => return rescanWithPath(cb, gpa, "/etc/ssl/cert.pem"), |
| 64 | | .netbsd => return rescanWithPath(cb, gpa, "/etc/openssl/certs/ca-certificates.crt"), |
| 65 | | .dragonfly => return rescanWithPath(cb, gpa, "/usr/local/etc/ssl/cert.pem"), |
| 66 | | .illumos => return rescanWithPath(cb, gpa, "/etc/ssl/cacert.pem"), |
| 67 | | .haiku => return rescanWithPath(cb, gpa, "/boot/system/data/ssl/CARootCertificates.pem"), |
| 77 | .freebsd, .openbsd => return rescanWithPath(cb, gpa, io, "/etc/ssl/cert.pem"), |
| 78 | .netbsd => return rescanWithPath(cb, gpa, io, "/etc/openssl/certs/ca-certificates.crt"), |
| 79 | .dragonfly => return rescanWithPath(cb, gpa, io, "/usr/local/etc/ssl/cert.pem"), |
| 80 | .illumos => return rescanWithPath(cb, gpa, io, "/etc/ssl/cacert.pem"), |
| 81 | .haiku => return rescanWithPath(cb, gpa, io, "/boot/system/data/ssl/CARootCertificates.pem"), |
| 68 | 82 | // https://github.com/SerenityOS/serenity/blob/222acc9d389bc6b490d4c39539761b043a4bfcb0/Ports/ca-certificates/package.sh#L19 |
| 69 | | .serenity => return rescanWithPath(cb, gpa, "/etc/ssl/certs/ca-certificates.crt"), |
| 83 | .serenity => return rescanWithPath(cb, gpa, io, "/etc/ssl/certs/ca-certificates.crt"), |
| 70 | 84 | .windows => return rescanWindows(cb, gpa), |
| 71 | 85 | else => {}, |
| 72 | 86 | } |
| ... | ... | @@ -77,7 +91,7 @@ const RescanMacError = @import("Bundle/macos.zig").RescanMacError; |
| 77 | 91 | |
| 78 | 92 | const RescanLinuxError = AddCertsFromFilePathError || AddCertsFromDirPathError; |
| 79 | 93 | |
| 80 | | fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { |
| 94 | fn rescanLinux(cb: *Bundle, gpa: Allocator, io: Io) RescanLinuxError!void { |
| 81 | 95 | // Possible certificate files; stop after finding one. |
| 82 | 96 | const cert_file_paths = [_][]const u8{ |
| 83 | 97 | "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. |
| ... | ... | @@ -100,7 +114,7 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { |
| 100 | 114 | |
| 101 | 115 | scan: { |
| 102 | 116 | for (cert_file_paths) |cert_file_path| { |
| 103 | | if (addCertsFromFilePathAbsolute(cb, gpa, cert_file_path)) |_| { |
| 117 | if (addCertsFromFilePathAbsolute(cb, gpa, io, cert_file_path)) |_| { |
| 104 | 118 | break :scan; |
| 105 | 119 | } else |err| switch (err) { |
| 106 | 120 | error.FileNotFound => continue, |
| ... | ... | @@ -109,7 +123,7 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { |
| 109 | 123 | } |
| 110 | 124 | |
| 111 | 125 | for (cert_dir_paths) |cert_dir_path| { |
| 112 | | addCertsFromDirPathAbsolute(cb, gpa, cert_dir_path) catch |err| switch (err) { |
| 126 | addCertsFromDirPathAbsolute(cb, gpa, io, cert_dir_path) catch |err| switch (err) { |
| 113 | 127 | error.FileNotFound => continue, |
| 114 | 128 | else => |e| return e, |
| 115 | 129 | }; |
| ... | ... | @@ -121,10 +135,10 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { |
| 121 | 135 | |
| 122 | 136 | const RescanWithPathError = AddCertsFromFilePathError; |
| 123 | 137 | |
| 124 | | fn rescanWithPath(cb: *Bundle, gpa: Allocator, cert_file_path: []const u8) RescanWithPathError!void { |
| 138 | fn rescanWithPath(cb: *Bundle, gpa: Allocator, io: Io, cert_file_path: []const u8) RescanWithPathError!void { |
| 125 | 139 | cb.bytes.clearRetainingCapacity(); |
| 126 | 140 | cb.map.clearRetainingCapacity(); |
| 127 | | try addCertsFromFilePathAbsolute(cb, gpa, cert_file_path); |
| 141 | try addCertsFromFilePathAbsolute(cb, gpa, io, cert_file_path); |
| 128 | 142 | cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len); |
| 129 | 143 | } |
| 130 | 144 | |
| ... | ... | @@ -160,28 +174,30 @@ pub const AddCertsFromDirPathError = fs.File.OpenError || AddCertsFromDirError; |
| 160 | 174 | pub fn addCertsFromDirPath( |
| 161 | 175 | cb: *Bundle, |
| 162 | 176 | gpa: Allocator, |
| 177 | io: Io, |
| 163 | 178 | dir: fs.Dir, |
| 164 | 179 | sub_dir_path: []const u8, |
| 165 | 180 | ) AddCertsFromDirPathError!void { |
| 166 | 181 | var iterable_dir = try dir.openDir(sub_dir_path, .{ .iterate = true }); |
| 167 | 182 | defer iterable_dir.close(); |
| 168 | | return addCertsFromDir(cb, gpa, iterable_dir); |
| 183 | return addCertsFromDir(cb, gpa, io, iterable_dir); |
| 169 | 184 | } |
| 170 | 185 | |
| 171 | 186 | pub fn addCertsFromDirPathAbsolute( |
| 172 | 187 | cb: *Bundle, |
| 173 | 188 | gpa: Allocator, |
| 189 | io: Io, |
| 174 | 190 | abs_dir_path: []const u8, |
| 175 | 191 | ) AddCertsFromDirPathError!void { |
| 176 | 192 | assert(fs.path.isAbsolute(abs_dir_path)); |
| 177 | 193 | var iterable_dir = try fs.openDirAbsolute(abs_dir_path, .{ .iterate = true }); |
| 178 | 194 | defer iterable_dir.close(); |
| 179 | | return addCertsFromDir(cb, gpa, iterable_dir); |
| 195 | return addCertsFromDir(cb, gpa, io, iterable_dir); |
| 180 | 196 | } |
| 181 | 197 | |
| 182 | 198 | pub const AddCertsFromDirError = AddCertsFromFilePathError; |
| 183 | 199 | |
| 184 | | pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.Dir) AddCertsFromDirError!void { |
| 200 | pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, io: Io, iterable_dir: fs.Dir) AddCertsFromDirError!void { |
| 185 | 201 | var it = iterable_dir.iterate(); |
| 186 | 202 | while (try it.next()) |entry| { |
| 187 | 203 | switch (entry.kind) { |
| ... | ... | @@ -189,32 +205,37 @@ pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.Dir) AddCer |
| 189 | 205 | else => continue, |
| 190 | 206 | } |
| 191 | 207 | |
| 192 | | try addCertsFromFilePath(cb, gpa, iterable_dir, entry.name); |
| 208 | try addCertsFromFilePath(cb, gpa, io, iterable_dir.adaptToNewApi(), entry.name); |
| 193 | 209 | } |
| 194 | 210 | } |
| 195 | 211 | |
| 196 | | pub const AddCertsFromFilePathError = fs.File.OpenError || AddCertsFromFileError; |
| 212 | pub const AddCertsFromFilePathError = fs.File.OpenError || AddCertsFromFileError || Io.Clock.Error; |
| 197 | 213 | |
| 198 | 214 | pub fn addCertsFromFilePathAbsolute( |
| 199 | 215 | cb: *Bundle, |
| 200 | 216 | gpa: Allocator, |
| 217 | io: Io, |
| 201 | 218 | abs_file_path: []const u8, |
| 202 | 219 | ) AddCertsFromFilePathError!void { |
| 203 | | assert(fs.path.isAbsolute(abs_file_path)); |
| 220 | const now = try Io.Clock.real.now(io); |
| 204 | 221 | var file = try fs.openFileAbsolute(abs_file_path, .{}); |
| 205 | 222 | defer file.close(); |
| 206 | | return addCertsFromFile(cb, gpa, file); |
| 223 | var file_reader = file.reader(io, &.{}); |
| 224 | return addCertsFromFile(cb, gpa, &file_reader, now.toSeconds()); |
| 207 | 225 | } |
| 208 | 226 | |
| 209 | 227 | pub fn addCertsFromFilePath( |
| 210 | 228 | cb: *Bundle, |
| 211 | 229 | gpa: Allocator, |
| 212 | | dir: fs.Dir, |
| 230 | io: Io, |
| 231 | dir: Io.Dir, |
| 213 | 232 | sub_file_path: []const u8, |
| 214 | 233 | ) AddCertsFromFilePathError!void { |
| 215 | | var file = try dir.openFile(sub_file_path, .{}); |
| 216 | | defer file.close(); |
| 217 | | return addCertsFromFile(cb, gpa, file); |
| 234 | const now = try Io.Clock.real.now(io); |
| 235 | var file = try dir.openFile(io, sub_file_path, .{}); |
| 236 | defer file.close(io); |
| 237 | var file_reader = file.reader(io, &.{}); |
| 238 | return addCertsFromFile(cb, gpa, &file_reader, now.toSeconds()); |
| 218 | 239 | } |
| 219 | 240 | |
| 220 | 241 | pub const AddCertsFromFileError = Allocator.Error || |
| ... | ... | @@ -222,10 +243,10 @@ pub const AddCertsFromFileError = Allocator.Error || |
| 222 | 243 | fs.File.ReadError || |
| 223 | 244 | ParseCertError || |
| 224 | 245 | std.base64.Error || |
| 225 | | error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker }; |
| 246 | error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker, Streaming }; |
| 226 | 247 | |
| 227 | | pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) AddCertsFromFileError!void { |
| 228 | | const size = try file.getEndPos(); |
| 248 | pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file_reader: *Io.File.Reader, now_sec: i64) AddCertsFromFileError!void { |
| 249 | const size = try file_reader.getSize(); |
| 229 | 250 | |
| 230 | 251 | // We borrow `bytes` as a temporary buffer for the base64-encoded data. |
| 231 | 252 | // This is possible by computing the decoded length and reserving the space |
| ... | ... | @@ -236,14 +257,14 @@ pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) AddCertsFrom |
| 236 | 257 | try cb.bytes.ensureUnusedCapacity(gpa, needed_capacity); |
| 237 | 258 | const end_reserved: u32 = @intCast(cb.bytes.items.len + decoded_size_upper_bound); |
| 238 | 259 | const buffer = cb.bytes.allocatedSlice()[end_reserved..]; |
| 239 | | const end_index = try file.readAll(buffer); |
| 260 | const end_index = file_reader.interface.readSliceShort(buffer) catch |err| switch (err) { |
| 261 | error.ReadFailed => return file_reader.err.?, |
| 262 | }; |
| 240 | 263 | const encoded_bytes = buffer[0..end_index]; |
| 241 | 264 | |
| 242 | 265 | const begin_marker = "-----BEGIN CERTIFICATE-----"; |
| 243 | 266 | const end_marker = "-----END CERTIFICATE-----"; |
| 244 | 267 | |
| 245 | | const now_sec = std.time.timestamp(); |
| 246 | | |
| 247 | 268 | var start_index: usize = 0; |
| 248 | 269 | while (mem.indexOfPos(u8, encoded_bytes, start_index, begin_marker)) |begin_marker_start| { |
| 249 | 270 | const cert_start = begin_marker_start + begin_marker.len; |
| ... | ... | @@ -288,19 +309,6 @@ pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) |
| 288 | 309 | } |
| 289 | 310 | } |
| 290 | 311 | |
| 291 | | const builtin = @import("builtin"); |
| 292 | | const std = @import("../../std.zig"); |
| 293 | | const assert = std.debug.assert; |
| 294 | | const fs = std.fs; |
| 295 | | const mem = std.mem; |
| 296 | | const crypto = std.crypto; |
| 297 | | const Allocator = std.mem.Allocator; |
| 298 | | const Certificate = std.crypto.Certificate; |
| 299 | | const der = Certificate.der; |
| 300 | | const Bundle = @This(); |
| 301 | | |
| 302 | | const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n"); |
| 303 | | |
| 304 | 312 | const MapContext = struct { |
| 305 | 313 | cb: *const Bundle, |
| 306 | 314 | |
| ... | ... | @@ -321,8 +329,11 @@ const MapContext = struct { |
| 321 | 329 | test "scan for OS-provided certificates" { |
| 322 | 330 | if (builtin.os.tag == .wasi) return error.SkipZigTest; |
| 323 | 331 | |
| 332 | const io = std.testing.io; |
| 333 | const gpa = std.testing.allocator; |
| 334 | |
| 324 | 335 | var bundle: Bundle = .{}; |
| 325 | | defer bundle.deinit(std.testing.allocator); |
| 336 | defer bundle.deinit(gpa); |
| 326 | 337 | |
| 327 | | try bundle.rescan(std.testing.allocator); |
| 338 | try bundle.rescan(gpa, io); |
| 328 | 339 | } |