nixfleet_control_plane/
tls.rs

1//! TLS server config builder; mTLS layered via `WebPkiClientVerifier` when `client_ca_path` is set.
2
3use anyhow::{Context, Result};
4use rustls::ServerConfig;
5use rustls::server::WebPkiClientVerifier;
6use rustls_pki_types::pem::PemObject;
7use rustls_pki_types::{CertificateDer, PrivateKeyDer};
8use std::path::Path;
9use std::sync::Arc;
10
11/// LOADBEARING: `allow_unauthenticated()` is required because `/v1/enroll`
12/// cannot present a client cert (it bootstraps the agent's identity). Per-
13/// route middleware enforces auth - don't tighten the TLS layer to require
14/// client certs without first carving out enroll.
15pub fn build_server_config(
16    cert_path: &Path,
17    key_path: &Path,
18    client_ca_path: Option<&Path>,
19) -> Result<ServerConfig> {
20    let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_file_iter(cert_path)
21        .with_context(|| format!("failed to open cert: {}", cert_path.display()))?
22        .collect::<std::result::Result<Vec<_>, _>>()
23        .context("failed to parse server certificates")?;
24
25    let key = PrivateKeyDer::from_pem_file(key_path)
26        .with_context(|| format!("failed to read private key: {}", key_path.display()))?;
27
28    let builder = if let Some(ca_path) = client_ca_path {
29        let mut root_store = rustls::RootCertStore::empty();
30        for cert in CertificateDer::pem_file_iter(ca_path)
31            .with_context(|| format!("failed to open CA: {}", ca_path.display()))?
32        {
33            root_store.add(cert.context("failed to parse CA cert")?)?;
34        }
35        let verifier = WebPkiClientVerifier::builder(Arc::new(root_store))
36            .allow_unauthenticated()
37            .build()
38            .context("failed to build client verifier")?;
39        ServerConfig::builder().with_client_cert_verifier(verifier)
40    } else {
41        ServerConfig::builder().with_no_client_auth()
42    };
43
44    builder
45        .with_single_cert(certs, key)
46        .context("failed to configure server TLS")
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn build_server_config_missing_cert_fails() {
55        let result = build_server_config(
56            Path::new("/nonexistent/cert.pem"),
57            Path::new("/nonexistent/key.pem"),
58            None,
59        );
60        assert!(result.is_err());
61    }
62}