1use std::io::BufReader;
29use std::sync::Arc;
30
31pub struct ServerTlsConfig {
36 cert_chain_path: Option<String>,
38 private_key_path: Option<String>,
40 client_ca_path: Option<String>,
42 require_client_auth: bool,
44}
45
46impl ServerTlsConfig {
47 #[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 #[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 #[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 #[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 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
138pub struct ClientTlsConfig {
143 root_ca_path: Option<String>,
145 client_cert_path: Option<String>,
147 client_key_path: Option<String>,
149 server_name: Option<String>,
151}
152
153impl ClientTlsConfig {
154 #[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 #[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 #[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 #[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 #[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 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
242fn 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
266fn 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}