signstar_yubihsm2/
connection.rs1use 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
12pub const DEFAULT_HTTP_TIMEOUT_MILLIS: u64 = 5000;
14
15pub const DEFAULT_USB_TIMEOUT_MILLIS: u64 = 5000;
17
18#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
20#[cfg_attr(feature = "serde", derive(Serialize))]
21pub struct YubiHsmUrl(Url);
22
23impl YubiHsmUrl {
24 pub fn host(&self) -> &str {
26 self.0.host_str().unwrap_or_default()
27 }
28
29 pub fn scheme(&self) -> &str {
31 self.0.scheme()
32 }
33
34 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#[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 Http {
65 url: YubiHsmUrl,
67 },
68
69 #[cfg(feature = "_yubihsm2-mockhsm")]
71 Mock,
72
73 Usb {
78 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 #[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 #[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 #[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 #[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}