Skip to main content

zerodds_bridge_security/
tls.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! §7.1 TLS — `rustls 0.23` ServerConfig builder.
5//!
6//! Entry point: [`load_server_config`] takes a PEM cert path + PEM key
7//! path and returns an `Arc<rustls::ServerConfig>`. This is called per
8//! daemon either at startup or in the SIGHUP reload path.
9//!
10//! Cipher suites: rustls default (TLS 1.3 + TLS 1.2 with AEAD suites).
11//! Spec §7.1: TLS 1.2 minimum, TLS 1.3 preferred — matches.
12
13use std::fs::File;
14use std::io::BufReader;
15use std::path::Path;
16use std::sync::Arc;
17
18use rustls::ServerConfig;
19use rustls_pemfile::Item;
20use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
21
22/// Error while building a `ServerConfig`.
23#[derive(Debug)]
24pub enum TlsConfigError {
25    /// Cert file read failed.
26    CertFileRead(String),
27    /// Key file read failed.
28    KeyFileRead(String),
29    /// Cert PEM contained no certificate section.
30    NoCertificateInPem,
31    /// Key PEM contained no supported private key.
32    NoSupportedPrivateKeyInPem,
33    /// rustls-internal: ServerConfig build failed
34    /// (e.g. cert/key mismatch).
35    Rustls(String),
36}
37
38impl core::fmt::Display for TlsConfigError {
39    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
40        match self {
41            Self::CertFileRead(m) => write!(f, "cert file read: {m}"),
42            Self::KeyFileRead(m) => write!(f, "key file read: {m}"),
43            Self::NoCertificateInPem => f.write_str("PEM had no CERTIFICATE block"),
44            Self::NoSupportedPrivateKeyInPem => {
45                f.write_str("PEM had no PKCS#8 / RSA / EC private key")
46            }
47            Self::Rustls(m) => write!(f, "rustls build: {m}"),
48        }
49    }
50}
51
52impl std::error::Error for TlsConfigError {}
53
54/// Loads a PEM cert + PEM key and builds a `ServerConfig` without
55/// client auth (no-mTLS default; mTLS is enabled via
56/// [`load_server_config_with_client_auth`]).
57///
58/// # Errors
59/// [`TlsConfigError`] on IO, PEM-parse, or build error.
60pub fn load_server_config(
61    cert_pem_path: &Path,
62    key_pem_path: &Path,
63) -> Result<Arc<ServerConfig>, TlsConfigError> {
64    let certs = read_certs(cert_pem_path)?;
65    let key = read_private_key(key_pem_path)?;
66    let provider = crate::tls_provider();
67    let cfg = ServerConfig::builder_with_provider(Arc::new(provider))
68        .with_safe_default_protocol_versions()
69        .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?
70        .with_no_client_auth()
71        .with_single_cert(certs, key)
72        .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?;
73    Ok(Arc::new(cfg))
74}
75
76/// Like [`load_server_config`], but with client cert auth (mTLS).
77///
78/// `client_ca_pem_path` is a PEM file with one or more root certs that
79/// serve as the trust anchor for client cert validation. Spec §7.2: in
80/// `mtls` auth mode, only a TLS handshake whose client cert validates
81/// against this CA chain may complete successfully.
82///
83/// # Errors
84/// [`TlsConfigError`] on IO, PEM, or build error.
85pub fn load_server_config_with_client_auth(
86    cert_pem_path: &Path,
87    key_pem_path: &Path,
88    client_ca_pem_path: &Path,
89) -> Result<Arc<ServerConfig>, TlsConfigError> {
90    let certs = read_certs(cert_pem_path)?;
91    let key = read_private_key(key_pem_path)?;
92    let client_cas = read_certs(client_ca_pem_path)?;
93
94    let mut roots = rustls::RootCertStore::empty();
95    for c in client_cas {
96        roots
97            .add(c)
98            .map_err(|e| TlsConfigError::Rustls(format!("client CA add: {e}")))?;
99    }
100    // The client-cert verifier MUST be built with the explicit provider too —
101    // its default builder pulls the *process-level* CryptoProvider, which is
102    // ambiguous (panics) when rustls carries both ring and aws-lc-rs.
103    let provider = Arc::new(crate::tls_provider());
104    let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(
105        Arc::new(roots),
106        provider.clone(),
107    )
108    .build()
109    .map_err(|e| TlsConfigError::Rustls(format!("client verifier: {e}")))?;
110
111    let cfg = ServerConfig::builder_with_provider(provider)
112        .with_safe_default_protocol_versions()
113        .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?
114        .with_client_cert_verifier(verifier)
115        .with_single_cert(certs, key)
116        .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?;
117    Ok(Arc::new(cfg))
118}
119
120pub(crate) fn read_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>, TlsConfigError> {
121    let f = File::open(path)
122        .map_err(|e| TlsConfigError::CertFileRead(format!("{}: {e}", path.display())))?;
123    let mut br = BufReader::new(f);
124    let mut out = Vec::new();
125    for item in rustls_pemfile::read_all(&mut br) {
126        let item = item.map_err(|e| TlsConfigError::CertFileRead(format!("{e}")))?;
127        if let Item::X509Certificate(d) = item {
128            out.push(d);
129        }
130    }
131    if out.is_empty() {
132        return Err(TlsConfigError::NoCertificateInPem);
133    }
134    Ok(out)
135}
136
137pub(crate) fn read_private_key(path: &Path) -> Result<PrivateKeyDer<'static>, TlsConfigError> {
138    let f = File::open(path)
139        .map_err(|e| TlsConfigError::KeyFileRead(format!("{}: {e}", path.display())))?;
140    let mut br = BufReader::new(f);
141    for item in rustls_pemfile::read_all(&mut br) {
142        let item = item.map_err(|e| TlsConfigError::KeyFileRead(format!("{e}")))?;
143        match item {
144            Item::Pkcs8Key(k) => return Ok(PrivateKeyDer::Pkcs8(k)),
145            Item::Pkcs1Key(k) => return Ok(PrivateKeyDer::Pkcs1(k)),
146            Item::Sec1Key(k) => return Ok(PrivateKeyDer::Sec1(k)),
147            _ => {}
148        }
149    }
150    let _ = PrivatePkcs8KeyDer::from(Vec::<u8>::new()); // touch type so rustdoc resolves the import even if unused
151    Err(TlsConfigError::NoSupportedPrivateKeyInPem)
152}
153
154#[cfg(test)]
155#[allow(clippy::expect_used, clippy::unwrap_used)]
156mod tests {
157    use super::*;
158    use std::io::Write;
159
160    fn write_temp(name: &str, body: &[u8]) -> std::path::PathBuf {
161        let dir =
162            std::env::temp_dir().join(format!("zd-bridge-sec-{}-{}", name, std::process::id()));
163        let _ = std::fs::create_dir_all(&dir);
164        let p = dir.join(name);
165        let mut f = std::fs::File::create(&p).unwrap();
166        f.write_all(body).unwrap();
167        p
168    }
169
170    fn gen_self_signed() -> (String, String) {
171        let ck = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
172        (ck.cert.pem(), ck.key_pair.serialize_pem())
173    }
174
175    #[test]
176    fn load_self_signed_cert_succeeds() {
177        let (cert_pem, key_pem) = gen_self_signed();
178        let c = write_temp("cert.pem", cert_pem.as_bytes());
179        let k = write_temp("key.pem", key_pem.as_bytes());
180        let cfg = load_server_config(&c, &k).expect("ServerConfig");
181        // Smoke: rustls validates cert/key internally in the with_single_cert call.
182        assert!(Arc::strong_count(&cfg) >= 1);
183    }
184
185    #[test]
186    fn missing_cert_file_returns_err() {
187        let p = std::path::PathBuf::from("/no/such/file.pem");
188        let err = load_server_config(&p, &p).unwrap_err();
189        assert!(matches!(err, TlsConfigError::CertFileRead(_)));
190    }
191
192    #[test]
193    fn empty_pem_rejected_as_no_cert() {
194        let c = write_temp(
195            "empty.pem",
196            b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n",
197        );
198        let k = c.clone();
199        let err = load_server_config(&c, &k).unwrap_err();
200        assert!(matches!(err, TlsConfigError::NoCertificateInPem));
201    }
202
203    #[test]
204    fn key_pem_without_supported_block_rejected() {
205        let (cert_pem, _) = gen_self_signed();
206        let c = write_temp("c2.pem", cert_pem.as_bytes());
207        let k = write_temp(
208            "k2.pem",
209            b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n",
210        );
211        let err = load_server_config(&c, &k).unwrap_err();
212        assert!(matches!(err, TlsConfigError::NoSupportedPrivateKeyInPem));
213    }
214
215    #[test]
216    fn mtls_config_loads_with_client_ca() {
217        let (cert_pem, key_pem) = gen_self_signed();
218        let c = write_temp("c3.pem", cert_pem.as_bytes());
219        let k = write_temp("k3.pem", key_pem.as_bytes());
220        // Re-use server cert as the client-CA bundle for the test —
221        // the API only checks DER-decode + chain-build, not trust path.
222        let ca = write_temp("ca.pem", cert_pem.as_bytes());
223        let cfg = load_server_config_with_client_auth(&c, &k, &ca).expect("mtls cfg");
224        assert!(Arc::strong_count(&cfg) >= 1);
225    }
226}