prosa_utils/config/
ssl.rs

1//! Definition of SSL configuration
2
3use serde::{Deserialize, Serialize};
4use std::{collections::HashMap, fmt, time::Duration};
5
6use super::ConfigError;
7
8#[cfg(feature = "config-openssl")]
9pub mod openssl;
10
11/// Trait to define an SSL store with custom SSL objects
12pub trait SslStore<C, S> {
13    /// Method to read certificates from its path. Get all certificates in subfolders
14    fn get_file_certificates(path: &std::path::Path) -> Result<Vec<C>, ConfigError>;
15
16    /// Method to get a cert store
17    ///
18    /// ```
19    /// use prosa_utils::config::ssl::{Store, SslStore};
20    ///
21    /// let store = Store::File { path: "./target".into() };
22    /// # #[cfg(feature="config-openssl")]
23    /// let ssl_store = store.get_store().unwrap();
24    /// ```
25    fn get_store(&self) -> Result<S, ConfigError>;
26
27    /// Method to get all OpenSSL certificate with their names as key
28    ///
29    /// ```
30    /// use prosa_utils::config::ssl::{Store, SslStore};
31    ///
32    /// let store = Store::File { path: "./target".into() };
33    /// # #[cfg(feature="config-openssl")]
34    /// let certs_map = store.get_certs().unwrap();
35    ///
36    /// // No cert in target
37    /// # #[cfg(feature="config-openssl")]
38    /// assert!(certs_map.is_empty());
39    /// ```
40    fn get_certs(&self) -> Result<HashMap<String, C>, ConfigError>;
41}
42
43/// SSL configuration object for store certificates
44#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
45#[serde(untagged)]
46pub enum Store {
47    /// Will use the system trusted certificates
48    #[default]
49    System,
50    /// Store path that contain certificate(s)
51    File {
52        /// Path of the store (can be directory, file, glob pattern)
53        path: String,
54    },
55    /// Store certs that contain PEMs
56    Cert {
57        /// List of string PEMs for certificates
58        certs: Vec<String>,
59    },
60}
61
62impl fmt::Display for Store {
63    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64        match self {
65            Store::System => write!(f, "System store"),
66            Store::File { path } => write!(f, "Store cert path [{path}]"),
67            Store::Cert { certs: _ } => write!(f, "Store cert list"),
68        }?;
69
70        #[cfg(feature = "config-openssl")]
71        {
72            writeln!(f, ":")?;
73            let certs: HashMap<String, ::openssl::x509::X509> =
74                self.get_certs().unwrap_or_default();
75            for (name, cert) in certs {
76                if f.alternate() {
77                    writeln!(f, "{name}:\n{cert:#?}")?;
78                } else {
79                    writeln!(f, "{name}")?;
80                }
81            }
82        }
83
84        Ok(())
85    }
86}
87
88/// Trait to define SSL configuration context for socket
89pub trait SslConfigContext<C, S> {
90    /// Method to init an SSL context for a client socket
91    ///
92    /// ```
93    /// use prosa_utils::config::ssl::{Store, SslConfig, SslConfigContext as _};
94    ///
95    /// let mut client_config = SslConfig::default();
96    /// client_config.set_store(Store::File { path: "./target".into() });
97    /// # #[cfg(feature="config-openssl")]
98    /// if let Ok(mut ssl_context_builder) = client_config.init_tls_client_context() {
99    ///     let ssl_context = ssl_context_builder.build();
100    /// }
101    /// ```
102    fn init_tls_client_context(&self) -> Result<C, ConfigError>;
103
104    /// Method to init an SSL context for a server socket
105    ///
106    /// ```
107    /// use prosa_utils::config::ssl::{SslConfig, SslConfigContext as _};
108    ///
109    /// let server_config = SslConfig::new_pkcs12("server.pkcs12".into());
110    /// # #[cfg(feature="config-openssl")]
111    /// if let Ok(mut ssl_context_builder) = server_config.init_tls_server_context(None) {
112    ///     let ssl_context = ssl_context_builder.build();
113    /// }
114    /// ```
115    fn init_tls_server_context(&self, host: Option<&str>) -> Result<S, ConfigError>;
116}
117
118/// SSL configuration for socket
119///
120/// Client SSL socket
121/// ```
122/// use std::io;
123/// use std::pin::Pin;
124/// use tokio::net::TcpStream;
125/// use tokio_openssl::SslStream;
126/// # #[cfg(feature="config-openssl")]
127/// use openssl::ssl::{ErrorCode, Ssl, SslMethod, SslVerifyMode};
128/// use prosa_utils::config::ssl::{SslConfig, SslConfigContext};
129///
130/// # #[cfg(feature="config-openssl")]
131/// async fn client() -> Result<(), io::Error> {
132///     let mut stream = TcpStream::connect("localhost:4443").await?;
133///
134///     let client_config = SslConfig::default();
135///     if let Ok(mut ssl_context_builder) = client_config.init_tls_client_context() {
136///         let ssl = ssl_context_builder.build().configure().unwrap().into_ssl("localhost").unwrap();
137///         let mut stream = SslStream::new(ssl, stream).unwrap();
138///         if let Err(e) = Pin::new(&mut stream).connect().await {
139///             if e.code() != ErrorCode::ZERO_RETURN {
140///                 eprintln!("Can't connect the client: {}", e);
141///             }
142///         }
143///
144///         // SSL stream ...
145///     }
146///
147///     Ok(())
148/// }
149/// ```
150///
151/// Server SSL socket
152/// ```
153/// use std::io;
154/// use std::pin::Pin;
155/// use tokio::net::TcpListener;
156/// use tokio_openssl::SslStream;
157/// # #[cfg(feature="config-openssl")]
158/// use openssl::ssl::{ErrorCode, Ssl, SslMethod, SslVerifyMode};
159/// use prosa_utils::config::ssl::{SslConfig, SslConfigContext};
160///
161/// # #[cfg(feature="config-openssl")]
162/// async fn server() -> Result<(), io::Error> {
163///     let listener = TcpListener::bind("0.0.0.0:4443").await?;
164///
165///     let server_config = SslConfig::new_cert_key("cert.pem".into(), "cert.key".into(), Some("passphrase".into()));
166///     if let Ok(mut ssl_context_builder) = server_config.init_tls_server_context(None) {
167///         ssl_context_builder.set_verify(SslVerifyMode::NONE);
168///         let ssl_context = ssl_context_builder.build();
169///
170///         loop {
171///             let (stream, cli_addr) = listener.accept().await?;
172///             let ssl = Ssl::new(&ssl_context.context()).unwrap();
173///             let mut stream = SslStream::new(ssl, stream).unwrap();
174///             if let Err(e) = Pin::new(&mut stream).accept().await {
175///                 if e.code() != ErrorCode::ZERO_RETURN {
176///                     eprintln!("Can't accept the client {}: {}", cli_addr, e);
177///                 }
178///             }
179///
180///             // SSL stream ...
181///         }
182///     }
183///
184///     Ok(())
185/// }
186/// ```
187#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
188pub struct SslConfig {
189    /// SSL store certificate to verify the remote certificate
190    store: Option<Store>,
191    /// PKCS12 object for certificate
192    pkcs12: Option<String>,
193    /// certificate
194    cert: Option<String>,
195    /// private key
196    key: Option<String>,
197    /// passphrase for private key or pkcs12
198    passphrase: Option<String>,
199    #[serde(default)]
200    /// ALPN list send by the client, or order of ALPN accepted by the server
201    alpn: Vec<String>,
202    #[serde(skip_serializing)]
203    #[serde(default = "SslConfig::default_modern_security")]
204    /// Security level. If `true`, it'll use the [modern version 5 of Mozilla's](https://wiki.mozilla.org/Security/Server_Side_TLS) TLS recommendations.
205    pub modern_security: bool,
206    #[serde(skip_serializing)]
207    #[serde(default = "SslConfig::default_ssl_timeout")]
208    /// SSL operation timeout in milliseconds
209    pub ssl_timeout: u64,
210}
211
212impl SslConfig {
213    fn default_modern_security() -> bool {
214        true
215    }
216
217    fn default_ssl_timeout() -> u64 {
218        3000
219    }
220
221    /// Method to create an ssl configuration from a pkcs12 manually
222    /// Should be use with config instead of building it manually
223    pub fn new_pkcs12(pkcs12_path: String) -> SslConfig {
224        SslConfig {
225            store: None,
226            pkcs12: Some(pkcs12_path),
227            cert: None,
228            key: None,
229            passphrase: None,
230            alpn: Vec::default(),
231            modern_security: Self::default_modern_security(),
232            ssl_timeout: Self::default_ssl_timeout(),
233        }
234    }
235
236    /// Method to create an ssl configuration from a certificate and its key manually
237    /// Should be use with config instead of building it manually
238    pub fn new_cert_key(
239        cert_path: String,
240        key_path: String,
241        passphrase: Option<String>,
242    ) -> SslConfig {
243        SslConfig {
244            store: None,
245            pkcs12: None,
246            cert: Some(cert_path),
247            key: Some(key_path),
248            passphrase,
249            alpn: Vec::default(),
250            modern_security: Self::default_modern_security(),
251            ssl_timeout: Self::default_ssl_timeout(),
252        }
253    }
254
255    /// Method to create an ssl configuration that will generate a self signed certificate and write it's certificate to the _cert_path_
256    /// Should be use with config instead of building it manually
257    pub fn new_self_cert(cert_path: String) -> SslConfig {
258        SslConfig {
259            store: None,
260            pkcs12: None,
261            cert: Some(cert_path),
262            key: None,
263            passphrase: None,
264            alpn: Vec::default(),
265            modern_security: Self::default_modern_security(),
266            ssl_timeout: Self::default_ssl_timeout(),
267        }
268    }
269
270    /// Getter of the SSL timeout
271    pub fn get_ssl_timeout(&self) -> Duration {
272        Duration::from_millis(self.ssl_timeout)
273    }
274
275    /// Setter of the store certificate
276    pub fn set_store(&mut self, store: Store) {
277        self.store = Some(store);
278    }
279
280    /// Setter of the ALPN list send by the client, or order of ALPN accepted by the server
281    pub fn set_alpn(&mut self, alpn: Vec<String>) {
282        self.alpn = alpn;
283    }
284}
285
286impl Default for SslConfig {
287    fn default() -> SslConfig {
288        SslConfig {
289            store: None,
290            pkcs12: None,
291            cert: None,
292            key: None,
293            passphrase: None,
294            alpn: Vec::default(),
295            modern_security: Self::default_modern_security(),
296            ssl_timeout: Self::default_ssl_timeout(),
297        }
298    }
299}
300
301#[cfg(feature = "config-openssl")]
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_store() {
308        let inline_store_le_x1_x2 = Store::Cert {
309            certs: vec![
310                "-----BEGIN CERTIFICATE-----
311MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
312TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
313cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
314WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
315ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
316MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
317h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
3180TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
319A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
320T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
321B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
322B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
323KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
324OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
325jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
326qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
327rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
328HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
329hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
330ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3313BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
332NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
333ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
334TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
335jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
336oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
3374RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
338mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
339emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
340-----END CERTIFICATE-----"
341                    .to_string(),
342                "-----BEGIN CERTIFICATE-----
343MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
344CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
345R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
346MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
347ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
348EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
349+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
350ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
351AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
352zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
353tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
354/q4AaOeMSQ+2b1tbFfLn
355-----END CERTIFICATE-----"
356                    .to_string(),
357            ],
358        };
359        assert!(format!("{inline_store_le_x1_x2}").contains("ISRG Root X"));
360
361        let config_store_le_x1_x2: Store = serde_yaml::from_str(
362            "certs:
363  - |
364    -----BEGIN CERTIFICATE-----
365    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
394    -----END CERTIFICATE-----
395  - |
396    -----BEGIN CERTIFICATE-----
397    MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
398    CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
399    R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
400    MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
401    ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
402    EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
403    +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
404    ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
405    AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
406    zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
407    tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
408    /q4AaOeMSQ+2b1tbFfLn
409    -----END CERTIFICATE-----",
410        )
411        .unwrap();
412        assert!(format!("{config_store_le_x1_x2}").contains("ISRG Root X"));
413
414        let config_store_file: Store = serde_yaml::from_str("path: \"/opt\"").unwrap();
415        assert_eq!(
416            Store::File {
417                path: "/opt".to_string()
418            },
419            config_store_file
420        );
421    }
422
423    #[test]
424    fn test_tls_server_context() {
425        let ssl_config = SslConfig::default();
426        let ssl_acceptor = ssl_config.init_tls_server_context(None).unwrap().build();
427
428        // Check for self signed certificate
429        assert!(ssl_acceptor.context().private_key().is_some());
430        assert!(ssl_acceptor.context().certificate().is_some());
431    }
432}