Skip to main content

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