mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 05:44:20 +00:00
344 lines
13 KiB
Zig
344 lines
13 KiB
Zig
//! A set of certificates. Typically pre-installed on every operating system,
|
|
//! these are "Certificate Authorities" used to validate SSL certificates.
|
|
//! This data structure stores certificates in DER-encoded form, all of them
|
|
//! concatenated together in the `bytes` array. The `map` field contains an
|
|
//! index from the DER-encoded subject name to the index of the containing
|
|
//! certificate within `bytes`.
|
|
const Bundle = @This();
|
|
const builtin = @import("builtin");
|
|
|
|
const std = @import("../../std.zig");
|
|
const Io = std.Io;
|
|
const assert = std.debug.assert;
|
|
const fs = std.fs;
|
|
const mem = std.mem;
|
|
const crypto = std.crypto;
|
|
const Allocator = std.mem.Allocator;
|
|
const Certificate = std.crypto.Certificate;
|
|
const der = Certificate.der;
|
|
|
|
const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n");
|
|
|
|
/// The key is the contents slice of the subject.
|
|
map: std.HashMapUnmanaged(der.Element.Slice, u32, MapContext, std.hash_map.default_max_load_percentage) = .empty,
|
|
bytes: std.ArrayList(u8) = .empty,
|
|
|
|
pub const VerifyError = Certificate.Parsed.VerifyError || error{
|
|
CertificateIssuerNotFound,
|
|
};
|
|
|
|
pub fn verify(cb: Bundle, subject: Certificate.Parsed, now_sec: i64) VerifyError!void {
|
|
const bytes_index = cb.find(subject.issuer()) orelse return error.CertificateIssuerNotFound;
|
|
const issuer_cert: Certificate = .{
|
|
.buffer = cb.bytes.items,
|
|
.index = bytes_index,
|
|
};
|
|
// Every certificate in the bundle is pre-parsed before adding it, ensuring
|
|
// that parsing will succeed here.
|
|
const issuer = issuer_cert.parse() catch unreachable;
|
|
try subject.verify(issuer, now_sec);
|
|
}
|
|
|
|
/// The returned bytes become invalid after calling any of the rescan functions
|
|
/// or add functions.
|
|
pub fn find(cb: Bundle, subject_name: []const u8) ?u32 {
|
|
const Adapter = struct {
|
|
cb: Bundle,
|
|
|
|
pub fn hash(ctx: @This(), k: []const u8) u64 {
|
|
_ = ctx;
|
|
return std.hash_map.hashString(k);
|
|
}
|
|
|
|
pub fn eql(ctx: @This(), a: []const u8, b_key: der.Element.Slice) bool {
|
|
const b = ctx.cb.bytes.items[b_key.start..b_key.end];
|
|
return mem.eql(u8, a, b);
|
|
}
|
|
};
|
|
return cb.map.getAdapted(subject_name, Adapter{ .cb = cb });
|
|
}
|
|
|
|
pub fn deinit(cb: *Bundle, gpa: Allocator) void {
|
|
cb.map.deinit(gpa);
|
|
cb.bytes.deinit(gpa);
|
|
cb.* = undefined;
|
|
}
|
|
|
|
pub const RescanError = RescanLinuxError || RescanMacError || RescanWithPathError || RescanWindowsError;
|
|
|
|
/// Clears the set of certificates and then scans the host operating system
|
|
/// file system standard locations for certificates.
|
|
/// For operating systems that do not have standard CA installations to be
|
|
/// found, this function clears the set of certificates.
|
|
pub fn rescan(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanError!void {
|
|
switch (builtin.os.tag) {
|
|
.linux => return rescanLinux(cb, gpa, io, now),
|
|
.maccatalyst, .macos => return rescanMac(cb, gpa, io, now),
|
|
.freebsd, .openbsd => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/cert.pem"),
|
|
.netbsd => return rescanWithPath(cb, gpa, io, now, "/etc/openssl/certs/ca-certificates.crt"),
|
|
.dragonfly => return rescanWithPath(cb, gpa, io, now, "/usr/local/etc/ssl/cert.pem"),
|
|
.illumos => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/cacert.pem"),
|
|
.haiku => return rescanWithPath(cb, gpa, io, now, "/boot/system/data/ssl/CARootCertificates.pem"),
|
|
// https://github.com/SerenityOS/serenity/blob/222acc9d389bc6b490d4c39539761b043a4bfcb0/Ports/ca-certificates/package.sh#L19
|
|
.serenity => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/certs/ca-certificates.crt"),
|
|
.windows => return rescanWindows(cb, gpa, io, now),
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
const rescanMac = @import("Bundle/macos.zig").rescanMac;
|
|
const RescanMacError = @import("Bundle/macos.zig").RescanMacError;
|
|
|
|
const RescanLinuxError = AddCertsFromFilePathError || AddCertsFromDirPathError;
|
|
|
|
fn rescanLinux(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanLinuxError!void {
|
|
// Possible certificate files; stop after finding one.
|
|
const cert_file_paths = [_][]const u8{
|
|
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc.
|
|
"/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6
|
|
"/etc/ssl/ca-bundle.pem", // OpenSUSE
|
|
"/etc/pki/tls/cacert.pem", // OpenELEC
|
|
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7
|
|
"/etc/ssl/cert.pem", // Alpine Linux
|
|
};
|
|
|
|
// Possible directories with certificate files; all will be read.
|
|
const cert_dir_paths = [_][]const u8{
|
|
"/etc/ssl/certs", // SLES10/SLES11
|
|
"/etc/pki/tls/certs", // Fedora/RHEL
|
|
"/system/etc/security/cacerts", // Android
|
|
};
|
|
|
|
cb.bytes.clearRetainingCapacity();
|
|
cb.map.clearRetainingCapacity();
|
|
|
|
scan: {
|
|
for (cert_file_paths) |cert_file_path| {
|
|
if (addCertsFromFilePathAbsolute(cb, gpa, io, now, cert_file_path)) |_| {
|
|
break :scan;
|
|
} else |err| switch (err) {
|
|
error.FileNotFound => continue,
|
|
else => |e| return e,
|
|
}
|
|
}
|
|
|
|
for (cert_dir_paths) |cert_dir_path| {
|
|
addCertsFromDirPathAbsolute(cb, gpa, io, now, cert_dir_path) catch |err| switch (err) {
|
|
error.FileNotFound => continue,
|
|
else => |e| return e,
|
|
};
|
|
}
|
|
}
|
|
|
|
cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
|
|
}
|
|
|
|
const RescanWithPathError = AddCertsFromFilePathError;
|
|
|
|
fn rescanWithPath(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, cert_file_path: []const u8) RescanWithPathError!void {
|
|
cb.bytes.clearRetainingCapacity();
|
|
cb.map.clearRetainingCapacity();
|
|
try addCertsFromFilePathAbsolute(cb, gpa, io, now, cert_file_path);
|
|
cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
|
|
}
|
|
|
|
const RescanWindowsError = Allocator.Error || ParseCertError || std.posix.UnexpectedError || error{FileNotFound};
|
|
|
|
fn rescanWindows(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanWindowsError!void {
|
|
cb.bytes.clearRetainingCapacity();
|
|
cb.map.clearRetainingCapacity();
|
|
|
|
_ = io;
|
|
|
|
const w = std.os.windows;
|
|
const GetLastError = w.GetLastError;
|
|
const root = [4:0]u16{ 'R', 'O', 'O', 'T' };
|
|
const store = w.crypt32.CertOpenSystemStoreW(null, &root) orelse switch (GetLastError()) {
|
|
.FILE_NOT_FOUND => return error.FileNotFound,
|
|
else => |err| return w.unexpectedError(err),
|
|
};
|
|
defer _ = w.crypt32.CertCloseStore(store, 0);
|
|
|
|
const now_sec = now.toSeconds();
|
|
|
|
var ctx = w.crypt32.CertEnumCertificatesInStore(store, null);
|
|
while (ctx) |context| : (ctx = w.crypt32.CertEnumCertificatesInStore(store, ctx)) {
|
|
const decoded_start = @as(u32, @intCast(cb.bytes.items.len));
|
|
const encoded_cert = context.pbCertEncoded[0..context.cbCertEncoded];
|
|
try cb.bytes.appendSlice(gpa, encoded_cert);
|
|
try cb.parseCert(gpa, decoded_start, now_sec);
|
|
}
|
|
cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
|
|
}
|
|
|
|
pub const AddCertsFromDirPathError = fs.File.OpenError || AddCertsFromDirError;
|
|
|
|
pub fn addCertsFromDirPath(
|
|
cb: *Bundle,
|
|
gpa: Allocator,
|
|
io: Io,
|
|
dir: fs.Dir,
|
|
sub_dir_path: []const u8,
|
|
) AddCertsFromDirPathError!void {
|
|
var iterable_dir = try dir.openDir(sub_dir_path, .{ .iterate = true });
|
|
defer iterable_dir.close();
|
|
return addCertsFromDir(cb, gpa, io, iterable_dir);
|
|
}
|
|
|
|
pub fn addCertsFromDirPathAbsolute(
|
|
cb: *Bundle,
|
|
gpa: Allocator,
|
|
io: Io,
|
|
now: Io.Timestamp,
|
|
abs_dir_path: []const u8,
|
|
) AddCertsFromDirPathError!void {
|
|
assert(fs.path.isAbsolute(abs_dir_path));
|
|
var iterable_dir = try fs.openDirAbsolute(abs_dir_path, .{ .iterate = true });
|
|
defer iterable_dir.close();
|
|
return addCertsFromDir(cb, gpa, io, now, iterable_dir);
|
|
}
|
|
|
|
pub const AddCertsFromDirError = AddCertsFromFilePathError;
|
|
|
|
pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, iterable_dir: fs.Dir) AddCertsFromDirError!void {
|
|
var it = iterable_dir.iterate();
|
|
while (try it.next()) |entry| {
|
|
switch (entry.kind) {
|
|
.file, .sym_link => {},
|
|
else => continue,
|
|
}
|
|
|
|
try addCertsFromFilePath(cb, gpa, io, now, iterable_dir.adaptToNewApi(), entry.name);
|
|
}
|
|
}
|
|
|
|
pub const AddCertsFromFilePathError = fs.File.OpenError || AddCertsFromFileError || Io.Clock.Error;
|
|
|
|
pub fn addCertsFromFilePathAbsolute(
|
|
cb: *Bundle,
|
|
gpa: Allocator,
|
|
io: Io,
|
|
now: Io.Timestamp,
|
|
abs_file_path: []const u8,
|
|
) AddCertsFromFilePathError!void {
|
|
var file = try fs.openFileAbsolute(abs_file_path, .{});
|
|
defer file.close();
|
|
var file_reader = file.reader(io, &.{});
|
|
return addCertsFromFile(cb, gpa, &file_reader, now.toSeconds());
|
|
}
|
|
|
|
pub fn addCertsFromFilePath(
|
|
cb: *Bundle,
|
|
gpa: Allocator,
|
|
io: Io,
|
|
now: Io.Timestamp,
|
|
dir: Io.Dir,
|
|
sub_file_path: []const u8,
|
|
) AddCertsFromFilePathError!void {
|
|
var file = try dir.openFile(io, sub_file_path, .{});
|
|
defer file.close(io);
|
|
var file_reader = file.reader(io, &.{});
|
|
return addCertsFromFile(cb, gpa, &file_reader, now.toSeconds());
|
|
}
|
|
|
|
pub const AddCertsFromFileError = Allocator.Error ||
|
|
fs.File.GetSeekPosError ||
|
|
fs.File.ReadError ||
|
|
ParseCertError ||
|
|
std.base64.Error ||
|
|
error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker, Streaming };
|
|
|
|
pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file_reader: *Io.File.Reader, now_sec: i64) AddCertsFromFileError!void {
|
|
const size = try file_reader.getSize();
|
|
|
|
// We borrow `bytes` as a temporary buffer for the base64-encoded data.
|
|
// This is possible by computing the decoded length and reserving the space
|
|
// for the decoded bytes first.
|
|
const decoded_size_upper_bound = size / 4 * 3;
|
|
const needed_capacity = std.math.cast(u32, decoded_size_upper_bound + size) orelse
|
|
return error.CertificateAuthorityBundleTooBig;
|
|
try cb.bytes.ensureUnusedCapacity(gpa, needed_capacity);
|
|
const end_reserved: u32 = @intCast(cb.bytes.items.len + decoded_size_upper_bound);
|
|
const buffer = cb.bytes.allocatedSlice()[end_reserved..];
|
|
const end_index = file_reader.interface.readSliceShort(buffer) catch |err| switch (err) {
|
|
error.ReadFailed => return file_reader.err.?,
|
|
};
|
|
const encoded_bytes = buffer[0..end_index];
|
|
|
|
const begin_marker = "-----BEGIN CERTIFICATE-----";
|
|
const end_marker = "-----END CERTIFICATE-----";
|
|
|
|
var start_index: usize = 0;
|
|
while (mem.indexOfPos(u8, encoded_bytes, start_index, begin_marker)) |begin_marker_start| {
|
|
const cert_start = begin_marker_start + begin_marker.len;
|
|
const cert_end = mem.indexOfPos(u8, encoded_bytes, cert_start, end_marker) orelse
|
|
return error.MissingEndCertificateMarker;
|
|
start_index = cert_end + end_marker.len;
|
|
const encoded_cert = mem.trim(u8, encoded_bytes[cert_start..cert_end], " \t\r\n");
|
|
const decoded_start: u32 = @intCast(cb.bytes.items.len);
|
|
const dest_buf = cb.bytes.allocatedSlice()[decoded_start..];
|
|
cb.bytes.items.len += try base64.decode(dest_buf, encoded_cert);
|
|
try cb.parseCert(gpa, decoded_start, now_sec);
|
|
}
|
|
}
|
|
|
|
pub const ParseCertError = Allocator.Error || Certificate.ParseError;
|
|
|
|
pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) ParseCertError!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 = Certificate.parse(.{
|
|
.buffer = cb.bytes.items,
|
|
.index = decoded_start,
|
|
}) catch |err| switch (err) {
|
|
error.CertificateHasUnrecognizedObjectId => {
|
|
cb.bytes.items.len = decoded_start;
|
|
return;
|
|
},
|
|
else => |e| return e,
|
|
};
|
|
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;
|
|
}
|
|
}
|
|
|
|
const MapContext = struct {
|
|
cb: *const Bundle,
|
|
|
|
pub fn hash(ctx: MapContext, k: der.Element.Slice) u64 {
|
|
return std.hash_map.hashString(ctx.cb.bytes.items[k.start..k.end]);
|
|
}
|
|
|
|
pub fn eql(ctx: MapContext, a: der.Element.Slice, b: der.Element.Slice) bool {
|
|
const bytes = ctx.cb.bytes.items;
|
|
return mem.eql(
|
|
u8,
|
|
bytes[a.start..a.end],
|
|
bytes[b.start..b.end],
|
|
);
|
|
}
|
|
};
|
|
|
|
test "scan for OS-provided certificates" {
|
|
if (builtin.os.tag == .wasi) return error.SkipZigTest;
|
|
|
|
const io = std.testing.io;
|
|
const gpa = std.testing.allocator;
|
|
|
|
var bundle: Bundle = .{};
|
|
defer bundle.deinit(gpa);
|
|
|
|
const now = try Io.Clock.real.now(io);
|
|
|
|
try bundle.rescan(gpa, io, now);
|
|
}
|