Skip to main content

signstar_config/yubihsm2/
config.rs

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