Skip to main content

nethsm/
tls.rs

1use std::sync::Arc;
2use std::thread::available_parallelism;
3use std::time::Duration;
4use std::{fmt::Display, str::FromStr};
5
6use log::info;
7use nethsm_sdk_rs::ureq::{
8    Agent,
9    tls::{Certificate, RootCerts, TlsConfig, TlsProvider},
10};
11use serde::{Deserialize, Serialize};
12
13use crate::Error;
14#[cfg(doc)]
15use crate::NetHsm;
16
17/// The default maximum idle TLS connections for a [`NetHsm`].
18pub const DEFAULT_MAX_IDLE_CONNECTIONS: usize = 100;
19
20/// The default timeout in seconds for a TLS connections for a [`NetHsm`].
21pub const DEFAULT_TIMEOUT_SECONDS: u64 = 10;
22
23/// A list of TLS certificates to validate TLS communication with.
24#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
25pub struct RootCertificates(Vec<Vec<u8>>);
26
27impl From<&RootCertificates> for RootCerts {
28    fn from(value: &RootCertificates) -> Self {
29        let certs = value
30            .0
31            .iter()
32            .map(|cert| Certificate::from_der(cert).to_owned())
33            .collect::<Vec<_>>();
34        RootCerts::Specific(Arc::new(certs))
35    }
36}
37
38/// The security model chosen for a [`crate::NetHsm`]'s TLS connection
39#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
40pub enum ConnectionSecurity {
41    /// Always trust the TLS certificate associated with a host
42    Unsafe,
43    /// Use the native trust store to evaluate the trust of a host
44    Native,
45    /// Use a list of root certificate objects to verify a host's TLS certificate.
46    RootCertificates(RootCertificates),
47}
48
49impl Display for ConnectionSecurity {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        match self {
52            Self::Unsafe => write!(f, "unsafe"),
53            Self::Native => write!(f, "native"),
54            Self::RootCertificates(_) => write!(f, "custom root certificates"),
55        }
56    }
57}
58
59impl FromStr for ConnectionSecurity {
60    type Err = Error;
61
62    /// Create a ConnectionSecurity from string
63    ///
64    /// Valid inputs are either "Unsafe" (or "unsafe"), "Native" (or "native") or "sha256:checksum"
65    /// where "checksum" denotes 64 ASCII hexadecimal chars.
66    ///
67    /// # Errors
68    ///
69    /// Returns an [`Error`] if the input is neither "Unsafe" nor "Native" and also no valid
70    /// certificate fingerprint can be derived from the input.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use std::str::FromStr;
76    ///
77    /// use nethsm::ConnectionSecurity;
78    ///
79    /// assert!(ConnectionSecurity::from_str("unsafe").is_ok());
80    /// assert!(ConnectionSecurity::from_str("native").is_ok());
81    /// assert!(ConnectionSecurity::from_str("something").is_err());
82    /// ```
83    fn from_str(s: &str) -> Result<Self, Self::Err> {
84        match s {
85            "unsafe" | "Unsafe" => Ok(Self::Unsafe),
86            "native" | "Native" => Ok(Self::Native),
87            _ => Err(Error::Default(format!("Invalid connection security: {s}"))),
88        }
89    }
90}
91
92/// Creates an [`Agent`] for the use in a [`NetHsm`] connection.
93///
94/// Takes a [`ConnectionSecurity`] to define the TLS security model for the connection.
95/// Allows setting the maximum idle connections per host using the optional
96/// `max_idle_connections` (defaults to [`available_parallelism`] and falls back to
97/// [`DEFAULT_MAX_IDLE_CONNECTIONS`] if unavailable).
98/// Also allows setting the timeout in seconds for a successful socket connection
99/// using the optional `timeout_seconds` (defaults to [`DEFAULT_TIMEOUT_SECONDS`]).
100///
101/// # Errors
102///
103/// Returns an error if
104///
105/// - the TLS client configuration can not be created,
106/// - [`ConnectionSecurity::Native`] is provided as `tls_security`, but no certification authority
107///   certificates are available on the system.
108pub(crate) fn create_agent(
109    tls_security: ConnectionSecurity,
110    max_idle_connections: Option<usize>,
111    timeout_seconds: Option<u64>,
112) -> Result<Agent, Error> {
113    let max_idle_connections = max_idle_connections
114        .or_else(|| available_parallelism().ok().map(Into::into))
115        .unwrap_or(DEFAULT_MAX_IDLE_CONNECTIONS);
116    let timeout_seconds = timeout_seconds.unwrap_or(DEFAULT_TIMEOUT_SECONDS);
117    info!(
118        "NetHSM connection configured with \"max_idle_connection\" {max_idle_connections} and \"timeout_seconds\" {timeout_seconds}."
119    );
120    let tls_config = {
121        let mut tls_config_builder = TlsConfig::builder().provider(TlsProvider::Rustls);
122
123        tls_config_builder = match &tls_security {
124            ConnectionSecurity::Unsafe => tls_config_builder.disable_verification(true),
125            ConnectionSecurity::Native => {
126                tls_config_builder.root_certs(RootCerts::PlatformVerifier)
127            }
128            ConnectionSecurity::RootCertificates(root_certs) => {
129                tls_config_builder.root_certs(RootCerts::from(root_certs))
130            }
131        };
132
133        tls_config_builder.build()
134    };
135    let agent = Agent::config_builder()
136        .max_idle_connections(max_idle_connections)
137        .max_idle_connections_per_host(max_idle_connections)
138        .timeout_connect(Some(Duration::from_secs(timeout_seconds)))
139        .tls_config(tls_config)
140        .build()
141        .new_agent();
142
143    Ok(agent)
144}
145
146#[cfg(test)]
147mod tests {
148    use rstest::rstest;
149    use testresult::TestResult;
150
151    use super::*;
152
153    #[rstest]
154    #[case(ConnectionSecurity::Native, "native")]
155    #[case(ConnectionSecurity::Unsafe, "unsafe")]
156    #[case(ConnectionSecurity::RootCertificates(RootCertificates(vec![vec![]])), "custom root certificates")]
157    fn connectionsecurity_display(
158        #[case] connection_security: ConnectionSecurity,
159        #[case] expected: &str,
160    ) -> TestResult {
161        assert_eq!(connection_security.to_string(), expected);
162        Ok(())
163    }
164}