Skip to main content

zerodds_bridge_security/
connection.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Connection wire-up helpers for the six bridge daemons.
5//!
6//! This layer sits between `TcpStream::accept()` and the protocol
7//! handshake (HTTP upgrade / MQTT CONNECT / GIOP Locate / HTTP/2 preface
8//! / AMQP Open / CoAP datagram) and provides:
9//!
10//! * [`serve_tls_handshake`] — wraps a `TcpStream` with
11//!   `rustls::ServerConnection` and blocks until the TLS handshake is
12//!   complete. Returns a TLS `Stream` object + extracted `AuthSubject`
13//!   (for mTLS).
14//! * [`RotatingTlsConfig`] — `Arc<RwLock<Arc<ServerConfig>>>` wrapper
15//!   for SIGHUP cert hot reload. Established connections keep their
16//!   session, new connections see the new cert.
17//! * [`build_client_tls_connector`] — counterpart for MQTT/AMQP client
18//!   daemons that connect to a broker and need TLS on the outbound
19//!   side.
20//!
21//! Spec: zerodds-{ws,mqtt,coap,amqp,grpc,corba}-bridge-1.0.md §7.1.
22
23use std::io::{Read, Write};
24use std::net::TcpStream;
25use std::path::PathBuf;
26use std::sync::{Arc, RwLock};
27use std::time::Duration;
28
29use rustls::{ClientConfig, ServerConfig, ServerConnection};
30use rustls_pki_types::ServerName;
31
32use crate::auth::AuthSubject;
33use crate::ctx::extract_mtls_subject;
34use crate::tls::{TlsConfigError, load_server_config, load_server_config_with_client_auth};
35
36/// Hot-reload-capable wrapper around `Arc<ServerConfig>`. Between reads
37/// (new connections read fresh) and writes (SIGHUP reload swaps the
38/// inner Arc), a `RwLock` is the natural form.
39///
40/// Connections that already hold an `Arc<ServerConfig>` at reload time
41/// keep their cert for the session. New connections after the reload
42/// see the new cert — this is the spec-conformant semantics (SIGHUP
43/// only rotates future sessions, no forced renegotiation).
44#[derive(Debug, Clone)]
45pub struct RotatingTlsConfig {
46    inner: Arc<RwLock<Arc<ServerConfig>>>,
47    cert_path: PathBuf,
48    key_path: PathBuf,
49    client_ca_path: Option<PathBuf>,
50}
51
52impl RotatingTlsConfig {
53    /// Initially loads cert+key (optional client CA for mTLS) and
54    /// returns a rotatable wrapper.
55    ///
56    /// # Errors
57    /// [`TlsConfigError`] on load/build error.
58    pub fn load(
59        cert_path: PathBuf,
60        key_path: PathBuf,
61        client_ca_path: Option<PathBuf>,
62    ) -> Result<Self, TlsConfigError> {
63        let cfg = match &client_ca_path {
64            Some(ca) => load_server_config_with_client_auth(&cert_path, &key_path, ca)?,
65            None => load_server_config(&cert_path, &key_path)?,
66        };
67        Ok(Self {
68            inner: Arc::new(RwLock::new(cfg)),
69            cert_path,
70            key_path,
71            client_ca_path,
72        })
73    }
74
75    /// Gets the current `Arc<ServerConfig>` for a new connection.
76    /// Block-free on the read-lock fast path; on a reload conflict
77    /// `read()` blocks briefly on the `RwLock`.
78    #[must_use]
79    pub fn current(&self) -> Arc<ServerConfig> {
80        match self.inner.read() {
81            Ok(g) => Arc::clone(&g),
82            Err(poisoned) => Arc::clone(&poisoned.into_inner()),
83        }
84    }
85
86    /// SIGHUP hook: reloads cert+key from disk and atomically swaps the
87    /// stored `Arc`.
88    ///
89    /// # Errors
90    /// [`TlsConfigError`] on load/build error. On error the existing
91    /// cert stays active (no-op).
92    pub fn reload(&self) -> Result<(), TlsConfigError> {
93        let new_cfg = match &self.client_ca_path {
94            Some(ca) => load_server_config_with_client_auth(&self.cert_path, &self.key_path, ca)?,
95            None => load_server_config(&self.cert_path, &self.key_path)?,
96        };
97        let mut g = match self.inner.write() {
98            Ok(g) => g,
99            Err(poisoned) => poisoned.into_inner(),
100        };
101        *g = new_cfg;
102        Ok(())
103    }
104}
105
106/// Drives a TLS handshake on an already-accepted `TcpStream` and
107/// returns the `ServerConnection` + extracted mTLS subject (or `None`).
108///
109/// The caller then wraps `(stream, conn)` in a `rustls::Stream` for its
110/// protocol path. The read timeout limits how long a handshake is
111/// waited for (Spec §7.1 makes no requirement — we use 5 s as a sane
112/// default).
113///
114/// # Errors
115/// `std::io::Error` on socket or handshake error.
116pub fn serve_tls_handshake(
117    cfg: Arc<ServerConfig>,
118    mut stream: TcpStream,
119    handshake_timeout: Duration,
120) -> std::io::Result<(TcpStream, ServerConnection, Option<AuthSubject>)> {
121    stream.set_read_timeout(Some(handshake_timeout))?;
122    stream.set_write_timeout(Some(handshake_timeout))?;
123    let mut conn = ServerConnection::new(cfg).map_err(|e| {
124        std::io::Error::new(std::io::ErrorKind::InvalidData, format!("rustls: {e}"))
125    })?;
126
127    // Drive the handshake to completion (or error).
128    while conn.is_handshaking() {
129        if conn.wants_write() {
130            let mut sink = TcpWriter(&mut stream);
131            conn.write_tls(&mut sink)?;
132        }
133        if conn.wants_read() {
134            let mut src = TcpReader(&mut stream);
135            let n = conn.read_tls(&mut src)?;
136            if n == 0 {
137                return Err(std::io::Error::new(
138                    std::io::ErrorKind::UnexpectedEof,
139                    "tls handshake eof",
140                ));
141            }
142            conn.process_new_packets().map_err(|e| {
143                std::io::Error::new(std::io::ErrorKind::InvalidData, format!("rustls: {e}"))
144            })?;
145        }
146    }
147    // Drain pending output (server-finished etc).
148    while conn.wants_write() {
149        let mut sink = TcpWriter(&mut stream);
150        conn.write_tls(&mut sink)?;
151    }
152
153    let mtls_subject = extract_mtls_subject(&conn);
154    Ok((stream, conn, mtls_subject))
155}
156
157/// Adapter: `&mut TcpStream` → `Read` sink for rustls.
158struct TcpReader<'a>(&'a mut TcpStream);
159impl Read for TcpReader<'_> {
160    fn read(&mut self, b: &mut [u8]) -> std::io::Result<usize> {
161        self.0.read(b)
162    }
163}
164
165/// Adapter: `&mut TcpStream` → `Write` sink for rustls.
166struct TcpWriter<'a>(&'a mut TcpStream);
167impl Write for TcpWriter<'_> {
168    fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
169        self.0.write(b)
170    }
171    fn flush(&mut self) -> std::io::Result<()> {
172        self.0.flush()
173    }
174}
175
176/// Builds a rustls `ClientConfig` for bridge clients (mqtt-bridge,
177/// amqp-endpoint) that connect outbound to a broker. The caller passes
178/// the optional client cert (mTLS) and a CA bundle for server cert
179/// validation.
180///
181/// `ca_pem_path = None` ⇒ uses OS-native roots (the `webpki-roots`
182/// fallback is not enabled here; the workspace has no
183/// `rustls-native-certs` as a dep). When `None` and the server cert
184/// cannot be validated against a supplied CA, the handshake fails —
185/// this is the secure-by-default path.
186///
187/// # Errors
188/// [`TlsConfigError`] on load/build error.
189pub fn build_client_tls_connector(
190    ca_pem_path: Option<&std::path::Path>,
191    client_cert_pem_path: Option<&std::path::Path>,
192    client_key_pem_path: Option<&std::path::Path>,
193) -> Result<Arc<ClientConfig>, TlsConfigError> {
194    use crate::tls::{read_certs, read_private_key};
195
196    let mut roots = rustls::RootCertStore::empty();
197    if let Some(ca) = ca_pem_path {
198        for c in read_certs(ca)? {
199            roots
200                .add(c)
201                .map_err(|e| TlsConfigError::Rustls(format!("ca add: {e}")))?;
202        }
203    }
204    let provider = crate::tls_provider();
205    let builder = ClientConfig::builder_with_provider(Arc::new(provider))
206        .with_safe_default_protocol_versions()
207        .map_err(|e| TlsConfigError::Rustls(format!("{e}")))?
208        .with_root_certificates(roots);
209
210    let cfg = match (client_cert_pem_path, client_key_pem_path) {
211        (Some(c), Some(k)) => {
212            let certs = read_certs(c)?;
213            let key = read_private_key(k)?;
214            builder
215                .with_client_auth_cert(certs, key)
216                .map_err(|e| TlsConfigError::Rustls(format!("client auth: {e}")))?
217        }
218        (None, None) => builder.with_no_client_auth(),
219        _ => {
220            return Err(TlsConfigError::Rustls(
221                "client cert and key must be set together".into(),
222            ));
223        }
224    };
225    Ok(Arc::new(cfg))
226}
227
228/// Validates a hostname as a rustls `ServerName` (for the
229/// client-connector side).
230///
231/// # Errors
232/// [`TlsConfigError`] if the name is not a valid DNS format.
233pub fn parse_server_name(host: &str) -> Result<ServerName<'static>, TlsConfigError> {
234    ServerName::try_from(host.to_string())
235        .map_err(|e| TlsConfigError::Rustls(format!("invalid server name '{host}': {e}")))
236}
237
238#[cfg(test)]
239#[allow(clippy::expect_used, clippy::unwrap_used)]
240mod tests {
241    use super::*;
242    #[allow(unused_imports)]
243    use std::io::Write as _;
244
245    fn write_temp(name: &str, body: &[u8]) -> PathBuf {
246        let dir =
247            std::env::temp_dir().join(format!("zd-bridge-conn-{}-{}", name, std::process::id()));
248        let _ = std::fs::create_dir_all(&dir);
249        let p = dir.join(name);
250        let mut f = std::fs::File::create(&p).unwrap();
251        f.write_all(body).unwrap();
252        p
253    }
254
255    fn gen_self_signed() -> (String, String) {
256        let ck = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
257        (ck.cert.pem(), ck.key_pair.serialize_pem())
258    }
259
260    #[test]
261    fn rotating_config_load_and_current_works() {
262        let (cert, key) = gen_self_signed();
263        let c = write_temp("rcert.pem", cert.as_bytes());
264        let k = write_temp("rkey.pem", key.as_bytes());
265        let r = RotatingTlsConfig::load(c, k, None).expect("load");
266        let cur1 = r.current();
267        let cur2 = r.current();
268        // Both reads return the same Arc identity until reload.
269        assert!(Arc::ptr_eq(&cur1, &cur2));
270    }
271
272    #[test]
273    fn rotating_config_reload_swaps_inner_arc() {
274        let (cert1, key1) = gen_self_signed();
275        let c = write_temp("rcert2.pem", cert1.as_bytes());
276        let k = write_temp("rkey2.pem", key1.as_bytes());
277        let r = RotatingTlsConfig::load(c.clone(), k.clone(), None).expect("load");
278        let before = r.current();
279        // Write a new cert into the same file.
280        let (cert2, key2) = gen_self_signed();
281        std::fs::write(&c, cert2.as_bytes()).unwrap();
282        std::fs::write(&k, key2.as_bytes()).unwrap();
283        r.reload().expect("reload");
284        let after = r.current();
285        // Reload must return a new Arc (cert data has changed).
286        assert!(!Arc::ptr_eq(&before, &after));
287    }
288
289    #[test]
290    fn rotating_config_reload_with_bad_path_keeps_old() {
291        let (cert, key) = gen_self_signed();
292        let c = write_temp("rcert3.pem", cert.as_bytes());
293        let k = write_temp("rkey3.pem", key.as_bytes());
294        let r = RotatingTlsConfig::load(c.clone(), k.clone(), None).expect("load");
295        let before = r.current();
296        // Corrupt the cert file.
297        std::fs::write(&c, b"-----BEGIN GARBAGE-----\n-----END GARBAGE-----\n").unwrap();
298        let err = r.reload().unwrap_err();
299        assert!(matches!(err, TlsConfigError::NoCertificateInPem));
300        // The old cert is still active.
301        let after = r.current();
302        assert!(Arc::ptr_eq(&before, &after));
303    }
304
305    #[test]
306    fn parse_server_name_accepts_dns_hostname() {
307        let _ = parse_server_name("example.com").expect("dns");
308    }
309
310    #[test]
311    fn parse_server_name_accepts_ip() {
312        let _ = parse_server_name("127.0.0.1").expect("ip");
313    }
314
315    #[test]
316    fn build_client_tls_connector_no_auth_succeeds() {
317        let (cert, _key) = gen_self_signed();
318        let ca = write_temp("ca.pem", cert.as_bytes());
319        let cfg = build_client_tls_connector(Some(&ca), None, None).expect("client cfg");
320        assert!(Arc::strong_count(&cfg) >= 1);
321    }
322
323    #[test]
324    fn build_client_tls_connector_with_mtls_succeeds() {
325        let (cert, key) = gen_self_signed();
326        let cap = write_temp("ca2.pem", cert.as_bytes());
327        let cp = write_temp("cli.pem", cert.as_bytes());
328        let kp = write_temp("clikey.pem", key.as_bytes());
329        let cfg = build_client_tls_connector(Some(&cap), Some(&cp), Some(&kp)).expect("mtls");
330        assert!(Arc::strong_count(&cfg) >= 1);
331    }
332
333    #[test]
334    fn build_client_tls_connector_partial_auth_rejected() {
335        let (cert, _key) = gen_self_signed();
336        let cap = write_temp("ca3.pem", cert.as_bytes());
337        let cp = write_temp("cli2.pem", cert.as_bytes());
338        let err = build_client_tls_connector(Some(&cap), Some(&cp), None).unwrap_err();
339        assert!(matches!(err, TlsConfigError::Rustls(_)));
340    }
341}