Skip to main content

hyperdb_api_core/client/
tls.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! TLS configuration and connection handling.
5//!
6//! This module provides TLS support for secure connections to Hyper servers
7//! using a pure Rust TLS implementation (rustls).
8//!
9//! # Certificate Formats
10//!
11//! | Item | Format |
12//! |------|--------|
13//! | CA Cert | PEM |
14//! | Client Cert | PEM |
15//! | Client Key | PEM (PKCS#8 or PKCS#1) |
16//!
17//! - **PEM format**: Base64-encoded with `-----BEGIN/END CERTIFICATE-----` headers
18//! - **PKCS#8**: Key format with `-----BEGIN PRIVATE KEY-----` header
19//! - **PKCS#1**: RSA-specific format with `-----BEGIN RSA PRIVATE KEY-----` header
20//!
21//! # Example
22//!
23//! ```ignore
24//! use hyperdb_api_core::client::{Client, Config};
25//! use hyperdb_api_core::client::tls::TlsConfig;
26//!
27//! # fn example() -> hyperdb_api_core::client::Result<()> {
28//! let config = Config::new()
29//!     .with_host("secure-hyper.example.com")
30//!     .with_port(7484)
31//!     .with_tls(TlsConfig::default()); // TODO: with_tls not yet on Config
32//!
33//! let client = Client::connect(&config)?;
34//! # Ok(())
35//! # }
36//! ```
37
38use std::path::PathBuf;
39
40/// TLS configuration options.
41#[derive(Debug, Clone)]
42pub struct TlsConfig {
43    /// Whether to verify the server certificate.
44    pub verify_server: bool,
45    /// Path to CA certificate file (PEM format).
46    pub ca_cert_path: Option<PathBuf>,
47    /// Path to client certificate file (PEM format).
48    pub client_cert_path: Option<PathBuf>,
49    /// Path to client key file (PEM format).
50    pub client_key_path: Option<PathBuf>,
51    /// Server name for SNI (Server Name Indication).
52    pub server_name: Option<String>,
53}
54
55impl Default for TlsConfig {
56    fn default() -> Self {
57        TlsConfig {
58            verify_server: true,
59            ca_cert_path: None,
60            client_cert_path: None,
61            client_key_path: None,
62            server_name: None,
63        }
64    }
65}
66
67impl TlsConfig {
68    /// Creates a new TLS configuration with default settings.
69    #[must_use]
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    #[must_use]
75    /// Disables server certificate verification.
76    ///
77    /// # Security Warning
78    ///
79    /// **This is a serious security risk in production environments.**
80    /// Disabling certificate verification allows man-in-the-middle attacks.
81    ///
82    /// Only use this for:
83    /// - Local development with self-signed certificates
84    /// - Testing environments
85    /// - Debugging certificate issues (temporarily)
86    ///
87    /// A warning will be logged at runtime when this is enabled.
88    pub fn danger_accept_invalid_certs(mut self) -> Self {
89        tracing::warn!(
90            "TLS certificate verification disabled - this should only be used for testing. \
91             Man-in-the-middle attacks are possible."
92        );
93
94        self.verify_server = false;
95        self
96    }
97
98    #[must_use]
99    /// Sets the CA certificate file for verifying the server.
100    pub fn ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
101        self.ca_cert_path = Some(path.into());
102        self
103    }
104
105    #[must_use]
106    /// Sets the client certificate for mutual TLS.
107    pub fn client_cert(
108        mut self,
109        cert_path: impl Into<PathBuf>,
110        key_path: impl Into<PathBuf>,
111    ) -> Self {
112        self.client_cert_path = Some(cert_path.into());
113        self.client_key_path = Some(key_path.into());
114        self
115    }
116
117    #[must_use]
118    /// Sets the server name for SNI.
119    pub fn server_name(mut self, name: impl Into<String>) -> Self {
120        self.server_name = Some(name.into());
121        self
122    }
123
124    /// Returns true if client certificates are configured.
125    #[must_use]
126    pub fn has_client_cert(&self) -> bool {
127        self.client_cert_path.is_some() && self.client_key_path.is_some()
128    }
129}
130
131/// TLS mode for the connection.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum TlsMode {
134    /// No TLS (plain TCP).
135    #[default]
136    Disable,
137    /// Prefer TLS if available, fall back to plain TCP.
138    Prefer,
139    /// Require TLS, fail if not available.
140    Require,
141    /// Require TLS and verify the server certificate.
142    VerifyCA,
143    /// Require TLS, verify server certificate, and verify hostname.
144    VerifyFull,
145}
146
147impl TlsMode {
148    /// Returns true if TLS is enabled.
149    #[must_use]
150    pub fn is_enabled(&self) -> bool {
151        !matches!(self, TlsMode::Disable)
152    }
153
154    /// Returns true if TLS is required (not just preferred).
155    #[must_use]
156    pub fn is_required(&self) -> bool {
157        matches!(
158            self,
159            TlsMode::Require | TlsMode::VerifyCA | TlsMode::VerifyFull
160        )
161    }
162
163    /// Returns true if server certificate verification is required.
164    #[must_use]
165    pub fn verify_server(&self) -> bool {
166        matches!(self, TlsMode::VerifyCA | TlsMode::VerifyFull)
167    }
168
169    /// Returns true if hostname verification is required.
170    #[must_use]
171    pub fn verify_hostname(&self) -> bool {
172        matches!(self, TlsMode::VerifyFull)
173    }
174}
175
176/// TLS implementation using the `rustls` crate.
177pub mod rustls_impl {
178    use super::TlsConfig;
179    use std::sync::Arc;
180
181    use rustls::pki_types::pem::PemObject;
182    use rustls::pki_types::{CertificateDer, PrivateKeyDer};
183    use tokio::net::TcpStream;
184    use tokio_rustls::rustls::{ClientConfig, RootCertStore};
185    use tokio_rustls::TlsConnector;
186
187    use crate::client::error::{Error, ErrorKind, Result};
188
189    /// Creates a TLS connector from the configuration.
190    ///
191    /// # Errors
192    ///
193    /// Returns [`ErrorKind::Config`] when:
194    /// - The CA cert path is set but cannot be opened or the PEM bytes
195    ///   cannot be parsed / added to the root store.
196    /// - The client cert / key path is set but cannot be opened, the
197    ///   PEM payload is invalid, the key section is missing, or
198    ///   `rustls` rejects the cert/key pair.
199    /// - Rustls cannot build a `ClientConfig` with the configured
200    ///   protocol versions.
201    ///
202    /// # Panics
203    ///
204    /// Does not panic in practice. The `client_cert_path` and
205    /// `client_key_path` `.unwrap()` calls are guarded by the
206    /// preceding [`TlsConfig::has_client_cert`] check, which only
207    /// returns `true` when both paths are `Some`.
208    pub fn create_connector(config: &TlsConfig, _host: &str) -> Result<TlsConnector> {
209        let mut root_store = RootCertStore::empty();
210
211        // Add system root certificates
212        root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
213
214        // Add custom CA certificate if provided
215        if let Some(ref ca_path) = config.ca_cert_path {
216            let certs = CertificateDer::pem_file_iter(ca_path)
217                .map_err(|e| Error::new(ErrorKind::Config, format!("failed to read CA cert: {e}")))?
218                .collect::<std::result::Result<Vec<_>, _>>()
219                .map_err(|e| Error::new(ErrorKind::Config, format!("invalid CA cert: {e}")))?;
220            for cert in certs {
221                root_store.add(cert).map_err(|e| {
222                    Error::new(ErrorKind::Config, format!("failed to add CA cert: {e}"))
223                })?;
224            }
225        }
226
227        let provider = Arc::new(rustls::crypto::ring::default_provider());
228        let builder = ClientConfig::builder_with_provider(provider)
229            .with_safe_default_protocol_versions()
230            .map_err(|e| Error::new(ErrorKind::Config, format!("TLS protocol config error: {e}")))?
231            .with_root_certificates(root_store);
232
233        let client_config = if config.has_client_cert() {
234            // Load client certificate and key
235            let cert_path = config.client_cert_path.as_ref().unwrap();
236            let key_path = config.client_key_path.as_ref().unwrap();
237
238            let certs = CertificateDer::pem_file_iter(cert_path)
239                .map_err(|e| {
240                    Error::new(
241                        ErrorKind::Config,
242                        format!("failed to read client cert: {e}"),
243                    )
244                })?
245                .collect::<std::result::Result<Vec<_>, _>>()
246                .map_err(|e| Error::new(ErrorKind::Config, format!("invalid client cert: {e}")))?;
247
248            // `from_pem_file` returns `Error::NoItemsFound` when the file is
249            // syntactically valid PEM but contains no private-key section, so
250            // we don't need a separate "no private key found" branch.
251            let key = PrivateKeyDer::from_pem_file(key_path)
252                .map_err(|e| Error::new(ErrorKind::Config, format!("invalid client key: {e}")))?;
253
254            builder
255                .with_client_auth_cert(certs, key)
256                .map_err(|e| Error::new(ErrorKind::Config, format!("invalid client auth: {e}")))?
257        } else {
258            builder.with_no_client_auth()
259        };
260
261        Ok(TlsConnector::from(Arc::new(client_config)))
262    }
263
264    /// Type alias for a TLS-wrapped TCP stream.
265    pub type TlsStream = tokio_rustls::client::TlsStream<TcpStream>;
266
267    /// Wraps a TCP stream with TLS.
268    ///
269    /// # Errors
270    ///
271    /// - Returns [`ErrorKind::Config`] if `server_name` is not a
272    ///   valid DNS name or IP literal accepted by `rustls`.
273    /// - Returns [`ErrorKind::Connection`] if the TLS handshake with
274    ///   the peer fails (certificate rejected, protocol error, I/O
275    ///   failure).
276    pub async fn wrap_stream(
277        stream: TcpStream,
278        connector: &TlsConnector,
279        server_name: &str,
280    ) -> Result<TlsStream> {
281        let domain = rustls::pki_types::ServerName::try_from(server_name.to_string())
282            .map_err(|_| Error::new(ErrorKind::Config, "invalid server name"))?;
283
284        connector
285            .connect(domain, stream)
286            .await
287            .map_err(|e| Error::new(ErrorKind::Connection, format!("TLS handshake failed: {e}")))
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_tls_config_default() {
297        let config = TlsConfig::default();
298        assert!(config.verify_server);
299        assert!(config.ca_cert_path.is_none());
300        assert!(!config.has_client_cert());
301    }
302
303    #[test]
304    fn test_tls_config_builder() {
305        let config = TlsConfig::new()
306            .ca_cert("/path/to/ca.pem")
307            .client_cert("/path/to/cert.pem", "/path/to/key.pem")
308            .server_name("example.com");
309
310        assert!(config.has_client_cert());
311        assert_eq!(config.server_name, Some("example.com".to_string()));
312    }
313
314    #[test]
315    fn test_tls_mode() {
316        assert!(!TlsMode::Disable.is_enabled());
317        assert!(TlsMode::Require.is_required());
318        assert!(TlsMode::VerifyFull.verify_hostname());
319    }
320}