Skip to main content

waf_tls_managed/
mtls.rs

1// SPDX-FileCopyrightText: 2026 0x00spor3
2// SPDX-License-Identifier: Apache-2.0
3
4//! mTLS / managed-PKI client-certificate verification — the L2 enterprise seam
5//! (`TlsCertSource::client_verifier`). Builds a rustls `WebPkiClientVerifier` from a
6//! configured CA bundle so the listener requires and verifies client certificates.
7//!
8//! v1: roots come from a PEM file (rotation = restart, like the acceptor — the hitless
9//! path is the SERVER cert via the ACME resolver, not the client roots).
10
11use std::fmt;
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14
15use tokio_rustls::rustls::crypto::ring;
16use tokio_rustls::rustls::pki_types::pem::PemObject;
17use tokio_rustls::rustls::pki_types::CertificateDer;
18use tokio_rustls::rustls::server::danger::ClientCertVerifier;
19use tokio_rustls::rustls::server::WebPkiClientVerifier;
20use tokio_rustls::rustls::RootCertStore;
21
22/// Why building the client-cert verifier failed (all fatal at boot — a required mTLS
23/// that cannot be built must stop the proxy, never silently disable client auth).
24#[derive(Debug)]
25pub enum MtlsError {
26    Io { path: PathBuf, source: std::io::Error },
27    NoRoots(PathBuf),
28    Verifier(tokio_rustls::rustls::server::VerifierBuilderError),
29}
30
31impl fmt::Display for MtlsError {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::Io { path, source } =>
35                write!(f, "cannot read client-CA file {}: {source}", path.display()),
36            Self::NoRoots(p) =>
37                write!(f, "no usable CA certificates in {}", p.display()),
38            Self::Verifier(e) => write!(f, "client-cert verifier build error: {e}"),
39        }
40    }
41}
42
43impl std::error::Error for MtlsError {}
44
45/// Build a client-certificate verifier from a PEM file of trusted CA certificates.
46/// Uses the `ring` crypto provider explicitly (no process-global default), matching
47/// the core.
48pub fn verifier_from_ca_file(ca_path: &Path) -> Result<Arc<dyn ClientCertVerifier>, MtlsError> {
49    let data = std::fs::read(ca_path)
50        .map_err(|source| MtlsError::Io { path: ca_path.to_path_buf(), source })?;
51
52    let mut roots = RootCertStore::empty();
53    for cert in CertificateDer::pem_slice_iter(&data) {
54        let cert = cert.map_err(|e| MtlsError::Io {
55            path: ca_path.to_path_buf(),
56            source: std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()),
57        })?;
58        // A malformed entry is skipped rather than fatal; "zero usable roots" below is the
59        // real failure (an empty/garbage bundle must not silently accept every client).
60        let _ = roots.add(cert);
61    }
62    if roots.is_empty() {
63        return Err(MtlsError::NoRoots(ca_path.to_path_buf()));
64    }
65
66    WebPkiClientVerifier::builder_with_provider(Arc::new(roots), Arc::new(ring::default_provider()))
67        .build()
68        .map_err(MtlsError::Verifier)
69}