Skip to main content

ember_client/
tls.rs

1//! TLS configuration and connection helpers for ember-client.
2//!
3//! All items in this module are only compiled when the `tls` feature is
4//! enabled (which is the default). Downstream code that disables the feature
5//! can use `Client::connect` for plain TCP without pulling in rustls.
6
7use std::io;
8use std::sync::Arc;
9
10use rustls::pki_types::pem::PemObject;
11use rustls::pki_types::ServerName;
12use tokio::net::TcpStream;
13use tokio_rustls::TlsConnector;
14
15use crate::connection::Transport;
16
17/// TLS configuration for client connections.
18///
19/// Pass this to `Client::connect_tls` to establish a TLS-encrypted connection
20/// to the server.
21#[derive(Clone, Debug, Default)]
22pub struct TlsClientConfig {
23    /// Path to a custom CA certificate (PEM format) for verifying the server.
24    ///
25    /// When `None`, the platform's native root certificate store is used.
26    pub ca_cert: Option<String>,
27
28    /// Skip server certificate verification entirely.
29    ///
30    /// Emits a warning to stderr when set. Only use in development with
31    /// self-signed certificates — never in production.
32    pub insecure: bool,
33}
34
35/// Establishes a TCP connection and upgrades it to TLS.
36///
37/// Used by `Client::connect_tls`; not typically called directly.
38pub(crate) async fn connect(
39    host: &str,
40    port: u16,
41    config: &TlsClientConfig,
42) -> io::Result<Transport> {
43    let tcp = TcpStream::connect((host, port)).await?;
44    let client_config = build_client_config(config)?;
45    let connector = TlsConnector::from(Arc::new(client_config));
46
47    let server_name = ServerName::try_from(host.to_string()).map_err(|e| {
48        io::Error::new(
49            io::ErrorKind::InvalidInput,
50            format!("invalid server name '{host}': {e}"),
51        )
52    })?;
53
54    let tls_stream = connector.connect(server_name, tcp).await?;
55    Ok(Transport::Tls(Box::new(tls_stream)))
56}
57
58/// Builds a `rustls::ClientConfig` from the provided options.
59fn build_client_config(config: &TlsClientConfig) -> io::Result<rustls::ClientConfig> {
60    if config.insecure {
61        eprintln!("warning: TLS certificate verification is disabled");
62        return build_insecure_config();
63    }
64
65    let roots = load_root_certs(config.ca_cert.as_deref())?;
66
67    rustls::ClientConfig::builder()
68        .with_root_certificates(roots)
69        .with_no_client_auth()
70        .pipe_ok()
71}
72
73/// Loads root certificates from a custom CA file or the system trust store.
74fn load_root_certs(ca_cert_path: Option<&str>) -> io::Result<rustls::RootCertStore> {
75    let mut roots = rustls::RootCertStore::empty();
76
77    if let Some(path) = ca_cert_path {
78        let pem = std::fs::read(path).map_err(|e| {
79            io::Error::new(
80                io::ErrorKind::NotFound,
81                format!("failed to read CA cert '{path}': {e}"),
82            )
83        })?;
84        let certs = rustls::pki_types::CertificateDer::pem_slice_iter(&pem)
85            .collect::<Result<Vec<_>, _>>()
86            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
87
88        if certs.is_empty() {
89            return Err(io::Error::new(
90                io::ErrorKind::InvalidData,
91                format!("no certificates found in '{path}'"),
92            ));
93        }
94
95        for cert in certs {
96            roots.add(cert).map_err(|e| {
97                io::Error::new(
98                    io::ErrorKind::InvalidData,
99                    format!("invalid CA certificate: {e}"),
100                )
101            })?;
102        }
103    } else {
104        let native_certs = rustls_native_certs::load_native_certs();
105        for cert in native_certs.certs {
106            roots.add(cert).map_err(|e| {
107                io::Error::new(
108                    io::ErrorKind::InvalidData,
109                    format!("invalid native CA certificate: {e}"),
110                )
111            })?;
112        }
113    }
114
115    Ok(roots)
116}
117
118/// Builds a client config that skips all certificate verification.
119///
120/// Only safe for development/testing with self-signed certificates.
121fn build_insecure_config() -> io::Result<rustls::ClientConfig> {
122    let config = rustls::ClientConfig::builder()
123        .dangerous()
124        .with_custom_certificate_verifier(Arc::new(NoVerifier))
125        .with_no_client_auth();
126    Ok(config)
127}
128
129/// A certificate verifier that accepts anything. Used with `insecure: true`.
130#[derive(Debug)]
131struct NoVerifier;
132
133impl rustls::client::danger::ServerCertVerifier for NoVerifier {
134    fn verify_server_cert(
135        &self,
136        _end_entity: &rustls::pki_types::CertificateDer<'_>,
137        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
138        _server_name: &ServerName<'_>,
139        _ocsp_response: &[u8],
140        _now: rustls::pki_types::UnixTime,
141    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
142        Ok(rustls::client::danger::ServerCertVerified::assertion())
143    }
144
145    fn verify_tls12_signature(
146        &self,
147        _message: &[u8],
148        _cert: &rustls::pki_types::CertificateDer<'_>,
149        _dss: &rustls::DigitallySignedStruct,
150    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
151        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
152    }
153
154    fn verify_tls13_signature(
155        &self,
156        _message: &[u8],
157        _cert: &rustls::pki_types::CertificateDer<'_>,
158        _dss: &rustls::DigitallySignedStruct,
159    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
160        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
161    }
162
163    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
164        rustls::crypto::aws_lc_rs::default_provider()
165            .signature_verification_algorithms
166            .supported_schemes()
167    }
168}
169
170/// Convenience helper to wrap a value in `Ok`.
171trait PipeOk: Sized {
172    fn pipe_ok(self) -> io::Result<Self> {
173        Ok(self)
174    }
175}
176
177impl PipeOk for rustls::ClientConfig {}