Skip to main content

dubbo_rs_tls/
lib.rs

1//! TLS/mTLS support for dubbo-rs.
2//!
3//! Provides server-side and client-side TLS configuration using rustls.
4//! Supports both one-way TLS (server certificate verification) and
5//! mutual TLS (mTLS) with client certificate authentication.
6//!
7//! ## Usage
8//!
9//! ### Server (TLS)
10//! ```ignore
11//! use dubbo_rs_tls::ServerTlsConfig;
12//!
13//! let config = ServerTlsConfig::new()
14//!     .with_cert_chain("path/to/cert.pem")?
15//!     .with_private_key("path/to/key.pem")?
16//!     .build()?;
17//! ```
18//!
19//! ### Client (TLS)
20//! ```ignore
21//! use dubbo_rs_tls::ClientTlsConfig;
22//!
23//! let config = ClientTlsConfig::new()
24//!     .with_root_ca("path/to/ca.pem")?
25//!     .build()?;
26//! ```
27
28use std::io::BufReader;
29use std::sync::Arc;
30
31/// Server-side TLS configuration.
32///
33/// Wraps `rustls::ServerConfig` and provides convenience methods
34/// for loading certificates and private keys from PEM files.
35pub struct ServerTlsConfig {
36    /// Path to the certificate chain PEM file.
37    cert_chain_path: Option<String>,
38    /// Path to the private key PEM file.
39    private_key_path: Option<String>,
40    /// Path to the client CA certificate PEM file (for mTLS).
41    client_ca_path: Option<String>,
42    /// Whether to require client certificates (mTLS).
43    require_client_auth: bool,
44}
45
46impl ServerTlsConfig {
47    /// Create a new server TLS configuration builder.
48    #[must_use]
49    pub fn new() -> Self {
50        Self {
51            cert_chain_path: None,
52            private_key_path: None,
53            client_ca_path: None,
54            require_client_auth: false,
55        }
56    }
57
58    /// Set the path to the server certificate chain PEM file.
59    #[must_use]
60    pub fn with_cert_chain(mut self, path: impl Into<String>) -> Self {
61        self.cert_chain_path = Some(path.into());
62        self
63    }
64
65    /// Set the path to the server private key PEM file.
66    #[must_use]
67    pub fn with_private_key(mut self, path: impl Into<String>) -> Self {
68        self.private_key_path = Some(path.into());
69        self
70    }
71
72    /// Enable mTLS by setting the client CA certificate path.
73    ///
74    /// When set, the server will require and verify client certificates.
75    #[must_use]
76    pub fn with_client_ca(mut self, path: impl Into<String>) -> Self {
77        self.client_ca_path = Some(path.into());
78        self.require_client_auth = true;
79        self
80    }
81
82    /// Build the `rustls::ServerConfig`.
83    ///
84    /// # Errors
85    /// Returns an error if:
86    /// - Certificate or key files cannot be read
87    /// - Certificate/key PEM parsing fails
88    /// - Private key is missing or does not match the certificate
89    pub fn build(self) -> Result<rustls::ServerConfig, anyhow::Error> {
90        let cert_chain_path = self
91            .cert_chain_path
92            .ok_or_else(|| anyhow::anyhow!("server certificate chain path is required"))?;
93        let private_key_path = self
94            .private_key_path
95            .ok_or_else(|| anyhow::anyhow!("server private key path is required"))?;
96
97        let certs = load_certs(&cert_chain_path)?;
98        let key = load_private_key(&private_key_path)?;
99
100        if self.require_client_auth {
101            let client_ca_path = self
102                .client_ca_path
103                .as_ref()
104                .ok_or_else(|| anyhow::anyhow!("client CA path is required for mTLS"))?;
105            let client_certs = load_certs(client_ca_path)?;
106
107            let mut root_store = rustls::RootCertStore::empty();
108            for (i, cert) in client_certs.into_iter().enumerate() {
109                root_store.add(cert).map_err(|e| {
110                    anyhow::anyhow!("failed to add client CA cert {i} to root store: {e}")
111                })?;
112            }
113
114            let client_verifier =
115                rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
116                    .build()
117                    .map_err(|e| anyhow::anyhow!("failed to build client verifier: {e}"))?;
118
119            rustls::ServerConfig::builder()
120                .with_client_cert_verifier(client_verifier)
121                .with_single_cert(certs, key)
122                .map_err(|e| anyhow::anyhow!("failed to build mTLS server config: {e}"))
123        } else {
124            rustls::ServerConfig::builder()
125                .with_no_client_auth()
126                .with_single_cert(certs, key)
127                .map_err(|e| anyhow::anyhow!("failed to build server TLS config: {e}"))
128        }
129    }
130}
131
132impl Default for ServerTlsConfig {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138/// Client-side TLS configuration.
139///
140/// Wraps `rustls::ClientConfig` and provides convenience methods
141/// for loading CA certificates and client certificates from PEM files.
142pub struct ClientTlsConfig {
143    /// Path to the root CA certificate PEM file.
144    root_ca_path: Option<String>,
145    /// Path to the client certificate chain PEM file (for mTLS).
146    client_cert_path: Option<String>,
147    /// Path to the client private key PEM file (for mTLS).
148    client_key_path: Option<String>,
149    /// Server name for SNI (Server Name Indication).
150    server_name: Option<String>,
151}
152
153impl ClientTlsConfig {
154    /// Create a new client TLS configuration builder.
155    #[must_use]
156    pub fn new() -> Self {
157        Self {
158            root_ca_path: None,
159            client_cert_path: None,
160            client_key_path: None,
161            server_name: None,
162        }
163    }
164
165    /// Set the path to the root CA certificate PEM file.
166    ///
167    /// This is used to verify the server's certificate.
168    /// If not set, the system's default CA store is used.
169    #[must_use]
170    pub fn with_root_ca(mut self, path: impl Into<String>) -> Self {
171        self.root_ca_path = Some(path.into());
172        self
173    }
174
175    /// Set the path to the client certificate chain PEM file (for mTLS).
176    #[must_use]
177    pub fn with_client_cert(mut self, path: impl Into<String>) -> Self {
178        self.client_cert_path = Some(path.into());
179        self
180    }
181
182    /// Set the path to the client private key PEM file (for mTLS).
183    #[must_use]
184    pub fn with_client_key(mut self, path: impl Into<String>) -> Self {
185        self.client_key_path = Some(path.into());
186        self
187    }
188
189    /// Set the expected server name for TLS SNI.
190    ///
191    /// Defaults to "localhost" if not set.
192    #[must_use]
193    pub fn with_server_name(mut self, name: impl Into<String>) -> Self {
194        self.server_name = Some(name.into());
195        self
196    }
197
198    /// Build the `rustls::ClientConfig`.
199    ///
200    /// # Errors
201    /// Returns an error if:
202    /// - CA certificate file cannot be read or parsed
203    /// - Client certificate/key cannot be loaded (mTLS)
204    pub fn build(self) -> Result<rustls::ClientConfig, anyhow::Error> {
205        let mut root_store = rustls::RootCertStore::empty();
206
207        if let Some(ca_path) = &self.root_ca_path {
208            let certs = load_certs(ca_path)?;
209            for (i, cert) in certs.into_iter().enumerate() {
210                root_store
211                    .add(cert)
212                    .map_err(|e| anyhow::anyhow!("failed to add CA cert {i} to root store: {e}"))?;
213            }
214        } else {
215            root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
216        }
217
218        if let (Some(client_cert_path), Some(client_key_path)) =
219            (&self.client_cert_path, &self.client_key_path)
220        {
221            let client_certs = load_certs(client_cert_path)?;
222            let client_key = load_private_key(client_key_path)?;
223
224            rustls::ClientConfig::builder()
225                .with_root_certificates(root_store)
226                .with_client_auth_cert(client_certs, client_key)
227                .map_err(|e| anyhow::anyhow!("failed to build mTLS client config: {e}"))
228        } else {
229            Ok(rustls::ClientConfig::builder()
230                .with_root_certificates(root_store)
231                .with_no_client_auth())
232        }
233    }
234}
235
236impl Default for ClientTlsConfig {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242/// Load certificate chain from a PEM file.
243///
244/// # Errors
245/// Returns an error if the file cannot be read or contains invalid PEM data.
246fn load_certs(
247    path: &str,
248) -> Result<Vec<rustls::pki_types::CertificateDer<'static>>, anyhow::Error> {
249    let cert_file = std::fs::File::open(path)
250        .map_err(|e| anyhow::anyhow!("failed to open certificate file '{path}': {e}"))?;
251    let mut reader = BufReader::new(cert_file);
252
253    let certs = rustls_pemfile::certs(&mut reader)
254        .collect::<Result<Vec<_>, _>>()
255        .map_err(|e| anyhow::anyhow!("failed to parse certificate PEM file '{path}': {e}"))?;
256
257    if certs.is_empty() {
258        return Err(anyhow::anyhow!(
259            "no certificates found in PEM file '{path}'"
260        ));
261    }
262
263    Ok(certs)
264}
265
266/// Load a private key from a PEM file.
267///
268/// Supports PKCS#8 and PKCS#1 RSA formats.
269///
270/// # Errors
271/// Returns an error if the file cannot be read, contains invalid PEM data,
272/// or no private key is found.
273fn load_private_key(
274    path: &str,
275) -> Result<rustls::pki_types::PrivateKeyDer<'static>, anyhow::Error> {
276    let key_file = std::fs::File::open(path)
277        .map_err(|e| anyhow::anyhow!("failed to open private key file '{path}': {e}"))?;
278    let mut reader = BufReader::new(key_file);
279
280    let key = rustls_pemfile::private_key(&mut reader)
281        .map_err(|e| anyhow::anyhow!("failed to parse private key PEM file '{path}': {e}"))?;
282
283    key.ok_or_else(|| anyhow::anyhow!("no private key found in PEM file '{path}'"))
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use std::io::Write;
290    use std::sync::Once;
291
292    static INIT: Once = Once::new();
293
294    fn ensure_crypto_provider() {
295        INIT.call_once(|| {
296            let _ = rustls::crypto::ring::default_provider().install_default();
297        });
298    }
299
300    const TEST_CERT_PEM: &str = "-----BEGIN CERTIFICATE-----\n\
301MIIDCTCCAfGgAwIBAgIUZZn+knMcUb3O2r+NrImQPLuIgaYwDQYJKoZIhvcNAQEL\n\
302BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDUwMTA2NDMwOFoXDTI3MDUw\n\
303MTA2NDMwOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF\n\
304AAOCAQ8AMIIBCgKCAQEA11sODwF/xm7inCHtJNSJiFNSrAMbzV7V5eHVzBME0fQU\n\
30599nhHqvTDPmZXhNoouk3Talf71yUGckvUjCew/dmafUHmffe23ium/HTLO4JhSk8\n\
3068/dC1rSJR6Mvx74qMfKR/pTf8Mzz7xhz5MzXHdUeLAyI9AhBfRsk9jq+1Vy+vol/\n\
307N0iyRfko/0y8IIL2sHcwAdpntfJFVAkB5D6cQjSypw+T055Bc4rcumVOKhKftHJh\n\
308yRyxx6sb3V3tHgpHWl9XkVZkm3sFPikIB5+2Xbozrjfl+QNFVEzcPsUCMhre+N9y\n\
309YlThpdyvraH+2pntsCioKJZhvuHxLyMiXBsGWzRcswIDAQABo1MwUTAdBgNVHQ4E\n\
310FgQU5/qbBXIAp9mZXbWPBT9eS8GC3GQwHwYDVR0jBBgwFoAU5/qbBXIAp9mZXbWP\n\
311BT9eS8GC3GQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAD+lP\n\
312q5WHBtlywxcDYfG+HVJm2R1Pa8eeD50GixsR1j4++GgCNTN6FauB71muIYc7Giv7\n\
313nkrGKNzzRLTdxA2F0zW/YmoUuhrVJRj7OjJXuA6Jkikz+4FDTxRWrj9R9XorM2Pg\n\
314LNpaRmz3TTDY6eDQpU31Mj9bCBngwYBpt4tO4FS7WKST6T8mAdguQ/6us6niCljj\n\
315BBPHaFk+aOrNZV4TmyqvgZFpCrWGaV6Qb0UpHhQb1zUPVT72DrQo52aQrsFVOv3s\n\
316uIoCBfI6qG7eNeXH26NjnnEWjD8hy524Bmw0s500mpOJgPK7QucEU9MGIZda95Sf\n\
3171efRVCAHDrrPTG/dwQ==\n\
318-----END CERTIFICATE-----";
319
320    const TEST_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\n\
321MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDXWw4PAX/GbuKc\n\
322Ie0k1ImIU1KsAxvNXtXl4dXMEwTR9BT32eEeq9MM+ZleE2ii6TdNqV/vXJQZyS9S\n\
323MJ7D92Zp9QeZ997beK6b8dMs7gmFKTzz90LWtIlHoy/Hviox8pH+lN/wzPPvGHPk\n\
324zNcd1R4sDIj0CEF9GyT2Or7VXL6+iX83SLJF+Sj/TLwggvawdzAB2me18kVUCQHk\n\
325PpxCNLKnD5PTnkFzity6ZU4qEp+0cmHJHLHHqxvdXe0eCkdaX1eRVmSbewU+KQgH\n\
326n7ZdujOuN+X5A0VUTNw+xQIyGt7433JiVOGl3K+tof7ame2wKKgolmG+4fEvIyJc\n\
327GwZbNFyzAgMBAAECggEAQO1AV1DV448Bvh3SX9S+JD4uwhJr2uZ5KYYFTbH8NYpX\n\
328mgPzxan7BsHntb+3P8p9NGpYtJMeSYnovOhQrXdUxqQrpwVeiJ+hUP2+86BOeXmd\n\
3292VXWLmIes1zlJlzUXtupnW3n+DLqZk7iffwt7N4YayJaVex5Rg0dfyjl6PC9xzai\n\
330Tq+ThHVZqTL7i8ZyrqD1HeOAFKZOSfKOdKtZXn02mUej7MIdZgUV/AE2yuGFbCkI\n\
331dOb1ziHmdpbNPI8UT4vVpCYQ9DaBG/B7LMnxNRWapn6PVFCc+KFJ6iPf0X/2IKcF\n\
332cO9mTbYHmH5tK3a+HLLz0hdUdnfhV11KUtvp7edRxQKBgQD1aU+827jsidE8+LKR\n\
333Uf3WwZiUCsQnPlk7+MSf2nLywWhbEYhIb8UYsqvDKkqvj+0PVdB9NKysefDTBUBD\n\
334O3PXZYVEEb0ayLU4uoYigKwfUp8ESt28n96F1yWrLNV0uWPG8KaYGS3ALLrND7HF\n\
335GcCq25idDzXokIcF7KoCBJQU3wKBgQDgpcN/aepv4D3fZi+8jvfb0/3U77vz2zqn\n\
336kQEQG5C79VpzvLpSv9Xoz/bRD1ZvhYoMto/pv99TT9h2GjXSaWeYWR3O+uXFrsHx\n\
337MRYoOjouI83S6wKh0VcHPeYfHXoF5hg4pN4JAxTfeDjUulSJy0YvkjqFOL8t5gl5\n\
338Wb4pkjP+rQKBgHSalCN09tmU5hElTZsUrRp0I+37a5YF3tpK6gnV/pXvZYkXvHxG\n\
339dwy0ID58Ar6GESofKQ/EjmLpEY8CSLVpMzJd70MXdpWaVdjdb0xHfQDo/dtJQzAT\n\
340eeR4BFLf25A5Yfotb8qG9CECX8N9OIchJFVKP6oohwG4Yh9jgqewyzdbAoGBAKa3\n\
341vm+Hrjma5LAviQvZ2m5lVIK76/Pc5hnHjk9i9bXYL2mnTWvt/JVMCXM7e71GEJ7A\n\
342uesSv21320BC0WC3Yu94a5vZLb7YpAwYjsYJ+HWXkr+OM6Td1EWGlYrP+Gf6TE11\n\
343ZWawx8PU1/Bf3C9rEUpqrk2CQLeSecN6a5s0aqv9AoGBAJPIsYPh7p102BjcZuWy\n\
344a2mydTtsuA8AdFesFdKz2I2htgkqpZV8051JNGEbn/x6rkfMu+KjkSrZ2goUZ4f8\n\
345r81JsM7cejDaORYK9WIv/Z75IWcqQBgm9u7YPAFEt5XSSRvqhMZ7NBstgTt84hoV\n\
346R2viDc74NFJGF1hRuf212NHH\n\
347-----END PRIVATE KEY-----";
348
349    fn create_temp_files(cert_pem: &str, key_pem: &str) -> (tempfile::TempDir, String, String) {
350        ensure_crypto_provider();
351        let dir = tempfile::tempdir().expect("create temp dir");
352
353        let cert_path = dir.path().join("cert.pem");
354        let key_path = dir.path().join("key.pem");
355
356        let mut cert_file = std::fs::File::create(&cert_path).expect("create cert file");
357        cert_file
358            .write_all(cert_pem.as_bytes())
359            .expect("write cert");
360        drop(cert_file);
361
362        let mut key_file = std::fs::File::create(&key_path).expect("create key file");
363        key_file.write_all(key_pem.as_bytes()).expect("write key");
364        drop(key_file);
365
366        (
367            dir,
368            cert_path.to_string_lossy().to_string(),
369            key_path.to_string_lossy().to_string(),
370        )
371    }
372
373    #[test]
374    fn test_server_tls_config_build_success() {
375        let (_dir, cert_path, key_path) = create_temp_files(TEST_CERT_PEM, TEST_KEY_PEM);
376
377        let result = ServerTlsConfig::new()
378            .with_cert_chain(&cert_path)
379            .with_private_key(&key_path)
380            .build();
381
382        assert!(result.is_ok(), "ServerTlsConfig build failed: {result:?}");
383    }
384
385    #[test]
386    fn test_server_tls_config_missing_cert() {
387        let result = ServerTlsConfig::new().build();
388        assert!(result.is_err());
389        assert!(result
390            .unwrap_err()
391            .to_string()
392            .contains("certificate chain"));
393    }
394
395    #[test]
396    fn test_server_tls_config_missing_key() {
397        let (_dir, cert_path, _key_path) = create_temp_files(TEST_CERT_PEM, TEST_KEY_PEM);
398
399        let result = ServerTlsConfig::new().with_cert_chain(&cert_path).build();
400
401        assert!(result.is_err());
402        assert!(result.unwrap_err().to_string().contains("private key"));
403    }
404
405    #[test]
406    fn test_server_tls_config_file_not_found() {
407        let result = ServerTlsConfig::new()
408            .with_cert_chain("/nonexistent/cert.pem")
409            .with_private_key("/nonexistent/key.pem")
410            .build();
411
412        assert!(result.is_err());
413    }
414
415    #[test]
416    fn test_client_tls_config_build_default() {
417        ensure_crypto_provider();
418        let result = ClientTlsConfig::new().build();
419        assert!(result.is_ok());
420    }
421
422    #[test]
423    fn test_client_tls_config_with_server_name() {
424        ensure_crypto_provider();
425        let config = ClientTlsConfig::new()
426            .with_server_name("example.com")
427            .build();
428
429        assert!(config.is_ok());
430    }
431
432    #[test]
433    fn test_client_tls_config_with_ca() {
434        let (_dir, ca_path, _key_path) = create_temp_files(TEST_CERT_PEM, TEST_KEY_PEM);
435
436        let result = ClientTlsConfig::new().with_root_ca(&ca_path).build();
437
438        assert!(result.is_ok(), "ClientTlsConfig build failed: {result:?}");
439    }
440
441    #[test]
442    fn test_client_tls_config_ca_file_not_found() {
443        let result = ClientTlsConfig::new()
444            .with_root_ca("/nonexistent/ca.pem")
445            .build();
446
447        assert!(result.is_err());
448    }
449
450    #[test]
451    fn test_server_tls_config_default() {
452        let config = ServerTlsConfig::new();
453        assert!(config.cert_chain_path.is_none());
454        assert!(config.private_key_path.is_none());
455        assert!(!config.require_client_auth);
456    }
457
458    #[test]
459    fn test_client_tls_config_default() {
460        let config = ClientTlsConfig::new();
461        assert!(config.root_ca_path.is_none());
462        assert!(config.client_cert_path.is_none());
463        assert!(config.client_key_path.is_none());
464    }
465
466    #[test]
467    fn test_load_certs_invalid_file() {
468        let result = load_certs("/nonexistent/cert.pem");
469        assert!(result.is_err());
470    }
471
472    #[test]
473    fn test_load_private_key_invalid_file() {
474        let result = load_private_key("/nonexistent/key.pem");
475        assert!(result.is_err());
476    }
477
478    #[test]
479    fn test_server_tls_config_mtls() {
480        let (_dir, cert_path, key_path) = create_temp_files(TEST_CERT_PEM, TEST_KEY_PEM);
481        let (_dir2, ca_path, _ca_key) = create_temp_files(TEST_CERT_PEM, TEST_KEY_PEM);
482
483        let result = ServerTlsConfig::new()
484            .with_cert_chain(&cert_path)
485            .with_private_key(&key_path)
486            .with_client_ca(&ca_path)
487            .build();
488
489        assert!(
490            result.is_ok(),
491            "mTLS server config build failed: {result:?}"
492        );
493    }
494
495    #[test]
496    fn test_server_tls_config_mtls_missing_ca() {
497        let (_dir, cert_path, key_path) = create_temp_files(TEST_CERT_PEM, TEST_KEY_PEM);
498
499        let result = ServerTlsConfig::new()
500            .with_cert_chain(&cert_path)
501            .with_private_key(&key_path)
502            .with_client_ca("/nonexistent/ca.pem")
503            .build();
504
505        assert!(result.is_err());
506    }
507
508    #[test]
509    fn test_client_tls_config_mtls() {
510        let (_dir, ca_path, _ca_key) = create_temp_files(TEST_CERT_PEM, TEST_KEY_PEM);
511        let (_dir2, cert_path, key_path) = create_temp_files(TEST_CERT_PEM, TEST_KEY_PEM);
512
513        let result = ClientTlsConfig::new()
514            .with_root_ca(&ca_path)
515            .with_client_cert(&cert_path)
516            .with_client_key(&key_path)
517            .build();
518
519        assert!(
520            result.is_ok(),
521            "mTLS client config build failed: {result:?}"
522        );
523    }
524
525    #[test]
526    fn test_client_tls_config_mtls_missing_key() {
527        let (_dir, ca_path, _ca_key) = create_temp_files(TEST_CERT_PEM, TEST_KEY_PEM);
528        let (_dir2, cert_path, _key_path) = create_temp_files(TEST_CERT_PEM, TEST_KEY_PEM);
529
530        let result = ClientTlsConfig::new()
531            .with_root_ca(&ca_path)
532            .with_client_cert(&cert_path)
533            .build();
534
535        assert!(result.is_ok());
536    }
537}