Skip to main content

nethsm/
nethsm_sdk.rs

1use std::fmt::Display;
2
3use log::Level;
4use nethsm_sdk_rs::models::{
5    KeyType,
6    Switch,
7    UnattendedBootConfig,
8    UserRole as NetHsmSdkRsUserRole,
9};
10use nethsm_sdk_rs::ureq::http::response::Response;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, thiserror::Error)]
14pub enum Error {
15    /// A variant of [`nethsm_sdk_rs::models::KeyType`] is unsupported.
16    #[error("The nethsm-sdk-rs key type {key_type} is not supported by Signstar")]
17    NetHsmSdkRsKeyTypeUnsupportedInSignstar {
18        /// The unsupported key type.
19        key_type: KeyType,
20    },
21
22    /// A switch for [`nethsm_sdk_rs::models::UnattendedBootConfig`] is unsupported.
23    #[error("The nethsm-sdk-rs switch {switch} is not supported by Signstar")]
24    NetHsmSdkRsSwitchUnsupportedInSignstar {
25        /// The unsupported switch for the unattended boot config.
26        switch: Switch,
27    },
28
29    /// A [`nethsm_sdk_rs::models::UserRole`] is unsupported.
30    #[error("The nethsm-sdk-rs user role {user_role} is not supported by Signstar")]
31    NetHsmSdkRsUserRoleUnsupportedInSignstar {
32        /// The unsupported user role.
33        user_role: NetHsmSdkRsUserRole,
34    },
35
36    /// A switch for [`nethsm_sdk_rs::models::UnattendedBootConfig`] is unsupported
37    #[error("The TLS key type {tls_key_type} is not supported by nethsm-sdk-rs")]
38    TlsKeyTypeUnsupportedInNetHsmSdkRs {
39        /// The unsupported switch for the unattended boot config.
40        tls_key_type: TlsKeyType,
41    },
42}
43
44/// A representation of a message body in an HTTP response
45///
46/// This type allows us to deserialize the message body when the NetHSM API triggers the return of a
47/// [`nethsm_sdk_rs::apis::Error::Ureq`].
48#[derive(Debug, Deserialize)]
49pub struct Message {
50    message: String,
51}
52
53impl From<Response<String>> for Message {
54    fn from(value: Response<String>) -> Self {
55        Message {
56            message: value.into_body(),
57        }
58    }
59}
60
61impl Display for Message {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.write_str(&self.message)
64    }
65}
66
67#[derive(Debug)]
68pub struct ApiErrorMessage {
69    pub status_code: u16,
70    pub message: Message,
71}
72
73impl From<(u16, Message)> for ApiErrorMessage {
74    fn from(value: (u16, Message)) -> Self {
75        Self {
76            status_code: value.0,
77            message: value.1,
78        }
79    }
80}
81
82impl Display for ApiErrorMessage {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.write_str(&format!(
85            "{} (status code {})",
86            self.message, self.status_code
87        ))
88    }
89}
90
91/// A helper Error for more readable output for [`nethsm_sdk_rs::apis::Error`]
92///
93/// This type allows us to create more readable output for [`nethsm_sdk_rs::apis::Error::Ureq`] and
94/// reuse the upstream handling otherwise.
95pub struct NetHsmApiError<T> {
96    error: Option<nethsm_sdk_rs::apis::Error<T>>,
97    message: Option<String>,
98}
99
100impl<T> From<nethsm_sdk_rs::apis::Error<T>> for NetHsmApiError<T> {
101    fn from(value: nethsm_sdk_rs::apis::Error<T>) -> Self {
102        match &value {
103            nethsm_sdk_rs::apis::Error::Ureq(error) => match error {
104                nethsm_sdk_rs::ureq::Error::StatusCode(code) => Self {
105                    error: None,
106                    message: Some(
107                        ApiErrorMessage::from((
108                            *code,
109                            Message {
110                                message: "".to_string(),
111                            },
112                        ))
113                        .to_string(),
114                    ),
115                },
116                nethsm_sdk_rs::ureq::Error::Http(transport) => Self {
117                    error: None,
118                    message: Some(format!("{transport}")),
119                },
120                _ => Self {
121                    error: Some(value),
122                    message: None,
123                },
124            },
125            nethsm_sdk_rs::apis::Error::ResponseError(resp) => Self {
126                error: None,
127                message: Some(format!(
128                    "Status code: {}: {}",
129                    resp.status,
130                    // First, try to deserialize the response as a `Message` object,
131                    // which is commonly returned by a majority of failures
132                    serde_json::from_slice::<Message>(&resp.content)
133                        .map(|m| m.message)
134                        // if that fails, as a last resort, try to return the response verbatim.
135                        .unwrap_or_else(|_| String::from_utf8_lossy(&resp.content).into())
136                )),
137            },
138            _ => Self {
139                error: Some(value),
140                message: None,
141            },
142        }
143    }
144}
145
146impl<T> Display for NetHsmApiError<T> {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        if let Some(message) = self.message.as_ref() {
149            write!(f, "{message}")?;
150        } else if let Some(error) = self.error.as_ref() {
151            write!(f, "{error}")?;
152        }
153        Ok(())
154    }
155}
156
157/// The NetHSM boot mode
158///
159/// Defines in which state the NetHSM is in during boot after provisioning (see
160/// [`crate::NetHsm::provision`]) and whether an unlock passphrase has to be provided for it to be
161/// of state [`crate::SystemState::Operational`].
162#[derive(
163    Clone,
164    Copy,
165    Debug,
166    strum::Display,
167    strum::EnumString,
168    strum::EnumIter,
169    strum::IntoStaticStr,
170    Eq,
171    PartialEq,
172)]
173#[strum(ascii_case_insensitive)]
174pub enum BootMode {
175    /// The device boots into state [`crate::SystemState::Locked`] and an unlock passphrase has to
176    /// be provided
177    Attended,
178    /// The device boots into state [`crate::SystemState::Operational`] and no unlock passphrase
179    /// has to be provided
180    Unattended,
181}
182
183impl TryFrom<UnattendedBootConfig> for BootMode {
184    type Error = crate::Error;
185
186    fn try_from(value: UnattendedBootConfig) -> Result<Self, Self::Error> {
187        Ok(match value.status {
188            Switch::On => BootMode::Unattended,
189            Switch::Off => BootMode::Attended,
190            // WARNING: Upstream has decided to set all models non-exhaustive.
191            //
192            // On each update to nethsm-sdk-rs, check whether Switch has gained further
193            // fields.
194            switch => return Err(Error::NetHsmSdkRsSwitchUnsupportedInSignstar { switch }.into()),
195        })
196    }
197}
198
199impl From<BootMode> for UnattendedBootConfig {
200    fn from(value: BootMode) -> Self {
201        match value {
202            BootMode::Unattended => UnattendedBootConfig::new(Switch::On),
203            BootMode::Attended => UnattendedBootConfig::new(Switch::Off),
204        }
205    }
206}
207
208/// A device log level
209#[derive(
210    Clone,
211    Copy,
212    Debug,
213    Default,
214    Deserialize,
215    strum::Display,
216    strum::EnumString,
217    strum::EnumIter,
218    strum::IntoStaticStr,
219    Eq,
220    Hash,
221    Ord,
222    PartialEq,
223    PartialOrd,
224    Serialize,
225)]
226#[strum(ascii_case_insensitive)]
227pub enum LogLevel {
228    /// Show debug, error, warning and info messages
229    Debug,
230
231    /// Show error, warning and info messages
232    Error,
233
234    /// Show info messages
235    #[default]
236    Info,
237
238    /// Show warning and info messages
239    Warning,
240}
241
242impl From<LogLevel> for nethsm_sdk_rs::models::LogLevel {
243    fn from(value: LogLevel) -> Self {
244        match value {
245            LogLevel::Debug => Self::Debug,
246            LogLevel::Error => Self::Error,
247            LogLevel::Info => Self::Info,
248            LogLevel::Warning => Self::Warning,
249        }
250    }
251}
252
253impl From<Level> for LogLevel {
254    /// Creates a new [`LogLevel`] from a [`Level`].
255    ///
256    /// # Note
257    ///
258    /// Creates a [`LogLevel::Debug`] from a [`Level::Trace`], as there is no equivalent level.
259    fn from(value: Level) -> Self {
260        match value {
261            Level::Trace => Self::Debug,
262            Level::Debug => Self::Debug,
263            Level::Error => Self::Error,
264            Level::Info => Self::Info,
265            Level::Warn => Self::Warning,
266        }
267    }
268}
269
270/// The algorithm type of a key used for TLS
271#[derive(
272    Clone,
273    Copy,
274    Debug,
275    Default,
276    Deserialize,
277    strum::Display,
278    strum::EnumString,
279    strum::EnumIter,
280    strum::IntoStaticStr,
281    Eq,
282    Hash,
283    Ord,
284    PartialEq,
285    PartialOrd,
286    Serialize,
287)]
288#[strum(ascii_case_insensitive)]
289pub enum TlsKeyType {
290    /// A Montgomery curve key over a prime field for the prime number 2^255-19
291    Curve25519,
292
293    /// An elliptic (Brainpool) curve key over a prime field for a prime of size 256 bit
294    EcBp256,
295
296    /// An elliptic (Brainpool) curve key over a prime field for a prime of size 384 bit
297    EcBp384,
298
299    /// An elliptic (Brainpool) curve key over a prime field for a prime of size 512 bit
300    EcBp512,
301
302    /// An elliptic-curve key over a prime field for a prime of size 224 bit
303    EcP224,
304
305    /// An elliptic-curve key over a prime field for a prime of size 256 bit
306    EcP256,
307
308    /// An elliptic-curve key over a prime field for a prime of size 384 bit
309    EcP384,
310
311    /// An elliptic-curve key over a prime field for a prime of size 521 bit
312    EcP521,
313
314    /// An RSA key
315    #[default]
316    Rsa,
317}
318
319impl TryFrom<TlsKeyType> for nethsm_sdk_rs::models::TlsKeyType {
320    type Error = crate::Error;
321
322    fn try_from(value: TlsKeyType) -> Result<Self, Self::Error> {
323        Ok(match value {
324            TlsKeyType::Curve25519 => Self::Curve25519,
325            TlsKeyType::EcBp256 => Self::BrainpoolP256,
326            TlsKeyType::EcBp384 => Self::BrainpoolP384,
327            TlsKeyType::EcBp512 => Self::BrainpoolP512,
328            TlsKeyType::EcP224 => {
329                return Err(Error::TlsKeyTypeUnsupportedInNetHsmSdkRs {
330                    tls_key_type: value,
331                }
332                .into());
333            }
334            TlsKeyType::EcP256 => Self::EcP256,
335            TlsKeyType::EcP384 => Self::EcP384,
336            TlsKeyType::EcP521 => Self::EcP521,
337            TlsKeyType::Rsa => Self::Rsa,
338        })
339    }
340}
341
342/// The role of a user on a NetHSM device
343#[derive(
344    Clone,
345    Copy,
346    Debug,
347    Default,
348    Deserialize,
349    strum::Display,
350    strum::EnumString,
351    strum::EnumIter,
352    strum::IntoStaticStr,
353    Eq,
354    PartialEq,
355    Ord,
356    PartialOrd,
357    Hash,
358    Serialize,
359)]
360#[strum(ascii_case_insensitive)]
361pub enum UserRole {
362    /// A role for administrating a device, its users and keys
363    Administrator,
364    /// A role for creating backups of a device
365    Backup,
366    /// A role for reading metrics of a device
367    Metrics,
368    /// A role for using one or more keys of a device
369    #[default]
370    Operator,
371}
372
373impl TryFrom<UserRole> for NetHsmSdkRsUserRole {
374    type Error = crate::Error;
375
376    fn try_from(value: UserRole) -> Result<Self, Self::Error> {
377        Ok(match value {
378            UserRole::Administrator => Self::Administrator,
379            UserRole::Backup => Self::Backup,
380            UserRole::Metrics => Self::Metrics,
381            UserRole::Operator => Self::Operator,
382        })
383    }
384}
385
386impl TryFrom<NetHsmSdkRsUserRole> for UserRole {
387    type Error = crate::Error;
388
389    fn try_from(value: NetHsmSdkRsUserRole) -> Result<Self, Self::Error> {
390        Ok(match value {
391            NetHsmSdkRsUserRole::Administrator => Self::Administrator,
392            NetHsmSdkRsUserRole::Backup => Self::Backup,
393            NetHsmSdkRsUserRole::Metrics => Self::Metrics,
394            NetHsmSdkRsUserRole::Operator => Self::Operator,
395            // WARNING: Upstream has decided to set all models non-exhaustive.
396            //
397            // On each update to nethsm-sdk-rs, check whether UserRole has gained further
398            // fields.
399            user_role => {
400                return Err(Error::NetHsmSdkRsUserRoleUnsupportedInSignstar { user_role }.into());
401            }
402        })
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use std::str::FromStr;
409
410    use rstest::rstest;
411    use testresult::TestResult;
412
413    use super::*;
414
415    #[rstest]
416    #[case("rsa", Some(TlsKeyType::Rsa))]
417    #[case("curve25519", Some(TlsKeyType::Curve25519))]
418    #[case("ecp256", Some(TlsKeyType::EcP256))]
419    #[case("ecp384", Some(TlsKeyType::EcP384))]
420    #[case("ecp521", Some(TlsKeyType::EcP521))]
421    #[case("foo", None)]
422    fn tlskeytype_fromstr(#[case] input: &str, #[case] expected: Option<TlsKeyType>) -> TestResult {
423        if let Some(expected) = expected {
424            assert_eq!(TlsKeyType::from_str(input)?, expected);
425        } else {
426            assert!(TlsKeyType::from_str(input).is_err());
427        }
428        Ok(())
429    }
430
431    #[rstest]
432    #[case("administrator", Some(UserRole::Administrator))]
433    #[case("backup", Some(UserRole::Backup))]
434    #[case("metrics", Some(UserRole::Metrics))]
435    #[case("operator", Some(UserRole::Operator))]
436    #[case("foo", None)]
437    fn userrole_fromstr(#[case] input: &str, #[case] expected: Option<UserRole>) -> TestResult {
438        if let Some(expected) = expected {
439            assert_eq!(UserRole::from_str(input)?, expected);
440        } else {
441            assert!(UserRole::from_str(input).is_err());
442        }
443        Ok(())
444    }
445}