signstar_config/yubihsm2/
admin_credentials.rs

1//! Administrative credentials for YubiHSM2 backends.
2
3use serde::{Deserialize, Serialize};
4use signstar_crypto::{passphrase::Passphrase, traits::UserWithPassphrase};
5use signstar_yubihsm2::Credentials;
6
7use crate::{AdminCredentials, admin_credentials::Error};
8
9/// Administrative credentials for YubiHSM2 backends.
10///
11/// Tracks the following items:
12///
13/// - the minimum iteration for which the credentials should apply,
14/// - the backup passphrase of the backend,
15/// - the administrator credentials of the backend,
16///
17/// # Note
18///
19/// There must be at least one set of [`Credentials`] in the list of administrators.
20/// The passphrases of administrator accounts must be at least
21/// [`Self::MINIMUM_PASSPHRASE_LENGTH_USER`] characters long.
22/// The backup passphrase must be at least [`Self::MINIMUM_PASSPHRASE_LENGTH_BACKUP`] characters
23/// long.
24///
25/// It is implied, that the administrator users of a YubiHSM2 backend have the necessary
26/// [capabilities] for the creation of other users and keys.
27///
28/// [capabilities]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#capability-protocol-details
29#[derive(Clone, Debug, Default, Deserialize, Serialize)]
30pub struct YubiHsm2AdminCredentials {
31    iteration: u32,
32    backup_passphrase: Passphrase,
33    administrators: Vec<Credentials>,
34}
35
36impl YubiHsm2AdminCredentials {
37    /// The default ID on an unprovisioned YubiHSM2 device.
38    pub const DEFAULT_ID: u16 = 1;
39
40    /// The default passphrase on an unprovisioned YubiHSM2 device.
41    pub const DEFAULT_PASSPHRASE: &str = "password";
42
43    /// The default passphrase on an unprovisioned YubiHSM2 device.
44    pub const MINIMUM_PASSPHRASE_LENGTH_USER: usize = 8;
45
46    /// The minimum length of a backup passphrase.
47    pub const MINIMUM_PASSPHRASE_LENGTH_BACKUP: usize = 10;
48
49    /// Creates a new [`YubiHsm2AdminCredentials`].
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if
54    ///
55    /// - there is no administrator user,
56    /// - a user passphrase is too short,
57    /// - or the backup passphrase is too short.
58    pub fn new(
59        iteration: u32,
60        backup_passphrase: Passphrase,
61        administrators: Vec<Credentials>,
62    ) -> Result<Self, crate::Error> {
63        let creds = Self {
64            iteration,
65            backup_passphrase,
66            administrators,
67        };
68        creds.validate()?;
69
70        Ok(creds)
71    }
72}
73
74impl AdminCredentials for YubiHsm2AdminCredentials {
75    /// Validates the [`YubiHsm2AdminCredentials`].
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if
80    ///
81    /// - there is no administrator user,
82    /// - a user passphrase is too short,
83    /// - or the backup passphrase is too short.
84    fn validate(&self) -> Result<(), crate::Error> {
85        // There is no administrator user.
86        if self.administrators.is_empty() {
87            return Err(Error::AdministratorMissing.into());
88        }
89
90        // An administrator user passphrase is too short.
91        for creds in self.administrators.iter() {
92            if creds.passphrase().expose_borrowed().len() < Self::MINIMUM_PASSPHRASE_LENGTH_USER {
93                return Err(Error::PassphraseTooShort {
94                    context: format!("user {}", creds.user()),
95                    minimum_length: Self::MINIMUM_PASSPHRASE_LENGTH_USER,
96                }
97                .into());
98            }
99        }
100
101        // The backup passphrase is too short.
102        if self.backup_passphrase.expose_borrowed().len() < Self::MINIMUM_PASSPHRASE_LENGTH_BACKUP {
103            return Err(Error::PassphraseTooShort {
104                context: "backups".to_string(),
105                minimum_length: Self::MINIMUM_PASSPHRASE_LENGTH_USER,
106            }
107            .into());
108        }
109
110        Ok(())
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use testresult::TestResult;
117
118    use super::*;
119
120    #[test]
121    fn yubihsm2_admin_credentials_new_succeeds() -> TestResult {
122        let _creds = YubiHsm2AdminCredentials::new(
123            1,
124            Passphrase::new("backup-passphrase".to_string()),
125            vec![Credentials::new(1, Passphrase::new("password".to_string()))],
126        )?;
127
128        Ok(())
129    }
130
131    #[test]
132    fn yubihsm2_admin_credentials_new_fails_on_no_admins() -> TestResult {
133        match YubiHsm2AdminCredentials::new(
134            1,
135            Passphrase::new("backup-passphrase".to_string()),
136            Vec::new(),
137        ) {
138            Ok(creds) => {
139                panic!("Expected Error::AdministratorMissing but succeeded instead:\n{creds:?}")
140            }
141
142            Err(crate::Error::AdminSecretHandling(Error::AdministratorMissing)) => {}
143            Err(error) => panic!(
144                "Expected Error::AdministratorMissing but failed differently instead:\n{error}"
145            ),
146        }
147
148        Ok(())
149    }
150
151    #[test]
152    fn yubihsm2_admin_credentials_new_fails_on_admin_passphrase_too_short() -> TestResult {
153        match YubiHsm2AdminCredentials::new(
154            1,
155            Passphrase::new("backup-passphrase".to_string()),
156            vec![Credentials::new(1, Passphrase::new("pass".to_string()))],
157        ) {
158            Ok(creds) => {
159                panic!("Expected Error::PassphraseTooShort but succeeded instead:\n{creds:?}")
160            }
161            Err(crate::Error::AdminSecretHandling(Error::PassphraseTooShort { .. })) => {}
162            Err(error) => panic!(
163                "Expected Error::PassphraseTooShort but failed differently instead:\n{error}"
164            ),
165        }
166
167        Ok(())
168    }
169
170    #[test]
171    fn yubihsm2_admin_credentials_new_fails_on_backup_passphrase_too_short() -> TestResult {
172        match YubiHsm2AdminCredentials::new(
173            1,
174            Passphrase::new("backup".to_string()),
175            vec![Credentials::new(1, Passphrase::new("password".to_string()))],
176        ) {
177            Ok(creds) => {
178                panic!("Expected Error::PassphraseTooShort but succeeded instead:\n{creds:?}")
179            }
180            Err(crate::Error::AdminSecretHandling(Error::PassphraseTooShort { .. })) => {}
181            Err(error) => panic!(
182                "Expected Error::PassphraseTooShort but failed differently instead:\n{error}"
183            ),
184        }
185
186        Ok(())
187    }
188}