---
url: /httpx.zig/examples/tls-mtls.md
---
# 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

```zig
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 + `CertificateVerify` presentation
* 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

```zig
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

```bash
zig build run-tls-mtls
```

## What to Verify

* Trusted client: handshake completes, HTTP 200 with the expected body.
* Empty-cert client with `.required`: the request fails — the client
  observes `error.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.ClientCertificateInvalid` server-side).

## mTLS Flow

1. Server requests client certificate (CertificateRequest)
2. Client sends certificate + CertificateVerify
3. Server verifies client cert against its trust store
4. Both parties have authenticated

## Common Use Cases

* Service mesh (Istio, Linkerd)
* Kubernetes API server auth
* Database connections (PostgreSQL, MySQL)
* gRPC service-to-service
