Skip to main content

signstar_config/nethsm/
admin_credentials.rs

1//! Administrative credentials for [`NetHsm`] backends.
2
3use log::{info, warn};
4use nethsm::{FullCredentials, Passphrase};
5#[cfg(doc)]
6use nethsm::{NetHsm, UserId};
7use serde::{Deserialize, Serialize};
8use signstar_crypto::passphrase::PassphrasePolicy;
9
10use crate::{
11    admin_credentials::{AdminCredentials, Error},
12    config::Config,
13    nethsm::{NetHsmConfig, NetHsmUserMapping},
14};
15
16/// Administrative credentials.
17///
18/// Tracks the following credentials and passphrases:
19/// - the backup passphrase of the backend,
20/// - the unlock passphrase of the backend,
21/// - the top-level administrator credentials of the backend,
22/// - the namespace administrator credentials of the backend.
23///
24/// # Note
25///
26/// The unlock and backup passphrase must be at least 10 characters long.
27/// The passphrases of top-level and namespace administrator accounts must be at least 10 characters
28/// long.
29/// The list of top-level administrator credentials must include an account with the username
30/// "admin".
31#[derive(Clone, Debug, Default, Deserialize, Serialize)]
32pub struct NetHsmAdminCredentials {
33    iteration: u32,
34    backup_passphrase: Passphrase,
35    unlock_passphrase: Passphrase,
36    administrators: Vec<FullCredentials>,
37    namespace_administrators: Vec<FullCredentials>,
38}
39
40impl NetHsmAdminCredentials {
41    /// The default [`PassphrasePolicy`] for a backup passphrase.
42    pub const BACKUP_PASSPHRASE_POLICY: PassphrasePolicy = PassphrasePolicy { minimum_length: 30 };
43
44    /// The default [`PassphrasePolicy`] for a backup passphrase.
45    pub const UNLOCK_PASSPHRASE_POLICY: PassphrasePolicy = PassphrasePolicy { minimum_length: 30 };
46
47    /// The default [`PassphrasePolicy`] for an admin passphrase.
48    pub const ADMIN_PASSPHRASE_POLICY: PassphrasePolicy = PassphrasePolicy { minimum_length: 30 };
49
50    /// Creates a new [`NetHsmAdminCredentials`] instance.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use nethsm::FullCredentials;
56    /// use signstar_config::nethsm::NetHsmAdminCredentials;
57    ///
58    /// # fn main() -> testresult::TestResult {
59    /// let creds = NetHsmAdminCredentials::new(
60    ///     1,
61    ///     "backup-passphrase-really-just-for-testing-i-promise".parse()?,
62    ///     "unlock-passphrase-really-just-for-testing-i-promise".parse()?,
63    ///     vec![FullCredentials::new(
64    ///         "admin".parse()?,
65    ///         "admin-passphrase-really-just-for-testing-i-promise".parse()?,
66    ///     )],
67    ///     vec![FullCredentials::new(
68    ///         "ns1~admin".parse()?,
69    ///         "ns1-admin-passphrase-really-just-for-testing-i-promise".parse()?,
70    ///     )],
71    /// )?;
72    /// # // the backup passphrase is too short
73    /// # assert!(NetHsmAdminCredentials::new(
74    /// #     1,
75    /// #     "short".parse()?,
76    /// #     "unlock-passphrase-really-just-for-testing-i-promise".parse()?,
77    /// #     vec![FullCredentials::new(
78    /// #         "admin".parse()?,
79    /// #         "admin-passphrase-really-just-for-testing-i-promise".parse()?,
80    /// #     )],
81    /// #     vec![FullCredentials::new(
82    /// #         "ns1~admin".parse()?,
83    /// #         "ns1-admin-passphrase-really-just-for-testing-i-promise".parse()?,
84    /// #     )],
85    /// # ).is_err());
86    /// #
87    /// # // the unlock passphrase is too short
88    /// # assert!(NetHsmAdminCredentials::new(
89    /// #     1,
90    /// #     "backup-passphrase-really-just-for-testing-i-promise".parse()?,
91    /// #     "short".parse()?,
92    /// #     vec![FullCredentials::new(
93    /// #         "admin".parse()?,
94    /// #         "admin-passphrase-really-just-for-testing-i-promise".parse()?,
95    /// #     )],
96    /// #     vec![FullCredentials::new(
97    /// #         "ns1~admin".parse()?,
98    /// #         "ns1-admin-passphrase-really-just-for-testing-i-promise".parse()?,
99    /// #     )],
100    /// # ).is_err());
101    /// #
102    /// # // there is no top-level administrator
103    /// # assert!(NetHsmAdminCredentials::new(
104    /// #     1,
105    /// #     "backup-passphrase-really-just-for-testing-i-promise".parse()?,
106    /// #     "unlock-passphrase-really-just-for-testing-i-promise".parse()?,
107    /// #     Vec::new(),
108    /// #     vec![FullCredentials::new(
109    /// #         "ns1~admin".parse()?,
110    /// #         "ns1-admin-passphrase-really-just-for-testing-i-promise".parse()?,
111    /// #     )],
112    /// # ).is_err());
113    /// #
114    /// # // there is no default top-level default administrator
115    /// # assert!(NetHsmAdminCredentials::new(
116    /// #     1,
117    /// #     "backup-passphrase-really-just-for-testing-i-promise".parse()?,
118    /// #     "unlock-passphrase-really-just-for-testing-i-promise".parse()?,
119    /// #     vec![FullCredentials::new(
120    /// #         "some".parse()?,
121    /// #         "admin-passphrase-really-just-for-testing-i-promise".parse()?,
122    /// #     )],
123    /// #     vec![FullCredentials::new(
124    /// #         "ns1~admin".parse()?,
125    /// #         "ns1-admin-passphrase-really-just-for-testing-i-promise".parse()?,
126    /// #     )],
127    /// # ).is_err());
128    /// #
129    /// # // a top-level administrator passphrase is too short
130    /// # assert!(NetHsmAdminCredentials::new(
131    /// #     1,
132    /// #     "backup-passphrase-really-just-for-testing-i-promise".parse()?,
133    /// #     "unlock-passphrase-really-just-for-testing-i-promise".parse()?,
134    /// #     vec![FullCredentials::new("admin".parse()?, "short".parse()?)],
135    /// #     vec![FullCredentials::new(
136    /// #         "ns1~admin".parse()?,
137    /// #         "ns1-admin-passphrase-really-just-for-testing-i-promise".parse()?,
138    /// #     )],
139    /// # ).is_err());
140    /// #
141    /// # // a namespace administrator passphrase is too short
142    /// # assert!(NetHsmAdminCredentials::new(
143    /// #     1,
144    /// #     "backup-passphrase-really-just-for-testing-i-promise".parse()?,
145    /// #     "unlock-passphrase-really-just-for-testing-i-promise".parse()?,
146    /// #     vec![FullCredentials::new(
147    /// #         "some".parse()?,
148    /// #         "admin-passphrase-really-just-for-testing-i-promise".parse()?,
149    /// #     )],
150    /// #     vec![FullCredentials::new(
151    /// #         "ns1~admin".parse()?,
152    /// #         "short".parse()?,
153    /// #     )],
154    /// # ).is_err());
155    /// # Ok(())
156    /// # }
157    /// ```
158    pub fn new(
159        iteration: u32,
160        backup_passphrase: Passphrase,
161        unlock_passphrase: Passphrase,
162        administrators: Vec<FullCredentials>,
163        namespace_administrators: Vec<FullCredentials>,
164    ) -> Result<Self, crate::Error> {
165        let admin_credentials = Self {
166            iteration,
167            backup_passphrase,
168            unlock_passphrase,
169            administrators,
170            namespace_administrators,
171        };
172        admin_credentials.validate()?;
173
174        Ok(admin_credentials)
175    }
176
177    /// Returns the unlock passphrase.
178    pub fn unlock_passphrase(&self) -> &Passphrase {
179        &self.unlock_passphrase
180    }
181
182    /// Returns the list of administrators.
183    pub fn administrators(&self) -> &[FullCredentials] {
184        &self.administrators
185    }
186
187    /// Returns the default system-wide administrator "admin".
188    ///
189    /// # Errors
190    ///
191    /// Returns an error if no administrative account with the system-wide [`UserId`] "admin" is
192    /// found.
193    pub fn default_administrator(&self) -> Result<&FullCredentials, crate::Error> {
194        let Some(first_admin) = self
195            .administrators
196            .iter()
197            .find(|user| user.name.to_string() == "admin")
198        else {
199            return Err(Error::AdministratorNoDefault.into());
200        };
201        Ok(first_admin)
202    }
203
204    /// Returns the list of namespace administrators.
205    pub fn namespace_administrators(&self) -> &[FullCredentials] {
206        &self.namespace_administrators
207    }
208
209    /// Returns the list of system-wide administrators, also present in a [`NetHsmConfig`].
210    ///
211    /// Retrieves the list of [`NetHsmUserMapping`] instances that represent system-wide
212    /// administrators from `config`.
213    /// Filters out all [`UserId`]s that cannot be matched and emits warnings for all unmatched
214    /// ones.
215    pub fn administrators_in_config(&self, config: &NetHsmConfig) -> Vec<&FullCredentials> {
216        let user_mappings = config
217            .mappings()
218            .iter()
219            .filter(|mapping| matches!(mapping, NetHsmUserMapping::Admin(..)))
220            .collect::<Vec<_>>();
221        // Only use administrative credentials that are also available in the NetHSM config.
222        {
223            let mut user_list = Vec::new();
224
225            for creds in self.administrators() {
226                if !user_mappings
227                    .iter()
228                    .any(|user_mapping| user_mapping.nethsm_user_ids().contains(&creds.name))
229                {
230                    warn!(
231                        "The administrative credentials for system-wide administrator {} are skipped because the user is not found in the Signstar configuration.",
232                        creds.name
233                    );
234                    continue;
235                }
236                user_list.push(creds);
237            }
238            // The available user IDs.
239            let available_users = user_list
240                .iter()
241                .map(|creds| &creds.name)
242                .collect::<Vec<_>>();
243
244            let unmatched_config_users = user_mappings
245                .iter()
246                .flat_map(|user_mapping| {
247                    user_mapping
248                        .nethsm_user_ids()
249                        .iter()
250                        .filter(|user_id| !available_users.contains(user_id))
251                        .cloned()
252                        .collect::<Vec<_>>()
253                })
254                .collect::<Vec<_>>();
255            if !unmatched_config_users.is_empty() {
256                warn!(
257                    "The following system-wide administrators (R-Administrators) in the Signstar configuration are skipped, because they cannot be found in the provided administrative credentials: {}",
258                    unmatched_config_users
259                        .iter()
260                        .map(ToString::to_string)
261                        .collect::<Vec<_>>()
262                        .join(", ")
263                );
264            }
265
266            user_list
267        }
268    }
269
270    /// Returns the list of namespace administrators, also present in a [`NetHsmConfig`].
271    ///
272    /// Retrieves the list of [`NetHsmUserMapping`] instances that represent namespace
273    /// administrators from `config`.
274    /// Filters out all [`UserId`]s that cannot be matched and emits warnings for all unmatched
275    /// ones.
276    pub fn namespace_administrators_in_config(
277        &self,
278        config: &NetHsmConfig,
279    ) -> Vec<&FullCredentials> {
280        // The list of namespace administrators.
281        let user_mappings = config
282            .mappings()
283            .iter()
284            .filter(|mapping| {
285                if let NetHsmUserMapping::Admin(user_id) = mapping {
286                    user_id.is_namespaced()
287                } else {
288                    false
289                }
290            })
291            .collect::<Vec<_>>();
292        // Only use administrative credentials that are also available in the NetHSM config.
293        {
294            let mut user_list = Vec::new();
295
296            for creds in self.namespace_administrators() {
297                if !user_mappings
298                    .iter()
299                    .any(|user_mapping| user_mapping.nethsm_user_ids().contains(&creds.name))
300                {
301                    warn!(
302                        "The administrative credentials for namespace administrator (N-Administrator) {} are skipped because the user is not found in the Signstar configuration.",
303                        creds.name
304                    );
305                    continue;
306                }
307                user_list.push(creds);
308            }
309            // The available user IDs.
310            let available_users = user_list
311                .iter()
312                .map(|creds| &creds.name)
313                .collect::<Vec<_>>();
314
315            let unmatched_config_users = user_mappings
316                .iter()
317                .flat_map(|user_mapping| {
318                    user_mapping
319                        .nethsm_user_ids()
320                        .iter()
321                        .filter(|user_id| !available_users.contains(user_id))
322                        .cloned()
323                        .collect::<Vec<_>>()
324                })
325                .collect::<Vec<_>>();
326            if !unmatched_config_users.is_empty() {
327                warn!(
328                    "The following namespace administrators (N-Administrators) in the Signstar configuration are skipped, because they cannot be found in the provided administrative credentials: {}",
329                    unmatched_config_users
330                        .iter()
331                        .map(ToString::to_string)
332                        .collect::<Vec<_>>()
333                        .join(", ")
334                );
335            }
336
337            user_list
338        }
339    }
340}
341
342impl AdminCredentials for NetHsmAdminCredentials {
343    /// Validates the [`NetHsmAdminCredentials`].
344    ///
345    /// # Errors
346    ///
347    /// Returns an error if
348    /// - there is no top-level administrator user,
349    /// - the default top-level administrator user (with the name "admin") is missing,
350    /// - a user passphrase is too short,
351    /// - the backup passphrase is too short,
352    /// - or the unlock passphrase is too short.
353    fn validate(&self) -> Result<(), crate::Error> {
354        // there is no top-level administrator user
355        if self.administrators().is_empty() {
356            return Err(crate::Error::AdminSecretHandling(
357                Error::AdministratorMissing,
358            ));
359        }
360
361        // there is no top-level administrator user with the name "admin"
362        if !self
363            .administrators()
364            .iter()
365            .any(|user| user.name.to_string() == "admin")
366        {
367            return Err(crate::Error::AdminSecretHandling(
368                Error::AdministratorNoDefault,
369            ));
370        }
371
372        // a top-level administrator user passphrase is too short
373        for user in self.administrators().iter() {
374            user.passphrase
375                .check_against_policy(&Self::ADMIN_PASSPHRASE_POLICY)?;
376        }
377
378        // a namespace administrator user passphrase is too short
379        for user in self.namespace_administrators().iter() {
380            user.passphrase
381                .check_against_policy(&Self::ADMIN_PASSPHRASE_POLICY)?;
382        }
383
384        // the backup passphrase is too short
385        self.backup_passphrase()
386            .check_against_policy(&Self::BACKUP_PASSPHRASE_POLICY)?;
387
388        // the unlock passphrase is too short
389        self.unlock_passphrase()
390            .check_against_policy(&Self::UNLOCK_PASSPHRASE_POLICY)?;
391
392        Ok(())
393    }
394
395    /// Returns the iteration of the administrative credentials.
396    fn iteration(&self) -> u32 {
397        self.iteration
398    }
399
400    /// Returns the backup passphrase.
401    fn backup_passphrase(&self) -> &Passphrase {
402        &self.backup_passphrase
403    }
404}
405
406impl TryFrom<&Config> for NetHsmAdminCredentials {
407    type Error = crate::Error;
408
409    /// Creates a new [`NetHsmAdminCredentials`] from a [`Config`].
410    ///
411    /// # Note
412    ///
413    /// This generates a new backup passphrase and administrative passphrases, adhering to the
414    /// hardcoded passphrase policies (e.g. minimum length).
415    ///
416    /// # Errors
417    ///
418    /// Returns an error, if
419    ///
420    /// - `config` does not contain a [`NetHsmConfig`][`crate::nethsm::NetHsmConfig`]
421    /// - [`NetHsmAdminCredentials::new`] fails on the generated data
422    fn try_from(config: &Config) -> Result<Self, Self::Error> {
423        info!(
424            "Create new administrative credentials for NetHSM based on the Signstar configuration."
425        );
426
427        let Some(nethsm_config) = config.nethsm() else {
428            return Err(crate::config::Error::NetHsmSectionMissing.into());
429        };
430
431        let administrators = nethsm_config
432            .mappings()
433            .iter()
434            .filter_map(|mapping| {
435                if let NetHsmUserMapping::Admin(user_id) = mapping
436                    && !user_id.is_namespaced()
437                {
438                    Some(FullCredentials::new(
439                        user_id.clone(),
440                        Passphrase::generate(Some(Self::ADMIN_PASSPHRASE_POLICY.minimum_length)),
441                    ))
442                } else {
443                    None
444                }
445            })
446            .collect::<Vec<_>>();
447        let namespace_administrators = nethsm_config
448            .mappings()
449            .iter()
450            .filter_map(|mapping| {
451                if let NetHsmUserMapping::Admin(user_id) = mapping
452                    && user_id.is_namespaced()
453                {
454                    Some(FullCredentials::new(
455                        user_id.clone(),
456                        Passphrase::generate(Some(Self::ADMIN_PASSPHRASE_POLICY.minimum_length)),
457                    ))
458                } else {
459                    None
460                }
461            })
462            .collect::<Vec<_>>();
463
464        Self::new(
465            config.system().iteration(),
466            Passphrase::generate(Some(Self::BACKUP_PASSPHRASE_POLICY.minimum_length)),
467            Passphrase::generate(Some(Self::UNLOCK_PASSPHRASE_POLICY.minimum_length)),
468            administrators,
469            namespace_administrators,
470        )
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use std::{collections::BTreeSet, str::FromStr};
477
478    use nethsm::{Connection, ConnectionSecurity, UserId};
479    use rstest::{fixture, rstest};
480    use signstar_crypto::{AdministrativeSecretHandling, NonAdministrativeSecretHandling};
481    use testresult::TestResult;
482
483    use super::*;
484    use crate::config::{ConfigBuilder, SystemConfig};
485
486    #[fixture]
487    fn nethsm_admin_credentials() -> TestResult<NetHsmAdminCredentials> {
488        Ok(NetHsmAdminCredentials::new(
489            1,
490            "backup-passphrase-really-just-for-testing-i-promise".parse()?,
491            "unlock-passphrase-really-just-for-testing-i-promise".parse()?,
492            vec![
493                FullCredentials::new(
494                    "admin".parse()?,
495                    "admin-passphrase-really-just-for-testing-i-promise".parse()?,
496                ),
497                FullCredentials::new(
498                    "admin2".parse()?,
499                    "admin2-passphrase-really-just-for-testing-i-promise".parse()?,
500                ),
501            ],
502            vec![
503                FullCredentials::new(
504                    "ns1~admin".parse()?,
505                    "ns1~admin-passphrase-really-just-for-testing-i-promise".parse()?,
506                ),
507                FullCredentials::new(
508                    "ns1~admin2".parse()?,
509                    "ns1~admin2-passphrase-really-just-for-testing-i-promise".parse()?,
510                ),
511            ],
512        )?)
513    }
514
515    #[fixture]
516    fn nethsm_config() -> TestResult<NetHsmConfig> {
517        Ok(NetHsmConfig::new(
518            BTreeSet::from_iter([Connection::new(
519                "https://nethsm1.example.org/".parse()?,
520                nethsm::ConnectionSecurity::Unsafe,
521            )]),
522            BTreeSet::from_iter([
523                NetHsmUserMapping::Admin("admin".parse()?),
524                NetHsmUserMapping::Admin("ns1~admin".parse()?),
525            ]),
526        )?)
527    }
528
529    #[rstest]
530    fn nethsm_admin_credentials_administrators_in_config(
531        nethsm_admin_credentials: TestResult<NetHsmAdminCredentials>,
532        nethsm_config: TestResult<NetHsmConfig>,
533    ) -> TestResult {
534        let nethsm_admin_credentials = nethsm_admin_credentials?;
535        let nethsm_config = nethsm_config?;
536        let users = nethsm_admin_credentials
537            .administrators_in_config(&nethsm_config)
538            .iter()
539            .map(|creds| creds.name.clone())
540            .collect::<Vec<_>>();
541
542        assert_eq!(users, vec![UserId::from_str("admin")?]);
543
544        Ok(())
545    }
546
547    #[rstest]
548    fn nethsm_admin_credentials_namespace_administrators_in_config(
549        nethsm_admin_credentials: TestResult<NetHsmAdminCredentials>,
550        nethsm_config: TestResult<NetHsmConfig>,
551    ) -> TestResult {
552        let nethsm_admin_credentials = nethsm_admin_credentials?;
553        let nethsm_config = nethsm_config?;
554        let users = nethsm_admin_credentials
555            .namespace_administrators_in_config(&nethsm_config)
556            .iter()
557            .map(|creds| creds.name.clone())
558            .collect::<Vec<_>>();
559
560        assert_eq!(users, vec![UserId::from_str("ns1~admin")?]);
561
562        Ok(())
563    }
564
565    /// Ensures, that creating [`NetHsmAdminCredentials`] from [`Config`] fails if it doesn't
566    /// contain a section for NetHSM devices.
567    #[test]
568    fn nethsm_admin_credentials_try_from_config_fails_on_no_nethsm_config() -> TestResult {
569        let config = ConfigBuilder::new(SystemConfig::new(
570            1,
571            AdministrativeSecretHandling::Plaintext,
572            NonAdministrativeSecretHandling::Plaintext,
573            BTreeSet::new(),
574        )?)
575        .finish()?;
576
577        match NetHsmAdminCredentials::try_from(&config) {
578            Err(crate::Error::Config(crate::config::Error::NetHsmSectionMissing)) => {}
579            Err(error) => panic!(
580                "Expected to fail with Error::NetHsmSectionMissing but failed differently: {error}"
581            ),
582            Ok(creds) => panic!(
583                "Expected to fail with Error::NetHsmSectionMissing but succeeded instead: {creds:?}"
584            ),
585        }
586
587        Ok(())
588    }
589
590    /// Ensures, that creating [`NetHsmAdminCredentials`] from [`Config`] succeeds if it contains
591    /// a section for NetHSM devices.
592    #[rstest]
593    #[case::only_system_wide_admin(ConfigBuilder::new(SystemConfig::new(
594            1,
595            AdministrativeSecretHandling::Plaintext,
596            NonAdministrativeSecretHandling::Plaintext,
597            BTreeSet::new(),
598        )?)
599        .set_nethsm_config(NetHsmConfig::new(
600            BTreeSet::from_iter([Connection::new(
601                "https://localhost".parse()?,
602                ConnectionSecurity::Unsafe,
603            )]),
604            BTreeSet::from_iter([
605                NetHsmUserMapping::Admin("admin".parse()?),
606            ]),
607        )?)
608        .finish()?)]
609    #[case::system_wide_and_namespace_admins(ConfigBuilder::new(SystemConfig::new(
610            1,
611            AdministrativeSecretHandling::Plaintext,
612            NonAdministrativeSecretHandling::Plaintext,
613            BTreeSet::new(),
614        )?)
615        .set_nethsm_config(NetHsmConfig::new(
616            BTreeSet::from_iter([Connection::new(
617                "https://localhost".parse()?,
618                ConnectionSecurity::Unsafe,
619            )]),
620            BTreeSet::from_iter([
621                NetHsmUserMapping::Admin("admin".parse()?),
622                NetHsmUserMapping::Admin("ns1~admin".parse()?),
623            ]),
624        )?)
625        .finish()?)]
626    fn nethsm_admin_credentials_try_from_config_succeeds(#[case] config: Config) -> TestResult {
627        let _ = NetHsmAdminCredentials::try_from(&config)?;
628
629        Ok(())
630    }
631}