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::TlsConnector;
185 use tokio_rustls::rustls::{ClientConfig, RootCertStore};
186
187 use crate::client::error::{Error, Result};
188
189 /// Creates a TLS connector from the configuration.
190 ///
191 /// # Errors
192 ///
193 /// Returns [`Error::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::config(format!("failed to read CA cert: {e}")))?
218 .collect::<std::result::Result<Vec<_>, _>>()
219 .map_err(|e| Error::config(format!("invalid CA cert: {e}")))?;
220 for cert in certs {
221 root_store
222 .add(cert)
223 .map_err(|e| Error::config(format!("failed to add CA cert: {e}")))?;
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::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| Error::config(format!("failed to read client cert: {e}")))?
240 .collect::<std::result::Result<Vec<_>, _>>()
241 .map_err(|e| Error::config(format!("invalid client cert: {e}")))?;
242
243 // `from_pem_file` returns `Error::NoItemsFound` when the file is
244 // syntactically valid PEM but contains no private-key section, so
245 // we don't need a separate "no private key found" branch.
246 let key = PrivateKeyDer::from_pem_file(key_path)
247 .map_err(|e| Error::config(format!("invalid client key: {e}")))?;
248
249 builder
250 .with_client_auth_cert(certs, key)
251 .map_err(|e| Error::config(format!("invalid client auth: {e}")))?
252 } else {
253 builder.with_no_client_auth()
254 };
255
256 Ok(TlsConnector::from(Arc::new(client_config)))
257 }
258
259 /// Type alias for a TLS-wrapped TCP stream.
260 pub type TlsStream = tokio_rustls::client::TlsStream<TcpStream>;
261
262 /// Wraps a TCP stream with TLS.
263 ///
264 /// # Errors
265 ///
266 /// - Returns [`Error::Config`] if `server_name` is not a
267 /// valid DNS name or IP literal accepted by `rustls`.
268 /// - Returns [`Error::Connection`] if the TLS handshake with
269 /// the peer fails (certificate rejected, protocol error, I/O
270 /// failure).
271 pub async fn wrap_stream(
272 stream: TcpStream,
273 connector: &TlsConnector,
274 server_name: &str,
275 ) -> Result<TlsStream> {
276 let domain = rustls::pki_types::ServerName::try_from(server_name.to_string())
277 .map_err(|_| Error::config("invalid server name"))?;
278
279 connector
280 .connect(domain, stream)
281 .await
282 .map_err(|e| Error::connection(format!("TLS handshake failed: {e}")))
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn test_tls_config_default() {
292 let config = TlsConfig::default();
293 assert!(config.verify_server);
294 assert!(config.ca_cert_path.is_none());
295 assert!(!config.has_client_cert());
296 }
297
298 #[test]
299 fn test_tls_config_builder() {
300 let config = TlsConfig::new()
301 .ca_cert("/path/to/ca.pem")
302 .client_cert("/path/to/cert.pem", "/path/to/key.pem")
303 .server_name("example.com");
304
305 assert!(config.has_client_cert());
306 assert_eq!(config.server_name, Some("example.com".to_string()));
307 }
308
309 #[test]
310 fn test_tls_mode() {
311 assert!(!TlsMode::Disable.is_enabled());
312 assert!(TlsMode::Require.is_required());
313 assert!(TlsMode::VerifyFull.verify_hostname());
314 }
315}