TLS Mutual Authentication (mTLS) ​
The server requests and enforces client certificates; only clients with a trusted certificate complete the handshake. See examples/tlsMtls.zig, which runs the whole flow over loopback through the high-level API: client.get(url, .{ .tls = .{ .clientCertPem, .clientKeyPem } }) performs HTTPS over mTLS, then a cert-less request is rejected during the handshake.
Client Configuration ​
var res = try client.get(url, .{
.tls = .{
.verify = .caBundle,
.caPem = ca_pem,
.clientCertPem = cert_pem, // presented when the server asks
.clientKeyPem = key_pem, // P-256 ECDSA key for the chain
},
.timeoutMs = 15_000,
});
defer res.deinit();Setting the pair routes the request through the native TLS 1.3 client (ALPN http/1.1, or h2 with .httpVersion = .http2); without it the request keeps the std transport, which cannot present certificates.
Features Demonstrated ​
- Server-side
CertificateRequest(required / optional modes) - Client certificate +
CertificateVerifypresentation - Chain anchoring in a client CA bundle + expiry/trust validation
- Missing/untrusted/forged credential rejection (fail closed)
- mTLS use cases (service mesh, gRPC, databases)
Server Configuration ​
var listener = try httpx.tls.Listener.init(allocator, io, .{
.host = "127.0.0.1",
.port = 0,
.defaultIdentity = .{
.certChainPem = cert_pem,
.privateKeyPem = key_pem,
},
// Require every client to present a certificate chaining to ca_pem.
// Use `.optional` to allow cert-less clients through instead.
.clientAuth = .required,
.clientCaPem = ca_pem,
});
defer listener.deinit();The same fields exist one layer down: tls.Server.Config.clientAuth / clientCaPem, fed from Server.init's .tls = .{ .clientAuth = .required, .clientCaPem = ca_pem }. Presented chains are verified with verifyCertificateChain (expiry, CA-ness, anchor match; no hostname check — client certificates identify a principal, not a host), the CertificateVerify P-256 signature is checked over the live transcript, and the client Finished MAC binds everything. Malformed DER fails closed via structural validation, never a panic.
Run ​
zig build run-tls-mtlsWhat to Verify ​
- Trusted client: handshake completes, HTTP 200 with the expected body.
- Empty-cert client with
.required: the request fails — the client observeserror.ClientCertificateRequired(native handshake) or a handshake failure (std transport, which cannot present certificates), while the server side only sees the connection vanish (error.IoError/error.TlsHandshakeFailed). - Untrusted CA bundle: handshake fails (
error.ClientCertificateInvalidserver-side).
mTLS Flow ​
- Server requests client certificate (CertificateRequest)
- Client sends certificate + CertificateVerify
- Server verifies client cert against its trust store
- Both parties have authenticated
Common Use Cases ​
- Service mesh (Istio, Linkerd)
- Kubernetes API server auth
- Database connections (PostgreSQL, MySQL)
- gRPC service-to-service
