Skip to main content

lnc_client/
tls.rs

1//! TLS Support for LANCE Client
2//!
3//! Provides TLS/mTLS configuration for LANCE client connections.
4//!
5//! # Example
6//!
7//! ```rust
8//! use lnc_client::TlsClientConfig;
9//!
10//! // Basic TLS configuration (uses system root certificates)
11//! let tls_config = TlsClientConfig::new();
12//!
13//! // TLS with custom CA
14//! let tls_config = TlsClientConfig::new()
15//!     .with_ca_cert("/path/to/ca.pem");
16//!
17//! // mTLS with client certificate
18//! let tls_config = TlsClientConfig::new()
19//!     .with_ca_cert("/path/to/ca.pem")
20//!     .with_client_cert("/path/to/client.pem", "/path/to/client-key.pem");
21//!
22//! // Check if mTLS is configured
23//! assert!(tls_config.is_mtls());
24//! ```
25
26use std::path::{Path, PathBuf};
27
28/// TLS configuration for client connections
29#[derive(Debug, Clone, Default)]
30pub struct TlsClientConfig {
31    /// Path to CA certificate for server verification
32    pub ca_cert_path: Option<PathBuf>,
33    /// Path to client certificate (for mTLS)
34    pub client_cert_path: Option<PathBuf>,
35    /// Path to client private key (for mTLS)
36    pub client_key_path: Option<PathBuf>,
37    /// Server name for SNI (defaults to address hostname)
38    pub server_name: Option<String>,
39    /// Skip server certificate verification (dangerous, testing only)
40    pub danger_accept_invalid_certs: bool,
41}
42
43impl TlsClientConfig {
44    /// Create a new TLS configuration with default settings
45    ///
46    /// Uses system root certificates for server verification.
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Set the CA certificate path for server verification
52    pub fn with_ca_cert(mut self, path: impl AsRef<Path>) -> Self {
53        self.ca_cert_path = Some(path.as_ref().to_path_buf());
54        self
55    }
56
57    /// Set client certificate and key for mTLS authentication
58    pub fn with_client_cert(
59        mut self,
60        cert_path: impl AsRef<Path>,
61        key_path: impl AsRef<Path>,
62    ) -> Self {
63        self.client_cert_path = Some(cert_path.as_ref().to_path_buf());
64        self.client_key_path = Some(key_path.as_ref().to_path_buf());
65        self
66    }
67
68    /// Set the server name for SNI
69    ///
70    /// If not set, the hostname from the connection address is used.
71    pub fn with_server_name(mut self, name: impl Into<String>) -> Self {
72        self.server_name = Some(name.into());
73        self
74    }
75
76    /// Skip server certificate verification (DANGEROUS)
77    ///
78    /// This should only be used for testing. It disables certificate
79    /// verification, making the connection vulnerable to MITM attacks.
80    #[cfg(any(test, feature = "dangerous-testing"))]
81    pub fn danger_accept_invalid_certs(mut self) -> Self {
82        self.danger_accept_invalid_certs = true;
83        self
84    }
85
86    /// Check if mTLS is configured
87    pub fn is_mtls(&self) -> bool {
88        self.client_cert_path.is_some() && self.client_key_path.is_some()
89    }
90
91    /// Convert to lnc-network TlsConfig
92    pub fn to_network_config(&self) -> lnc_network::TlsConfig {
93        if self.is_mtls() {
94            lnc_network::TlsConfig::mtls(
95                self.client_cert_path
96                    .as_ref()
97                    .map(|p| p.to_string_lossy().to_string())
98                    .unwrap_or_default(),
99                self.client_key_path
100                    .as_ref()
101                    .map(|p| p.to_string_lossy().to_string())
102                    .unwrap_or_default(),
103                self.ca_cert_path
104                    .as_ref()
105                    .map(|p| p.to_string_lossy().to_string())
106                    .unwrap_or_default(),
107            )
108        } else if let Some(ref ca_path) = self.ca_cert_path {
109            lnc_network::TlsConfig::client(Some(ca_path.to_string_lossy().to_string()))
110        } else {
111            lnc_network::TlsConfig::client(None::<String>)
112        }
113    }
114}
115
116#[cfg(test)]
117#[allow(clippy::unwrap_used)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_tls_config_default() {
123        let config = TlsClientConfig::new();
124        assert!(config.ca_cert_path.is_none());
125        assert!(config.client_cert_path.is_none());
126        assert!(!config.is_mtls());
127    }
128
129    #[test]
130    fn test_tls_config_with_ca() {
131        let config = TlsClientConfig::new().with_ca_cert("/path/to/ca.pem");
132
133        assert_eq!(
134            config.ca_cert_path.as_ref().map(|p| p.to_str()),
135            Some(Some("/path/to/ca.pem"))
136        );
137        assert!(!config.is_mtls());
138    }
139
140    #[test]
141    fn test_tls_config_mtls() {
142        let config = TlsClientConfig::new()
143            .with_ca_cert("/path/to/ca.pem")
144            .with_client_cert("/path/to/cert.pem", "/path/to/key.pem");
145
146        assert!(config.is_mtls());
147    }
148
149    #[test]
150    fn test_tls_config_with_server_name() {
151        let config = TlsClientConfig::new().with_server_name("lance.example.com");
152
153        assert_eq!(config.server_name, Some("lance.example.com".to_string()));
154    }
155
156    #[test]
157    fn test_client_config_with_tls() {
158        use crate::ClientConfig;
159
160        let tls = TlsClientConfig::new().with_server_name("lance.example.com");
161
162        let config = ClientConfig::new("127.0.0.1:1992").with_tls(tls);
163
164        assert!(config.is_tls_enabled());
165        assert!(config.tls.is_some());
166        assert_eq!(
167            config.tls.as_ref().unwrap().server_name,
168            Some("lance.example.com".to_string())
169        );
170    }
171
172    #[test]
173    fn test_client_config_without_tls() {
174        use crate::ClientConfig;
175
176        let config = ClientConfig::new("127.0.0.1:1992");
177
178        assert!(!config.is_tls_enabled());
179        assert!(config.tls.is_none());
180    }
181}