1use serde::{Deserialize, Serialize};
4use std::{collections::HashMap, fmt, io, path::Path, time::Duration};
5
6use super::ConfigError;
7
8#[cfg(feature = "config-openssl")]
9pub mod openssl;
10
11pub trait SslStore<C, S> {
13 fn get_file_certificates(path: &std::path::Path) -> Result<Vec<C>, ConfigError>;
15
16 fn get_store(&self) -> Result<S, ConfigError>;
26
27 fn get_certs(&self) -> Result<HashMap<String, C>, ConfigError>;
41}
42
43#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
45#[serde(untagged)]
46pub enum Store {
47 #[default]
49 System,
50 File {
52 path: String,
54 },
55 Cert {
57 certs: Vec<String>,
59 },
60}
61
62impl TryFrom<&Path> for Store {
63 type Error = io::Error;
64
65 fn try_from(path: &Path) -> io::Result<Self> {
75 path.to_str()
76 .map(|s| Store::File {
77 path: s.to_string(),
78 })
79 .ok_or(io::Error::new(
80 io::ErrorKind::InvalidFilename,
81 "Invalid store path",
82 ))
83 }
84}
85
86impl fmt::Display for Store {
87 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
88 match self {
89 Store::System => write!(f, "System store"),
90 Store::File { path } => write!(f, "Store cert path [{path}]"),
91 Store::Cert { certs: _ } => write!(f, "Store cert list"),
92 }?;
93
94 #[cfg(feature = "config-openssl")]
95 {
96 writeln!(f, ":")?;
97 let certs: HashMap<String, ::openssl::x509::X509> =
98 self.get_certs().unwrap_or_default();
99 for (name, cert) in certs {
100 if f.alternate() {
101 writeln!(f, "{name}:\n{cert:#?}")?;
102 } else {
103 writeln!(f, "{name}")?;
104 }
105 }
106 }
107
108 Ok(())
109 }
110}
111
112pub trait SslConfigContext<C, S> {
114 fn init_tls_client_context(&self) -> Result<C, ConfigError>;
127
128 fn init_tls_server_context(&self, host: Option<&str>) -> Result<S, ConfigError>;
140}
141
142#[derive(Clone, PartialEq, Deserialize, Serialize)]
211pub struct SslConfig {
212 store: Option<Store>,
214 pkcs12: Option<String>,
216 cert: Option<String>,
218 key: Option<String>,
220 passphrase: Option<String>,
222 #[serde(default)]
223 alpn: Vec<String>,
225 #[serde(skip_serializing)]
226 #[serde(default = "SslConfig::default_modern_security")]
227 pub modern_security: bool,
229 #[serde(skip_serializing)]
230 #[serde(default = "SslConfig::default_ssl_timeout")]
231 pub ssl_timeout: u64,
233}
234
235impl SslConfig {
236 fn default_modern_security() -> bool {
237 true
238 }
239
240 fn default_ssl_timeout() -> u64 {
241 3000
242 }
243
244 pub fn new_pkcs12(pkcs12_path: String) -> SslConfig {
247 SslConfig {
248 store: None,
249 pkcs12: Some(pkcs12_path),
250 cert: None,
251 key: None,
252 passphrase: None,
253 alpn: Vec::default(),
254 modern_security: Self::default_modern_security(),
255 ssl_timeout: Self::default_ssl_timeout(),
256 }
257 }
258
259 pub fn new_cert_key(
262 cert_path: String,
263 key_path: String,
264 passphrase: Option<String>,
265 ) -> SslConfig {
266 SslConfig {
267 store: None,
268 pkcs12: None,
269 cert: Some(cert_path),
270 key: Some(key_path),
271 passphrase,
272 alpn: Vec::default(),
273 modern_security: Self::default_modern_security(),
274 ssl_timeout: Self::default_ssl_timeout(),
275 }
276 }
277
278 pub fn new_self_cert(cert_path: String) -> SslConfig {
281 SslConfig {
282 store: None,
283 pkcs12: None,
284 cert: Some(cert_path),
285 key: None,
286 passphrase: None,
287 alpn: Vec::default(),
288 modern_security: Self::default_modern_security(),
289 ssl_timeout: Self::default_ssl_timeout(),
290 }
291 }
292
293 pub fn get_ssl_timeout(&self) -> Duration {
295 Duration::from_millis(self.ssl_timeout)
296 }
297
298 pub fn set_store(&mut self, store: Store) {
300 self.store = Some(store);
301 }
302
303 pub fn set_alpn(&mut self, alpn: Vec<String>) {
305 self.alpn = alpn;
306 }
307}
308
309impl Default for SslConfig {
310 fn default() -> SslConfig {
311 SslConfig {
312 store: None,
313 pkcs12: None,
314 cert: None,
315 key: None,
316 passphrase: None,
317 alpn: Vec::default(),
318 modern_security: Self::default_modern_security(),
319 ssl_timeout: Self::default_ssl_timeout(),
320 }
321 }
322}
323
324impl fmt::Debug for SslConfig {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 f.debug_struct("SslConfig")
327 .field("store", &self.store)
328 .field("pkcs12", &self.pkcs12)
329 .field("cert", &self.cert)
330 .field("key", &self.key)
331 .field("alpn", &self.alpn)
332 .field("modern_security", &self.modern_security)
333 .field("ssl_timeout", &self.ssl_timeout)
334 .finish()
335 }
336}
337
338#[cfg(feature = "config-openssl")]
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn test_store() {
345 let inline_store_le_x1_x2 = Store::Cert {
346 certs: vec![
347 "-----BEGIN CERTIFICATE-----
348MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
349TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
350cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
351WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
352ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
353MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
354h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
3550TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
356A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
357T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
358B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
359B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
360KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
361OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
362jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
363qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
364rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
365HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
366hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
367ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3683BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
369NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
370ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
371TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
372jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
373oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
3744RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
375mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
376emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
377-----END CERTIFICATE-----"
378 .to_string(),
379 "-----BEGIN CERTIFICATE-----
380MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
381CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
382R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
383MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
384ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
385EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
386+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
387ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
388AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
389zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
390tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
391/q4AaOeMSQ+2b1tbFfLn
392-----END CERTIFICATE-----"
393 .to_string(),
394 ],
395 };
396 assert!(format!("{inline_store_le_x1_x2}").contains("ISRG Root X"));
397
398 let config_store_le_x1_x2: Store = serde_yaml::from_str(
399 "certs:
400 - |
401 -----BEGIN CERTIFICATE-----
402 MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
403 TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
404 cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
405 WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
406 ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
407 MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
408 h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
409 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
410 A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
411 T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
412 B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
413 B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
414 KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
415 OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
416 jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
417 qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
418 rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
419 HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
420 hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
421 ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
422 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
423 NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
424 ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
425 TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
426 jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
427 oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
428 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
429 mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
430 emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
431 -----END CERTIFICATE-----
432 - |
433 -----BEGIN CERTIFICATE-----
434 MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
435 CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
436 R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
437 MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
438 ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
439 EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
440 +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
441 ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
442 AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
443 zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
444 tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
445 /q4AaOeMSQ+2b1tbFfLn
446 -----END CERTIFICATE-----",
447 )
448 .expect("SSL certificate configuration should be read");
449 assert!(format!("{config_store_le_x1_x2}").contains("ISRG Root X"));
450
451 let config_store_file: Store = serde_yaml::from_str("path: \"/opt\"")
452 .expect("Certificate configuration path should be read");
453 assert_eq!(
454 Store::File {
455 path: "/opt".to_string()
456 },
457 config_store_file
458 );
459 }
460
461 #[test]
462 fn test_tls_server_context() {
463 let ssl_config = SslConfig::default();
464 assert_eq!(
465 "SslConfig { store: None, pkcs12: None, cert: None, key: None, alpn: [], modern_security: true, ssl_timeout: 3000 }",
466 format!("{ssl_config:?}")
467 );
468 let ssl_acceptor = ssl_config
469 .init_tls_server_context(None)
470 .expect("The TLS server context should be init")
471 .build();
472
473 assert!(ssl_acceptor.context().private_key().is_some());
475 assert!(ssl_acceptor.context().certificate().is_some());
476 }
477
478 #[test]
479 fn ssl_config_debug_redacts_passphrase() {
480 let ssl_config = SslConfig::new_cert_key(
481 "cert.pem".into(),
482 "key.pem".into(),
483 Some("sensitive-passphrase".into()),
484 );
485
486 let debug = format!("{ssl_config:?}");
487 assert!(debug.contains("cert.pem"));
488 assert!(debug.contains("key.pem"));
489 assert!(!debug.contains("sensitive-passphrase"));
490 assert!(!debug.contains("passphrase"));
491 }
492}