mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 06:13:07 +00:00
There is no straightforward way for the Zig team to access the Solaris system headers; to do this, one has to create an Oracle account, accept their EULA to download the installer ISO, and finally install it on a machine or VM. We do not have to jump through hoops like this for any other OS that we support, and no one on the team has expressed willingness to do it. As a result, we cannot audit any Solaris contributions to std.c or other similarly sensitive parts of the standard library. The best we would be able to do is assume that Solaris and illumos are 100% compatible with no way to verify that assumption. But at that point, the solaris and illumos OS tags would be functionally identical anyway. For Solaris especially, any contributions that involve APIs introduced after the OS was made closed-source would also be inherently more risky than equivalent contributions for other proprietary OSs due to the case of Google LLC v. Oracle America, Inc., wherein Oracle clearly demonstrated its willingness to pursue legal action against entities that merely copy API declarations. Finally, Oracle laid off most of the Solaris team in 2017; the OS has been in maintenance mode since, presumably to be retired completely sometime in the 2030s. For these reasons, this commit removes all Oracle Solaris support. Anyone who still wishes to use Zig on Solaris can try their luck by simply using illumos instead of solaris in target triples - chances are it'll work. But there will be no effort from the Zig team to support this use case; we recommend that people move to illumos instead.
329 lines
12 KiB
Zig
329 lines
12 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`.
|
|
|
|
/// 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.ArrayListUnmanaged(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) RescanError!void {
|
|
switch (builtin.os.tag) {
|
|
.linux => return rescanLinux(cb, gpa),
|
|
.macos => return rescanMac(cb, gpa),
|
|
.freebsd, .openbsd => return rescanWithPath(cb, gpa, "/etc/ssl/cert.pem"),
|
|
.netbsd => return rescanWithPath(cb, gpa, "/etc/openssl/certs/ca-certificates.crt"),
|
|
.dragonfly => return rescanWithPath(cb, gpa, "/usr/local/etc/ssl/cert.pem"),
|
|
.illumos => return rescanWithPath(cb, gpa, "/etc/ssl/cacert.pem"),
|
|
.haiku => return rescanWithPath(cb, gpa, "/boot/system/data/ssl/CARootCertificates.pem"),
|
|
// https://github.com/SerenityOS/serenity/blob/222acc9d389bc6b490d4c39539761b043a4bfcb0/Ports/ca-certificates/package.sh#L19
|
|
.serenity => return rescanWithPath(cb, gpa, "/etc/ssl/certs/ca-certificates.crt"),
|
|
.windows => return rescanWindows(cb, gpa),
|
|
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) 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, 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, 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, cert_file_path: []const u8) RescanWithPathError!void {
|
|
cb.bytes.clearRetainingCapacity();
|
|
cb.map.clearRetainingCapacity();
|
|
try addCertsFromFilePathAbsolute(cb, gpa, 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) RescanWindowsError!void {
|
|
cb.bytes.clearRetainingCapacity();
|
|
cb.map.clearRetainingCapacity();
|
|
|
|
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 = std.time.timestamp();
|
|
|
|
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,
|
|
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, iterable_dir);
|
|
}
|
|
|
|
pub fn addCertsFromDirPathAbsolute(
|
|
cb: *Bundle,
|
|
gpa: Allocator,
|
|
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, iterable_dir);
|
|
}
|
|
|
|
pub const AddCertsFromDirError = AddCertsFromFilePathError;
|
|
|
|
pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, 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, iterable_dir, entry.name);
|
|
}
|
|
}
|
|
|
|
pub const AddCertsFromFilePathError = fs.File.OpenError || AddCertsFromFileError;
|
|
|
|
pub fn addCertsFromFilePathAbsolute(
|
|
cb: *Bundle,
|
|
gpa: Allocator,
|
|
abs_file_path: []const u8,
|
|
) AddCertsFromFilePathError!void {
|
|
assert(fs.path.isAbsolute(abs_file_path));
|
|
var file = try fs.openFileAbsolute(abs_file_path, .{});
|
|
defer file.close();
|
|
return addCertsFromFile(cb, gpa, file);
|
|
}
|
|
|
|
pub fn addCertsFromFilePath(
|
|
cb: *Bundle,
|
|
gpa: Allocator,
|
|
dir: fs.Dir,
|
|
sub_file_path: []const u8,
|
|
) AddCertsFromFilePathError!void {
|
|
var file = try dir.openFile(sub_file_path, .{});
|
|
defer file.close();
|
|
return addCertsFromFile(cb, gpa, file);
|
|
}
|
|
|
|
pub const AddCertsFromFileError = Allocator.Error ||
|
|
fs.File.GetSeekPosError ||
|
|
fs.File.ReadError ||
|
|
ParseCertError ||
|
|
std.base64.Error ||
|
|
error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker };
|
|
|
|
pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) AddCertsFromFileError!void {
|
|
const size = try file.getEndPos();
|
|
|
|
// 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 = try file.readAll(buffer);
|
|
const encoded_bytes = buffer[0..end_index];
|
|
|
|
const begin_marker = "-----BEGIN CERTIFICATE-----";
|
|
const end_marker = "-----END CERTIFICATE-----";
|
|
|
|
const now_sec = std.time.timestamp();
|
|
|
|
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 builtin = @import("builtin");
|
|
const std = @import("../../std.zig");
|
|
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 Bundle = @This();
|
|
|
|
const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n");
|
|
|
|
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;
|
|
|
|
var bundle: Bundle = .{};
|
|
defer bundle.deinit(std.testing.allocator);
|
|
|
|
try bundle.rescan(std.testing.allocator);
|
|
}
|