signstar_config/yubihsm2/
config.rs

1//! YubiHSM2 specific integration for the [`crate::config`] module.
2#![cfg(feature = "yubihsm2")]
3
4use std::collections::{BTreeSet, HashSet};
5
6use garde::Validate;
7use serde::{Deserialize, Serialize};
8use signstar_crypto::{key::SigningKeySetup, passphrase::Passphrase, traits::UserWithPassphrase};
9use signstar_yubihsm2::{
10    Credentials,
11    object::{Domain, Id},
12};
13
14use crate::{
15    AuthorizedKeyEntry,
16    SystemUserId,
17    config::{
18        BackendDomainFilter,
19        BackendKeyIdFilter,
20        BackendUserIdFilter,
21        BackendUserIdKind,
22        ConfigAuthorizedKeyEntries,
23        ConfigSystemUserIds,
24        MappingAuthorizedKeyEntry,
25        MappingBackendDomain,
26        MappingBackendKeyId,
27        MappingBackendUserIds,
28        MappingBackendUserSecrets,
29        MappingSystemUserId,
30        duplicate_authorized_keys,
31        duplicate_backend_user_ids,
32        duplicate_domains,
33        duplicate_key_ids,
34        duplicate_system_user_ids,
35    },
36    yubihsm2::backend::YubiHsmConnection,
37};
38
39/// An error that may occur when using YubiHSM2 config objects.
40#[derive(Debug, thiserror::Error)]
41pub enum Error {
42    /// An authentication key ID does not match an expectation.
43    #[error("Expected the YubiHSM2 authentication key ID {expected}, but found {actual} instead")]
44    AuthenticationKeyIdMismatch {
45        /// The expected authentication key ID.
46        expected: String,
47
48        /// The actually found authentication key ID.
49        actual: String,
50    },
51
52    /// An invalid key domain.
53    #[error("Error while constructing a YubiHSM2 key domain from {key_domain}, because {reason}")]
54    InvalidDomain {
55        /// The reason why the key domain is invalid.
56        ///
57        /// This is meant to complete the sentence "Error while constructing a YubiHSM2 key domain
58        /// from {key_domain}, because ".
59        reason: String,
60
61        /// The invalid key domain.
62        key_domain: String,
63    },
64}
65
66/// User and data mapping between system users and YubiHSM2 users.
67#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
68#[serde(rename_all = "snake_case")]
69pub enum YubiHsm2UserMapping {
70    /// A YubiHSM2 user in the administrator role, without a system user mapped to it.
71    ///
72    /// Tracks an [authentication key object] with a specific `authentication_key_id`.
73    ///
74    /// # Note
75    ///
76    /// This variant implies, that the created [authentication key object] has all relevant
77    /// [capabilities] necessary for the creation of users and keys and to restore from backup,
78    /// i.e.:
79    ///
80    /// - `delete-asymmetric-key`
81    /// - `generate-asymmetric-key`
82    /// - `put-asymmetric-key`
83    /// - `delete-authentication-key`
84    /// - `put-authentication-key`
85    /// - `change-authentication-key`
86    /// - `get-option`
87    /// - `set-option`
88    /// - `delete-hmac-key`
89    /// - `generate-hmac-key`
90    /// - `put-mac-key`
91    /// - `sign-hmac`
92    /// - `verify-hmac`
93    /// - `delete-opaque`
94    /// - `generate-opaque`
95    /// - `get-opaque`
96    /// - `put-opaque`
97    /// - `reset-device`
98    /// - `delete-template`
99    /// - `get-template`
100    /// - `put-template`
101    /// - `delete-wrap-key`
102    /// - `exportable-under-wrap`
103    /// - `generate-wrap-key`
104    /// - `import-wrapped`
105    /// - `put-wrap-key`
106    /// - `unwrap-data`
107    /// - `wrap-data`
108    /// - `put-public-wrap-key`
109    /// - `delete-public-wrap-key`
110    /// - `generate-symmetric-key`
111    /// - `put-symmetric-key`
112    /// - `delete-symmetric-key`
113    ///
114    /// Further, it is assumed that the [authentication key object] is added to all [domains].
115    ///
116    /// [authentication key object]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#authentication-key-object
117    /// [capabilities]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#capability-protocol-details
118    /// [domains]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#domains
119    Admin {
120        /// The identifier of the authentication key used to create a session with the YubiHSM2.
121        authentication_key_id: Id,
122    },
123
124    /// A system user, with SSH access, mapped to a YubiHSM2 authentication key.
125    ///
126    /// This variant tracks
127    ///
128    /// - an [authentication key object] with a specific `authentication_key_id`
129    /// - an SSH authorized key with a specific `ssh_authorized_key`
130    /// - a system user ID using `system_user`
131    ///
132    /// Its data is used to create relevant system and backend users for the retrieval of audit logs
133    /// over the network, made available by the YubiHSM2.
134    ///
135    /// # Note
136    ///
137    /// This variant implies, that the created [authentication key object] has all relevant
138    /// [capabilities] for audit log retrieval (i.e. `get-log-entries`).
139    ///
140    /// [authentication key object]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#authentication-key-object
141    /// [capabilities]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#capability-protocol-details
142    AuditLog {
143        /// The identifier of the authentication key used to create a session with the YubiHSM2.
144        authentication_key_id: Id,
145
146        /// The SSH public key used for connecting to the `system_user`.
147        ssh_authorized_key: AuthorizedKeyEntry,
148
149        /// The name of the system user.
150        system_user: SystemUserId,
151    },
152
153    /// A mapping used for the creation of YubiHSM2 backups.
154    ///
155    /// This variant tracks
156    ///
157    /// - an [authentication key object] with a specific `authentication_key_id`
158    /// - a [wrap key object] with a specific `wrapping_key_id`
159    /// - an SSH authorized key with a specific `ssh_authorized_key`
160    /// - a system user ID using `system_user`
161    ///
162    /// Its data is used to create relevant system and backend users for the creation of backups of
163    /// all keys (including [authentication key object]s) and non-key material (e.g. OpenPGP
164    /// certificates) of a YubiHSM2.
165    ///
166    /// # Note
167    ///
168    /// This variant implies, that the created [authentication key object] has all relevant
169    /// [capabilities] for backup related actions (i.e. `export-wrapped`, `wrap-data`).
170    ///
171    /// Further, it is assumed that both the [authentication key object] and [wrap key object] are
172    /// added to all [domains].
173    ///
174    /// [capabilities]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#capability-protocol-details
175    /// [domains]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#domains
176    /// [wrap key object]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#hsm2-wrap-key-obj
177    Backup {
178        /// The identifier of the authentication key used to create a session with the YubiHSM2.
179        ///
180        /// This represents an [authentication key object].
181        ///
182        /// [authentication key object]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#authentication-key-object
183        authentication_key_id: Id,
184
185        /// The identifier of the wrapping key in the YubiHSM2 backend.
186        ///
187        /// This identifies the encryption key used for wrapping backups of all keys of the
188        /// YubiHSM2.
189        ///
190        /// # Note
191        ///
192        /// The wrapping key is automatically added to all [domains].
193        ///
194        /// [domains]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#domains
195        wrapping_key_id: Id,
196
197        /// The SSH public key used for connecting to the `system_user`.
198        ssh_authorized_key: AuthorizedKeyEntry,
199
200        /// The name of the system user.
201        system_user: SystemUserId,
202    },
203
204    /// A system user, without SSH access, mapped to a YubiHSM2 authentication key for collecting
205    /// audit logs.
206    ///
207    /// This variant tracks
208    ///
209    /// - an [authentication key object] with a specific `authentication_key_id`
210    /// - a system user ID using `system_user`
211    ///
212    /// Its data is used to create relevant system and backend users for the retrieval of audit logs
213    /// made available by the YubiHSM2.
214    ///
215    /// # Note
216    ///
217    /// This variant implies, that the created [authentication key object] has all relevant
218    /// [capabilities] for audit log retrieval (i.e. `get-log-entries`).
219    ///
220    /// [authentication key object]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#authentication-key-object
221    /// [capabilities]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#capability-protocol-details
222    HermeticAuditLog {
223        /// The identifier of the authentication key used to create a session with the YubiHSM2.
224        authentication_key_id: Id,
225
226        /// The name of the system user.
227        system_user: SystemUserId,
228    },
229
230    /// A system user, with SSH access, mapped to a YubiHSM2 user in the
231    /// Operator role with access to a single signing key.
232    ///
233    /// This variant tracks
234    ///
235    /// - an [authentication key object] identified by an `authentication_key_id`
236    /// - a [domain] (`domain`) assigned to both objects identified by `authentication_key_id` and
237    ///   `signing_key_id`
238    /// - a [`SigningKeySetup`] using `key_setup`
239    /// - an [asymmetric key object] identified by a `signing_key_id`
240    /// - an SSH authorized key (`ssh_authorized_key`) for a `system_user`
241    /// - a system user ID (`system_user`)
242    ///
243    /// Its data is used to create relevant system and backend users for the creation of backups of
244    /// all keys (including [authentication key object]s) and non-key material (e.g. OpenPGP
245    /// certificates) of a YubiHSM2.
246    ///
247    /// # Note
248    ///
249    /// This variant implies, that the created [authentication key object] has all relevant
250    /// [capabilities] for signing with the [asymmetric key object] (i.e. `sign-ecdsa`,
251    /// `sign-eddsa`, `sign-pkcs` and `sign-pss`).
252    ///
253    /// Further, it is assumed that both the [authentication key object] and [asymmetric key object]
254    /// are added to the single [domain] `domain`.
255    ///
256    /// [asymmetric key object]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#asymmetric-key-object
257    /// [authentication key object]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#authentication-key-object
258    /// [capabilities]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#capability-protocol-details
259    /// [domain]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#domains
260    Signing {
261        /// The identifier of the authentication key used to create a session with the YubiHSM2.
262        authentication_key_id: Id,
263
264        /// The setup of a YubiHSM2 key.
265        key_setup: SigningKeySetup,
266
267        /// The [domain] the signing and authentication key belong to.
268        ///
269        /// [domain]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#domains
270        domain: Domain,
271
272        /// The identifier of the signing key in the YubiHSM2 backend.
273        signing_key_id: Id,
274
275        /// The SSH public key used for connecting to the `system_user`.
276        ssh_authorized_key: AuthorizedKeyEntry,
277
278        /// The name of the system user.
279        system_user: SystemUserId,
280    },
281}
282
283impl YubiHsm2UserMapping {
284    /// Returns the optional [`Domain`] of the [`YubiHsm2UserMapping`].
285    pub fn domain(&self) -> Option<&Domain> {
286        match self {
287            Self::Admin { .. }
288            | Self::Backup { .. }
289            | Self::AuditLog { .. }
290            | Self::HermeticAuditLog { .. } => None,
291            Self::Signing {
292                domain: key_domain, ..
293            } => Some(key_domain),
294        }
295    }
296
297    /// Returns the authentication key ID of the [`YubiHsm2UserMapping`].
298    pub fn backend_user_id(&self) -> Id {
299        match self {
300            Self::Admin {
301                authentication_key_id,
302            }
303            | Self::AuditLog {
304                authentication_key_id,
305                ..
306            }
307            | Self::Backup {
308                authentication_key_id,
309                ..
310            }
311            | Self::HermeticAuditLog {
312                authentication_key_id,
313                ..
314            }
315            | Self::Signing {
316                authentication_key_id,
317                ..
318            } => *authentication_key_id,
319        }
320    }
321}
322
323impl MappingSystemUserId for YubiHsm2UserMapping {
324    fn system_user_id(&self) -> Option<&SystemUserId> {
325        match self {
326            Self::Admin { .. } => None,
327            Self::AuditLog { system_user, .. }
328            | Self::Backup { system_user, .. }
329            | Self::HermeticAuditLog { system_user, .. }
330            | Self::Signing { system_user, .. } => Some(system_user),
331        }
332    }
333}
334
335impl MappingBackendUserIds for YubiHsm2UserMapping {
336    fn backend_user_ids(&self, filter: BackendUserIdFilter) -> Vec<String> {
337        match self {
338            Self::Admin {
339                authentication_key_id,
340            } => {
341                if [BackendUserIdKind::Admin, BackendUserIdKind::Any]
342                    .contains(&filter.backend_user_id_kind)
343                {
344                    Some(vec![authentication_key_id.to_string()])
345                } else {
346                    None
347                }
348            }
349            Self::AuditLog {
350                authentication_key_id,
351                ..
352            } => {
353                if [
354                    BackendUserIdKind::Any,
355                    BackendUserIdKind::Metrics,
356                    BackendUserIdKind::NonAdmin,
357                ]
358                .contains(&filter.backend_user_id_kind)
359                {
360                    Some(vec![authentication_key_id.to_string()])
361                } else {
362                    None
363                }
364            }
365            Self::Backup {
366                authentication_key_id,
367                ..
368            } => {
369                if [
370                    BackendUserIdKind::Any,
371                    BackendUserIdKind::Backup,
372                    BackendUserIdKind::NonAdmin,
373                ]
374                .contains(&filter.backend_user_id_kind)
375                {
376                    Some(vec![authentication_key_id.to_string()])
377                } else {
378                    None
379                }
380            }
381            Self::HermeticAuditLog {
382                authentication_key_id,
383                ..
384            } => {
385                if [
386                    BackendUserIdKind::Any,
387                    BackendUserIdKind::Metrics,
388                    BackendUserIdKind::NonAdmin,
389                ]
390                .contains(&filter.backend_user_id_kind)
391                {
392                    Some(vec![authentication_key_id.to_string()])
393                } else {
394                    None
395                }
396            }
397            Self::Signing {
398                authentication_key_id,
399                ..
400            } => {
401                if [
402                    BackendUserIdKind::Any,
403                    BackendUserIdKind::NonAdmin,
404                    BackendUserIdKind::Signing,
405                ]
406                .contains(&filter.backend_user_id_kind)
407                {
408                    Some(vec![authentication_key_id.to_string()])
409                } else {
410                    None
411                }
412            }
413        }
414        .unwrap_or_default()
415    }
416
417    fn backend_user_with_passphrase(
418        &self,
419        name: &str,
420        passphrase: Passphrase,
421    ) -> Result<Box<dyn UserWithPassphrase>, crate::Error> {
422        let backend_user_id = self.backend_user_id();
423        if backend_user_id.to_string() != name {
424            return Err(Error::AuthenticationKeyIdMismatch {
425                expected: name.to_string(),
426                actual: backend_user_id.to_string(),
427            }
428            .into());
429        }
430
431        Ok(Box::new(Credentials::new(backend_user_id, passphrase)))
432    }
433
434    fn backend_users_with_new_passphrase(
435        &self,
436        filter: BackendUserIdFilter,
437    ) -> Vec<Box<dyn UserWithPassphrase>> {
438        if let Some(authentication_key_id) = match self {
439            Self::Admin {
440                authentication_key_id,
441            } => {
442                if [BackendUserIdKind::Admin, BackendUserIdKind::Any]
443                    .contains(&filter.backend_user_id_kind)
444                {
445                    Some(authentication_key_id)
446                } else {
447                    None
448                }
449            }
450            Self::AuditLog {
451                authentication_key_id,
452                ..
453            } => {
454                if [
455                    BackendUserIdKind::Any,
456                    BackendUserIdKind::Metrics,
457                    BackendUserIdKind::NonAdmin,
458                ]
459                .contains(&filter.backend_user_id_kind)
460                {
461                    Some(authentication_key_id)
462                } else {
463                    None
464                }
465            }
466            Self::Backup {
467                authentication_key_id,
468                ..
469            } => {
470                if [
471                    BackendUserIdKind::Any,
472                    BackendUserIdKind::Backup,
473                    BackendUserIdKind::NonAdmin,
474                ]
475                .contains(&filter.backend_user_id_kind)
476                {
477                    Some(authentication_key_id)
478                } else {
479                    None
480                }
481            }
482            Self::HermeticAuditLog {
483                authentication_key_id,
484                ..
485            } => {
486                if [
487                    BackendUserIdKind::Any,
488                    BackendUserIdKind::Metrics,
489                    BackendUserIdKind::NonAdmin,
490                ]
491                .contains(&filter.backend_user_id_kind)
492                {
493                    Some(authentication_key_id)
494                } else {
495                    None
496                }
497            }
498            Self::Signing {
499                authentication_key_id,
500                ..
501            } => {
502                if [
503                    BackendUserIdKind::Any,
504                    BackendUserIdKind::NonAdmin,
505                    BackendUserIdKind::Signing,
506                ]
507                .contains(&filter.backend_user_id_kind)
508                {
509                    Some(authentication_key_id)
510                } else {
511                    None
512                }
513            }
514        } {
515            vec![Box::new(Credentials::new(
516                *authentication_key_id,
517                Passphrase::generate(None),
518            ))]
519        } else {
520            Vec::new()
521        }
522    }
523}
524
525impl MappingAuthorizedKeyEntry for YubiHsm2UserMapping {
526    fn authorized_key_entry(&self) -> Option<&AuthorizedKeyEntry> {
527        match self {
528            Self::Admin { .. } | Self::HermeticAuditLog { .. } => None,
529            Self::AuditLog {
530                ssh_authorized_key, ..
531            }
532            | Self::Backup {
533                ssh_authorized_key, ..
534            }
535            | Self::Signing {
536                ssh_authorized_key, ..
537            } => Some(ssh_authorized_key),
538        }
539    }
540}
541
542/// A filter for filtering sets of tags used in a YubiHSM2.
543#[derive(Clone, Copy, Debug)]
544pub struct YubiHsm2DomainFilter {}
545
546impl BackendDomainFilter for YubiHsm2DomainFilter {}
547
548impl MappingBackendDomain<YubiHsm2DomainFilter> for YubiHsm2UserMapping {
549    fn backend_domain(&self, _filter: Option<&YubiHsm2DomainFilter>) -> Option<String> {
550        self.domain().map(|domain| domain.to_string())
551    }
552}
553
554/// An understood key [object type].
555///
556/// # Note
557///
558/// Only a subset of all [object types][object type] are supported.
559///
560/// [object type]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#object-type
561#[derive(Clone, Copy, Debug, Eq, PartialEq)]
562pub enum KeyObjectType {
563    /// An [asymmetric key object].
564    ///
565    /// [asymmetric key object]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#hsm2-asymmetric-key-obj
566    Signing,
567
568    /// A [wrap key object].
569    ///
570    /// [wrap key object]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#hsm2-wrap-key-obj
571    Wrapping,
572}
573
574/// A filter when search for key IDs in the [`YubiHsm2Config`].
575#[derive(Clone, Debug)]
576pub struct YubiHsm2BackendKeyIdFilter {
577    /// The key object type to look for.
578    ///
579    /// [object type]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#object-type
580    pub key_type: KeyObjectType,
581
582    /// The optional [domain] to match the mapping against.
583    ///
584    /// [domain]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#domains
585    pub key_domain: Option<Domain>,
586}
587
588impl BackendKeyIdFilter for YubiHsm2BackendKeyIdFilter {}
589
590impl MappingBackendKeyId<YubiHsm2BackendKeyIdFilter> for YubiHsm2UserMapping {
591    fn backend_key_id(&self, filter: &YubiHsm2BackendKeyIdFilter) -> Option<String> {
592        match self {
593            Self::Admin { .. } | Self::AuditLog { .. } | Self::HermeticAuditLog { .. } => None,
594            Self::Backup {
595                wrapping_key_id, ..
596            } => {
597                if filter.key_type == KeyObjectType::Wrapping {
598                    // NOTE: Implicitly, wrapping key objects are in all domains.
599                    Some(wrapping_key_id.to_string())
600                } else {
601                    None
602                }
603            }
604            Self::Signing {
605                signing_key_id,
606                domain: key_domain,
607                ..
608            } => {
609                if filter.key_type == KeyObjectType::Signing {
610                    if let Some(filter_key_domain) = filter.key_domain {
611                        if &filter_key_domain == key_domain {
612                            Some(signing_key_id.to_string())
613                        } else {
614                            None
615                        }
616                    } else {
617                        Some(signing_key_id.to_string())
618                    }
619                } else {
620                    None
621                }
622            }
623        }
624    }
625}
626
627impl MappingBackendUserSecrets for YubiHsm2UserMapping {}
628
629/// Validates a set of [`YubiHsmConnection`] objects.
630///
631/// Ensures that `value` is not empty.
632///
633/// # Errors
634///
635/// Returns an error if `value` is empty.
636fn validate_yubihsm2_config_connections(
637    value: &BTreeSet<YubiHsmConnection>,
638    _context: &(),
639) -> garde::Result {
640    if value.is_empty() {
641        return Err(garde::Error::new("contains no connections".to_string()));
642    }
643
644    Ok(())
645}
646
647/// Validates a set of [`YubiHsm2UserMapping`] objects.
648///
649/// Ensures that `value` is not empty.
650///
651/// Further ensures that there are no
652///
653/// - duplicate system users
654/// - duplicate SSH authorized keys (by comparing the actual SSH public keys)
655/// - missing administrator backend users
656/// - duplicate backend users
657/// - duplicate signing key IDs
658/// - duplicate wrapping key IDs
659/// - duplicate domains
660///
661/// # Errors
662///
663/// Returns an error if there are
664///
665/// - no items in `value`
666/// - duplicate system users
667/// - duplicate SSH authorized keys (by comparing the actual SSH public keys)
668/// - missing administrator backend users
669/// - duplicate backend users
670/// - duplicate signing key IDs
671/// - duplicate wrapping key IDs
672/// - duplicate domains
673fn validate_yubihsm2_config_mappings(
674    value: &BTreeSet<YubiHsm2UserMapping>,
675    _context: &(),
676) -> garde::Result {
677    if value.is_empty() {
678        return Err(garde::Error::new("contains no user mappings".to_string()));
679    }
680
681    // Collect all duplicate system user IDs.
682    let duplicate_system_user_ids = duplicate_system_user_ids(value);
683
684    // Collect all duplicate SSH public keys used as authorized_keys.
685    let duplicate_authorized_keys = duplicate_authorized_keys(value);
686
687    // Check whether there is at least one backend administrator.
688    let missing_admin = {
689        let num_system_admins = value
690            .iter()
691            .filter_map(|mapping| {
692                if let YubiHsm2UserMapping::Admin {
693                    authentication_key_id,
694                } = mapping
695                {
696                    Some(authentication_key_id)
697                } else {
698                    None
699                }
700            })
701            .count();
702
703        if num_system_admins == 0 {
704            Some("no administrator user".to_string())
705        } else {
706            None
707        }
708    };
709
710    // Collect all duplicate backend user IDs.
711    let duplicate_backend_user_ids = duplicate_backend_user_ids(value);
712
713    // Collect all duplicate signing key IDs.
714    let duplicate_signing_key_ids = duplicate_key_ids(
715        value,
716        &YubiHsm2BackendKeyIdFilter {
717            key_type: KeyObjectType::Signing,
718            key_domain: None,
719        },
720        Some(" signing".to_string()),
721    );
722
723    // Collect all duplicate wrapping (backup) key IDs.
724    let duplicate_wrapping_key_ids = duplicate_key_ids(
725        value,
726        &YubiHsm2BackendKeyIdFilter {
727            key_type: KeyObjectType::Wrapping,
728            key_domain: None,
729        },
730        Some(" wrapping".to_string()),
731    );
732
733    // Collect all duplicate domains.
734    let duplicate_domains = duplicate_domains(value, None, None, None);
735
736    let messages = [
737        duplicate_system_user_ids,
738        duplicate_authorized_keys,
739        missing_admin,
740        duplicate_backend_user_ids,
741        duplicate_signing_key_ids,
742        duplicate_wrapping_key_ids,
743        duplicate_domains,
744    ];
745    let error_messages = {
746        let mut error_messages = Vec::new();
747
748        for message in messages.iter().flatten() {
749            error_messages.push(message.as_str());
750        }
751
752        error_messages
753    };
754
755    match error_messages.len() {
756        0 => Ok(()),
757        1 => Err(garde::Error::new(format!(
758            "contains {}",
759            error_messages.join("\n")
760        ))),
761        _ => Err(garde::Error::new(format!(
762            "contains multiple issues:\n⤷ {}",
763            error_messages.join("\n⤷ ")
764        ))),
765    }
766}
767
768/// The configuration items for a YubiHSM2 backend.
769///
770/// Tracks a set of connections to a YubiHSM2 backend and user mappings that are present on each of
771/// them.
772#[derive(Clone, Debug, Default, Deserialize, Serialize, Validate)]
773#[serde(rename_all = "snake_case")]
774pub struct YubiHsm2Config {
775    /// A set of connections to YubiHSM2 backends.
776    #[garde(custom(validate_yubihsm2_config_connections))]
777    connections: BTreeSet<YubiHsmConnection>,
778
779    /// User mappings present in each YubiHSM2 backend.
780    #[garde(custom(validate_yubihsm2_config_mappings))]
781    mappings: BTreeSet<YubiHsm2UserMapping>,
782}
783
784impl YubiHsm2Config {
785    /// Creates a new [`YubiHsm2Config`] from a set of [`YubiHsmConnection`] and a set of
786    /// [`YubiHsm2UserMapping`] items.
787    pub fn new(
788        connections: BTreeSet<YubiHsmConnection>,
789        mappings: BTreeSet<YubiHsm2UserMapping>,
790    ) -> Result<Self, crate::Error> {
791        let config = Self {
792            connections,
793            mappings,
794        };
795        config
796            .validate()
797            .map_err(|source| crate::Error::Validation {
798                context: "validating a YubiHSM2 specific configuration item".to_string(),
799                source,
800            })?;
801
802        Ok(config)
803    }
804
805    /// Returns a reference to the set of [`YubiHsmConnection`] objects.
806    pub fn connections(&self) -> &BTreeSet<YubiHsmConnection> {
807        &self.connections
808    }
809
810    /// Returns a reference to the set of [`YubiHsm2UserMapping`] objects.
811    pub fn mappings(&self) -> &BTreeSet<YubiHsm2UserMapping> {
812        &self.mappings
813    }
814}
815
816impl ConfigAuthorizedKeyEntries for YubiHsm2Config {
817    fn authorized_key_entries(&self) -> HashSet<&AuthorizedKeyEntry> {
818        self.mappings
819            .iter()
820            .filter_map(|mapping| mapping.authorized_key_entry())
821            .collect()
822    }
823}
824
825impl ConfigSystemUserIds for YubiHsm2Config {
826    fn system_user_ids(&self) -> HashSet<&SystemUserId> {
827        self.mappings
828            .iter()
829            .filter_map(|mapping| mapping.system_user_id())
830            .collect()
831    }
832}
833
834#[cfg(test)]
835mod tests {
836    use std::thread::current;
837
838    use insta::{assert_snapshot, with_settings};
839    use rstest::{fixture, rstest};
840    use signstar_crypto::{
841        key::{CryptographicKeyContext, KeyMechanism, KeyType, SignatureType, SigningKeySetup},
842        openpgp::OpenPgpUserIdList,
843    };
844    use testresult::TestResult;
845
846    use super::*;
847
848    const SNAPSHOT_PATH: &str = "fixtures/yubihsm2_config/";
849
850    #[rstest]
851    #[case::admin(YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? })]
852    #[case::audit_log(
853        YubiHsm2UserMapping::AuditLog {
854            authentication_key_id: "1".parse()?,
855            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
856            system_user: "metrics-user".parse()?,
857        },
858    )]
859    #[case::backup(
860        YubiHsm2UserMapping::Backup{
861            authentication_key_id: "1".parse()?,
862            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
863            system_user: "backup-user".parse()?,
864            wrapping_key_id: "1".parse()?,
865        },
866    )]
867    #[case::hermetic_audit_log(
868        YubiHsm2UserMapping::HermeticAuditLog {
869            authentication_key_id: "1".parse()?,
870            system_user: "metrics-user".parse()?,
871        },
872    )]
873    #[case::signing(
874        YubiHsm2UserMapping::Signing {
875            authentication_key_id: "1".parse()?,
876            signing_key_id: "1".parse()?,
877            key_setup: SigningKeySetup::new(
878                KeyType::Curve25519,
879                vec![KeyMechanism::EdDsaSignature],
880                None,
881                SignatureType::EdDsa,
882                CryptographicKeyContext::OpenPgp {
883                    user_ids: OpenPgpUserIdList::new(vec![
884                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
885                    ])?,
886                    version: "v4".parse()?,
887                },
888            )?,
889            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
890            system_user: "signing-user".parse()?,
891            domain: Domain::One,
892        }
893    )]
894    fn yubihsm2_user_mapping_backend_user_id(#[case] mapping: YubiHsm2UserMapping) -> TestResult {
895        let id: Id = "1".parse()?;
896        assert_eq!(mapping.backend_user_id(), id);
897
898        Ok(())
899    }
900
901    #[rstest]
902    #[case::admin_filter_admin(
903        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
904        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
905    )]
906    #[case::admin_filter_any(
907        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
908        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
909    )]
910    #[case::audit_log_filter_metrics(
911        YubiHsm2UserMapping::AuditLog {
912            authentication_key_id: "1".parse()?,
913            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
914            system_user: "metrics-user".parse()?,
915        },
916        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
917    )]
918    #[case::audit_log_filter_any(
919        YubiHsm2UserMapping::AuditLog {
920            authentication_key_id: "1".parse()?,
921            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
922            system_user: "metrics-user".parse()?,
923        },
924        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
925    )]
926    #[case::audit_log_filter_non_admin(
927        YubiHsm2UserMapping::AuditLog {
928            authentication_key_id: "1".parse()?,
929            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
930            system_user: "metrics-user".parse()?,
931        },
932        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
933    )]
934    #[case::backup_filter_backup(
935        YubiHsm2UserMapping::Backup{
936            authentication_key_id: "1".parse()?,
937            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
938            system_user: "backup-user".parse()?,
939            wrapping_key_id: "1".parse()?,
940        },
941        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
942    )]
943    #[case::backup_filter_any(
944        YubiHsm2UserMapping::Backup{
945            authentication_key_id: "1".parse()?,
946            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
947            system_user: "backup-user".parse()?,
948            wrapping_key_id: "1".parse()?,
949        },
950        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
951    )]
952    #[case::backup_filter_non_admin(
953        YubiHsm2UserMapping::Backup{
954            authentication_key_id: "1".parse()?,
955            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
956            system_user: "backup-user".parse()?,
957            wrapping_key_id: "1".parse()?,
958        },
959        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
960    )]
961    #[case::hermetic_audit_log_filter_metrics(
962        YubiHsm2UserMapping::HermeticAuditLog {
963            authentication_key_id: "1".parse()?,
964            system_user: "metrics-user".parse()?,
965        },
966        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
967    )]
968    #[case::hermetic_audit_log_filter_any(
969        YubiHsm2UserMapping::HermeticAuditLog {
970            authentication_key_id: "1".parse()?,
971            system_user: "metrics-user".parse()?,
972        },
973        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
974    )]
975    #[case::hermetic_audit_log_filter_non_admin(
976        YubiHsm2UserMapping::HermeticAuditLog {
977            authentication_key_id: "1".parse()?,
978            system_user: "metrics-user".parse()?,
979        },
980        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
981    )]
982    #[case::signing_filter_signing(
983        YubiHsm2UserMapping::Signing {
984            authentication_key_id: "1".parse()?,
985            signing_key_id: "1".parse()?,
986            key_setup: SigningKeySetup::new(
987                KeyType::Curve25519,
988                vec![KeyMechanism::EdDsaSignature],
989                None,
990                SignatureType::EdDsa,
991                CryptographicKeyContext::OpenPgp {
992                    user_ids: OpenPgpUserIdList::new(vec![
993                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
994                    ])?,
995                    version: "v4".parse()?,
996                },
997            )?,
998            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
999            system_user: "signing-user".parse()?,
1000            domain: Domain::One,
1001        },
1002        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1003    )]
1004    #[case::signing_filter_any(
1005        YubiHsm2UserMapping::Signing {
1006            authentication_key_id: "1".parse()?,
1007            signing_key_id: "1".parse()?,
1008            key_setup: SigningKeySetup::new(
1009                KeyType::Curve25519,
1010                vec![KeyMechanism::EdDsaSignature],
1011                None,
1012                SignatureType::EdDsa,
1013                CryptographicKeyContext::OpenPgp {
1014                    user_ids: OpenPgpUserIdList::new(vec![
1015                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1016                    ])?,
1017                    version: "v4".parse()?,
1018                },
1019            )?,
1020            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1021            system_user: "signing-user".parse()?,
1022            domain: Domain::One,
1023        },
1024        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1025    )]
1026    #[case::signing_filter_non_admin(
1027        YubiHsm2UserMapping::Signing {
1028            authentication_key_id: "1".parse()?,
1029            signing_key_id: "1".parse()?,
1030            key_setup: SigningKeySetup::new(
1031                KeyType::Curve25519,
1032                vec![KeyMechanism::EdDsaSignature],
1033                None,
1034                SignatureType::EdDsa,
1035                CryptographicKeyContext::OpenPgp {
1036                    user_ids: OpenPgpUserIdList::new(vec![
1037                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1038                    ])?,
1039                    version: "v4".parse()?,
1040                },
1041            )?,
1042            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1043            system_user: "signing-user".parse()?,
1044            domain: Domain::One,
1045        },
1046        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1047    )]
1048    fn yubihsm2_user_mapping_backend_user_ids_filter_matches(
1049        #[case] mapping: YubiHsm2UserMapping,
1050        #[case] filter: BackendUserIdFilter,
1051    ) -> TestResult {
1052        assert_eq!(mapping.backend_user_ids(filter), vec!["1".to_string()]);
1053
1054        Ok(())
1055    }
1056
1057    #[rstest]
1058    #[case::admin_filter_non_admin(
1059        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1060        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1061    )]
1062    #[case::admin_filter_backup(
1063        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1064        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1065    )]
1066    #[case::admin_filter_metrics(
1067        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1068        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1069    )]
1070    #[case::admin_filter_observer(
1071        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1072        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1073    )]
1074    #[case::admin_filter_signing(
1075        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1076        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1077    )]
1078    #[case::audit_log_filter_admin(
1079        YubiHsm2UserMapping::AuditLog {
1080            authentication_key_id: "1".parse()?,
1081            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1082            system_user: "metrics-user".parse()?,
1083        },
1084        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1085    )]
1086    #[case::audit_log_filter_backup(
1087        YubiHsm2UserMapping::AuditLog {
1088            authentication_key_id: "1".parse()?,
1089            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1090            system_user: "metrics-user".parse()?,
1091        },
1092        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1093    )]
1094    #[case::audit_log_filter_observer(
1095        YubiHsm2UserMapping::AuditLog {
1096            authentication_key_id: "1".parse()?,
1097            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1098            system_user: "metrics-user".parse()?,
1099        },
1100        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1101    )]
1102    #[case::audit_log_filter_signing(
1103        YubiHsm2UserMapping::AuditLog {
1104            authentication_key_id: "1".parse()?,
1105            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1106            system_user: "metrics-user".parse()?,
1107        },
1108        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1109    )]
1110    #[case::backup_filter_admin(
1111        YubiHsm2UserMapping::Backup{
1112            authentication_key_id: "1".parse()?,
1113            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1114            system_user: "backup-user".parse()?,
1115            wrapping_key_id: "1".parse()?,
1116        },
1117        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1118    )]
1119    #[case::backup_filter_metrics(
1120        YubiHsm2UserMapping::Backup{
1121            authentication_key_id: "1".parse()?,
1122            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1123            system_user: "backup-user".parse()?,
1124            wrapping_key_id: "1".parse()?,
1125        },
1126        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1127    )]
1128    #[case::backup_filter_observer(
1129        YubiHsm2UserMapping::Backup{
1130            authentication_key_id: "1".parse()?,
1131            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1132            system_user: "backup-user".parse()?,
1133            wrapping_key_id: "1".parse()?,
1134        },
1135        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1136    )]
1137    #[case::backup_filter_signing(
1138        YubiHsm2UserMapping::Backup{
1139            authentication_key_id: "1".parse()?,
1140            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1141            system_user: "backup-user".parse()?,
1142            wrapping_key_id: "1".parse()?,
1143        },
1144        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1145    )]
1146    #[case::hermetic_audit_log_filter_admin(
1147        YubiHsm2UserMapping::HermeticAuditLog {
1148            authentication_key_id: "1".parse()?,
1149            system_user: "metrics-user".parse()?,
1150        },
1151        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1152    )]
1153    #[case::hermetic_audit_log_filter_backup(
1154        YubiHsm2UserMapping::HermeticAuditLog {
1155            authentication_key_id: "1".parse()?,
1156            system_user: "metrics-user".parse()?,
1157        },
1158        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1159    )]
1160    #[case::hermetic_audit_log_filter_observer(
1161        YubiHsm2UserMapping::HermeticAuditLog {
1162            authentication_key_id: "1".parse()?,
1163            system_user: "metrics-user".parse()?,
1164        },
1165        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1166    )]
1167    #[case::hermetic_audit_log_filter_signing(
1168        YubiHsm2UserMapping::HermeticAuditLog {
1169            authentication_key_id: "1".parse()?,
1170            system_user: "metrics-user".parse()?,
1171        },
1172        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1173    )]
1174    #[case::signing_filter_admin(
1175        YubiHsm2UserMapping::Signing {
1176            authentication_key_id: "1".parse()?,
1177            signing_key_id: "1".parse()?,
1178            key_setup: SigningKeySetup::new(
1179                KeyType::Curve25519,
1180                vec![KeyMechanism::EdDsaSignature],
1181                None,
1182                SignatureType::EdDsa,
1183                CryptographicKeyContext::OpenPgp {
1184                    user_ids: OpenPgpUserIdList::new(vec![
1185                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1186                    ])?,
1187                    version: "v4".parse()?,
1188                },
1189            )?,
1190            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1191            system_user: "signing-user".parse()?,
1192            domain: Domain::One,
1193        },
1194        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1195    )]
1196    #[case::signing_filter_backup(
1197        YubiHsm2UserMapping::Signing {
1198            authentication_key_id: "1".parse()?,
1199            signing_key_id: "1".parse()?,
1200            key_setup: SigningKeySetup::new(
1201                KeyType::Curve25519,
1202                vec![KeyMechanism::EdDsaSignature],
1203                None,
1204                SignatureType::EdDsa,
1205                CryptographicKeyContext::OpenPgp {
1206                    user_ids: OpenPgpUserIdList::new(vec![
1207                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1208                    ])?,
1209                    version: "v4".parse()?,
1210                },
1211            )?,
1212            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1213            system_user: "signing-user".parse()?,
1214            domain: Domain::One,
1215        },
1216        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1217    )]
1218    #[case::signing_filter_metrics(
1219        YubiHsm2UserMapping::Signing {
1220            authentication_key_id: "1".parse()?,
1221            signing_key_id: "1".parse()?,
1222            key_setup: SigningKeySetup::new(
1223                KeyType::Curve25519,
1224                vec![KeyMechanism::EdDsaSignature],
1225                None,
1226                SignatureType::EdDsa,
1227                CryptographicKeyContext::OpenPgp {
1228                    user_ids: OpenPgpUserIdList::new(vec![
1229                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1230                    ])?,
1231                    version: "v4".parse()?,
1232                },
1233            )?,
1234            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1235            system_user: "signing-user".parse()?,
1236            domain: Domain::One,
1237        },
1238        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1239    )]
1240    #[case::signing_filter_observer(
1241        YubiHsm2UserMapping::Signing {
1242            authentication_key_id: "1".parse()?,
1243            signing_key_id: "1".parse()?,
1244            key_setup: SigningKeySetup::new(
1245                KeyType::Curve25519,
1246                vec![KeyMechanism::EdDsaSignature],
1247                None,
1248                SignatureType::EdDsa,
1249                CryptographicKeyContext::OpenPgp {
1250                    user_ids: OpenPgpUserIdList::new(vec![
1251                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1252                    ])?,
1253                    version: "v4".parse()?,
1254                },
1255            )?,
1256            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1257            system_user: "signing-user".parse()?,
1258            domain: Domain::One,
1259        },
1260        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1261    )]
1262    fn yubihsm2_user_mapping_backend_user_ids_filter_mismatches(
1263        #[case] mapping: YubiHsm2UserMapping,
1264        #[case] filter: BackendUserIdFilter,
1265    ) -> TestResult {
1266        assert!(mapping.backend_user_ids(filter).is_empty());
1267
1268        Ok(())
1269    }
1270
1271    #[test]
1272    fn yubihsm2_user_mapping_backend_user_with_passphrase_succeeds() -> TestResult {
1273        let mapping = YubiHsm2UserMapping::Admin {
1274            authentication_key_id: "1".parse()?,
1275        };
1276        let passphrase = Passphrase::generate(None);
1277        let creds = mapping.backend_user_with_passphrase("1", passphrase.clone())?;
1278
1279        assert_eq!(creds.user(), "1");
1280        assert_eq!(
1281            creds.passphrase().expose_borrowed(),
1282            passphrase.expose_borrowed()
1283        );
1284
1285        Ok(())
1286    }
1287
1288    #[test]
1289    fn yubihsm2_user_mapping_backend_user_with_passphrase_fails() -> TestResult {
1290        let mapping = YubiHsm2UserMapping::Admin {
1291            authentication_key_id: "1".parse()?,
1292        };
1293        assert!(
1294            mapping
1295                .backend_user_with_passphrase("2", Passphrase::generate(None))
1296                .is_err()
1297        );
1298
1299        Ok(())
1300    }
1301
1302    #[rstest]
1303    #[case::admin_filter_admin(
1304        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1305        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1306    )]
1307    #[case::admin_filter_any(
1308        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1309        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1310    )]
1311    #[case::audit_log_filter_metrics(
1312        YubiHsm2UserMapping::AuditLog {
1313            authentication_key_id: "1".parse()?,
1314            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1315            system_user: "metrics-user".parse()?,
1316        },
1317        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1318    )]
1319    #[case::audit_log_filter_any(
1320        YubiHsm2UserMapping::AuditLog {
1321            authentication_key_id: "1".parse()?,
1322            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1323            system_user: "metrics-user".parse()?,
1324        },
1325        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1326    )]
1327    #[case::audit_log_filter_non_admin(
1328        YubiHsm2UserMapping::AuditLog {
1329            authentication_key_id: "1".parse()?,
1330            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1331            system_user: "metrics-user".parse()?,
1332        },
1333        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1334    )]
1335    #[case::backup_filter_backup(
1336        YubiHsm2UserMapping::Backup{
1337            authentication_key_id: "1".parse()?,
1338            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1339            system_user: "backup-user".parse()?,
1340            wrapping_key_id: "1".parse()?,
1341        },
1342        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1343    )]
1344    #[case::backup_filter_any(
1345        YubiHsm2UserMapping::Backup{
1346            authentication_key_id: "1".parse()?,
1347            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1348            system_user: "backup-user".parse()?,
1349            wrapping_key_id: "1".parse()?,
1350        },
1351        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1352    )]
1353    #[case::backup_filter_non_admin(
1354        YubiHsm2UserMapping::Backup{
1355            authentication_key_id: "1".parse()?,
1356            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1357            system_user: "backup-user".parse()?,
1358            wrapping_key_id: "1".parse()?,
1359        },
1360        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1361    )]
1362    #[case::hermetic_audit_log_filter_metrics(
1363        YubiHsm2UserMapping::HermeticAuditLog {
1364            authentication_key_id: "1".parse()?,
1365            system_user: "metrics-user".parse()?,
1366        },
1367        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1368    )]
1369    #[case::hermetic_audit_log_filter_any(
1370        YubiHsm2UserMapping::HermeticAuditLog {
1371            authentication_key_id: "1".parse()?,
1372            system_user: "metrics-user".parse()?,
1373        },
1374        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1375    )]
1376    #[case::hermetic_audit_log_filter_non_admin(
1377        YubiHsm2UserMapping::HermeticAuditLog {
1378            authentication_key_id: "1".parse()?,
1379            system_user: "metrics-user".parse()?,
1380        },
1381        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1382    )]
1383    #[case::signing_filter_signing(
1384        YubiHsm2UserMapping::Signing {
1385            authentication_key_id: "1".parse()?,
1386            signing_key_id: "1".parse()?,
1387            key_setup: SigningKeySetup::new(
1388                KeyType::Curve25519,
1389                vec![KeyMechanism::EdDsaSignature],
1390                None,
1391                SignatureType::EdDsa,
1392                CryptographicKeyContext::OpenPgp {
1393                    user_ids: OpenPgpUserIdList::new(vec![
1394                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1395                    ])?,
1396                    version: "v4".parse()?,
1397                },
1398            )?,
1399            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1400            system_user: "signing-user".parse()?,
1401            domain: Domain::One,
1402        },
1403        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1404    )]
1405    #[case::signing_filter_any(
1406        YubiHsm2UserMapping::Signing {
1407            authentication_key_id: "1".parse()?,
1408            signing_key_id: "1".parse()?,
1409            key_setup: SigningKeySetup::new(
1410                KeyType::Curve25519,
1411                vec![KeyMechanism::EdDsaSignature],
1412                None,
1413                SignatureType::EdDsa,
1414                CryptographicKeyContext::OpenPgp {
1415                    user_ids: OpenPgpUserIdList::new(vec![
1416                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1417                    ])?,
1418                    version: "v4".parse()?,
1419                },
1420            )?,
1421            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1422            system_user: "signing-user".parse()?,
1423            domain: Domain::One,
1424        },
1425        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Any },
1426    )]
1427    #[case::signing_filter_non_admin(
1428        YubiHsm2UserMapping::Signing {
1429            authentication_key_id: "1".parse()?,
1430            signing_key_id: "1".parse()?,
1431            key_setup: SigningKeySetup::new(
1432                KeyType::Curve25519,
1433                vec![KeyMechanism::EdDsaSignature],
1434                None,
1435                SignatureType::EdDsa,
1436                CryptographicKeyContext::OpenPgp {
1437                    user_ids: OpenPgpUserIdList::new(vec![
1438                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1439                    ])?,
1440                    version: "v4".parse()?,
1441                },
1442            )?,
1443            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1444            system_user: "signing-user".parse()?,
1445            domain: Domain::One,
1446        },
1447        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1448    )]
1449    fn yubihsm2_user_mapping_backend_users_with_new_passphrase_filter_matches(
1450        #[case] mapping: YubiHsm2UserMapping,
1451        #[case] filter: BackendUserIdFilter,
1452    ) -> TestResult {
1453        let creds = mapping.backend_users_with_new_passphrase(filter);
1454        assert!(creds.first().is_some_and(|creds| creds.user() == "1"));
1455
1456        Ok(())
1457    }
1458
1459    #[rstest]
1460    #[case::admin_filter_non_admin(
1461        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1462        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::NonAdmin },
1463    )]
1464    #[case::admin_filter_backup(
1465        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1466        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1467    )]
1468    #[case::admin_filter_metrics(
1469        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1470        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1471    )]
1472    #[case::admin_filter_observer(
1473        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1474        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1475    )]
1476    #[case::admin_filter_signing(
1477        YubiHsm2UserMapping::Admin{ authentication_key_id: "1".parse()? },
1478        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1479    )]
1480    #[case::audit_log_filter_admin(
1481        YubiHsm2UserMapping::AuditLog {
1482            authentication_key_id: "1".parse()?,
1483            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1484            system_user: "metrics-user".parse()?,
1485        },
1486        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1487    )]
1488    #[case::audit_log_filter_backup(
1489        YubiHsm2UserMapping::AuditLog {
1490            authentication_key_id: "1".parse()?,
1491            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1492            system_user: "metrics-user".parse()?,
1493        },
1494        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1495    )]
1496    #[case::audit_log_filter_observer(
1497        YubiHsm2UserMapping::AuditLog {
1498            authentication_key_id: "1".parse()?,
1499            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1500            system_user: "metrics-user".parse()?,
1501        },
1502        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1503    )]
1504    #[case::audit_log_filter_signing(
1505        YubiHsm2UserMapping::AuditLog {
1506            authentication_key_id: "1".parse()?,
1507            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1508            system_user: "metrics-user".parse()?,
1509        },
1510        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1511    )]
1512    #[case::backup_filter_admin(
1513        YubiHsm2UserMapping::Backup{
1514            authentication_key_id: "1".parse()?,
1515            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1516            system_user: "backup-user".parse()?,
1517            wrapping_key_id: "1".parse()?,
1518        },
1519        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1520    )]
1521    #[case::backup_filter_metrics(
1522        YubiHsm2UserMapping::Backup{
1523            authentication_key_id: "1".parse()?,
1524            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1525            system_user: "backup-user".parse()?,
1526            wrapping_key_id: "1".parse()?,
1527        },
1528        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1529    )]
1530    #[case::backup_filter_observer(
1531        YubiHsm2UserMapping::Backup{
1532            authentication_key_id: "1".parse()?,
1533            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1534            system_user: "backup-user".parse()?,
1535            wrapping_key_id: "1".parse()?,
1536        },
1537        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1538    )]
1539    #[case::backup_filter_signing(
1540        YubiHsm2UserMapping::Backup{
1541            authentication_key_id: "1".parse()?,
1542            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1543            system_user: "backup-user".parse()?,
1544            wrapping_key_id: "1".parse()?,
1545        },
1546        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1547    )]
1548    #[case::hermetic_audit_log_filter_admin(
1549        YubiHsm2UserMapping::HermeticAuditLog {
1550            authentication_key_id: "1".parse()?,
1551            system_user: "metrics-user".parse()?,
1552        },
1553        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1554    )]
1555    #[case::hermetic_audit_log_filter_backup(
1556        YubiHsm2UserMapping::HermeticAuditLog {
1557            authentication_key_id: "1".parse()?,
1558            system_user: "metrics-user".parse()?,
1559        },
1560        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1561    )]
1562    #[case::hermetic_audit_log_filter_observer(
1563        YubiHsm2UserMapping::HermeticAuditLog {
1564            authentication_key_id: "1".parse()?,
1565            system_user: "metrics-user".parse()?,
1566        },
1567        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1568    )]
1569    #[case::hermetic_audit_log_filter_signing(
1570        YubiHsm2UserMapping::HermeticAuditLog {
1571            authentication_key_id: "1".parse()?,
1572            system_user: "metrics-user".parse()?,
1573        },
1574        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Signing },
1575    )]
1576    #[case::signing_filter_admin(
1577        YubiHsm2UserMapping::Signing {
1578            authentication_key_id: "1".parse()?,
1579            signing_key_id: "1".parse()?,
1580            key_setup: SigningKeySetup::new(
1581                KeyType::Curve25519,
1582                vec![KeyMechanism::EdDsaSignature],
1583                None,
1584                SignatureType::EdDsa,
1585                CryptographicKeyContext::OpenPgp {
1586                    user_ids: OpenPgpUserIdList::new(vec![
1587                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1588                    ])?,
1589                    version: "v4".parse()?,
1590                },
1591            )?,
1592            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1593            system_user: "signing-user".parse()?,
1594            domain: Domain::One,
1595        },
1596        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Admin },
1597    )]
1598    #[case::signing_filter_backup(
1599        YubiHsm2UserMapping::Signing {
1600            authentication_key_id: "1".parse()?,
1601            signing_key_id: "1".parse()?,
1602            key_setup: SigningKeySetup::new(
1603                KeyType::Curve25519,
1604                vec![KeyMechanism::EdDsaSignature],
1605                None,
1606                SignatureType::EdDsa,
1607                CryptographicKeyContext::OpenPgp {
1608                    user_ids: OpenPgpUserIdList::new(vec![
1609                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1610                    ])?,
1611                    version: "v4".parse()?,
1612                },
1613            )?,
1614            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1615            system_user: "signing-user".parse()?,
1616            domain: Domain::One,
1617        },
1618        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Backup },
1619    )]
1620    #[case::signing_filter_metrics(
1621        YubiHsm2UserMapping::Signing {
1622            authentication_key_id: "1".parse()?,
1623            signing_key_id: "1".parse()?,
1624            key_setup: SigningKeySetup::new(
1625                KeyType::Curve25519,
1626                vec![KeyMechanism::EdDsaSignature],
1627                None,
1628                SignatureType::EdDsa,
1629                CryptographicKeyContext::OpenPgp {
1630                    user_ids: OpenPgpUserIdList::new(vec![
1631                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1632                    ])?,
1633                    version: "v4".parse()?,
1634                },
1635            )?,
1636            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1637            system_user: "signing-user".parse()?,
1638            domain: Domain::One,
1639        },
1640        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Metrics },
1641    )]
1642    #[case::signing_filter_observer(
1643        YubiHsm2UserMapping::Signing {
1644            authentication_key_id: "1".parse()?,
1645            signing_key_id: "1".parse()?,
1646            key_setup: SigningKeySetup::new(
1647                KeyType::Curve25519,
1648                vec![KeyMechanism::EdDsaSignature],
1649                None,
1650                SignatureType::EdDsa,
1651                CryptographicKeyContext::OpenPgp {
1652                    user_ids: OpenPgpUserIdList::new(vec![
1653                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1654                    ])?,
1655                    version: "v4".parse()?,
1656                },
1657            )?,
1658            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1659            system_user: "signing-user".parse()?,
1660            domain: Domain::One,
1661        },
1662        BackendUserIdFilter{ backend_user_id_kind: BackendUserIdKind::Observer },
1663    )]
1664    fn yubihsm2_user_mapping_backend_users_with_new_passphrase_filter_mismatches(
1665        #[case] mapping: YubiHsm2UserMapping,
1666        #[case] filter: BackendUserIdFilter,
1667    ) -> TestResult {
1668        assert!(mapping.backend_users_with_new_passphrase(filter).is_empty());
1669
1670        Ok(())
1671    }
1672
1673    #[rstest]
1674    #[case::backup_filter_wrapping_no_domain(
1675        YubiHsm2UserMapping::Backup{
1676            authentication_key_id: "1".parse()?,
1677            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1678            system_user: "backup-user".parse()?,
1679            wrapping_key_id: "1".parse()?,
1680        },
1681        YubiHsm2BackendKeyIdFilter{ key_type: KeyObjectType::Wrapping, key_domain: None },
1682    )]
1683    #[case::backup_filter_wrapping_some_domain(
1684        YubiHsm2UserMapping::Backup{
1685            authentication_key_id: "1".parse()?,
1686            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1687            system_user: "backup-user".parse()?,
1688            wrapping_key_id: "1".parse()?,
1689        },
1690        YubiHsm2BackendKeyIdFilter{ key_type: KeyObjectType::Wrapping, key_domain: Some(Domain::One) },
1691    )]
1692    #[case::signing_filter_signing_matching_domain(
1693        YubiHsm2UserMapping::Signing {
1694            authentication_key_id: "1".parse()?,
1695            signing_key_id: "1".parse()?,
1696            key_setup: SigningKeySetup::new(
1697                KeyType::Curve25519,
1698                vec![KeyMechanism::EdDsaSignature],
1699                None,
1700                SignatureType::EdDsa,
1701                CryptographicKeyContext::OpenPgp {
1702                    user_ids: OpenPgpUserIdList::new(vec![
1703                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1704                    ])?,
1705                    version: "v4".parse()?,
1706                },
1707            )?,
1708            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1709            system_user: "signing-user".parse()?,
1710            domain: Domain::One,
1711        },
1712        YubiHsm2BackendKeyIdFilter{ key_type: KeyObjectType::Signing, key_domain: Some(Domain::One) },
1713    )]
1714    fn yubihsm2_user_mapping_backend_key_id_filter_matches(
1715        #[case] mapping: YubiHsm2UserMapping,
1716        #[case] filter: YubiHsm2BackendKeyIdFilter,
1717    ) -> TestResult {
1718        assert!(mapping.backend_key_id(&filter).is_some_and(|id| id == "1"));
1719
1720        Ok(())
1721    }
1722
1723    #[rstest]
1724    #[case::backup_filter_signing_no_domain(
1725        YubiHsm2UserMapping::Backup{
1726            authentication_key_id: "1".parse()?,
1727            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1728            system_user: "backup-user".parse()?,
1729            wrapping_key_id: "1".parse()?,
1730        },
1731        YubiHsm2BackendKeyIdFilter{ key_type: KeyObjectType::Signing, key_domain: None },
1732    )]
1733    #[case::backup_filter_signing_some_domain(
1734        YubiHsm2UserMapping::Backup{
1735            authentication_key_id: "1".parse()?,
1736            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1737            system_user: "backup-user".parse()?,
1738            wrapping_key_id: "1".parse()?,
1739        },
1740        YubiHsm2BackendKeyIdFilter{ key_type: KeyObjectType::Signing, key_domain: Some(Domain::One) },
1741    )]
1742    #[case::signing_filter_signing_wrong_domain(
1743        YubiHsm2UserMapping::Signing {
1744            authentication_key_id: "1".parse()?,
1745            signing_key_id: "1".parse()?,
1746            key_setup: SigningKeySetup::new(
1747                KeyType::Curve25519,
1748                vec![KeyMechanism::EdDsaSignature],
1749                None,
1750                SignatureType::EdDsa,
1751                CryptographicKeyContext::OpenPgp {
1752                    user_ids: OpenPgpUserIdList::new(vec![
1753                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1754                    ])?,
1755                    version: "v4".parse()?,
1756                },
1757            )?,
1758            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1759            system_user: "signing-user".parse()?,
1760            domain: Domain::One,
1761        },
1762        YubiHsm2BackendKeyIdFilter{ key_type: KeyObjectType::Signing, key_domain: Some(Domain::Two) },
1763    )]
1764    #[case::signing_filter_wrapping_same_domain(
1765        YubiHsm2UserMapping::Signing {
1766            authentication_key_id: "1".parse()?,
1767            signing_key_id: "1".parse()?,
1768            key_setup: SigningKeySetup::new(
1769                KeyType::Curve25519,
1770                vec![KeyMechanism::EdDsaSignature],
1771                None,
1772                SignatureType::EdDsa,
1773                CryptographicKeyContext::OpenPgp {
1774                    user_ids: OpenPgpUserIdList::new(vec![
1775                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1776                    ])?,
1777                    version: "v4".parse()?,
1778                },
1779            )?,
1780            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1781            system_user: "signing-user".parse()?,
1782            domain: Domain::One,
1783        },
1784        YubiHsm2BackendKeyIdFilter{ key_type: KeyObjectType::Wrapping, key_domain: Some(Domain::One) },
1785    )]
1786    #[case::signing_filter_wrapping_wrong_domain(
1787        YubiHsm2UserMapping::Signing {
1788            authentication_key_id: "1".parse()?,
1789            signing_key_id: "1".parse()?,
1790            key_setup: SigningKeySetup::new(
1791                KeyType::Curve25519,
1792                vec![KeyMechanism::EdDsaSignature],
1793                None,
1794                SignatureType::EdDsa,
1795                CryptographicKeyContext::OpenPgp {
1796                    user_ids: OpenPgpUserIdList::new(vec![
1797                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1798                    ])?,
1799                    version: "v4".parse()?,
1800                },
1801            )?,
1802            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1803            system_user: "signing-user".parse()?,
1804            domain: Domain::One,
1805        },
1806        YubiHsm2BackendKeyIdFilter{ key_type: KeyObjectType::Wrapping, key_domain: Some(Domain::Two) },
1807    )]
1808    #[case::signing_filter_wrapping_no_domain(
1809        YubiHsm2UserMapping::Signing {
1810            authentication_key_id: "1".parse()?,
1811            signing_key_id: "1".parse()?,
1812            key_setup: SigningKeySetup::new(
1813                KeyType::Curve25519,
1814                vec![KeyMechanism::EdDsaSignature],
1815                None,
1816                SignatureType::EdDsa,
1817                CryptographicKeyContext::OpenPgp {
1818                    user_ids: OpenPgpUserIdList::new(vec![
1819                        "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1820                    ])?,
1821                    version: "v4".parse()?,
1822                },
1823            )?,
1824            ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1825            system_user: "signing-user".parse()?,
1826            domain: Domain::One,
1827        },
1828        YubiHsm2BackendKeyIdFilter{ key_type: KeyObjectType::Wrapping, key_domain: None },
1829    )]
1830    fn yubihsm2_user_mapping_backend_key_id_filter_mismatches(
1831        #[case] mapping: YubiHsm2UserMapping,
1832        #[case] filter: YubiHsm2BackendKeyIdFilter,
1833    ) -> TestResult {
1834        assert!(mapping.backend_key_id(&filter).is_none());
1835
1836        Ok(())
1837    }
1838
1839    #[fixture]
1840    fn yubihsm2_yubihsm_connections() -> TestResult<[YubiHsmConnection; 2]> {
1841        Ok([
1842            YubiHsmConnection::Usb {
1843                serial_number: "0012345678".parse()?,
1844            },
1845            YubiHsmConnection::Usb {
1846                serial_number: "0087654321".parse()?,
1847            },
1848        ])
1849    }
1850
1851    #[fixture]
1852    fn yubihsm2_mappings() -> TestResult<[YubiHsm2UserMapping; 5]> {
1853        Ok([
1854                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1855                    YubiHsm2UserMapping::Backup{
1856                        authentication_key_id: "2".parse()?,
1857                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1858                        system_user: "backup-user".parse()?,
1859                        wrapping_key_id: "1".parse()?,
1860                    },
1861                    YubiHsm2UserMapping::AuditLog {
1862                        authentication_key_id: "3".parse()?,
1863                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1864                        system_user: "metrics-user".parse()?,
1865                    },
1866                    YubiHsm2UserMapping::HermeticAuditLog {
1867                        authentication_key_id: "4".parse()?,
1868                        system_user: "hermetic-metrics".parse()?,
1869                    },
1870                    YubiHsm2UserMapping::Signing {
1871                        authentication_key_id: "5".parse()?,
1872                        signing_key_id: "1".parse()?,
1873                        key_setup: SigningKeySetup::new(
1874                            KeyType::Curve25519,
1875                            vec![KeyMechanism::EdDsaSignature],
1876                            None,
1877                            SignatureType::EdDsa,
1878                            CryptographicKeyContext::OpenPgp {
1879                                user_ids: OpenPgpUserIdList::new(vec![
1880                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1881                                ])?,
1882                                version: "v4".parse()?,
1883                            },
1884                        )?,
1885                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1886                        system_user: "signing-user".parse()?,
1887                        domain: Domain::One,
1888                    }
1889                ])
1890    }
1891
1892    #[fixture]
1893    fn yubihsm2_config(
1894        yubihsm2_yubihsm_connections: TestResult<[YubiHsmConnection; 2]>,
1895        yubihsm2_mappings: TestResult<[YubiHsm2UserMapping; 5]>,
1896    ) -> TestResult<YubiHsm2Config> {
1897        let yubihsm2_yubihsm_connections = yubihsm2_yubihsm_connections?;
1898        let yubihsm2_mappings = yubihsm2_mappings?;
1899        let config = YubiHsm2Config::new(
1900            BTreeSet::from_iter(yubihsm2_yubihsm_connections),
1901            BTreeSet::from_iter(yubihsm2_mappings),
1902        )?;
1903
1904        Ok(config)
1905    }
1906
1907    #[rstest]
1908    fn yubihsm2_config_connections(
1909        yubihsm2_yubihsm_connections: TestResult<[YubiHsmConnection; 2]>,
1910        yubihsm2_config: TestResult<YubiHsm2Config>,
1911    ) -> TestResult {
1912        let yubihsm2_config = yubihsm2_config?;
1913        let yubihsm2_yubihsm_connections = yubihsm2_yubihsm_connections?;
1914        let connections = yubihsm2_config.connections();
1915
1916        assert_eq!(connections.len(), 2);
1917        assert!(
1918            connections
1919                .first()
1920                .is_some_and(|connection| connection == &yubihsm2_yubihsm_connections[0]),
1921        );
1922        assert!(
1923            connections
1924                .last()
1925                .is_some_and(|connection| connection == &yubihsm2_yubihsm_connections[1]),
1926        );
1927
1928        Ok(())
1929    }
1930
1931    #[rstest]
1932    fn yubihsm2_config_mappings(
1933        yubihsm2_mappings: TestResult<[YubiHsm2UserMapping; 5]>,
1934        yubihsm2_config: TestResult<YubiHsm2Config>,
1935    ) -> TestResult {
1936        let yubihsm2_config = yubihsm2_config?;
1937        let yubihsm2_mappings = yubihsm2_mappings?;
1938        let mappings = yubihsm2_config.mappings();
1939
1940        assert_eq!(mappings.len(), 5);
1941        for mapping in yubihsm2_mappings.iter() {
1942            assert!(mappings.contains(mapping));
1943        }
1944
1945        Ok(())
1946    }
1947
1948    #[rstest]
1949    fn yubihsm2_config_authorized_key_entries(
1950        yubihsm2_mappings: TestResult<[YubiHsm2UserMapping; 5]>,
1951        yubihsm2_config: TestResult<YubiHsm2Config>,
1952    ) -> TestResult {
1953        let yubihsm2_config = yubihsm2_config?;
1954        let authorized_key_entries = yubihsm2_config.authorized_key_entries();
1955
1956        let yubihsm2_mappings = yubihsm2_mappings?;
1957        let initial_entries = yubihsm2_mappings
1958            .iter()
1959            .filter_map(|mapping| mapping.authorized_key_entry())
1960            .collect::<HashSet<_>>();
1961
1962        assert_eq!(initial_entries, authorized_key_entries);
1963
1964        Ok(())
1965    }
1966
1967    #[rstest]
1968    fn yubihsm2_config_system_user_ids(
1969        yubihsm2_mappings: TestResult<[YubiHsm2UserMapping; 5]>,
1970        yubihsm2_config: TestResult<YubiHsm2Config>,
1971    ) -> TestResult {
1972        let yubihsm2_config = yubihsm2_config?;
1973        let system_user_ids = yubihsm2_config.system_user_ids();
1974
1975        let yubihsm2_mappings = yubihsm2_mappings?;
1976        let initial_entries = yubihsm2_mappings
1977            .iter()
1978            .filter_map(|mapping| mapping.system_user_id())
1979            .collect::<HashSet<_>>();
1980
1981        assert_eq!(initial_entries, system_user_ids);
1982
1983        Ok(())
1984    }
1985
1986    #[rstest]
1987    #[case::no_connection(
1988        "Error message for YubiHsm2Config::new with no connection",
1989        BTreeSet::new(),
1990        BTreeSet::from_iter([
1991            YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1992            YubiHsm2UserMapping::Backup{
1993                authentication_key_id: "2".parse()?,
1994                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1995                system_user: "backup-user".parse()?,
1996                wrapping_key_id: "1".parse()?,
1997            },
1998            YubiHsm2UserMapping::AuditLog {
1999                authentication_key_id: "3".parse()?,
2000                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2001                system_user: "metrics-user".parse()?,
2002            },
2003            YubiHsm2UserMapping::Signing {
2004                authentication_key_id: "4".parse()?,
2005                signing_key_id: "1".parse()?,
2006                key_setup: SigningKeySetup::new(
2007                    KeyType::Curve25519,
2008                    vec![KeyMechanism::EdDsaSignature],
2009                    None,
2010                    SignatureType::EdDsa,
2011                    CryptographicKeyContext::OpenPgp {
2012                        user_ids: OpenPgpUserIdList::new(vec![
2013                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2014                        ])?,
2015                        version: "v4".parse()?,
2016                    },
2017                )?,
2018                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2019                system_user: "signing-user".parse()?,
2020                domain: Domain::One,
2021            }
2022        ]),
2023    )]
2024    #[case::no_mappings(
2025        "Error message for YubiHsm2Config::new with no user mappings",
2026        BTreeSet::from_iter([
2027            YubiHsmConnection::Usb {serial_number: "0012345678".parse()? },
2028            YubiHsmConnection::Usb {serial_number: "0087654321".parse()? },
2029        ]),
2030        BTreeSet::new(),
2031    )]
2032    #[case::duplicate_system_user_ids(
2033        "Error message for YubiHsm2Config::new with two duplicate system user IDs",
2034        BTreeSet::from_iter([
2035            YubiHsmConnection::Usb {serial_number: "0012345678".parse()? },
2036            YubiHsmConnection::Usb {serial_number: "0087654321".parse()? },
2037        ]),
2038        BTreeSet::from_iter([
2039            YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2040            YubiHsm2UserMapping::Backup{
2041                authentication_key_id: "2".parse()?,
2042                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2043                system_user: "backup-user".parse()?,
2044                wrapping_key_id: "1".parse()?,
2045            },
2046            YubiHsm2UserMapping::AuditLog {
2047                authentication_key_id: "3".parse()?,
2048                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2049                system_user: "backup-user".parse()?,
2050            },
2051            YubiHsm2UserMapping::Signing {
2052                authentication_key_id: "4".parse()?,
2053                signing_key_id: "1".parse()?,
2054                key_setup: SigningKeySetup::new(
2055                    KeyType::Curve25519,
2056                    vec![KeyMechanism::EdDsaSignature],
2057                    None,
2058                    SignatureType::EdDsa,
2059                    CryptographicKeyContext::OpenPgp {
2060                        user_ids: OpenPgpUserIdList::new(vec![
2061                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2062                        ])?,
2063                        version: "v4".parse()?,
2064                    },
2065                )?,
2066                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2067                system_user: "signing-user".parse()?,
2068                domain: Domain::One,
2069            }
2070        ]),
2071    )]
2072    #[case::duplicate_ssh_public_keys(
2073        "Error message for YubiHsm2Config::new with two duplicate SSH public keys as authorized keys",
2074        BTreeSet::from_iter([
2075            YubiHsmConnection::Usb {serial_number: "0012345678".parse()? },
2076            YubiHsmConnection::Usb {serial_number: "0087654321".parse()? },
2077        ]),
2078        BTreeSet::from_iter([
2079            YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2080            YubiHsm2UserMapping::Backup{
2081                authentication_key_id: "2".parse()?,
2082                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2083                system_user: "backup-user".parse()?,
2084                wrapping_key_id: "1".parse()?,
2085            },
2086            YubiHsm2UserMapping::AuditLog {
2087                authentication_key_id: "3".parse()?,
2088                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2089                system_user: "metrics-user".parse()?,
2090            },
2091            YubiHsm2UserMapping::Signing {
2092                authentication_key_id: "4".parse()?,
2093                signing_key_id: "1".parse()?,
2094                key_setup: SigningKeySetup::new(
2095                    KeyType::Curve25519,
2096                    vec![KeyMechanism::EdDsaSignature],
2097                    None,
2098                    SignatureType::EdDsa,
2099                    CryptographicKeyContext::OpenPgp {
2100                        user_ids: OpenPgpUserIdList::new(vec![
2101                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2102                        ])?,
2103                        version: "v4".parse()?,
2104                    },
2105                )?,
2106                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2107                system_user: "signing-user".parse()?,
2108                domain: Domain::One,
2109            }
2110        ]),
2111    )]
2112    #[case::no_administrator(
2113        "Error message for YubiHsm2Config::new with no administrator",
2114        BTreeSet::from_iter([
2115            YubiHsmConnection::Usb {serial_number: "0012345678".parse()? },
2116            YubiHsmConnection::Usb {serial_number: "0087654321".parse()? },
2117        ]),
2118        BTreeSet::from_iter([
2119            YubiHsm2UserMapping::Backup{
2120                authentication_key_id: "2".parse()?,
2121                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2122                system_user: "backup-user".parse()?,
2123                wrapping_key_id: "1".parse()?,
2124            },
2125            YubiHsm2UserMapping::AuditLog {
2126                authentication_key_id: "3".parse()?,
2127                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2128                system_user: "metrics-user".parse()?,
2129            },
2130            YubiHsm2UserMapping::Signing {
2131                authentication_key_id: "4".parse()?,
2132                signing_key_id: "1".parse()?,
2133                key_setup: SigningKeySetup::new(
2134                    KeyType::Curve25519,
2135                    vec![KeyMechanism::EdDsaSignature],
2136                    None,
2137                    SignatureType::EdDsa,
2138                    CryptographicKeyContext::OpenPgp {
2139                        user_ids: OpenPgpUserIdList::new(vec![
2140                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2141                        ])?,
2142                        version: "v4".parse()?,
2143                    },
2144                )?,
2145                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2146                system_user: "signing-user".parse()?,
2147                domain: Domain::One,
2148            }
2149        ]),
2150    )]
2151    #[case::duplicate_backend_user_ids(
2152        "Error message for YubiHsm2Config::new with two duplicate backend user IDs",
2153        BTreeSet::from_iter([
2154            YubiHsmConnection::Usb {serial_number: "0012345678".parse()? },
2155            YubiHsmConnection::Usb {serial_number: "0087654321".parse()? },
2156        ]),
2157        BTreeSet::from_iter([
2158            YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2159            YubiHsm2UserMapping::Backup{
2160                authentication_key_id: "2".parse()?,
2161                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2162                system_user: "backup-user".parse()?,
2163                wrapping_key_id: "1".parse()?,
2164            },
2165            YubiHsm2UserMapping::AuditLog {
2166                authentication_key_id: "3".parse()?,
2167                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2168                system_user: "metrics-user".parse()?,
2169            },
2170            YubiHsm2UserMapping::Signing {
2171                authentication_key_id: "3".parse()?,
2172                signing_key_id: "1".parse()?,
2173                key_setup: SigningKeySetup::new(
2174                    KeyType::Curve25519,
2175                    vec![KeyMechanism::EdDsaSignature],
2176                    None,
2177                    SignatureType::EdDsa,
2178                    CryptographicKeyContext::OpenPgp {
2179                        user_ids: OpenPgpUserIdList::new(vec![
2180                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2181                        ])?,
2182                        version: "v4".parse()?,
2183                    },
2184                )?,
2185                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2186                system_user: "signing-user".parse()?,
2187                domain: Domain::One,
2188            }
2189        ]),
2190    )]
2191    #[case::duplicate_signing_key_ids(
2192        "Error message for YubiHsm2Config::new with two duplicate signing key IDs",
2193        BTreeSet::from_iter([
2194            YubiHsmConnection::Usb {serial_number: "0012345678".parse()? },
2195            YubiHsmConnection::Usb {serial_number: "0087654321".parse()? },
2196        ]),
2197        BTreeSet::from_iter([
2198            YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2199            YubiHsm2UserMapping::Backup{
2200                authentication_key_id: "2".parse()?,
2201                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2202                system_user: "backup-user".parse()?,
2203                wrapping_key_id: "1".parse()?,
2204            },
2205            YubiHsm2UserMapping::AuditLog {
2206                authentication_key_id: "3".parse()?,
2207                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2208                system_user: "metrics-user".parse()?,
2209            },
2210            YubiHsm2UserMapping::Signing {
2211                authentication_key_id: "4".parse()?,
2212                signing_key_id: "1".parse()?,
2213                key_setup: SigningKeySetup::new(
2214                    KeyType::Curve25519,
2215                    vec![KeyMechanism::EdDsaSignature],
2216                    None,
2217                    SignatureType::EdDsa,
2218                    CryptographicKeyContext::OpenPgp {
2219                        user_ids: OpenPgpUserIdList::new(vec![
2220                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2221                        ])?,
2222                        version: "v4".parse()?,
2223                    },
2224                )?,
2225                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2226                system_user: "signing-user".parse()?,
2227                domain: Domain::One,
2228            },
2229            YubiHsm2UserMapping::Signing {
2230                authentication_key_id: "5".parse()?,
2231                signing_key_id: "1".parse()?,
2232                key_setup: SigningKeySetup::new(
2233                    KeyType::Curve25519,
2234                    vec![KeyMechanism::EdDsaSignature],
2235                    None,
2236                    SignatureType::EdDsa,
2237                    CryptographicKeyContext::OpenPgp {
2238                        user_ids: OpenPgpUserIdList::new(vec![
2239                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2240                        ])?,
2241                        version: "v4".parse()?,
2242                    },
2243                )?,
2244                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2245                system_user: "signing-user2".parse()?,
2246                domain: Domain::Two,
2247            },
2248        ]),
2249    )]
2250    #[case::duplicate_wrapping_key_ids(
2251        "Error message for YubiHsm2Config::new with two duplicate wrapping key IDs",
2252        BTreeSet::from_iter([
2253            YubiHsmConnection::Usb {serial_number: "0012345678".parse()? },
2254            YubiHsmConnection::Usb {serial_number: "0087654321".parse()? },
2255        ]),
2256        BTreeSet::from_iter([
2257            YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2258            YubiHsm2UserMapping::Backup{
2259                authentication_key_id: "2".parse()?,
2260                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2261                system_user: "backup-user".parse()?,
2262                wrapping_key_id: "1".parse()?,
2263            },
2264            YubiHsm2UserMapping::Backup{
2265                authentication_key_id: "3".parse()?,
2266                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2267                system_user: "backup-user2".parse()?,
2268                wrapping_key_id: "1".parse()?,
2269            },
2270            YubiHsm2UserMapping::AuditLog {
2271                authentication_key_id: "4".parse()?,
2272                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2273                system_user: "metrics-user".parse()?,
2274            },
2275            YubiHsm2UserMapping::Signing {
2276                authentication_key_id: "5".parse()?,
2277                signing_key_id: "1".parse()?,
2278                key_setup: SigningKeySetup::new(
2279                    KeyType::Curve25519,
2280                    vec![KeyMechanism::EdDsaSignature],
2281                    None,
2282                    SignatureType::EdDsa,
2283                    CryptographicKeyContext::OpenPgp {
2284                        user_ids: OpenPgpUserIdList::new(vec![
2285                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2286                        ])?,
2287                        version: "v4".parse()?,
2288                    },
2289                )?,
2290                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2291                system_user: "signing-user".parse()?,
2292                domain: Domain::One,
2293            },
2294        ]),
2295    )]
2296    #[case::duplicate_domains(
2297        "Error message for YubiHsm2Config::new with two duplicate domains",
2298        BTreeSet::from_iter([
2299            YubiHsmConnection::Usb {serial_number: "0012345678".parse()? },
2300            YubiHsmConnection::Usb {serial_number: "0087654321".parse()? },
2301        ]),
2302        BTreeSet::from_iter([
2303            YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2304            YubiHsm2UserMapping::Backup{
2305                authentication_key_id: "2".parse()?,
2306                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2307                system_user: "backup-user".parse()?,
2308                wrapping_key_id: "1".parse()?,
2309            },
2310            YubiHsm2UserMapping::AuditLog {
2311                authentication_key_id: "3".parse()?,
2312                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2313                system_user: "metrics-user".parse()?,
2314            },
2315            YubiHsm2UserMapping::Signing {
2316                authentication_key_id: "4".parse()?,
2317                signing_key_id: "1".parse()?,
2318                key_setup: SigningKeySetup::new(
2319                    KeyType::Curve25519,
2320                    vec![KeyMechanism::EdDsaSignature],
2321                    None,
2322                    SignatureType::EdDsa,
2323                    CryptographicKeyContext::OpenPgp {
2324                        user_ids: OpenPgpUserIdList::new(vec![
2325                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2326                        ])?,
2327                        version: "v4".parse()?,
2328                    },
2329                )?,
2330                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2331                system_user: "signing-user".parse()?,
2332                domain: Domain::One,
2333            },
2334            YubiHsm2UserMapping::Signing {
2335                authentication_key_id: "5".parse()?,
2336                signing_key_id: "2".parse()?,
2337                key_setup: SigningKeySetup::new(
2338                    KeyType::Curve25519,
2339                    vec![KeyMechanism::EdDsaSignature],
2340                    None,
2341                    SignatureType::EdDsa,
2342                    CryptographicKeyContext::OpenPgp {
2343                        user_ids: OpenPgpUserIdList::new(vec![
2344                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2345                        ])?,
2346                        version: "v4".parse()?,
2347                    },
2348                )?,
2349                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2350                system_user: "signing-user2".parse()?,
2351                domain: Domain::One,
2352            },
2353        ]),
2354    )]
2355    #[case::all_the_issues(
2356        "Error message for YubiHsm2Config::new with multiple validation issues (connections and mappings)",
2357        BTreeSet::new(),
2358        BTreeSet::from_iter([
2359            YubiHsm2UserMapping::Backup{
2360                authentication_key_id: "2".parse()?,
2361                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2362                system_user: "backup-user".parse()?,
2363                wrapping_key_id: "1".parse()?,
2364            },
2365            YubiHsm2UserMapping::Backup{
2366                authentication_key_id: "3".parse()?,
2367                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2368                system_user: "backup-user".parse()?,
2369                wrapping_key_id: "1".parse()?,
2370            },
2371            YubiHsm2UserMapping::AuditLog {
2372                authentication_key_id: "3".parse()?,
2373                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2374                system_user: "metrics-backupuser".parse()?,
2375            },
2376            YubiHsm2UserMapping::Signing {
2377                authentication_key_id: "5".parse()?,
2378                signing_key_id: "1".parse()?,
2379                key_setup: SigningKeySetup::new(
2380                    KeyType::Curve25519,
2381                    vec![KeyMechanism::EdDsaSignature],
2382                    None,
2383                    SignatureType::EdDsa,
2384                    CryptographicKeyContext::OpenPgp {
2385                        user_ids: OpenPgpUserIdList::new(vec![
2386                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2387                        ])?,
2388                        version: "v4".parse()?,
2389                    },
2390                )?,
2391                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2392                system_user: "signing-user".parse()?,
2393                domain: Domain::One,
2394            },
2395            YubiHsm2UserMapping::Signing {
2396                authentication_key_id: "5".parse()?,
2397                signing_key_id: "1".parse()?,
2398                key_setup: SigningKeySetup::new(
2399                    KeyType::Curve25519,
2400                    vec![KeyMechanism::EdDsaSignature],
2401                    None,
2402                    SignatureType::EdDsa,
2403                    CryptographicKeyContext::OpenPgp {
2404                        user_ids: OpenPgpUserIdList::new(vec![
2405                            "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2406                        ])?,
2407                        version: "v4".parse()?,
2408                    },
2409                )?,
2410                ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2411                system_user: "signing-user2".parse()?,
2412                domain: Domain::One,
2413            },
2414        ]),
2415    )]
2416    fn yubihsm2_config_new_fails_validation(
2417        #[case] description: &str,
2418        #[case] connections: BTreeSet<YubiHsmConnection>,
2419        #[case] mappings: BTreeSet<YubiHsm2UserMapping>,
2420    ) -> TestResult {
2421        let error_msg = match YubiHsm2Config::new(connections, mappings) {
2422            Err(crate::Error::Validation { source, .. }) => source.to_string(),
2423            Ok(config) => {
2424                panic!("Expected to fail with Error::Validation, but succeeded instead: {config:?}")
2425            }
2426            Err(error) => panic!(
2427                "Expected to fail with Error::Validation, but failed with a different error instead: {error}"
2428            ),
2429        };
2430
2431        with_settings!({
2432            description => description,
2433            snapshot_path => SNAPSHOT_PATH,
2434            prepend_module_to_snapshot => false,
2435        }, {
2436            assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_msg);
2437        });
2438        Ok(())
2439    }
2440}