Skip to main content

signstar_config/yubihsm2/
admin_credentials.rs

1//! Administrative credentials for YubiHSM2 backends.
2
3use log::info;
4use serde::{Deserialize, Serialize};
5use signstar_crypto::{
6    passphrase::{Passphrase, PassphrasePolicy},
7    traits::UserWithPassphrase,
8};
9use signstar_yubihsm2::{Credentials, object::WrapKey, yubihsm::Id};
10
11use crate::{
12    admin_credentials::{AdminCredentials, Error},
13    config::Config,
14    yubihsm2::YubiHsm2UserMapping,
15};
16
17/// Administrative credentials for YubiHSM2 backends.
18///
19/// Tracks the following items:
20///
21/// - the minimum iteration for which the credentials should apply,
22/// - the backup passphrase of the backend,
23/// - the administrator credentials of the backend,
24///
25/// # Note
26///
27/// There must be at least one set of [`Credentials`] in the list of administrators.
28/// The passphrases of administrator users are checked against [`Self::ADMIN_PASSPHRASE_POLICY`].
29/// The backup passphrase is checked against [`Self::BACKUP_PASSPHRASE_POLICY`].
30///
31/// It is implied, that the administrator users of a YubiHSM2 backend have the necessary
32/// [capabilities] for the creation of other users and keys.
33///
34/// [capabilities]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#capability-protocol-details
35#[derive(Clone, Debug, Default, Deserialize, Serialize)]
36pub struct YubiHsm2AdminCredentials {
37    iteration: u32,
38    backup_passphrase: Passphrase,
39    administrators: Vec<Credentials>,
40}
41
42impl YubiHsm2AdminCredentials {
43    /// The [default ID] on an unprovisioned YubiHSM2 device.
44    ///
45    /// [default ID]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-intro-access-control.html#authentication-key-as-a-credential-holder
46    pub const DEFAULT_ID: Id = 1;
47
48    /// The [default passphrase] on an unprovisioned YubiHSM2 device.
49    ///
50    /// [default passphrase]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-intro-access-control.html#authentication-key-as-a-credential-holder
51    pub const DEFAULT_PASSPHRASE: &str = "password";
52
53    /// The minimum passphrase length for the backup key.
54    ///
55    /// # Note
56    ///
57    /// This reuses [`WrapKey::PASSPHRASE_POLICY`].
58    pub const BACKUP_PASSPHRASE_POLICY: PassphrasePolicy = WrapKey::PASSPHRASE_POLICY;
59
60    /// The minimum passphrase length for an administrative user.
61    pub const ADMIN_PASSPHRASE_POLICY: PassphrasePolicy = PassphrasePolicy { minimum_length: 30 };
62
63    /// Creates a new [`YubiHsm2AdminCredentials`].
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if
68    ///
69    /// - there is no administrator user,
70    /// - a user passphrase is too short,
71    /// - or the backup passphrase is too short.
72    pub fn new(
73        iteration: u32,
74        backup_passphrase: Passphrase,
75        administrators: Vec<Credentials>,
76    ) -> Result<Self, crate::Error> {
77        let creds = Self {
78            iteration,
79            backup_passphrase,
80            administrators,
81        };
82        creds.validate()?;
83
84        Ok(creds)
85    }
86
87    /// Returns the list of administrators.
88    pub fn administrators(&self) -> &[Credentials] {
89        &self.administrators
90    }
91
92    /// Returns the [default ID].
93    ///
94    /// [default ID]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-intro-access-control.html#authentication-key-as-a-credential-holder
95    pub fn default_id() -> Id {
96        Self::DEFAULT_ID
97    }
98
99    /// Returns the [default credentials].
100    ///
101    /// [default credentials]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-intro-access-control.html#authentication-key-as-a-credential-holder
102    pub fn default_credentials() -> Credentials {
103        Credentials::new(
104            Self::default_id(),
105            Passphrase::new(Self::DEFAULT_PASSPHRASE.to_string()),
106        )
107    }
108}
109
110impl AdminCredentials for YubiHsm2AdminCredentials {
111    /// Validates the [`YubiHsm2AdminCredentials`].
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if
116    ///
117    /// - there is no administrator user,
118    /// - a user passphrase is too short,
119    /// - or the backup passphrase is too short.
120    fn validate(&self) -> Result<(), crate::Error> {
121        // There is no administrator user.
122        if self.administrators.is_empty() {
123            return Err(Error::AdministratorMissing.into());
124        }
125
126        // An administrator user passphrase is too short.
127        for creds in self.administrators.iter() {
128            creds
129                .passphrase()
130                .check_against_policy(&Self::ADMIN_PASSPHRASE_POLICY)?;
131        }
132
133        // The backup passphrase is too short.
134        self.backup_passphrase
135            .check_against_policy(&Self::BACKUP_PASSPHRASE_POLICY)?;
136
137        Ok(())
138    }
139
140    /// Returns the iteration of the administrative credentials.
141    fn iteration(&self) -> u32 {
142        self.iteration
143    }
144
145    /// Returns the backup passphrase.
146    fn backup_passphrase(&self) -> &Passphrase {
147        &self.backup_passphrase
148    }
149}
150
151impl TryFrom<&Config> for YubiHsm2AdminCredentials {
152    type Error = crate::Error;
153
154    /// Creates a new [`YubiHsm2AdminCredentials`] from a [`Config`].
155    ///
156    /// # Note
157    ///
158    /// This generates a new backup passphrase and administrative passphrases, adhering to the
159    /// hardcoded passphrase policies (e.g. minimum length).
160    ///
161    /// # Errors
162    ///
163    /// Returns an error, if
164    ///
165    /// - `config` does not contain a [`YubiHsm2Config`][`crate::yubihsm2::YubiHsm2Config`]
166    /// - [`YubiHsm2AdminCredentials::new`] fails on the generated data
167    fn try_from(config: &Config) -> Result<Self, Self::Error> {
168        info!("Create new administrative credentials for the Signstar configuration...");
169
170        let Some(yubihsm2_config) = config.yubihsm2() else {
171            return Err(crate::config::Error::YubiHsm2SectionMissing.into());
172        };
173
174        let administrators = yubihsm2_config
175            .mappings()
176            .iter()
177            .filter_map(|mapping| {
178                let YubiHsm2UserMapping::Admin {
179                    authentication_key_id,
180                } = mapping
181                else {
182                    return None;
183                };
184                Some(Credentials::new(
185                    *authentication_key_id,
186                    Passphrase::generate(Some(
187                        YubiHsm2AdminCredentials::ADMIN_PASSPHRASE_POLICY.minimum_length,
188                    )),
189                ))
190            })
191            .collect::<Vec<_>>();
192
193        YubiHsm2AdminCredentials::new(
194            config.system().iteration(),
195            Passphrase::generate(Some(
196                YubiHsm2AdminCredentials::BACKUP_PASSPHRASE_POLICY.minimum_length,
197            )),
198            administrators,
199        )
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use std::collections::BTreeSet;
206
207    use signstar_crypto::{AdministrativeSecretHandling, NonAdministrativeSecretHandling};
208    #[cfg(feature = "_yubihsm2-mockhsm")]
209    use signstar_yubihsm2::Connection;
210    use testresult::TestResult;
211
212    use super::*;
213    use crate::config::{ConfigBuilder, SystemConfig};
214    #[cfg(feature = "_yubihsm2-mockhsm")]
215    use crate::yubihsm2::YubiHsm2Config;
216
217    #[test]
218    fn yubihsm2_admin_credentials_new_succeeds() -> TestResult {
219        let _creds = YubiHsm2AdminCredentials::new(
220            1,
221            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
222            vec![Credentials::new(
223                "1".parse()?,
224                Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string()),
225            )],
226        )?;
227
228        Ok(())
229    }
230
231    #[test]
232    fn yubihsm2_admin_credentials_new_fails_on_no_admins() -> TestResult {
233        match YubiHsm2AdminCredentials::new(
234            1,
235            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
236            Vec::new(),
237        ) {
238            Ok(creds) => {
239                panic!("Expected Error::AdministratorMissing but succeeded instead:\n{creds:?}")
240            }
241
242            Err(crate::Error::AdminSecretHandling(Error::AdministratorMissing)) => {}
243            Err(error) => panic!(
244                "Expected Error::AdministratorMissing but failed differently instead:\n{error}"
245            ),
246        }
247
248        Ok(())
249    }
250
251    #[test]
252    fn yubihsm2_admin_credentials_new_fails_on_admin_passphrase_too_short() -> TestResult {
253        match YubiHsm2AdminCredentials::new(
254            1,
255            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
256            vec![Credentials::new(
257                "1".parse()?,
258                Passphrase::new("short".to_string()),
259            )],
260        ) {
261            Ok(creds) => {
262                panic!("Expected Error::PassphraseTooShort but succeeded instead:\n{creds:?}")
263            }
264            Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(
265                signstar_crypto::passphrase::Error::Length { .. },
266            ))) => {}
267            Err(error) => panic!(
268                "Expected crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(
269                signstar_crypto::passphrase::Error::Length)) but failed differently instead:\n{error}"
270            ),
271        }
272
273        Ok(())
274    }
275
276    #[test]
277    fn yubihsm2_admin_credentials_new_fails_on_backup_passphrase_too_short() -> TestResult {
278        match YubiHsm2AdminCredentials::new(
279            1,
280            Passphrase::new("short".to_string()),
281            vec![Credentials::new(
282                "1".parse()?,
283                Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string()),
284            )],
285        ) {
286            Ok(creds) => {
287                panic!("Expected Error::PassphraseTooShort but succeeded instead:\n{creds:?}")
288            }
289            Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(
290                signstar_crypto::passphrase::Error::Length { .. },
291            ))) => {}
292            Err(error) => panic!(
293                "Expected crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(
294                signstar_crypto::passphrase::Error::Length)) but failed differently instead:\n{error}"
295            ),
296        }
297
298        Ok(())
299    }
300
301    /// Ensures, that creating [`YubiHsm2AdminCredentials`] from [`Config`] fails if it doesn't
302    /// contain a section for YubiHSM2 devices.
303    #[test]
304    fn yubihsm2_admin_credentials_try_from_config_fails_on_no_yubihsm2_config() -> TestResult {
305        let config = ConfigBuilder::new(SystemConfig::new(
306            1,
307            AdministrativeSecretHandling::Plaintext,
308            NonAdministrativeSecretHandling::Plaintext,
309            BTreeSet::new(),
310        )?)
311        .finish()?;
312
313        match YubiHsm2AdminCredentials::try_from(&config) {
314            Err(crate::Error::Config(crate::config::Error::YubiHsm2SectionMissing)) => {}
315            Err(error) => panic!(
316                "Expected to fail with Error::YubiHsm2SectionMissing but failed differently: {error}"
317            ),
318            Ok(creds) => panic!(
319                "Expected to fail with Error::YubiHsm2SectionMissing but succeeded instead: {creds:?}"
320            ),
321        }
322
323        Ok(())
324    }
325
326    /// Ensures, that creating [`YubiHsm2AdminCredentials`] from [`Config`] succeeds if it contains
327    /// a section for YubiHSM2 devices.
328    #[cfg(feature = "_yubihsm2-mockhsm")]
329    #[test]
330    fn yubihsm2_admin_credentials_try_from_config_succeeds() -> TestResult {
331        let config = ConfigBuilder::new(SystemConfig::new(
332            1,
333            AdministrativeSecretHandling::Plaintext,
334            NonAdministrativeSecretHandling::Plaintext,
335            BTreeSet::new(),
336        )?)
337        .set_yubihsm2_config(YubiHsm2Config::new(
338            BTreeSet::from_iter([Connection::Mock]),
339            BTreeSet::from_iter([YubiHsm2UserMapping::Admin {
340                authentication_key_id: 1,
341            }]),
342        )?)
343        .finish()?;
344
345        let _ = YubiHsm2AdminCredentials::try_from(&config)?;
346
347        Ok(())
348    }
349}