Skip to main content

signstar_config/yubihsm2/
admin_credentials.rs

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