Skip to main content

signstar_yubihsm2/
connection.rs

1use std::fmt::Debug;
2
3use log::{debug, error, warn};
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6use signstar_common::traits::BackendCheck;
7use url::Url;
8use yubihsm::{Client, Connector, Credentials, HttpConfig, UsbConfig, client::ErrorKind};
9
10use crate::yubihsm::SerialNumber;
11
12/// Default timeouts for reading and writing (5 seconds) for HTTP connections
13pub const DEFAULT_HTTP_TIMEOUT_MILLIS: u64 = 5000;
14
15/// Default timeouts for reading and writing (5 seconds) for USB connections
16pub const DEFAULT_USB_TIMEOUT_MILLIS: u64 = 5000;
17
18/// URL pointing at the YubiHSM-connector compatible service.
19#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
20#[cfg_attr(feature = "serde", derive(Serialize))]
21pub struct YubiHsmUrl(Url);
22
23impl YubiHsmUrl {
24    /// Host part of the URL.
25    pub fn host(&self) -> &str {
26        self.0.host_str().unwrap_or_default()
27    }
28
29    /// Scheme of the URL - either `http` or `https`.
30    pub fn scheme(&self) -> &str {
31        self.0.scheme()
32    }
33
34    /// Port of the service.
35    pub fn port(&self) -> u16 {
36        self.0.port_or_known_default().unwrap_or_default()
37    }
38}
39
40#[cfg(feature = "serde")]
41impl<'de> serde::Deserialize<'de> for YubiHsmUrl {
42    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
43    where
44        D: serde::Deserializer<'de>,
45    {
46        use serde::de::Error;
47        let url = Url::deserialize(deserializer)?;
48        if url.scheme() != "http" && url.scheme() != "https" {
49            return Err(Error::custom("only http and https schemes are supported"));
50        }
51        if !url.has_host() {
52            return Err(Error::custom("the host in the URL is mandatory"));
53        }
54        Ok(Self(url))
55    }
56}
57
58/// A connection to a YubiHSM2.
59#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
60#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
61#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
62pub enum Connection {
63    /// Connection to a device over HTTP(S).
64    Http {
65        /// Address to connect to.
66        url: YubiHsmUrl,
67    },
68
69    /// Connection to a Mock HSM.
70    #[cfg(feature = "_yubihsm2-mockhsm")]
71    Mock,
72
73    /// Connection to a device over USB.
74    ///
75    /// Each YubiHSM2 is identified by a unique serial number.
76    /// This number is printed on the enclosure of the physical device.
77    Usb {
78        /// Serial number of the connected YubiHSM2.
79        serial_number: SerialNumber,
80    },
81}
82
83impl BackendCheck for Connection {
84    fn is_available(&self) -> bool {
85        let connector = match self {
86            #[cfg(feature = "_yubihsm2-mockhsm")]
87            Self::Mock => Connector::mockhsm(),
88            Self::Usb { serial_number } => Connector::usb(&UsbConfig {
89                serial: Some(*serial_number),
90                timeout_ms: DEFAULT_USB_TIMEOUT_MILLIS,
91            }),
92            Connection::Http { url } => Connector::http(&HttpConfig {
93                addr: url.host().into(),
94                port: url.port(),
95                tls: url.scheme() == "https",
96                timeout_ms: DEFAULT_HTTP_TIMEOUT_MILLIS,
97            }),
98        };
99
100        if let Err(error) = connector.device_info() {
101            warn!(
102                "The YubiHSM2 connection {:?} is not available from this host: {error}",
103                self
104            );
105            return false;
106        }
107
108        debug!(
109            "The YubiHSM2 connection {:?} is available from this host.",
110            self
111        );
112
113        true
114    }
115
116    fn is_provisioned(&self) -> bool {
117        let connector = match self {
118            #[cfg(feature = "_yubihsm2-mockhsm")]
119            Self::Mock => Connector::mockhsm(),
120            Self::Usb { serial_number } => Connector::usb(&UsbConfig {
121                serial: Some(*serial_number),
122                timeout_ms: 5000,
123            }),
124            Connection::Http { url } => Connector::http(&HttpConfig {
125                addr: url.host().into(),
126                port: url.port(),
127                tls: url.scheme() == "https",
128                timeout_ms: DEFAULT_HTTP_TIMEOUT_MILLIS,
129            }),
130        };
131
132        if let Err(error) = Client::open(connector, Credentials::default(), false) {
133            if error.kind() == &ErrorKind::AuthenticationError {
134                debug!(
135                    "Authentication against the YubiHSM2 backend {:?} using the default credentials failed, assuming it to be provisioned: {error}",
136                    self
137                );
138                return true;
139            }
140
141            error!(
142                "The connection to YubiHSM2 backend {:?} cannot be established due to an error: {error}",
143                self
144            );
145            return false;
146        }
147
148        warn!(
149            "Authentication against the YubiHSM2 backend {:?} using the default credentials succeeded, assuming it to be unprovisioned.",
150            self
151        );
152        false
153    }
154}
155
156impl From<&Connection> for Connector {
157    fn from(value: &Connection) -> Self {
158        match value {
159            #[cfg(feature = "_yubihsm2-mockhsm")]
160            Connection::Mock => Connector::mockhsm(),
161            Connection::Usb { serial_number } => Connector::usb(&UsbConfig {
162                serial: Some(*serial_number),
163                timeout_ms: UsbConfig::DEFAULT_TIMEOUT_MILLIS,
164            }),
165            Connection::Http { url } => Connector::http(&HttpConfig {
166                addr: url.host().into(),
167                port: url.port(),
168                tls: url.scheme() == "https",
169                timeout_ms: DEFAULT_HTTP_TIMEOUT_MILLIS,
170            }),
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use log::LevelFilter;
178    use signstar_common::logging::setup_terminal_logging;
179    use testresult::TestResult;
180
181    use super::*;
182
183    /// Ensures, that [`Connection::is_available`] succeeds, when using [`Connection::Mock`].
184    #[cfg(feature = "_yubihsm2-mockhsm")]
185    #[test]
186    fn connection_is_available_succeeds_with_mockhsm() -> TestResult {
187        setup_terminal_logging(LevelFilter::Debug)?;
188        let connection = Connection::Mock;
189        assert!(connection.is_available());
190
191        Ok(())
192    }
193
194    /// Ensures, that [`Connection::is_available`] fails, when using a [`Connection::Usb`] with
195    /// serial number `0012345678`.
196    #[test]
197    fn connection_is_available_fails_with_hardware() -> TestResult {
198        setup_terminal_logging(LevelFilter::Debug)?;
199        let connection = Connection::Usb {
200            serial_number: "0012345678".parse()?,
201        };
202        assert!(!connection.is_available());
203
204        Ok(())
205    }
206
207    /// Ensures, that [`Connection::uses_default_credentials`] returns `false`, when using
208    /// a default [`Connection::Mock`].
209    #[cfg(feature = "_yubihsm2-mockhsm")]
210    #[test]
211    fn connection_uses_default_credentials_returns_false_with_mockhsm() -> TestResult {
212        setup_terminal_logging(LevelFilter::Debug)?;
213        let connection = Connection::Mock;
214        assert!(!connection.is_provisioned());
215
216        Ok(())
217    }
218
219    /// Ensures, that [`Connection::uses_default_credentials`] returns `false`, when using a
220    /// [`Connection::Usb`] with serial number `0012345678`.
221    #[test]
222    fn connection_uses_default_credentials_returns_false_with_hardware() -> TestResult {
223        setup_terminal_logging(LevelFilter::Debug)?;
224        let connection = Connection::Usb {
225            serial_number: "0012345678".parse()?,
226        };
227        assert!(!connection.is_provisioned());
228
229        Ok(())
230    }
231}