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//! Eingangspunkt: [`load_server_config`] nimmt PEM-Cert-Pfad +
7//! PEM-Key-Pfad und liefert ein `Arc<rustls::ServerConfig>`. Das wird
8//! pro Daemon entweder beim Start oder im SIGHUP-Reload-Pfad
9//! aufgerufen.
10//!
11//! Cipher-Suites: rustls-Default (TLS 1.3 + TLS 1.2 mit AEAD-Suiten).
12//! Spec §7.1: TLS 1.2 minimum, TLS 1.3 bevorzugt — passt.
13
14use std::fs::File;
15use std::io::BufReader;
16use std::path::Path;
17use std::sync::Arc;
18
19use rustls::ServerConfig;
20use rustls_pemfile::Item;
21use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
22
23/// Fehler beim Aufbau einer `ServerConfig`.
24#[derive(Debug)]
25pub enum TlsConfigError {
26    /// Cert-File-Read schlug fehl.
27    CertFileRead(String),
28    /// Key-File-Read schlug fehl.
29    KeyFileRead(String),
30    /// Cert-PEM enthielt keine Cert-Sektion.
31    NoCertificateInPem,
32    /// Key-PEM enthielt keinen unterstützten Private-Key.
33    NoSupportedPrivateKeyInPem,
34    /// rustls-internal: ServerConfig-Build fehlgeschlagen
35    /// (z.B. Cert/Key-Mismatch).
36    Rustls(String),
37}
38
39impl core::fmt::Display for TlsConfigError {
40    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
41        match self {
42            Self::CertFileRead(m) => write!(f, "cert file read: {m}"),
43            Self::KeyFileRead(m) => write!(f, "key file read: {m}"),
44            Self::NoCertificateInPem => f.write_str("PEM had no CERTIFICATE block"),
45            Self::NoSupportedPrivateKeyInPem => {
46                f.write_str("PEM had no PKCS#8 / RSA / EC private key")
47            }
48            Self::Rustls(m) => write!(f, "rustls build: {m}"),
49        }
50    }
51}
52
53impl std::error::Error for TlsConfigError {}
54
55/// Lädt ein PEM-Cert + PEM-Key und baut eine `ServerConfig` ohne
56/// Client-Auth (no-mTLS-Default; mTLS wird über
57/// [`load_server_config_with_client_auth`] aktiviert).
58///
59/// # Errors
60/// [`TlsConfigError`] bei IO-, PEM-Parse- oder Build-Fehler.
61pub fn load_server_config(
62    cert_pem_path: &Path,
63    key_pem_path: &Path,
64) -> Result<Arc<ServerConfig>, TlsConfigError> {
65    let certs = read_certs(cert_pem_path)?;
66    let key = read_private_key(key_pem_path)?;
67    let provider = rustls::crypto::ring::default_provider();
68    let cfg = ServerConfig::builder_with_provider(Arc::new(provider))
69        .with_safe_default_protocol_versions()
70        .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?
71        .with_no_client_auth()
72        .with_single_cert(certs, key)
73        .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?;
74    Ok(Arc::new(cfg))
75}
76
77/// Wie [`load_server_config`], aber mit Client-Cert-Auth (mTLS).
78///
79/// `client_ca_pem_path` ist eine PEM-Datei mit ein oder mehr Root-Certs,
80/// die als Trust-Anchor für Client-Cert-Validation dienen. Spec §7.2:
81/// im `mtls`-Auth-Mode darf nur ein TLS-Handshake erfolgreich enden,
82/// dessen Client-Cert in dieser CA-Chain validiert.
83///
84/// # Errors
85/// [`TlsConfigError`] bei IO-, PEM- oder Build-Fehler.
86pub fn load_server_config_with_client_auth(
87    cert_pem_path: &Path,
88    key_pem_path: &Path,
89    client_ca_pem_path: &Path,
90) -> Result<Arc<ServerConfig>, TlsConfigError> {
91    let certs = read_certs(cert_pem_path)?;
92    let key = read_private_key(key_pem_path)?;
93    let client_cas = read_certs(client_ca_pem_path)?;
94
95    let mut roots = rustls::RootCertStore::empty();
96    for c in client_cas {
97        roots
98            .add(c)
99            .map_err(|e| TlsConfigError::Rustls(format!("client CA add: {e}")))?;
100    }
101    let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots))
102        .build()
103        .map_err(|e| TlsConfigError::Rustls(format!("client verifier: {e}")))?;
104
105    let provider = rustls::crypto::ring::default_provider();
106    let cfg = ServerConfig::builder_with_provider(Arc::new(provider))
107        .with_safe_default_protocol_versions()
108        .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?
109        .with_client_cert_verifier(verifier)
110        .with_single_cert(certs, key)
111        .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?;
112    Ok(Arc::new(cfg))
113}
114
115pub(crate) fn read_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>, TlsConfigError> {
116    let f = File::open(path)
117        .map_err(|e| TlsConfigError::CertFileRead(format!("{}: {e}", path.display())))?;
118    let mut br = BufReader::new(f);
119    let mut out = Vec::new();
120    for item in rustls_pemfile::read_all(&mut br) {
121        let item = item.map_err(|e| TlsConfigError::CertFileRead(format!("{e}")))?;
122        if let Item::X509Certificate(d) = item {
123            out.push(d);
124        }
125    }
126    if out.is_empty() {
127        return Err(TlsConfigError::NoCertificateInPem);
128    }
129    Ok(out)
130}
131
132pub(crate) fn read_private_key(path: &Path) -> Result<PrivateKeyDer<'static>, TlsConfigError> {
133    let f = File::open(path)
134        .map_err(|e| TlsConfigError::KeyFileRead(format!("{}: {e}", path.display())))?;
135    let mut br = BufReader::new(f);
136    for item in rustls_pemfile::read_all(&mut br) {
137        let item = item.map_err(|e| TlsConfigError::KeyFileRead(format!("{e}")))?;
138        match item {
139            Item::Pkcs8Key(k) => return Ok(PrivateKeyDer::Pkcs8(k)),
140            Item::Pkcs1Key(k) => return Ok(PrivateKeyDer::Pkcs1(k)),
141            Item::Sec1Key(k) => return Ok(PrivateKeyDer::Sec1(k)),
142            _ => {}
143        }
144    }
145    let _ = PrivatePkcs8KeyDer::from(Vec::<u8>::new()); // touch type so rustdoc resolves the import even if unused
146    Err(TlsConfigError::NoSupportedPrivateKeyInPem)
147}
148
149#[cfg(test)]
150#[allow(clippy::expect_used, clippy::unwrap_used)]
151mod tests {
152    use super::*;
153    use std::io::Write;
154
155    fn write_temp(name: &str, body: &[u8]) -> std::path::PathBuf {
156        let dir =
157            std::env::temp_dir().join(format!("zd-bridge-sec-{}-{}", name, std::process::id()));
158        let _ = std::fs::create_dir_all(&dir);
159        let p = dir.join(name);
160        let mut f = std::fs::File::create(&p).unwrap();
161        f.write_all(body).unwrap();
162        p
163    }
164
165    fn gen_self_signed() -> (String, String) {
166        let ck = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
167        (ck.cert.pem(), ck.key_pair.serialize_pem())
168    }
169
170    #[test]
171    fn load_self_signed_cert_succeeds() {
172        let (cert_pem, key_pem) = gen_self_signed();
173        let c = write_temp("cert.pem", cert_pem.as_bytes());
174        let k = write_temp("key.pem", key_pem.as_bytes());
175        let cfg = load_server_config(&c, &k).expect("ServerConfig");
176        // Smoke: rustls validiert Cert/Key intern beim with_single_cert-Aufruf.
177        assert!(Arc::strong_count(&cfg) >= 1);
178    }
179
180    #[test]
181    fn missing_cert_file_returns_err() {
182        let p = std::path::PathBuf::from("/no/such/file.pem");
183        let err = load_server_config(&p, &p).unwrap_err();
184        assert!(matches!(err, TlsConfigError::CertFileRead(_)));
185    }
186
187    #[test]
188    fn empty_pem_rejected_as_no_cert() {
189        let c = write_temp(
190            "empty.pem",
191            b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n",
192        );
193        let k = c.clone();
194        let err = load_server_config(&c, &k).unwrap_err();
195        assert!(matches!(err, TlsConfigError::NoCertificateInPem));
196    }
197
198    #[test]
199    fn key_pem_without_supported_block_rejected() {
200        let (cert_pem, _) = gen_self_signed();
201        let c = write_temp("c2.pem", cert_pem.as_bytes());
202        let k = write_temp(
203            "k2.pem",
204            b"-----BEGIN GARBAGE-----\nXX\n-----END GARBAGE-----\n",
205        );
206        let err = load_server_config(&c, &k).unwrap_err();
207        assert!(matches!(err, TlsConfigError::NoSupportedPrivateKeyInPem));
208    }
209
210    #[test]
211    fn mtls_config_loads_with_client_ca() {
212        let (cert_pem, key_pem) = gen_self_signed();
213        let c = write_temp("c3.pem", cert_pem.as_bytes());
214        let k = write_temp("k3.pem", key_pem.as_bytes());
215        // Re-use server cert as the client-CA bundle for the test —
216        // the API only checks DER-decode + chain-build, not trust path.
217        let ca = write_temp("ca.pem", cert_pem.as_bytes());
218        let cfg = load_server_config_with_client_auth(&c, &k, &ca).expect("mtls cfg");
219        assert!(Arc::strong_count(&cfg) >= 1);
220    }
221}