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}