Skip to main content

signstar_config/yubihsm2/
config.rs

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