milestone 10: doh and dot listeners, cert store with hot reload and cert reload api

This commit is contained in:
2026-08-02 14:39:18 +02:00
parent 617cc966a2
commit a589df7515
20 changed files with 4398 additions and 21 deletions
+1 -1
View File
@@ -190,7 +190,7 @@ test "TlsStream.flush puts the record on the wire" {
defer threaded.deinit();
const io = threaded.io();
var ctx = try tls_server.ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem);
var ctx = try tls_server.ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem, null);
defer ctx.deinit(gpa);
const listen_address: net.IpAddress = .{ .ip4 = .loopback(0) };
+71 -7
View File
@@ -62,7 +62,19 @@ pub const ServerContext = struct {
const Config = Block(SslConfig);
};
pub fn init(gpa: std.mem.Allocator, cert_pem: [:0]const u8, key_pem: [:0]const u8) InitError!ServerContext {
/// `alpn`, when non-null, is a NULL-terminated list of protocol names in
/// decreasing preference order (e.g. `&.{"http/1.1"}` as a
/// comptime-constant `[_:null]?[*:0]const u8` array). Mbed TLS records the
/// pointer, not a copy, so the array must outlive this `ServerContext` —
/// pass a comptime-constant array, never stack or heap memory that can go
/// away first. Clients that send no ALPN extension still connect; Mbed TLS
/// only enforces the list against clients that offer one.
pub fn init(
gpa: std.mem.Allocator,
cert_pem: [:0]const u8,
key_pem: [:0]const u8,
alpn: ?[*:null]const ?[*:0]const u8,
) InitError!ServerContext {
assert(nx_max_context_alignment() <= context_alignment);
// TLS 1.3 is on in the stock config; its key schedule runs through PSA.
@@ -148,6 +160,12 @@ pub const ServerContext = struct {
error.ConfigFailed,
);
if (alpn) |protos| try check(
mbedtls_ssl_conf_alpn_protocols(config.ptr, protos),
"ssl_conf_alpn_protocols",
error.ConfigFailed,
);
return .{
.entropy = entropy,
.drbg = drbg,
@@ -518,6 +536,9 @@ extern fn mbedtls_ssl_config_defaults(
extern fn mbedtls_ssl_conf_rng(conf: *SslConfig, f_rng: *const RngFn, p_rng: ?*anyopaque) void;
extern fn mbedtls_ssl_conf_authmode(conf: *SslConfig, authmode: c_int) void;
extern fn mbedtls_ssl_conf_own_cert(conf: *SslConfig, own_cert: *X509Crt, pk_key: *PkContext) c_int;
/// Records the pointer to `protos` (NULL-terminated, decreasing preference);
/// the list must outlive the configuration.
extern fn mbedtls_ssl_conf_alpn_protocols(conf: *SslConfig, protos: [*:null]const ?[*:0]const u8) c_int;
extern fn mbedtls_x509_crt_init(crt: *X509Crt) void;
extern fn mbedtls_x509_crt_free(crt: *X509Crt) void;
@@ -583,7 +604,7 @@ test "ServerContext.init accepts the fixture cert and key" {
const fixtures = @import("test_fixtures");
const gpa = std.testing.allocator;
var ctx = try ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem);
var ctx = try ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem, null);
defer ctx.deinit(gpa);
}
@@ -596,7 +617,7 @@ test "ServerContext.init rejects a truncated certificate" {
try std.testing.expectError(
error.CertParse,
ServerContext.init(gpa, truncated, fixtures.key_pem),
ServerContext.init(gpa, truncated, fixtures.key_pem, null),
);
}
@@ -609,7 +630,7 @@ test "ServerContext.init rejects a truncated private key" {
try std.testing.expectError(
error.KeyParse,
ServerContext.init(gpa, fixtures.cert_pem, truncated),
ServerContext.init(gpa, fixtures.cert_pem, truncated, null),
);
}
@@ -619,10 +640,19 @@ test "ServerContext.init rejects a key that is not the certificate's" {
try std.testing.expectError(
error.KeyMismatch,
ServerContext.init(gpa, fixtures.cert_pem, fixtures.mismatched_key_pem),
ServerContext.init(gpa, fixtures.cert_pem, fixtures.mismatched_key_pem, null),
);
}
test "ServerContext.init accepts a NULL-terminated ALPN protocol list" {
const fixtures = @import("test_fixtures");
const gpa = std.testing.allocator;
const protocols: [*:null]const ?[*:0]const u8 = &.{ "http/1.1", "dot" };
var ctx = try ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem, protocols);
defer ctx.deinit(gpa);
}
test "the shim agrees with the alignment contexts are allocated at" {
try std.testing.expect(nx_max_context_alignment() <= context_alignment);
try std.testing.expect(nx_sizeof_ssl_context() > 0);
@@ -652,7 +682,7 @@ test "loopback echo between the mbedtls server and std.crypto.tls.Client" {
defer threaded.deinit();
const io = threaded.io();
var ctx = try ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem);
var ctx = try ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem, null);
defer ctx.deinit(gpa);
const listen_address: Io.net.IpAddress = .{ .ip4 = .loopback(0) };
@@ -671,6 +701,40 @@ test "loopback echo between the mbedtls server and std.crypto.tls.Client" {
try server_result;
}
// std.crypto.tls.Client in Zig 0.16.0 cannot send the ALPN extension
// (Client.Options has no such field), so negotiation itself is untestable
// here; this proves the ruling that a client offering no ALPN still
// completes the handshake against a context advertising a list.
test "loopback echo succeeds against a context advertising ALPN" {
const build_options = @import("build_options");
if (!build_options.integration) return error.SkipZigTest;
const fixtures = @import("test_fixtures");
const gpa = std.testing.allocator;
var threaded: Io.Threaded = .init(gpa, .{});
defer threaded.deinit();
const io = threaded.io();
const protocols: [*:null]const ?[*:0]const u8 = &.{"http/1.1"};
var ctx = try ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem, protocols);
defer ctx.deinit(gpa);
const listen_address: Io.net.IpAddress = .{ .ip4 = .loopback(0) };
var server = try listen_address.listen(io, .{ .reuse_address = true });
defer server.deinit(io);
var server_task = try io.concurrent(echoOnce, .{ gpa, &ctx, io, &server });
const client_result = runEchoClient(io, server.socket.address);
const server_result = if (client_result) |_|
server_task.await(io)
else |_|
server_task.cancel(io);
try client_result;
try server_result;
}
test "a transport EOF without close_notify reads as a truncated stream" {
const build_options = @import("build_options");
if (!build_options.integration) return error.SkipZigTest;
@@ -682,7 +746,7 @@ test "a transport EOF without close_notify reads as a truncated stream" {
defer threaded.deinit();
const io = threaded.io();
var ctx = try ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem);
var ctx = try ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem, null);
defer ctx.deinit(gpa);
const listen_address: Io.net.IpAddress = .{ .ip4 = .loopback(0) };