From b7f61e57661bf3ccb2c6c5a05c85d965dc469bbd Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Sat, 4 Jul 2026 17:52:42 +0300 Subject: [PATCH 01/24] `Network`: support custom CA install --- src/Config.zig | 11 +++++++++++ src/network/Network.zig | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Config.zig b/src/Config.zig index 2ec4b82a2..8576a987d 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -118,6 +118,9 @@ const CommonOptions = .{ .{ .name = "v8_flags_unsafe", .type = ?[]const u8 }, .{ .name = "v8_max_heap_mb", .type = ?u32 }, .{ .name = "watchdog_ms", .type = ?u32 }, + .{ .name = "ca_cert", .type = [:0]const u8, .multiple = true }, + .{ .name = "ca_path", .type = [:0]const u8, .multiple = true }, + .{ .name = "disable_root_certificates", .type = bool }, }; fn dumpValidator(_: Allocator, args: *std.process.Args.Iterator) !?DumpFormat { @@ -388,6 +391,14 @@ pub fn v8MaxHeapMb(self: *const Config) ?u32 { }; } +pub fn disableRootCertificates(self: *const Config) bool { + return switch (self.mode) { + inline .serve, .fetch, .mcp, .agent => |opts| opts.disable_root_certificates, + .version => false, + else => unreachable, + }; +} + pub fn httpProxy(self: *const Config) ?[:0]const u8 { return switch (self.mode) { inline .serve, .fetch, .mcp, .agent => |opts| opts.http_proxy, diff --git a/src/network/Network.zig b/src/network/Network.zig index 868e82680..5cf2c8fd3 100644 --- a/src/network/Network.zig +++ b/src/network/Network.zig @@ -183,7 +183,7 @@ pub fn init(allocator: Allocator, app: *App, config: *const Config) !Network { const x509_store = blk: { if (config.tlsVerifyHost()) { - break :blk try createX509Store(allocator); + break :blk try createX509Store(allocator, config); } break :blk crypto.X509_STORE_new() orelse { return error.FailedToCreateX509Store; @@ -734,10 +734,38 @@ const CreateX509StoreError = std.crypto.Certificate.Bundle.RescanError || error{ /// NEVER give full ownership of store to `SSL_CTX`, always rely on ref counting. /// Allocations made through passed `allocator` are freed before this function returns. -pub fn createX509Store(allocator: Allocator) CreateX509StoreError!*crypto.X509_STORE { +fn createX509Store(allocator: Allocator, config: *const Config) CreateX509StoreError!*crypto.X509_STORE { const store = crypto.X509_STORE_new() orelse return error.FailedToCreateX509Store; errdefer crypto.X509_STORE_free(store); + switch (config.mode) { + // Load custom CA if provided. + inline .serve, .fetch, .mcp, .agent => |opts| { + const files = opts.ca_cert.items; + const directories = opts.ca_path.items; + + for (directories) |ca_path| { + if (crypto.X509_STORE_load_locations(store, null, ca_path) != 1) { + log.warn(.app, "Invalid CA path", .{ .ca_path = ca_path }); + } + } + + for (files) |ca_cert| { + if (crypto.X509_STORE_load_locations(store, ca_cert, null) != 1) { + log.warn(.app, "Invalid CA cert", .{ .ca_cert = ca_cert }); + } + } + }, + .help => unreachable, + .version => {}, // Don't load custom CA for version command. + } + + // Early return if root certificates are unwanted. + if (config.disableRootCertificates()) { + log.warn(.app, "Root certificates are disabled", .{}); + return store; + } + switch (comptime builtin.os.tag) { .linux, .openbsd, .netbsd, .freebsd => blk: { // Iterate over known directories. From 11570202f393f66ffcbdff79735c2d252ae5198d Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Sun, 5 Jul 2026 13:50:31 +0300 Subject: [PATCH 02/24] `Config`: ensure passed `--ca-path` paths contain hashed certs --- src/Config.zig | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Config.zig b/src/Config.zig index 8576a987d..f49b01cf5 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -85,6 +85,37 @@ fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator) !?log.Level }; } +pub fn isHashedDirectory(dir: []const u8) bool { + var handle = std.fs.openDirAbsolute(dir, .{ .iterate = true }) catch return false; + defer handle.close(); + + var hashed = false; + var it = handle.iterate(); + while (it.next() catch return false) |entry| { + if (std.mem.endsWith(u8, entry.name, ".0")) { + hashed = true; + break; + } + } + return hashed; +} + +fn caPathValidator( + allocator: Allocator, + args: *std.process.ArgIterator, + list: *std.ArrayList([:0]const u8), +) !void { + const dir = args.next() orelse return error.MissingArgument; + if (!isHashedDirectory(dir)) { + log.fatal(.app, "invalid CA path", .{ .arg = "--ca-path", .value = dir }); + return error.InvalidArgument; + } + + return list.append(allocator, dir); +} + +const crypto = @import("sys/libcrypto.zig"); + /// Common CLI args. const CommonOptions = .{ .{ .name = "obey_robots", .type = bool }, @@ -119,7 +150,7 @@ const CommonOptions = .{ .{ .name = "v8_max_heap_mb", .type = ?u32 }, .{ .name = "watchdog_ms", .type = ?u32 }, .{ .name = "ca_cert", .type = [:0]const u8, .multiple = true }, - .{ .name = "ca_path", .type = [:0]const u8, .multiple = true }, + .{ .name = "ca_path", .type = [:0]const u8, .multiple = true, .validator = caPathValidator }, .{ .name = "disable_root_certificates", .type = bool }, }; From 3c4d09831599a266923cc8b1d601d2bc8020502e Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Sun, 5 Jul 2026 13:52:57 +0300 Subject: [PATCH 03/24] `Network`: prefer `Config.isHashedDirectory` for consistency --- src/network/Network.zig | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/network/Network.zig b/src/network/Network.zig index 5cf2c8fd3..cac4d1540 100644 --- a/src/network/Network.zig +++ b/src/network/Network.zig @@ -828,18 +828,8 @@ fn createX509Store(allocator: Allocator, config: *const Config) CreateX509StoreE } fn loadHashedDirectory(store: *crypto.X509_STORE, dir: [:0]const u8) bool { - var handle = std.Io.Dir.openDirAbsolute(lp.io, dir, .{ .iterate = true }) catch return false; - defer handle.close(lp.io); - - var hashed = false; - var it = handle.iterate(); - while (it.next(lp.io) catch return false) |entry| { - if (std.mem.endsWith(u8, entry.name, ".0")) { - hashed = true; - break; - } + if (Config.isHashedDirectory(dir)) { + return crypto.X509_STORE_load_locations(store, null, dir.ptr) == 1; } - if (!hashed) return false; - - return crypto.X509_STORE_load_locations(store, null, dir.ptr) == 1; + return false; } From bf916e71af4ed14d24a4c4675ae7072a679dc959 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Sun, 5 Jul 2026 14:26:26 +0300 Subject: [PATCH 04/24] `Config`: remove unnecessary import --- src/Config.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Config.zig b/src/Config.zig index f49b01cf5..9d6f283f1 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -114,8 +114,6 @@ fn caPathValidator( return list.append(allocator, dir); } -const crypto = @import("sys/libcrypto.zig"); - /// Common CLI args. const CommonOptions = .{ .{ .name = "obey_robots", .type = bool }, From fdb90a8defb77ebf55d4cac9bc8a8c8166526bdf Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 6 Jul 2026 13:02:22 +0300 Subject: [PATCH 05/24] `help.zon`: obey alphabetical order --- src/help.zon | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/help.zon b/src/help.zon index 70809b9b7..24015fd04 100644 --- a/src/help.zon +++ b/src/help.zon @@ -305,12 +305,23 @@ \\ Block HTTP requests to private/internal IP addresses after DNS \\ resolution. \\ Defaults to false. + \\ --ca-cert + \\ Load TLS root certificates from a PEM file, in addition to the + \\ system trust store. Can be passed multiple times. + \\ --ca-path + \\ Load TLS root certificates from a directory of hashed certificates, + \\ as prepared by c_rehash / openssl rehash. Can be passed multiple + \\ times. \\ --cookie \\ Path to a JSON file to load cookies from (read-only). \\ Defaults to no cookie loading. \\ --cookie-jar \\ Path to a JSON file to save cookies to on exit (write-only). \\ Defaults to no cookie saving. + \\ --disable-root-certificates + \\ Skip loading the system trust store; only certificates given via + \\ --ca-cert and --ca-path are trusted. + \\ Defaults to false. \\ --disable-subframes \\ Skip loading