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(
126                "1".parse()?,
127                Passphrase::new("password".to_string()),
128            )],
129        )?;
130
131        Ok(())
132    }
133
134    #[test]
135    fn yubihsm2_admin_credentials_new_fails_on_no_admins() -> TestResult {
136        match YubiHsm2AdminCredentials::new(
137            1,
138            Passphrase::new("backup-passphrase".to_string()),
139            Vec::new(),
140        ) {
141            Ok(creds) => {
142                panic!("Expected Error::AdministratorMissing but succeeded instead:\n{creds:?}")
143            }
144
145            Err(crate::Error::AdminSecretHandling(Error::AdministratorMissing)) => {}
146            Err(error) => panic!(
147                "Expected Error::AdministratorMissing but failed differently instead:\n{error}"
148            ),
149        }
150
151        Ok(())
152    }
153
154    #[test]
155    fn yubihsm2_admin_credentials_new_fails_on_admin_passphrase_too_short() -> TestResult {
156        match YubiHsm2AdminCredentials::new(
157            1,
158            Passphrase::new("backup-passphrase".to_string()),
159            vec![Credentials::new(
160                "1".parse()?,
161                Passphrase::new("pass".to_string()),
162            )],
163        ) {
164            Ok(creds) => {
165                panic!("Expected Error::PassphraseTooShort but succeeded instead:\n{creds:?}")
166            }
167            Err(crate::Error::AdminSecretHandling(Error::PassphraseTooShort { .. })) => {}
168            Err(error) => panic!(
169                "Expected Error::PassphraseTooShort but failed differently instead:\n{error}"
170            ),
171        }
172
173        Ok(())
174    }
175
176    #[test]
177    fn yubihsm2_admin_credentials_new_fails_on_backup_passphrase_too_short() -> TestResult {
178        match YubiHsm2AdminCredentials::new(
179            1,
180            Passphrase::new("backup".to_string()),
181            vec![Credentials::new(
182                "1".parse()?,
183                Passphrase::new("password".to_string()),
184            )],
185        ) {
186            Ok(creds) => {
187                panic!("Expected Error::PassphraseTooShort but succeeded instead:\n{creds:?}")
188            }
189            Err(crate::Error::AdminSecretHandling(Error::PassphraseTooShort { .. })) => {}
190            Err(error) => panic!(
191                "Expected Error::PassphraseTooShort but failed differently instead:\n{error}"
192            ),
193        }
194
195        Ok(())
196    }
197}