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