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