Skip to main content

thunder/
tls.rs

1//! Optional TLS transport (SPEC-008 CAN-020, SRV-040, FR-29).
2//!
3//! TLS is an **additive, off-by-default** capability: the plaintext path is
4//! untouched and carries no rustls dependency unless the crate is built with
5//! `--features tls`. There is **no STARTTLS** — TLS is decided at connect time,
6//! before any Thunder frame is exchanged, so the wire codec never sees the
7//! difference between a plaintext and an encrypted byte stream.
8//!
9//! The config types ([`ServerTls`], [`ClientTls`]) are plain data and always
10//! compile, so an application can carry TLS settings regardless of the feature;
11//! only the rustls acceptor/connector builders below are feature-gated. A
12//! deployment that sets TLS config without the `tls` feature is refused at
13//! connect time with a clear error rather than silently running plaintext.
14
15use std::path::PathBuf;
16
17/// Server-side TLS material (SRV-040). Presence of this on the listener config
18/// turns TLS on for that deployment; absence keeps it plaintext.
19#[derive(Clone, Debug)]
20pub struct ServerTls {
21    /// PEM certificate chain path.
22    pub cert_path: PathBuf,
23    /// PEM private key path (PKCS#8 / RSA / SEC1).
24    pub key_path: PathBuf,
25}
26
27/// Client-side TLS material (FR-29). Presence of this on the client config
28/// makes the client dial TLS; absence keeps it plaintext.
29#[derive(Clone, Debug, Default)]
30pub struct ClientTls {
31    /// Name to verify the server certificate against (SNI). When `None`, the
32    /// endpoint host is used.
33    pub server_name: Option<String>,
34    /// A PEM file of trusted root(s) to pin. When `None`, the platform's
35    /// native root store is used.
36    pub ca_path: Option<PathBuf>,
37}
38
39#[cfg(feature = "tls")]
40mod imp {
41    use std::fs::File;
42    use std::io::BufReader;
43    use std::sync::Arc;
44
45    use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
46    use tokio_rustls::rustls::{self, ClientConfig, RootCertStore, ServerConfig};
47    use tokio_rustls::{TlsAcceptor, TlsConnector};
48
49    use super::{ClientTls, ServerTls};
50
51    fn provider() -> Arc<rustls::crypto::CryptoProvider> {
52        Arc::new(rustls::crypto::ring::default_provider())
53    }
54
55    fn load_certs(path: &std::path::Path) -> Result<Vec<CertificateDer<'static>>, String> {
56        let mut reader = BufReader::new(
57            File::open(path).map_err(|e| format!("open cert {}: {e}", path.display()))?,
58        );
59        rustls_pemfile::certs(&mut reader)
60            .collect::<Result<Vec<_>, _>>()
61            .map_err(|e| format!("read certs {}: {e}", path.display()))
62    }
63
64    fn load_key(path: &std::path::Path) -> Result<PrivateKeyDer<'static>, String> {
65        let mut reader = BufReader::new(
66            File::open(path).map_err(|e| format!("open key {}: {e}", path.display()))?,
67        );
68        rustls_pemfile::private_key(&mut reader)
69            .map_err(|e| format!("read key {}: {e}", path.display()))?
70            .ok_or_else(|| format!("no private key in {}", path.display()))
71    }
72
73    /// Build the server's `TlsAcceptor` from its cert/key (SRV-040). No client
74    /// auth (mTLS is a later, additive capability).
75    pub fn build_acceptor(cfg: &ServerTls) -> Result<TlsAcceptor, String> {
76        let certs = load_certs(&cfg.cert_path)?;
77        let key = load_key(&cfg.key_path)?;
78        let config = ServerConfig::builder_with_provider(provider())
79            .with_safe_default_protocol_versions()
80            .map_err(|e| format!("rustls protocol versions: {e}"))?
81            .with_no_client_auth()
82            .with_single_cert(certs, key)
83            .map_err(|e| format!("server cert/key: {e}"))?;
84        Ok(TlsAcceptor::from(Arc::new(config)))
85    }
86
87    /// Build the client's `TlsConnector` (FR-29): pin the configured CA, or fall
88    /// back to the platform's native root store.
89    pub fn build_connector(cfg: &ClientTls) -> Result<TlsConnector, String> {
90        let mut roots = RootCertStore::empty();
91        match &cfg.ca_path {
92            Some(path) => {
93                for cert in load_certs(path)? {
94                    roots
95                        .add(cert)
96                        .map_err(|e| format!("add CA from {}: {e}", path.display()))?;
97                }
98            }
99            None => {
100                let loaded = rustls_native_certs::load_native_certs();
101                if roots.is_empty() && loaded.certs.is_empty() {
102                    return Err(format!(
103                        "no native root certificates available ({} load error(s))",
104                        loaded.errors.len()
105                    ));
106                }
107                for cert in loaded.certs {
108                    let _ = roots.add(cert);
109                }
110            }
111        }
112        let config = ClientConfig::builder_with_provider(provider())
113            .with_safe_default_protocol_versions()
114            .map_err(|e| format!("rustls protocol versions: {e}"))?
115            .with_root_certificates(roots)
116            .with_no_client_auth();
117        Ok(TlsConnector::from(Arc::new(config)))
118    }
119
120    /// The SNI / verification name: the configured `server_name`, else `host`.
121    pub fn server_name(cfg: &ClientTls, host: &str) -> Result<ServerName<'static>, String> {
122        let name = cfg.server_name.clone().unwrap_or_else(|| host.to_owned());
123        ServerName::try_from(name).map_err(|e| format!("invalid TLS server name: {e}"))
124    }
125}
126
127#[cfg(feature = "tls")]
128pub use imp::{build_acceptor, build_connector, server_name};