mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-30 09:16:07 -04:00
Merge pull request #2891 from lightpanda-io/nikneym/custom-cert-load
Support loading custom CAs (`--ca-cert`, `--ca-path`)
This commit is contained in:
145
src/Config.zig
145
src/Config.zig
@@ -28,6 +28,7 @@ const Storage = @import("storage/Storage.zig");
|
||||
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;
|
||||
|
||||
const log = lp.log;
|
||||
const crypto = @import("sys/libcrypto.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// TCP keepalive parameters applied to accepted CDP connections.
|
||||
@@ -73,18 +74,108 @@ fn logFilterScopesValidator(allocator: Allocator, args: *std.process.Args.Iterat
|
||||
}
|
||||
}
|
||||
|
||||
fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator) !?log.Level {
|
||||
fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?log.Level) !void {
|
||||
const str = args.next() orelse return error.MissingArgument;
|
||||
if (std.mem.eql(u8, str, "error")) {
|
||||
return .err;
|
||||
target.* = .err;
|
||||
return;
|
||||
}
|
||||
|
||||
return std.meta.stringToEnum(log.Level, str) orelse {
|
||||
target.* = std.meta.stringToEnum(log.Level, str) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log-level", .value = str });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
}
|
||||
|
||||
const Cert = struct {
|
||||
/// On successful CLI argument parsing phase, ownership of this transferred
|
||||
/// to `Network`. Consider it as invalid.
|
||||
store: ?*crypto.X509_STORE = null,
|
||||
// Number of certificate sources loaded into `store`.
|
||||
count: usize = 0,
|
||||
|
||||
fn deinit(self: *Cert) void {
|
||||
if (self.store) |store| {
|
||||
crypto.X509_STORE_free(store);
|
||||
}
|
||||
self.* = .{};
|
||||
}
|
||||
|
||||
/// Returns the store, creating it on first use. The store is shared by
|
||||
/// every `--ca-cert`/`--ca-path` occurrence.
|
||||
fn getOrCreate(self: *Cert) !*crypto.X509_STORE {
|
||||
if (self.store) |store| {
|
||||
return store;
|
||||
}
|
||||
const store = crypto.X509_STORE_new() orelse
|
||||
return error.FailedToCreateCertStore;
|
||||
self.store = store;
|
||||
return store;
|
||||
}
|
||||
};
|
||||
|
||||
fn caCertValidator(
|
||||
_: Allocator,
|
||||
args: *std.process.Args.Iterator,
|
||||
cert: *Cert,
|
||||
) !void {
|
||||
const file_name = args.next() orelse return error.MissingArgument;
|
||||
const store = try cert.getOrCreate();
|
||||
errdefer cert.deinit();
|
||||
|
||||
if (crypto.X509_STORE_load_locations(store, file_name, null) != 1) {
|
||||
log.fatal(.app, "Invalid CA cert", .{ .arg = "--ca-cert", .value = file_name });
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
cert.count += 1;
|
||||
}
|
||||
|
||||
fn caPathValidator(
|
||||
allocator: Allocator,
|
||||
args: *std.process.Args.Iterator,
|
||||
cert: *Cert,
|
||||
) !void {
|
||||
const dir_path = args.next() orelse return error.MissingArgument;
|
||||
|
||||
var dir = std.Io.Dir.cwd().openDir(lp.io, dir_path, .{ .iterate = true }) catch {
|
||||
log.fatal(.app, "Invalid CA path", .{ .arg = "--ca-path", .value = dir_path });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
defer dir.close(lp.io);
|
||||
|
||||
const store = try cert.getOrCreate();
|
||||
errdefer cert.deinit();
|
||||
|
||||
// Eagerly load every certificate in the directory rather than
|
||||
// registering a lazy hashed lookup: the directory doesn't need to be
|
||||
// c_rehash'ed, bad entries surface at startup and `count` reflects
|
||||
// what was actually loaded.
|
||||
const count_before = cert.count;
|
||||
var it = dir.iterate();
|
||||
while (it.next(lp.io) catch {
|
||||
log.fatal(.app, "Invalid CA path", .{ .arg = "--ca-path", .value = dir_path });
|
||||
return error.InvalidArgument;
|
||||
}) |entry| {
|
||||
if (entry.kind != .file and entry.kind != .sym_link) continue;
|
||||
|
||||
const path = try std.fs.path.joinZ(allocator, &.{ dir_path, entry.name });
|
||||
defer allocator.free(path);
|
||||
|
||||
if (crypto.X509_STORE_load_locations(store, path, null) != 1) {
|
||||
log.warn(.app, "Skipping invalid CA cert", .{ .arg = "--ca-path", .value = path });
|
||||
continue;
|
||||
}
|
||||
cert.count += 1;
|
||||
}
|
||||
|
||||
// An empty directory (or one with no readable certificates) is
|
||||
// indistinguishable from a typo; treat it as an error.
|
||||
if (cert.count == count_before) {
|
||||
log.fatal(.app, "No certificates loaded", .{ .arg = "--ca-path", .value = dir_path });
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
}
|
||||
|
||||
/// Common CLI args.
|
||||
const CommonOptions = .{
|
||||
.{ .name = "obey_robots", .type = bool },
|
||||
@@ -118,24 +209,46 @@ const CommonOptions = .{
|
||||
.{ .name = "v8_flags_unsafe", .type = ?[]const u8 },
|
||||
.{ .name = "v8_max_heap_mb", .type = ?u32 },
|
||||
.{ .name = "watchdog_ms", .type = ?u32 },
|
||||
.{
|
||||
.name = "ca_cert",
|
||||
.field_name = "cert",
|
||||
.type = .{
|
||||
.cli = [:0]const u8,
|
||||
.memory = Cert,
|
||||
},
|
||||
.default = Cert{},
|
||||
.validator = caCertValidator,
|
||||
},
|
||||
.{
|
||||
.name = "ca_path",
|
||||
.field_name = "cert",
|
||||
.type = .{
|
||||
.cli = []const u8,
|
||||
.memory = Cert,
|
||||
},
|
||||
.default = Cert{},
|
||||
.validator = caPathValidator,
|
||||
},
|
||||
};
|
||||
|
||||
fn dumpValidator(_: Allocator, args: *std.process.Args.Iterator) !?DumpFormat {
|
||||
fn dumpValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?DumpFormat) !void {
|
||||
// Peek next argument.
|
||||
var peek_args = args.*;
|
||||
if (peek_args.next()) |next_arg| {
|
||||
const mode = std.meta.stringToEnum(DumpFormat, next_arg) orelse {
|
||||
return .html;
|
||||
target.* = .html;
|
||||
return;
|
||||
};
|
||||
|
||||
// Skip the argument we peek if successful.
|
||||
_ = args.next();
|
||||
return mode;
|
||||
target.* = mode;
|
||||
return;
|
||||
}
|
||||
|
||||
// Means we couldn't get something like `--dump html` but we do have
|
||||
// `--dump`; which should fall to `html` by default.
|
||||
return .html;
|
||||
target.* = .html;
|
||||
}
|
||||
|
||||
pub const AiProvider = std.meta.Tag(zenai.provider.Client);
|
||||
@@ -160,13 +273,13 @@ pub const AgentVerbosity = enum {
|
||||
}
|
||||
};
|
||||
|
||||
fn waitScriptFileValidator(allocator: Allocator, args: *std.process.Args.Iterator) !?[:0]const u8 {
|
||||
fn waitScriptFileValidator(allocator: Allocator, args: *std.process.Args.Iterator, target: *?[:0]const u8) !void {
|
||||
const path = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--wait-script-file" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
return std.Io.Dir.cwd().readFileAllocOptions(lp.io, path, allocator, .limited(1024 * 1024), .of(u8), 0) catch |err| {
|
||||
target.* = std.Io.Dir.cwd().readFileAllocOptions(lp.io, path, allocator, .limited(1024 * 1024), .of(u8), 0) catch |err| {
|
||||
log.fatal(.app, "failed to read file", .{ .arg = "--wait-script-file", .path = path, .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
@@ -635,6 +748,20 @@ pub fn storageSqlitePath(self: *const Config) ?[:0]const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the user-supplied certificate store (`--ca-cert`/`--ca-path`),
|
||||
/// if any was loaded during argument parsing. The caller takes ownership.
|
||||
pub fn customCertStore(self: *const Config) ?*crypto.X509_STORE {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| {
|
||||
const store = opts.cert.store orelse return null;
|
||||
// Validators guarantee a created store loaded something.
|
||||
lp.assert(opts.cert.count > 0, "empty custom cert store", .{});
|
||||
return store;
|
||||
},
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub const DumpFormat = enum {
|
||||
html,
|
||||
markdown,
|
||||
|
||||
@@ -38,7 +38,7 @@ config: *const Config,
|
||||
pub fn init(allocator: Allocator, config: *const Config) !Updater {
|
||||
Network.globalInit(allocator);
|
||||
errdefer Network.globalDeinit();
|
||||
const x509_store = try Network.createX509Store(allocator);
|
||||
const x509_store = try Network.prepareX509Store(allocator, config);
|
||||
|
||||
return .{
|
||||
.x509_store = x509_store,
|
||||
|
||||
120
src/cli.zig
120
src/cli.zig
@@ -48,7 +48,10 @@ const log = lp.log;
|
||||
/// - `options: tuple` — tuple of option descriptors (see below). Use `.{}`
|
||||
/// for none.
|
||||
/// - `shared_options: tuple` (optional) — extra options merged into this
|
||||
/// command. Useful for common flags shared across commands.
|
||||
/// command. Useful for common flags shared across commands. An option
|
||||
/// that appears in both with the same field name and type is collapsed
|
||||
/// into one field (the command's own option wins); reusing a name with
|
||||
/// a different type is a compile error.
|
||||
/// - `positional: struct` (optional) — a positional argument with `.name`
|
||||
/// and `.type` that may appear anywhere in argv. By default it holds a
|
||||
/// single value: `.type` must be an optional pointer-to-u8 slice (e.g.
|
||||
@@ -61,7 +64,12 @@ const log = lp.log;
|
||||
///
|
||||
/// - `name: []const u8` — snake_case field name. Both `--snake_case` and
|
||||
/// `--kebab-case` are accepted on the command line.
|
||||
/// - `field_name: []const u8` (optional) — name of the struct field the
|
||||
/// value is written to, when it should differ from the flag name.
|
||||
/// - `type` — the Zig type of the parsed value (see supported types below).
|
||||
/// When the stored representation differs from the CLI-facing one, use
|
||||
/// `.{ .cli = T, .memory = T }`; a `validator` must then produce the
|
||||
/// memory type.
|
||||
/// - `default` (optional) — compile-time default when the flag is absent.
|
||||
/// Rules vary by type; see the defaults section below.
|
||||
/// - `multiple: bool` (optional) — when `true`, the field becomes a
|
||||
@@ -92,15 +100,20 @@ const log = lp.log;
|
||||
/// ## Validators
|
||||
///
|
||||
/// A `validator` is a custom parse function that takes over argument
|
||||
/// consumption for an option. Its signature depends on whether `multiple`
|
||||
/// consumption for an option. It receives a pointer to the generated struct
|
||||
/// field and writes through it; the pointee depends on whether `multiple`
|
||||
/// is set:
|
||||
///
|
||||
/// - Single: `fn (Allocator, *ArgIterator) !T` — returns the parsed value.
|
||||
/// - Single: `fn (Allocator, *ArgIterator, *T) !void` — writes the parsed
|
||||
/// value through the field pointer.
|
||||
/// - Multiple: `fn (Allocator, *ArgIterator, *std.ArrayList(T)) !void` —
|
||||
/// appends directly into the list.
|
||||
///
|
||||
/// When a validator is present, the built-in type switch is skipped entirely.
|
||||
/// The validator owns advancing the iterator and is free to peek ahead.
|
||||
/// Because the validator sees the field itself, a repeated flag can
|
||||
/// accumulate state in place (e.g. add each certificate to a store the
|
||||
/// field points at) instead of collecting values in a list.
|
||||
///
|
||||
/// ## Variants
|
||||
///
|
||||
@@ -196,7 +209,22 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
// Whether option has a default value.
|
||||
const has_default = @hasField(@TypeOf(option), "default");
|
||||
|
||||
const T = if (is_multiple) std.ArrayList(option.type) else option.type;
|
||||
const Type = blk: {
|
||||
if (@typeInfo(@TypeOf(option.type)) == .@"struct") {
|
||||
break :blk option.type.memory;
|
||||
}
|
||||
break :blk option.type;
|
||||
};
|
||||
|
||||
const T = if (is_multiple) std.ArrayList(Type) else Type;
|
||||
|
||||
// Prefer `option.field_name` if memory representation will differ.
|
||||
const name = blk: {
|
||||
if (@hasField(@TypeOf(option), "field_name")) {
|
||||
break :blk option.field_name;
|
||||
}
|
||||
break :blk option.name;
|
||||
};
|
||||
|
||||
const default = blk: {
|
||||
if (is_multiple) {
|
||||
@@ -208,7 +236,7 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
break :blk @as(*const anyopaque, @ptrCast(&@as(T, .empty)));
|
||||
}
|
||||
|
||||
switch (@typeInfo(option.type)) {
|
||||
switch (@typeInfo(Type)) {
|
||||
.optional => |optional| {
|
||||
if (optional.child == bool) {
|
||||
@compileError("?bool is not supported, prefer enum");
|
||||
@@ -228,7 +256,7 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
},
|
||||
inline else => {
|
||||
if (!has_default) {
|
||||
@compileError("option `" ++ option.name ++ "` is not optional type and has no default value");
|
||||
@compileError("option `" ++ name ++ "` is not optional type and has no default value");
|
||||
}
|
||||
break :blk @as(*const anyopaque, @ptrCast(&@as(T, option.default)));
|
||||
},
|
||||
@@ -236,7 +264,7 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
};
|
||||
|
||||
fields[j] = .{
|
||||
.name = option.name,
|
||||
.name = name,
|
||||
.type = T,
|
||||
.default_value_ptr = default,
|
||||
.is_comptime = false,
|
||||
@@ -247,6 +275,34 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
return fields;
|
||||
}
|
||||
|
||||
/// Drops duplicate fields, keeping the first occurrence. Only an exact
|
||||
/// duplicate (same name and type) is deduplicated; a name that
|
||||
/// reappears with a different type is a conflict.
|
||||
fn dedupeStructFields(comptime fields: []const std.builtin.Type.StructField) []const std.builtin.Type.StructField {
|
||||
// The pairwise name comparisons blow the default 1000-branch quota.
|
||||
@setEvalBranchQuota(1000 + fields.len * fields.len * 100);
|
||||
|
||||
var out: [fields.len]std.builtin.Type.StructField = undefined;
|
||||
var len: usize = 0;
|
||||
|
||||
outer: for (fields) |field| {
|
||||
for (out[0..len]) |existing| {
|
||||
if (!std.mem.eql(u8, existing.name, field.name)) continue;
|
||||
|
||||
if (existing.type != field.type) {
|
||||
@compileError("field `" ++ field.name ++ "` is duplicated with a different type");
|
||||
}
|
||||
continue :outer;
|
||||
}
|
||||
|
||||
out[len] = field;
|
||||
len += 1;
|
||||
}
|
||||
|
||||
const frozen = out;
|
||||
return frozen[0..len];
|
||||
}
|
||||
|
||||
/// Union type for provided commands.
|
||||
pub const Union = blk: {
|
||||
const len = commands.len + 1;
|
||||
@@ -258,7 +314,7 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
const Command = @TypeOf(command);
|
||||
const options = command.options;
|
||||
|
||||
const fields = optionsToStructFields(options) ++
|
||||
const all_fields = optionsToStructFields(options) ++
|
||||
(if (@hasField(Command, "shared_options"))
|
||||
optionsToStructFields(command.shared_options)
|
||||
else
|
||||
@@ -268,7 +324,7 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
else
|
||||
.{});
|
||||
|
||||
const T = StructFromFields(&fields);
|
||||
const T = StructFromFields(dedupeStructFields(&all_fields));
|
||||
|
||||
union_fields[i] = .{ .name = command.name, .type = T, .alignment = @alignOf(T) };
|
||||
}
|
||||
@@ -435,7 +491,7 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
return *const fn (Allocator, *std.process.Args.Iterator, *std.ArrayList(T)) anyerror!void;
|
||||
}
|
||||
|
||||
return *const fn (Allocator, *std.process.Args.Iterator) anyerror!T;
|
||||
return *const fn (Allocator, *std.process.Args.Iterator, *T) anyerror!void;
|
||||
}
|
||||
|
||||
/// Turns a snake_case string to kebab-case in comptime.
|
||||
@@ -456,7 +512,9 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
/// ```zig
|
||||
/// Option{
|
||||
/// .name = "option_name",
|
||||
/// .type = T,
|
||||
/// // If provided, names the struct field written to instead of `name`.
|
||||
/// .field_name = "struct_field_name",
|
||||
/// .type = T, // or .{ .cli = T, .memory = T }
|
||||
/// .multiple = ?bool,
|
||||
/// .validator = ?ValidatorFn(T, is_multiple),
|
||||
/// };
|
||||
@@ -469,23 +527,19 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
const is_multiple = @hasField(OptionType, "multiple") and option.multiple;
|
||||
const has_validator = @hasField(OptionType, "validator");
|
||||
|
||||
// Prefer validator for parsing if provided.
|
||||
// Prefer validator for parsing if provided. The validator writes
|
||||
// through the field pointer (the list itself for multiples).
|
||||
if (has_validator) {
|
||||
const validator = option.validator;
|
||||
if (is_multiple) {
|
||||
// Pass the list.
|
||||
try @call(.auto, validator, .{ allocator, args, target });
|
||||
} else {
|
||||
// Receive the value from return.
|
||||
const v = try @call(.auto, validator, .{ allocator, args });
|
||||
target.* = v;
|
||||
}
|
||||
|
||||
return;
|
||||
return @call(.auto, option.validator, .{ allocator, args, target });
|
||||
}
|
||||
|
||||
// Extract type info.
|
||||
const T = option.type;
|
||||
// Extract type info. We need the type that's used in the CLI.
|
||||
const T = blk: {
|
||||
if (@typeInfo(@TypeOf(option.type)) == .@"struct") {
|
||||
break :blk option.type.cli;
|
||||
}
|
||||
break :blk option.type;
|
||||
};
|
||||
const option_info = blk: {
|
||||
const info = @typeInfo(T);
|
||||
// If wrapped in optional, prefer the child type.
|
||||
@@ -639,15 +693,23 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
};
|
||||
iter_args: while (args.next()) |option_name| {
|
||||
inline for (options) |option| {
|
||||
const name = option.name;
|
||||
const field_name = blk: {
|
||||
if (@hasField(@TypeOf(option), "field_name")) {
|
||||
break :blk option.field_name;
|
||||
}
|
||||
break :blk option.name;
|
||||
};
|
||||
|
||||
// We allow both `--my-option` and `--my_option` variants;
|
||||
// assuming given `option` struct prefer snake_case for `name`.
|
||||
// Match an option.
|
||||
const matches_short = comptime @hasField(@TypeOf(option), "short");
|
||||
if (std.mem.eql(u8, option_name, "--" ++ option.name) or
|
||||
std.mem.eql(u8, option_name, "--" ++ comptime toKebabCase(option.name)) or
|
||||
if (std.mem.eql(u8, option_name, "--" ++ name) or
|
||||
std.mem.eql(u8, option_name, "--" ++ comptime toKebabCase(name)) or
|
||||
(matches_short and std.mem.eql(u8, option_name, "-" ++ [_]u8{option.short})))
|
||||
{
|
||||
try parseValue(allocator, args, &@field(c, option.name), option);
|
||||
try parseValue(allocator, args, &@field(c, field_name), option);
|
||||
continue :iter_args;
|
||||
}
|
||||
|
||||
@@ -672,7 +734,7 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
break :blk .{ .name = variant.name, .type = option.type, .multiple = is_multiple };
|
||||
};
|
||||
|
||||
try parseValue(allocator, args, &@field(c, option.name), opts);
|
||||
try parseValue(allocator, args, &@field(c, field_name), opts);
|
||||
continue :iter_args;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +305,15 @@
|
||||
\\ Block HTTP requests to private/internal IP addresses after DNS
|
||||
\\ resolution.
|
||||
\\ Defaults to false.
|
||||
\\ --ca-cert <PATH>
|
||||
\\ Load TLS root certificates from a PEM file. Can be passed
|
||||
\\ multiple times. When any --ca-cert or --ca-path is given, the
|
||||
\\ system trust store is replaced by these certificates.
|
||||
\\ --ca-path <PATH>
|
||||
\\ Load TLS root certificates from every file in a directory. Unlike
|
||||
\\ openssl, the directory does not need to be c_rehash'ed. Can be
|
||||
\\ passed multiple times. When any --ca-cert or --ca-path is given,
|
||||
\\ the system trust store is replaced by these certificates.
|
||||
\\ --cookie <PATH>
|
||||
\\ Path to a JSON file to load cookies from (read-only).
|
||||
\\ Defaults to no cookie loading.
|
||||
|
||||
@@ -183,7 +183,14 @@ 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 prepareX509Store(allocator, config);
|
||||
}
|
||||
// Verification is off, so the store is never consulted — but still
|
||||
// take ownership of a user-supplied one so the flags compose and
|
||||
// nothing leaks.
|
||||
if (config.customCertStore()) |store| {
|
||||
log.warn(.app, "custom CA ignored", .{ .arg = "--ca-cert, --ca-path", .reason = "TLS verification disabled" });
|
||||
break :blk store;
|
||||
}
|
||||
break :blk crypto.X509_STORE_new() orelse {
|
||||
return error.FailedToCreateX509Store;
|
||||
@@ -730,24 +737,38 @@ pub fn newConnection(self: *Network) ?*http.Connection {
|
||||
return conn;
|
||||
}
|
||||
|
||||
const CreateX509StoreError = std.crypto.Certificate.Bundle.RescanError || error{FailedToCreateX509Store};
|
||||
|
||||
/// 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 {
|
||||
pub fn prepareX509Store(allocator: Allocator, config: *const Config) !*crypto.X509_STORE {
|
||||
// A user-supplied store replaces system trust entirely.
|
||||
if (config.customCertStore()) |store| {
|
||||
return store;
|
||||
}
|
||||
return storeFromSystemCA(allocator);
|
||||
}
|
||||
|
||||
/// Creates an X509_STORE from system root CA.
|
||||
fn storeFromSystemCA(allocator: Allocator) !*crypto.X509_STORE {
|
||||
const store = crypto.X509_STORE_new() orelse return error.FailedToCreateX509Store;
|
||||
errdefer crypto.X509_STORE_free(store);
|
||||
|
||||
var count: usize = 0;
|
||||
defer {
|
||||
if (count == 0) {
|
||||
log.warn(.app, "No certificates loaded", .{});
|
||||
}
|
||||
}
|
||||
|
||||
switch (comptime builtin.os.tag) {
|
||||
.linux, .openbsd, .netbsd, .freebsd => blk: {
|
||||
// Iterate over known directories.
|
||||
inline for ([_][:0]const u8{
|
||||
// Iterate over known directories; this may or may not succeed.
|
||||
const cwd = std.Io.Dir.cwd();
|
||||
inline for ([_][]const u8{
|
||||
"/etc/ssl/certs", // Debian/Ubuntu/Gentoo/Alpine, SUSE
|
||||
"/etc/pki/tls/certs", // Fedora/RHEL
|
||||
}) |dir| {
|
||||
if (loadHashedDirectory(store, dir)) {
|
||||
break :blk;
|
||||
}
|
||||
}) |dir_path| {
|
||||
count += try loadFromDirectory(allocator, store, cwd, dir_path);
|
||||
if (count > 0) break :blk;
|
||||
}
|
||||
|
||||
// Iterate over known files.
|
||||
@@ -760,11 +781,10 @@ pub fn createX509Store(allocator: Allocator) CreateX509StoreError!*crypto.X509_S
|
||||
"/etc/ssl/cert.pem", // Alpine, *BSD
|
||||
}) |file| {
|
||||
if (crypto.X509_STORE_load_locations(store, file, null) == 1) {
|
||||
count += 1;
|
||||
break :blk;
|
||||
}
|
||||
}
|
||||
|
||||
log.warn(.app, "No system certificates", .{});
|
||||
},
|
||||
else => {
|
||||
// Prefer stdlib's cert scanner.
|
||||
@@ -773,10 +793,6 @@ pub fn createX509Store(allocator: Allocator) CreateX509StoreError!*crypto.X509_S
|
||||
defer bundle.deinit(allocator);
|
||||
|
||||
const bytes = bundle.bytes.items;
|
||||
if (bytes.len == 0) {
|
||||
log.warn(.app, "No system certificates", .{});
|
||||
return store;
|
||||
}
|
||||
var it = bundle.map.valueIterator();
|
||||
while (it.next()) |index| {
|
||||
// d2i_X509 reads the cert's own DER length header to find its end and
|
||||
@@ -792,6 +808,7 @@ pub fn createX509Store(allocator: Allocator) CreateX509StoreError!*crypto.X509_S
|
||||
if (result != 1) {
|
||||
log.warn(.app, "Failed to add X509 cert to store", .{});
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -799,19 +816,27 @@ pub fn createX509Store(allocator: Allocator) CreateX509StoreError!*crypto.X509_S
|
||||
return store;
|
||||
}
|
||||
|
||||
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);
|
||||
/// Loads certificates from given `path`; returning how many CA loaded.
|
||||
fn loadFromDirectory(
|
||||
allocator: Allocator,
|
||||
store: *crypto.X509_STORE,
|
||||
cwd: std.Io.Dir,
|
||||
dir_path: []const u8,
|
||||
) Allocator.Error!usize {
|
||||
var count: usize = 0;
|
||||
var dir = cwd.openDir(lp.io, dir_path, .{ .iterate = true }) catch return count;
|
||||
defer dir.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;
|
||||
var it = dir.iterate();
|
||||
while (it.next(lp.io) catch return count) |entry| {
|
||||
if (entry.kind != .file and entry.kind != .sym_link) continue;
|
||||
|
||||
const path = try std.fs.path.joinZ(allocator, &.{ dir_path, entry.name });
|
||||
defer allocator.free(path);
|
||||
|
||||
if (crypto.X509_STORE_load_locations(store, path, null) == 1) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
if (!hashed) return false;
|
||||
|
||||
return crypto.X509_STORE_load_locations(store, null, dir.ptr) == 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -20,196 +20,13 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const pthread_rwlock_t = std.c.pthread_rwlock_t;
|
||||
|
||||
pub const struct_env_md_st = opaque {};
|
||||
pub const EVP_MD = struct_env_md_st;
|
||||
pub const evp_pkey_alg_st = opaque {};
|
||||
pub const EVP_PKEY_ALG = evp_pkey_alg_st;
|
||||
pub const struct_engine_st = opaque {};
|
||||
pub const ENGINE = struct_engine_st;
|
||||
pub const CRYPTO_THREADID = c_int;
|
||||
pub const struct_asn1_null_st = opaque {};
|
||||
pub const ASN1_NULL = struct_asn1_null_st;
|
||||
pub const ASN1_BOOLEAN = c_int;
|
||||
pub const struct_ASN1_ITEM_st = opaque {};
|
||||
pub const ASN1_ITEM = struct_ASN1_ITEM_st;
|
||||
pub const struct_asn1_object_st = opaque {};
|
||||
pub const ASN1_OBJECT = struct_asn1_object_st;
|
||||
pub const struct_asn1_pctx_st = opaque {};
|
||||
pub const ASN1_PCTX = struct_asn1_pctx_st;
|
||||
pub const struct_asn1_string_st = extern struct {
|
||||
length: c_int,
|
||||
type: c_int,
|
||||
data: [*c]u8,
|
||||
flags: c_long,
|
||||
};
|
||||
pub const ASN1_BIT_STRING = struct_asn1_string_st;
|
||||
pub const ASN1_BMPSTRING = struct_asn1_string_st;
|
||||
pub const ASN1_ENUMERATED = struct_asn1_string_st;
|
||||
pub const ASN1_GENERALIZEDTIME = struct_asn1_string_st;
|
||||
pub const ASN1_GENERALSTRING = struct_asn1_string_st;
|
||||
pub const ASN1_IA5STRING = struct_asn1_string_st;
|
||||
pub const ASN1_INTEGER = struct_asn1_string_st;
|
||||
pub const ASN1_OCTET_STRING = struct_asn1_string_st;
|
||||
pub const ASN1_PRINTABLESTRING = struct_asn1_string_st;
|
||||
pub const ASN1_STRING = struct_asn1_string_st;
|
||||
pub const ASN1_T61STRING = struct_asn1_string_st;
|
||||
pub const ASN1_TIME = struct_asn1_string_st;
|
||||
pub const ASN1_UNIVERSALSTRING = struct_asn1_string_st;
|
||||
pub const ASN1_UTCTIME = struct_asn1_string_st;
|
||||
pub const ASN1_UTF8STRING = struct_asn1_string_st;
|
||||
pub const ASN1_VISIBLESTRING = struct_asn1_string_st;
|
||||
pub const struct_ASN1_VALUE_st = opaque {};
|
||||
pub const ASN1_VALUE = struct_ASN1_VALUE_st;
|
||||
const union_unnamed_1 = extern union {
|
||||
ptr: [*c]u8,
|
||||
boolean: ASN1_BOOLEAN,
|
||||
asn1_string: [*c]ASN1_STRING,
|
||||
object: ?*ASN1_OBJECT,
|
||||
integer: [*c]ASN1_INTEGER,
|
||||
enumerated: [*c]ASN1_ENUMERATED,
|
||||
bit_string: [*c]ASN1_BIT_STRING,
|
||||
octet_string: [*c]ASN1_OCTET_STRING,
|
||||
printablestring: [*c]ASN1_PRINTABLESTRING,
|
||||
t61string: [*c]ASN1_T61STRING,
|
||||
ia5string: [*c]ASN1_IA5STRING,
|
||||
generalstring: [*c]ASN1_GENERALSTRING,
|
||||
bmpstring: [*c]ASN1_BMPSTRING,
|
||||
universalstring: [*c]ASN1_UNIVERSALSTRING,
|
||||
utctime: [*c]ASN1_UTCTIME,
|
||||
generalizedtime: [*c]ASN1_GENERALIZEDTIME,
|
||||
visiblestring: [*c]ASN1_VISIBLESTRING,
|
||||
utf8string: [*c]ASN1_UTF8STRING,
|
||||
set: [*c]ASN1_STRING,
|
||||
sequence: [*c]ASN1_STRING,
|
||||
asn1_value: ?*ASN1_VALUE,
|
||||
};
|
||||
pub const struct_asn1_type_st = extern struct {
|
||||
type: c_int,
|
||||
value: union_unnamed_1,
|
||||
};
|
||||
pub const ASN1_TYPE = struct_asn1_type_st;
|
||||
pub const struct_AUTHORITY_KEYID_st = opaque {};
|
||||
pub const AUTHORITY_KEYID = struct_AUTHORITY_KEYID_st;
|
||||
pub const struct_BASIC_CONSTRAINTS_st = opaque {};
|
||||
pub const BASIC_CONSTRAINTS = struct_BASIC_CONSTRAINTS_st;
|
||||
pub const struct_DIST_POINT_st = opaque {};
|
||||
pub const DIST_POINT = struct_DIST_POINT_st;
|
||||
pub const BN_ULONG = u64;
|
||||
pub const struct_bignum_st = extern struct {
|
||||
d: [*c]BN_ULONG,
|
||||
width: c_int,
|
||||
dmax: c_int,
|
||||
neg: c_int,
|
||||
flags: c_int,
|
||||
};
|
||||
pub const BIGNUM = struct_bignum_st;
|
||||
pub const struct_DSA_SIG_st = extern struct {
|
||||
r: [*c]BIGNUM,
|
||||
s: [*c]BIGNUM,
|
||||
};
|
||||
pub const DSA_SIG = struct_DSA_SIG_st;
|
||||
pub const struct_ISSUING_DIST_POINT_st = opaque {};
|
||||
pub const ISSUING_DIST_POINT = struct_ISSUING_DIST_POINT_st;
|
||||
pub const struct_NAME_CONSTRAINTS_st = opaque {};
|
||||
pub const NAME_CONSTRAINTS = struct_NAME_CONSTRAINTS_st;
|
||||
pub const struct_X509_pubkey_st = opaque {};
|
||||
pub const X509_PUBKEY = struct_X509_pubkey_st;
|
||||
pub const struct_Netscape_spkac_st = extern struct {
|
||||
pubkey: ?*X509_PUBKEY,
|
||||
challenge: [*c]ASN1_IA5STRING,
|
||||
};
|
||||
pub const NETSCAPE_SPKAC = struct_Netscape_spkac_st;
|
||||
pub const struct_X509_algor_st = extern struct {
|
||||
algorithm: ?*ASN1_OBJECT,
|
||||
parameter: [*c]ASN1_TYPE,
|
||||
};
|
||||
pub const X509_ALGOR = struct_X509_algor_st;
|
||||
pub const struct_Netscape_spki_st = extern struct {
|
||||
spkac: [*c]NETSCAPE_SPKAC,
|
||||
sig_algor: [*c]X509_ALGOR,
|
||||
signature: [*c]ASN1_BIT_STRING,
|
||||
};
|
||||
pub const NETSCAPE_SPKI = struct_Netscape_spki_st;
|
||||
pub const struct_RIPEMD160state_st = opaque {};
|
||||
pub const RIPEMD160_CTX = struct_RIPEMD160state_st;
|
||||
pub const struct_X509_VERIFY_PARAM_st = opaque {};
|
||||
pub const X509_VERIFY_PARAM = struct_X509_VERIFY_PARAM_st;
|
||||
pub const struct_X509_crl_st = opaque {};
|
||||
pub const X509_CRL = struct_X509_crl_st;
|
||||
pub const struct_X509_extension_st = opaque {};
|
||||
pub const X509_EXTENSION = struct_X509_extension_st;
|
||||
pub const struct_x509_st = opaque {};
|
||||
pub const X509 = struct_x509_st;
|
||||
pub const CRYPTO_refcount_t = u32;
|
||||
pub const struct_openssl_method_common_st = extern struct {
|
||||
references: c_int,
|
||||
is_static: u8,
|
||||
};
|
||||
pub const struct_rsa_meth_st = extern struct {
|
||||
common: struct_openssl_method_common_st,
|
||||
app_data: ?*anyopaque,
|
||||
init: ?*const fn (?*RSA) callconv(.c) c_int,
|
||||
finish: ?*const fn (?*RSA) callconv(.c) c_int,
|
||||
size: ?*const fn (?*const RSA) callconv(.c) usize,
|
||||
sign: ?*const fn (c_int, [*c]const u8, c_uint, [*c]u8, [*c]c_uint, ?*const RSA) callconv(.c) c_int,
|
||||
sign_raw: ?*const fn (?*RSA, [*c]usize, [*c]u8, usize, [*c]const u8, usize, c_int) callconv(.c) c_int,
|
||||
decrypt: ?*const fn (?*RSA, [*c]usize, [*c]u8, usize, [*c]const u8, usize, c_int) callconv(.c) c_int,
|
||||
private_transform: ?*const fn (?*RSA, [*c]u8, [*c]const u8, usize) callconv(.c) c_int,
|
||||
flags: c_int,
|
||||
};
|
||||
pub const RSA_METHOD = struct_rsa_meth_st;
|
||||
pub const struct_stack_st_void = opaque {};
|
||||
pub const struct_crypto_ex_data_st = extern struct {
|
||||
sk: ?*struct_stack_st_void,
|
||||
};
|
||||
pub const CRYPTO_EX_DATA = struct_crypto_ex_data_st;
|
||||
pub const CRYPTO_MUTEX = pthread_rwlock_t;
|
||||
pub const struct_bn_mont_ctx_st = extern struct {
|
||||
RR: BIGNUM,
|
||||
N: BIGNUM,
|
||||
n0: [2]BN_ULONG,
|
||||
};
|
||||
pub const BN_MONT_CTX = struct_bn_mont_ctx_st;
|
||||
pub const struct_bn_blinding_st = opaque {};
|
||||
pub const BN_BLINDING = struct_bn_blinding_st; // boringssl/include/openssl/rsa.h:788:12: warning: struct demoted to opaque type - has bitfield
|
||||
pub const struct_rsa_st = opaque {};
|
||||
pub const RSA = struct_rsa_st;
|
||||
pub const struct_dsa_st = extern struct {
|
||||
version: c_long,
|
||||
p: [*c]BIGNUM,
|
||||
q: [*c]BIGNUM,
|
||||
g: [*c]BIGNUM,
|
||||
pub_key: [*c]BIGNUM,
|
||||
priv_key: [*c]BIGNUM,
|
||||
flags: c_int,
|
||||
method_mont_lock: CRYPTO_MUTEX,
|
||||
method_mont_p: [*c]BN_MONT_CTX,
|
||||
method_mont_q: [*c]BN_MONT_CTX,
|
||||
references: CRYPTO_refcount_t,
|
||||
ex_data: CRYPTO_EX_DATA,
|
||||
};
|
||||
pub const DSA = struct_dsa_st;
|
||||
pub const struct_dh_st = opaque {};
|
||||
pub const DH = struct_dh_st;
|
||||
pub const struct_ec_key_st = opaque {};
|
||||
pub const EC_KEY = struct_ec_key_st;
|
||||
const union_unnamed_2 = extern union {
|
||||
ptr: ?*anyopaque,
|
||||
rsa: ?*RSA,
|
||||
dsa: [*c]DSA,
|
||||
dh: ?*DH,
|
||||
ec: ?*EC_KEY,
|
||||
};
|
||||
pub const struct_evp_pkey_asn1_method_st = opaque {};
|
||||
pub const EVP_PKEY_ASN1_METHOD = struct_evp_pkey_asn1_method_st;
|
||||
pub const struct_evp_pkey_st = extern struct {
|
||||
references: CRYPTO_refcount_t,
|
||||
type: c_int,
|
||||
pkey: union_unnamed_2,
|
||||
ameth: ?*const EVP_PKEY_ASN1_METHOD,
|
||||
};
|
||||
pub const struct_evp_pkey_st = opaque {};
|
||||
pub const EVP_PKEY = struct_evp_pkey_st;
|
||||
pub const struct_evp_pkey_ctx_st = opaque {};
|
||||
pub const EVP_PKEY_CTX = struct_evp_pkey_ctx_st;
|
||||
@@ -264,7 +81,6 @@ pub extern fn HKDF(
|
||||
|
||||
pub const X25519_PRIVATE_KEY_LEN = 32;
|
||||
pub const X25519_PUBLIC_VALUE_LEN = 32;
|
||||
pub const X25519_SHARED_KEY_LEN = 32;
|
||||
|
||||
pub extern fn X25519_keypair(out_public_value: *[32]u8, out_private_key: *[32]u8) void;
|
||||
|
||||
@@ -326,13 +142,13 @@ pub const EVP_PKEY_X25519 = NID_X25519;
|
||||
pub const NID_ED25519 = 949;
|
||||
pub const EVP_PKEY_ED25519 = NID_ED25519;
|
||||
|
||||
pub extern fn EVP_PKEY_new_raw_private_key(@"type": c_int, unused: ?*ENGINE, in: [*c]const u8, len: usize) [*c]EVP_PKEY;
|
||||
pub extern fn EVP_PKEY_new_raw_public_key(@"type": c_int, unused: ?*ENGINE, in: [*c]const u8, len: usize) [*c]EVP_PKEY;
|
||||
pub extern fn EVP_PKEY_CTX_new(pkey: [*c]EVP_PKEY, e: ?*ENGINE) ?*EVP_PKEY_CTX;
|
||||
pub extern fn EVP_PKEY_new_raw_private_key(@"type": c_int, unused: ?*ENGINE, in: [*c]const u8, len: usize) ?*EVP_PKEY;
|
||||
pub extern fn EVP_PKEY_new_raw_public_key(@"type": c_int, unused: ?*ENGINE, in: [*c]const u8, len: usize) ?*EVP_PKEY;
|
||||
pub extern fn EVP_PKEY_CTX_new(pkey: ?*EVP_PKEY, e: ?*ENGINE) ?*EVP_PKEY_CTX;
|
||||
pub extern fn EVP_PKEY_CTX_free(ctx: ?*EVP_PKEY_CTX) void;
|
||||
pub extern fn EVP_PKEY_derive_init(ctx: ?*EVP_PKEY_CTX) c_int;
|
||||
pub extern fn EVP_PKEY_derive(ctx: ?*EVP_PKEY_CTX, key: [*c]u8, out_key_len: [*c]usize) c_int;
|
||||
pub extern fn EVP_PKEY_derive_set_peer(ctx: ?*EVP_PKEY_CTX, peer: [*c]EVP_PKEY) c_int;
|
||||
pub extern fn EVP_PKEY_derive_set_peer(ctx: ?*EVP_PKEY_CTX, peer: ?*EVP_PKEY) c_int;
|
||||
pub extern fn EVP_PKEY_free(pkey: ?*EVP_PKEY) void;
|
||||
|
||||
pub extern fn EVP_DigestSignInit(ctx: ?*EVP_MD_CTX, pctx: ?*?*EVP_PKEY_CTX, typ: ?*const EVP_MD, e: ?*ENGINE, pkey: ?*EVP_PKEY) c_int;
|
||||
@@ -343,6 +159,9 @@ pub extern fn EVP_MD_CTX_free(ctx: ?*EVP_MD_CTX) void;
|
||||
pub const struct_evp_md_ctx_st = opaque {};
|
||||
pub const EVP_MD_CTX = struct_evp_md_ctx_st;
|
||||
|
||||
pub const struct_x509_st = opaque {};
|
||||
pub const X509 = struct_x509_st;
|
||||
|
||||
pub extern fn X509_free(x509: ?*X509) void;
|
||||
pub extern fn d2i_X509(out: [*c]?*X509, inp: *[*]const u8, len: c_long) ?*X509;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user