Skip to main content

signstar_config/yubihsm2/
backend.rs

1//! Backend handling for YubiHSM2.
2//!
3//! Using this module, a YubiHSM2 can be synchronized against a Signstar configuration file,
4//! describing its desired state.
5//!
6//! While the configuration allows for setting up administrative and non-administrative users of the
7//! backend in a declarative fashion, there are also certain implicit elements, which this module
8//! takes care of.
9//! Most notably, the wrap key, used for backups is always stored using the ID `1`.
10//! Further, certificates created using a specific asymmetric key are always stored as opaque
11//! objects using the same ID as the asymmetric key.
12use std::{cell::RefCell, collections::HashSet, fmt::Debug};
13
14use log::{debug, error, info, warn};
15use pgp::types::Timestamp;
16use signstar_crypto::{
17    key::CryptographicKeyContext,
18    openpgp::OpenPgpKeyUsageFlags,
19    signer::openpgp::generate_certificate,
20    traits::UserWithPassphrase,
21};
22use signstar_yubihsm2::{
23    Credentials,
24    YubiHsm2SigningKey,
25    automation::{
26        AuthenticatedCommandChain,
27        Command,
28        CommandName,
29        CommandReturnValue,
30        ListObjectFilter,
31        ObjectType,
32        OpaqueData,
33        OpaqueDataAlgorithm,
34        OpaqueDataCapabilities,
35        Scenario,
36        ScenarioRunner,
37    },
38    object::{
39        AuthenticationKey,
40        Capabilities,
41        Domains,
42        KeyInfo,
43        ObjectId,
44        WrapKey,
45        WrapKeyFromPassphrase,
46        WrapKeyKind,
47    },
48    yubihsm::{Client, Connector, Id, Info},
49};
50
51use crate::{
52    admin_credentials::AdminCredentials,
53    config::Config,
54    yubihsm2::{
55        Error,
56        YubiHsm2Config,
57        YubiHsm2UserMapping,
58        admin_credentials::YubiHsm2AdminCredentials,
59        state::{YubiHsm2BackendUserData, YubiHsm2BackendUserKeyData},
60    },
61};
62
63/// Return `true` if given administrative credentials are currently usable.
64///
65/// Returns `false` if the `credentials` cannot be used, have no object information, or do not match
66/// the capabilities or domains they should have.
67fn are_admin_creds_usable(runner: &ScenarioRunner, credentials: &Credentials) -> bool {
68    info!(
69        "Checking whether the authentication key {} is usable...",
70        credentials.id()
71    );
72
73    let object_infos = match get_object_infos_for_object_ids(
74        runner,
75        credentials,
76        &[ObjectId::AuthenticationKey(credentials.id())],
77    ) {
78        Ok(object_infos) => object_infos,
79        Err(error) => {
80            warn!("{error}");
81            return false;
82        }
83    };
84
85    let Some(info) = object_infos.first() else {
86        warn!(
87            "{}",
88            crate::Error::YubiHsm2Backend(Error::ScenarioLogic {
89                context: format!(
90                    "there is no object info for the authentication key ID {}",
91                    credentials.id()
92                ),
93            })
94        );
95        return false;
96    };
97
98    let temp_admin_mapping = YubiHsm2UserMapping::Admin {
99        authentication_key_id: credentials.id(),
100    };
101
102    // Ensure, that the capabilities for the object match at least the ones that we require.
103    let admin_capabilities = temp_admin_mapping.capabilities();
104    let device_capabilities = Capabilities::from(info.capabilities);
105    if !admin_capabilities
106        .as_ref()
107        .is_subset(device_capabilities.as_ref())
108    {
109        error!(
110            "The admin credentials for authentication key ID {} are valid, but its capabilities ({device_capabilities}) do not include all of the necessary ones ({admin_capabilities})!",
111            credentials.id(),
112        );
113        return false;
114    }
115
116    // Ensure, that the domains for the object match at least the ones that we require.
117    let admin_domains = temp_admin_mapping.domains();
118    let device_domains = Domains::from(info.domains);
119    if !admin_domains.as_ref().is_subset(device_domains.as_ref()) {
120        error!(
121            "The admin credentials for authentication key ID {} are valid, but its domains ({device_domains}) do not include all of the necessary ones ({admin_domains})!",
122            credentials.id(),
123        );
124        return false;
125    }
126
127    true
128}
129
130/// Returns a list of [`Id`]s that are of a specific [`ObjectType`].
131///
132/// # Note
133///
134/// Only [`Id`]s of keys visible to the provided [`Credentials`] are returned.
135///
136/// # Errors
137///
138/// Returns an error, if
139///
140/// - running a scenario fails
141/// - there is a logic error in the value(s) returned by the backend
142fn get_key_ids(
143    runner: &ScenarioRunner,
144    credentials: &Credentials,
145    object_type: ObjectType,
146) -> Result<Vec<Id>, crate::Error> {
147    debug!(
148        "Retrieving list of {object_type} using the credentials {}",
149        credentials.id()
150    );
151
152    let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(
153        credentials.clone(),
154        vec![Command::ListObjects(vec![ListObjectFilter::Type(
155            object_type,
156        )])],
157    )]);
158    let scenario_result = runner.run(&scenario)?;
159
160    let Some(command_return_values) = scenario_result.chains().first() else {
161        return Err(Error::ScenarioLogic {
162            context: format!("there are no command return values when retrieving the list of keys for the object type {object_type}"),
163        }
164        .into());
165    };
166    let Some(command_return_value) = command_return_values.first() else {
167        return Err(Error::ScenarioLogic {
168            context: format!("there is no command return value when retrieving the list of keys for the object type {object_type}"),
169        }
170        .into());
171    };
172    let CommandReturnValue::ListObjects(entries) = command_return_value else {
173        return Err(Error::ScenarioLogic {
174            context: format!("there are no entries when retrieving the list of keys for the object type {object_type}"),
175        }
176        .into());
177    };
178
179    Ok(entries
180        .iter()
181        .map(|entry| entry.object_id)
182        .collect::<Vec<_>>())
183}
184
185/// Returns a list of [`Info`] objects for a list of [`ObjectId`] objects.
186///
187/// # Errors
188///
189/// Returns an error, if
190///
191/// - retrieving object information from the backend fails
192/// - there is a logic error in the value(s) returned by the backend
193fn get_object_infos_for_object_ids(
194    runner: &ScenarioRunner,
195    credentials: &Credentials,
196    object_ids: &[ObjectId],
197) -> Result<Vec<Info>, crate::Error> {
198    debug!(
199        "Retrieving list of object infos for object IDs {} using the credentials {}",
200        object_ids
201            .iter()
202            .map(|id| format!("{id:?}"))
203            .collect::<Vec<_>>()
204            .join(", "),
205        credentials.id()
206    );
207
208    if object_ids.is_empty() {
209        return Ok(Vec::new());
210    }
211
212    let commands = object_ids
213        .iter()
214        .map(|id| Command::GetObjectInfo(*id))
215        .collect::<Vec<Command>>();
216    let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(
217        credentials.clone(),
218        commands,
219    )]);
220    let scenario_return_value = runner.run(&scenario)?;
221
222    let command_return_values = {
223        let return_values: Vec<Vec<CommandReturnValue>> = scenario_return_value.into();
224        let mut return_values_iter = return_values.into_iter();
225
226        let Some(command_return_values) = return_values_iter.next() else {
227            return Err(Error::ScenarioLogic {
228                    context: format!("there are no command return values when retrieving object infos for the objects {}",
229                        object_ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")),
230                }
231                .into());
232        };
233        if return_values_iter.next().is_some() {
234            return Err(Error::ScenarioLogic {
235                context: format!("there are more command return values than there were chains of commands when retrieving object infos for the objects {}",
236                        object_ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
237                ),
238            }
239            .into());
240        }
241        if command_return_values.len() != object_ids.len() {
242            return Err(Error::ScenarioLogic {
243                    context: format!("the number of returned object infos ({}) does not match the number of requested ones ({}) when retrieving object infos for the objects {}",
244                        command_return_values.len(),
245                        object_ids.len(),
246                        object_ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
247                    ),
248                }
249                .into());
250        }
251
252        command_return_values
253    };
254
255    let infos = {
256        let mut infos = Vec::new();
257
258        for (command_return_value, object_id) in command_return_values.into_iter().zip(object_ids) {
259            let CommandReturnValue::GetObjectInfo(info) = command_return_value else {
260                return Err(Error::ScenarioLogic {
261                            context: format!("something different from object information was returned when requesting object information for the objects {}",
262                                object_ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
263                            ),
264                        }
265                        .into());
266            };
267
268            if object_id.id() != info.object_id {
269                return Err(Error::ScenarioLogic {
270                    context: format!(
271                        "the retrieved object information with ID {} does not match the object {object_id} used for the request", info.object_id
272                    ),
273                }
274                .into());
275            }
276
277            infos.push(info);
278        }
279        infos
280    };
281
282    Ok(infos)
283}
284
285/// Returns a list of object infos for a list of object types.
286///
287/// # Errors
288///
289/// Returns an error, if
290///
291/// - key IDs for objects of a specific type cannot be retrieved,
292/// - object infos for a specific set of object type and object ID cannot be retrieved
293fn get_object_infos_for_object_types(
294    runner: &ScenarioRunner,
295    credentials: &Credentials,
296    object_types: &[ObjectType],
297) -> Result<Vec<Info>, crate::Error> {
298    debug!(
299        "Retrieve object infos for objects of the types {}",
300        object_types
301            .iter()
302            .map(ToString::to_string)
303            .collect::<Vec<_>>()
304            .join(", ")
305    );
306
307    let output = {
308        let mut output = Vec::new();
309        for object_type in object_types {
310            let ids = get_key_ids(runner, credentials, *object_type)?;
311            let mut infos = get_object_infos_for_object_ids(
312                runner,
313                credentials,
314                &ids.iter()
315                    .map(|id| ObjectId::from((*object_type, *id)))
316                    .collect::<Vec<_>>(),
317            )?;
318            output.append(&mut infos);
319        }
320        output
321    };
322
323    Ok(output)
324}
325
326/// A list of user mappings and their credentials for a YubiHSM2.
327///
328/// # Note
329///
330/// This type does not inherently distinguish between user mappings for administrative and
331/// non-administrative users!
332///
333/// Use [`Self::new_from_non_admin`] to create this struct for non-administrative user mappings and
334/// [`Self::new_from_admin`] to create it for administrative user mappings.
335#[derive(Clone, Debug)]
336struct UserMappingsAndCredentials<'config, 'creds>(
337    Vec<(&'config YubiHsm2UserMapping, &'creds Credentials)>,
338);
339
340impl<'config, 'creds> UserMappingsAndCredentials<'config, 'creds> {
341    /// Creates a new [`UserMappingsAndCredentials`] for the non-administrative user mappings in a
342    /// [`YubiHsm2Config`] and a list of [`Credentials`].
343    ///
344    /// # Errors
345    ///
346    /// Returns an error if
347    ///
348    /// - there are duplicates in `creds`
349    /// - not every user mapping has credentials assigned to it
350    /// - not every credentials have user mappings assigned to them
351    pub fn new_from_non_admin(
352        config: &'config YubiHsm2Config,
353        creds: &'creds [Credentials],
354    ) -> Result<Self, crate::Error> {
355        let mappings = config
356            .mappings()
357            .iter()
358            .filter(|mapping| !matches!(mapping, YubiHsm2UserMapping::Admin { .. }))
359            .collect::<HashSet<_>>();
360
361        Self::try_from((&mappings, creds))
362    }
363
364    /// Creates a new [`UserMappingsAndCredentials`] for the administrative user mappings in a
365    /// [`YubiHsm2Config`] and the administrative credentials of a [`YubiHsm2AdminCredentials`].
366    ///
367    /// # Errors
368    ///
369    /// Returns an error if
370    ///
371    /// - there are duplicates in `creds`
372    /// - not every user mapping has credentials assigned to it
373    /// - not every credentials have user mappings assigned to them
374    pub fn new_from_admin(
375        config: &'config YubiHsm2Config,
376        creds: &'creds YubiHsm2AdminCredentials,
377    ) -> Result<Self, crate::Error> {
378        let mappings = config
379            .mappings()
380            .iter()
381            .filter(|mapping| matches!(mapping, YubiHsm2UserMapping::Admin { .. }))
382            .collect::<HashSet<_>>();
383        let creds = creds.administrators();
384
385        Self::try_from((&mappings, creds))
386    }
387}
388
389impl<'config, 'creds>
390    TryFrom<(
391        &HashSet<&'config YubiHsm2UserMapping>,
392        &'creds [Credentials],
393    )> for UserMappingsAndCredentials<'config, 'creds>
394{
395    type Error = crate::Error;
396
397    /// Creates a new [`UserMappingsAndCredentials`] from user mappings and credentials.
398    ///
399    /// # Errors
400    ///
401    /// Returns an error if
402    ///
403    /// - there are duplicate [`Credentials`]
404    /// - not every user mapping has credentials assigned to it
405    /// - not every credentials have user mappings assigned to them
406    fn try_from(
407        value: (
408            &HashSet<&'config YubiHsm2UserMapping>,
409            &'creds [Credentials],
410        ),
411    ) -> Result<Self, Self::Error> {
412        let (mappings, creds) = value;
413
414        // Ensure, that there are no duplicate credentials.
415        {
416            let mut dupes = HashSet::new();
417            for credentials in creds {
418                if creds
419                    .iter()
420                    .filter(|creds| creds.id() == credentials.id())
421                    .count()
422                    > 1
423                {
424                    dupes.insert(credentials.id());
425                }
426            }
427            if !dupes.is_empty() {
428                let duplicates = {
429                    let mut duplicates = Vec::from_iter(dupes);
430                    duplicates.sort_unstable();
431                    duplicates
432                };
433                return Err(Error::DuplicateCredentials {
434                    context: "creating a validated set of user mappings and respective credentials",
435                    duplicates,
436                }
437                .into());
438            }
439        }
440
441        // Ensure, that each set of credentials has a user mapping assigned to it.
442        {
443            let mut ids = Vec::new();
444            for credentials in creds {
445                if !mappings
446                    .iter()
447                    .any(|mapping| mapping.backend_user_id() == credentials.id())
448                {
449                    ids.push(credentials.id());
450                }
451            }
452            if !ids.is_empty() {
453                return Err(Error::CredentialsWithoutUserMapping {
454                    context: "creating a validated set of user mappings and respective credentials",
455                    ids,
456                }
457                .into());
458            }
459        }
460
461        // Create list of user mappings and matching credentials.
462        let output = {
463            let mut output = Vec::new();
464            for user_mapping in mappings {
465                let Some(matching_creds) = creds
466                    .iter()
467                    .find(|credentials| credentials.id() == user_mapping.backend_user_id())
468                else {
469                    return Err(Error::UserMappingWithoutCredentials {
470                        context: "creating a validated set of user mappings and respective credentials",
471                        ids: vec![user_mapping.backend_user_id()],
472                    }
473                    .into());
474                };
475                output.push((*user_mapping, matching_creds));
476            }
477
478            output
479        };
480
481        Ok(Self(output))
482    }
483}
484
485impl<'config, 'creds> AsRef<[(&'config YubiHsm2UserMapping, &'creds Credentials)]>
486    for UserMappingsAndCredentials<'config, 'creds>
487{
488    fn as_ref(&self) -> &[(&'config YubiHsm2UserMapping, &'creds Credentials)] {
489        &self.0
490    }
491}
492
493/// A YubiHSM2 backend that provides control over a YubiHSM2 and its data.
494///
495/// Using a specific [`Connector`], it is possible to synchronize a YubiHSM2 with the data provided
496/// by a [`YubiHsm2AdminCredentials`] and a [`YubiHsm2Config`].
497#[derive(Debug)]
498pub struct YubiHsm2Backend<'admin_creds, 'config> {
499    connector: Connector,
500    runner: ScenarioRunner,
501    admin_credentials: &'admin_creds YubiHsm2AdminCredentials,
502    yubihsm2_config: &'config YubiHsm2Config,
503    admin_user_mappings_and_creds: UserMappingsAndCredentials<'config, 'admin_creds>,
504    /// Indication whether the default credentials are in use currently.
505    default_credentials: RefCell<bool>,
506}
507
508impl<'admin_creds, 'config> YubiHsm2Backend<'admin_creds, 'config> {
509    /// Creates a new [`YubiHsm2Backend`].
510    ///
511    /// Returns `Ok(None)` if `signstar_config` contains no [`YubiHsm2Config`].
512    ///
513    /// # Errors
514    ///
515    /// Returns an error if
516    ///
517    /// - the iteration of the `admin_credentials` does not match that of the `signstar_config`
518    /// - a set of administrative user mappings and corresponding credentials cannot be created from
519    ///   the `admin_credentials` and `signstar_config`
520    pub fn new(
521        connector: Connector,
522        admin_credentials: &'admin_creds YubiHsm2AdminCredentials,
523        signstar_config: &'config Config,
524    ) -> Result<Option<Self>, crate::Error> {
525        debug!("Create a new YubiHSM2 backend for Signstar config");
526
527        let Some(yubihsm2_config) = signstar_config.yubihsm2() else {
528            return Ok(None);
529        };
530
531        // Ensure that the iterations of administrative credentials and signstar config match.
532        if admin_credentials.iteration() != signstar_config.system().iteration() {
533            return Err(crate::Error::IterationMismatch {
534                admin_creds: admin_credentials.iteration(),
535                signstar_config: signstar_config.system().iteration(),
536            });
537        }
538
539        let admin_user_mappings_and_creds =
540            UserMappingsAndCredentials::new_from_admin(yubihsm2_config, admin_credentials)?;
541
542        let backend = Self {
543            connector: connector.clone(),
544            runner: ScenarioRunner::new(connector),
545            admin_credentials,
546            yubihsm2_config,
547            admin_user_mappings_and_creds,
548            default_credentials: RefCell::new(true),
549        };
550        backend.check_set_default_credentials();
551
552        Ok(Some(backend))
553    }
554
555    /// Returns a reference to the [`YubiHsm2Config`] used for the backend.
556    pub fn yubihsm2_config(&self) -> &YubiHsm2Config {
557        self.yubihsm2_config
558    }
559
560    /// Returns whether the default administrative credentials are in use.
561    ///
562    /// # Note
563    ///
564    /// The default administrative credentials are considered no longer in use, if logging in with
565    /// them failed once.
566    pub fn default_credentials_in_use(&self) -> bool {
567        *self.default_credentials.borrow()
568    }
569
570    /// Checks whether the default credentials are in use and sets up [`YubiHsm2Backend`]
571    /// accordingly.
572    ///
573    /// Returns `true`, if [`YubiHsm2AdminCredentials::default_credentials`] can be used to connect
574    /// to the YubiHSM2 and the authentication key object contains the required capabilities and
575    /// domains. Returns `false` in all other cases.
576    fn check_set_default_credentials(&self) -> bool {
577        info!("Checking whether the default credentials are still set...");
578
579        *self.default_credentials.borrow_mut() = are_admin_creds_usable(
580            &self.runner,
581            &YubiHsm2AdminCredentials::default_credentials(),
582        );
583
584        *self.default_credentials.borrow()
585    }
586
587    /// Syncs the state of a Signstar configuration with the backend using credentials for users in
588    /// non-administrative roles.
589    pub fn sync(&self, user_credentials: &[Credentials]) -> Result<(), crate::Error> {
590        info!("Syncing the YubiHSM2 backend with the Signstar configuration...");
591
592        let non_admin_users_and_creds =
593            UserMappingsAndCredentials::new_from_non_admin(self.yubihsm2_config, user_credentials)?;
594        debug!(
595            "valid mappings and their creds: {}",
596            non_admin_users_and_creds
597                .as_ref()
598                .iter()
599                .map(|(mapping, creds)| format!("{mapping:?}: {creds:?}"))
600                .collect::<Vec<_>>()
601                .join(", ")
602        );
603
604        self.add_admin_users()?;
605        self.add_wrap_key()?;
606        self.add_non_admin_users(&non_admin_users_and_creds)?;
607        self.add_signing_keys()?;
608        self.add_openpgp_certificates(&non_admin_users_and_creds)?;
609
610        Ok(())
611    }
612
613    /// Adds the OpenPGP certificates for keys that use them.
614    ///
615    /// # Note
616    ///
617    /// Does **not** overwrite existing data!
618    ///
619    /// # Errors
620    ///
621    /// Returns an error, if
622    ///
623    /// - a usable administrative authentication key cannot be found
624    /// - retrieving of opaque data info fails
625    /// - creating an OpenPGP certificate for a signgin key fails
626    /// - the scenario of adding OpenPGP certificates as opaque data fails
627    /// - the return values of the scenario do not match the requested actions
628    fn add_openpgp_certificates(
629        &self,
630        non_admin_user_mappings_and_creds: &UserMappingsAndCredentials,
631    ) -> Result<(), crate::Error> {
632        info!("Adding OpenPGP certificates for signing keys...");
633
634        let credentials = self.usable_admin_creds()?;
635
636        let (commands, ids) = {
637            let opaque_data_infos = get_object_infos_for_object_types(
638                &self.runner,
639                &credentials,
640                &[ObjectType::Opaque],
641            )?;
642            let mut commands = Vec::new();
643            let mut ids = Vec::new();
644
645            for (mapping, credentials) in non_admin_user_mappings_and_creds.as_ref() {
646                // NOTE: We are disregarding the key information here, because we currently only
647                // consider ed25519 keys.
648                if let YubiHsm2UserMapping::Signing {
649                    domain,
650                    signing_key_id,
651                    key_setup,
652                    ..
653                } = mapping
654                {
655                    let CryptographicKeyContext::OpenPgp {
656                        user_ids, version, ..
657                    } = key_setup.key_context()
658                    else {
659                        debug!(
660                            "Skipping the generation of an OpenPGP certificate for signing key {signing_key_id}, because it is not setup for use with OpenPGP..."
661                        );
662                        continue;
663                    };
664
665                    if opaque_data_infos
666                        .iter()
667                        .any(|info| info.object_id == *signing_key_id)
668                    {
669                        warn!(
670                            "Skipping the generation of an OpenPGP certificate for signing key {signing_key_id}, because data exists already..."
671                        );
672                        continue;
673                    }
674
675                    let client = Client::create(self.connector.clone(), (*credentials).into())
676                        .map_err(|source| signstar_yubihsm2::Error::Client { context: "creating a client for adding OpenPGP certificates for a signing key", source })?;
677                    let signer = YubiHsm2SigningKey::new(client, *signing_key_id);
678                    let flags = {
679                        let mut flags = OpenPgpKeyUsageFlags::default();
680                        flags.set_sign();
681                        flags
682                    };
683                    let certificate = generate_certificate(
684                        &signer,
685                        flags,
686                        user_ids.as_ref(),
687                        Default::default(),
688                        Timestamp::now(),
689                        *version,
690                    )?;
691                    signer.close_session()?;
692
693                    commands.push(Command::PutOpaque {
694                        id: *signing_key_id,
695                        label: YubiHsm2Config::openpgp_certificate_label(),
696                        domains: Domains::from(*domain),
697                        capabilities: OpaqueDataCapabilities::ExportableUnderWrap,
698                        algorithm: OpaqueDataAlgorithm::OpaqueData,
699                        data: OpaqueData::new(certificate)?,
700                    });
701                    ids.push(*signing_key_id);
702                }
703            }
704
705            (commands, ids)
706        };
707
708        // If there is nothing to do, exit early.
709        if commands.is_empty() {
710            return Ok(());
711        }
712
713        let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
714        let scenario_result = self.runner.run(&scenario)?;
715
716        // Ensure that the requested and returned authentication key IDs match.
717        let Some(command_return_values) = scenario_result.chains().first() else {
718            return Err(Error::ScenarioLogic {
719                context: format!(
720                    "there are no command return values when adding OpenPGP certificates for signing keys {}",
721                    ids.iter()
722                        .map(ToString::to_string)
723                        .collect::<Vec<_>>()
724                        .join(", ")
725                ),
726            }
727            .into());
728        };
729        if command_return_values.len() != ids.len() {
730            return Err(Error::ScenarioLogic {
731            context: format!("the number of command return values ({}) does not match the number of requested IDs ({}) when adding OpenPGP certificates for signing keys {}",
732                command_return_values.len(),
733                ids.len(),
734                ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
735            ),
736        }
737        .into());
738        };
739
740        for (command_return_value, requested_id) in command_return_values.iter().zip(ids.iter()) {
741            let returned_id = match command_return_value {
742                CommandReturnValue::PutOpaque(returned_id) => *returned_id,
743                command => {
744                    let command_name = CommandName::from(command);
745                    return Err(Error::ScenarioLogic {
746                        context: format!("instead of the command return value for adding an OpenPGP certificate for signing key {requested_id} the return value for {command_name} was returned"),
747                    }
748                    .into());
749                }
750            };
751
752            if *requested_id != returned_id {
753                return Err(Error::ScenarioLogic {
754                    context: format!("the returned signing key ID ({returned_id}) does not match the requested one ({requested_id}) when adding OpenPGP certificates for the signing keys {}",
755                        ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
756                    ),
757                }
758                .into());
759            }
760        }
761
762        Ok(())
763    }
764
765    /// Adds the asymmetric signing keys.
766    ///
767    /// # Note
768    ///
769    /// Does **not** overwrite existing signing keys!
770    ///
771    /// # Errors
772    ///
773    /// Returns an error, if
774    ///
775    /// - a usable administrative authentication key cannot be found
776    /// - retrieving of signing key info fails
777    /// - the scenario of generating asymmetric signging keys fails
778    /// - the return values of the scenario do not match the requested actions
779    fn add_signing_keys(&self) -> Result<(), crate::Error> {
780        info!("Adding asymmetric signing keys...");
781
782        let credentials = self.usable_admin_creds()?;
783        let (commands, ids) = {
784            let signing_key_infos = get_object_infos_for_object_types(
785                &self.runner,
786                &credentials,
787                &[ObjectType::AsymmetricKey],
788            )?;
789            let mut commands = Vec::new();
790            let mut ids = Vec::new();
791
792            for mapping in self.yubihsm2_config.mappings() {
793                // NOTE: We are disregarding the key information here, because we only ever consider
794                // ed25519 keys.
795                if let YubiHsm2UserMapping::Signing {
796                    domain,
797                    signing_key_id,
798                    ..
799                } = mapping
800                {
801                    // Do not replace already existing signing keys.
802                    if signing_key_infos
803                        .iter()
804                        .any(|info| info.object_id == *signing_key_id)
805                    {
806                        warn!("Not adding signing key {signing_key_id}, as it exists already...");
807                        continue;
808                    }
809
810                    commands.push(Command::GenerateAsymmetricKey {
811                        info: KeyInfo {
812                            key_id: *signing_key_id,
813                            domains: Domains::from(*domain),
814                            caps: mapping.capabilities(),
815                            label: mapping.label(),
816                        },
817                    });
818                    ids.push(*signing_key_id);
819                }
820            }
821
822            (commands, ids)
823        };
824
825        // If there is nothing to do, exit early.
826        if commands.is_empty() {
827            return Ok(());
828        }
829
830        let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
831        let scenario_result = self.runner.run(&scenario)?;
832
833        let Some(command_return_values) = scenario_result.chains().first() else {
834            return Err(Error::ScenarioLogic {
835                context: format!(
836                    "there are no command return values when adding the signing keys {}",
837                    ids.iter()
838                        .map(ToString::to_string)
839                        .collect::<Vec<_>>()
840                        .join(", ")
841                ),
842            }
843            .into());
844        };
845        if command_return_values.len() != ids.len() {
846            return Err(Error::ScenarioLogic {
847            context: format!("the number of command return values ({}) does not match the number of requested IDs ({}) when adding the signing keys {}",
848                command_return_values.len(),
849                ids.len(),
850                ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
851            ),
852        }
853        .into());
854        };
855
856        for (number, command_return_value) in command_return_values.iter().enumerate() {
857            let Some(id) = ids.get(number) else {
858                return Err(Error::ScenarioLogic {
859                    context: format!("the signing key ID number {number} does not exist in the list of signing keys {}",
860                        ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
861                    ),
862                }
863                .into());
864            };
865            let CommandReturnValue::GenerateAsymmetricKey(returned_id) = command_return_value
866            else {
867                return Err(Error::ScenarioLogic {
868                    context: format!("the command return value when adding the signing key {id} is that of a different action"),
869                }
870                .into());
871            };
872            if id != returned_id {
873                return Err(Error::ScenarioLogic {
874                    context: format!("the returned signing key ID ({returned_id}) does not match the requested one ({id}) when adding the signing keys {}",
875                        ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
876                    ),
877                }
878                .into());
879            }
880        }
881
882        Ok(())
883    }
884
885    /// Adds non-administrative users.
886    ///
887    /// # Note
888    ///
889    /// Existing authentication keys are replaced!
890    ///
891    /// # Errors
892    ///
893    /// Returns an error, if
894    ///
895    /// - a usable administrative authentication key cannot be found
896    /// - the list of authentication key objects cannot be retrieved from the backend
897    /// - a new authentication key cannot be created
898    /// - the scenario cannot be run successfully
899    /// - one or more return values of the scenario do not match the requested actions
900    fn add_non_admin_users(
901        &self,
902        non_admin_user_mappings_and_creds: &UserMappingsAndCredentials,
903    ) -> Result<(), crate::Error> {
904        info!("Setting up non-administrative users...");
905
906        let credentials = self.usable_admin_creds()?;
907
908        let (commands, ids) = {
909            let authentication_key_infos = get_object_infos_for_object_types(
910                &self.runner,
911                &credentials,
912                &[ObjectType::AuthenticationKey],
913            )?;
914            let mut commands = Vec::new();
915            let mut ids = Vec::new();
916
917            for (mapping, creds) in non_admin_user_mappings_and_creds.as_ref() {
918                let id = mapping.backend_user_id();
919
920                if authentication_key_infos
921                    .iter()
922                    .any(|info| info.object_id == id)
923                {
924                    warn!("The existing authentication key {id} will be replaced...");
925                    commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(id)));
926                }
927
928                commands.push(Command::PutAuthenticationKey {
929                    info: mapping.authentication_key_info(),
930                    delegated_caps: Capabilities::from(vec![].as_slice()),
931                    authentication_key: AuthenticationKey::try_from(creds.passphrase())
932                        .map_err(crate::Error::SignstarYubiHsm2)?,
933                });
934                ids.push(id);
935            }
936
937            (commands, ids)
938        };
939
940        // If there is nothing to do, exit early.
941        if commands.is_empty() {
942            return Ok(());
943        }
944
945        let number_of_commands = commands.len();
946        let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
947        let scenario_result = self.runner.run(&scenario)?;
948
949        // Validate the return values against the acclaimed actions.
950        let Some(command_return_values) = scenario_result.chains().first() else {
951            return Err(Error::ScenarioLogic {
952                context: format!("there are no command return values when adding the non-administrative users {}",
953                    ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
954                ),
955            }
956            .into());
957        };
958        if command_return_values.len() != number_of_commands {
959            return Err(Error::ScenarioLogic {
960            context: format!("the number of command return values ({}) does not match the number of requested IDs ({number_of_commands}) when adding the non-administrative users {}",
961                command_return_values.len(),
962                ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
963            ),
964        }
965        .into());
966        };
967
968        // Ensure that the requested and returned authentication key IDs match.
969        for (number, command_return_value) in command_return_values
970            .iter()
971            .filter(|command_return_value| {
972                matches!(
973                    command_return_value,
974                    CommandReturnValue::PutAuthenticationKey(_)
975                )
976            })
977            .enumerate()
978        {
979            let Some(id) = ids.get(number) else {
980                return Err(Error::ScenarioLogic {
981                    context: format!("the authentication key ID number {number} does not exist in the list of non-administrative users {}",
982                        ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
983                    ),
984                }
985                .into());
986            };
987            let CommandReturnValue::PutAuthenticationKey(returned_id) = command_return_value else {
988                return Err(Error::ScenarioLogic {
989                    context: format!("the command return value when adding the non-administrative user {id} is that of a different action"),
990                }
991                .into());
992            };
993            if id != returned_id {
994                return Err(Error::ScenarioLogic {
995                    context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({id}) when adding the non-administrative users {}",
996                        ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
997                    ),
998                }
999                .into());
1000            }
1001        }
1002
1003        Ok(())
1004    }
1005
1006    /// Adds a wrap key based on the backup passphrase.
1007    ///
1008    /// # Note
1009    ///
1010    /// Existing wrap keys are replaced!
1011    ///
1012    /// # Errors
1013    ///
1014    /// Returns an error, if
1015    ///
1016    /// - usable administrative credentials cannot be found
1017    /// - infos about wrap keys cannot be retrieved from the backend
1018    /// - a wrap key cannot be created from the backup passphrase
1019    /// - running the scenario against the backend fails
1020    /// - the scenario's return values do not match the request
1021    fn add_wrap_key(&self) -> Result<(), crate::Error> {
1022        info!("Setting up wrapping key...");
1023
1024        let credentials = self.usable_admin_creds()?;
1025
1026        let commands = {
1027            let wrap_key_infos = get_object_infos_for_object_types(
1028                &self.runner,
1029                &credentials,
1030                &[ObjectType::WrapKey],
1031            )?;
1032            let mut commands = Vec::new();
1033
1034            // If the wrap key exists already, delete it first.
1035            if wrap_key_infos
1036                .iter()
1037                .any(|info| info.object_id == YubiHsm2Config::WRAP_KEY_ID)
1038            {
1039                warn!(
1040                    "The existing wrapping key ({}) will be replaced...",
1041                    YubiHsm2Config::WRAP_KEY_ID
1042                );
1043                commands.push(Command::DeleteObject(ObjectId::WrappingKey(
1044                    YubiHsm2Config::WRAP_KEY_ID,
1045                )))
1046            }
1047
1048            let passphrase = self.admin_credentials.backup_passphrase();
1049            let wrapping_key: WrapKey =
1050                WrapKeyFromPassphrase::new(passphrase, WrapKeyKind::Aes256)?.try_into()?;
1051            commands.push(Command::PutWrapKey {
1052                info: KeyInfo {
1053                    key_id: YubiHsm2Config::WRAP_KEY_ID,
1054                    domains: Domains::all(),
1055                    caps: Capabilities::from(YubiHsm2UserMapping::CAP_BACKUP),
1056                    label: YubiHsm2Config::wrap_key_label(),
1057                },
1058                delegated_caps: Capabilities::from(YubiHsm2UserMapping::CAP_BACKUP),
1059                wrapping_key,
1060            });
1061            commands
1062        };
1063        let number_of_commands = commands.len();
1064
1065        let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(
1066            credentials.clone(),
1067            commands,
1068        )]);
1069        let scenario_return_value = self.runner.run(&scenario)?;
1070
1071        // Validate the return values against the acclaimed actions.
1072        let command_return_values = {
1073            let return_values: Vec<Vec<CommandReturnValue>> = scenario_return_value.into();
1074            let return_values_len = return_values.len();
1075            let mut return_values_iter = return_values.into_iter();
1076
1077            let Some(command_return_values) = return_values_iter.next() else {
1078                return Err(Error::ScenarioLogic {
1079                    context: format!(
1080                        "there are no command return values when adding the wrap key {}",
1081                        YubiHsm2Config::WRAP_KEY_ID
1082                    ),
1083                }
1084                .into());
1085            };
1086            if return_values_iter.next().is_some() {
1087                return Err(Error::ScenarioLogic {
1088                    context: format!("there are {} return value lists when adding the wrap key {}, but we expected exactly 1",
1089                        return_values_len,
1090                        YubiHsm2Config::WRAP_KEY_ID
1091                    ),
1092                }
1093                .into());
1094            }
1095            command_return_values
1096        };
1097        let returned_id = {
1098            match command_return_values.len() {
1099                2 => {
1100                    let Some(CommandReturnValue::DeleteObject) = command_return_values.first()
1101                    else {
1102                        return Err(Error::ScenarioLogic {
1103                            context: format!(
1104                                "there is no return value for removing a previously existing wrap key {}",
1105                                YubiHsm2Config::WRAP_KEY_ID
1106                            ),
1107                        }
1108                        .into());
1109                    };
1110                    let Some(CommandReturnValue::PutWrapKey(id)) = command_return_values.get(1)
1111                    else {
1112                        return Err(Error::ScenarioLogic {
1113                            context: format!(
1114                                "there is no return value for adding the wrap key {}",
1115                                YubiHsm2Config::WRAP_KEY_ID
1116                            ),
1117                        }
1118                        .into());
1119                    };
1120                    *id
1121                }
1122                1 => {
1123                    let Some(CommandReturnValue::PutWrapKey(id)) = command_return_values.first()
1124                    else {
1125                        return Err(Error::ScenarioLogic {
1126                            context: format!(
1127                                "there is no return value for adding the wrap key {}",
1128                                YubiHsm2Config::WRAP_KEY_ID
1129                            ),
1130                        }
1131                        .into());
1132                    };
1133                    *id
1134                }
1135                _ => {
1136                    return Err(Error::ScenarioLogic {
1137                        context: format!("there are {} return values when removing and/or adding the wrap key {}, but we expected exactly {number_of_commands}",
1138                            command_return_values.len(),
1139                            YubiHsm2Config::WRAP_KEY_ID
1140                        ),
1141                    }
1142                    .into());
1143                }
1144            }
1145        };
1146        if returned_id != YubiHsm2Config::WRAP_KEY_ID {
1147            return Err(Error::ScenarioLogic {
1148                context: format!("the returned key ID ({returned_id}) does not match the requested one when adding the wrap key {}",
1149                    YubiHsm2Config::WRAP_KEY_ID
1150                ),
1151            }
1152            .into());
1153        }
1154
1155        Ok(())
1156    }
1157
1158    /// Sets up all admin users in the backend.
1159    ///
1160    /// # Note
1161    ///
1162    /// Existing authentication keys are replaced!
1163    ///
1164    /// # Errors
1165    ///
1166    /// Returns an error, if
1167    ///
1168    /// - no usable administrative credentials can be found
1169    /// - currently used authentication keys cannot be retrieved from the backend
1170    /// - the currently used credentials cannot be found in the set of available credentials
1171    /// - the scenario for removing and/or adding all relevant administrative authentication keys
1172    ///   fails
1173    /// - the return values for the scenario do not match the requested actions
1174    /// - the default admin credentials are still usable at the end of this function
1175    fn add_admin_users(&self) -> Result<(), crate::Error> {
1176        info!("Setting up administrative users...");
1177
1178        let credentials = self.usable_admin_creds()?;
1179        let (commands, ids) = {
1180            let authentication_key_infos = get_object_infos_for_object_types(
1181                &self.runner,
1182                &credentials,
1183                &[ObjectType::AuthenticationKey],
1184            )?;
1185            let mut commands = Vec::new();
1186            let mut ids = Vec::new();
1187
1188            // Delete and/or put all administrative credentials, except the one currently in use.
1189            for (mapping, creds) in self
1190                .admin_user_mappings_and_creds
1191                .as_ref()
1192                .iter()
1193                .filter(|(mapping, ..)| mapping.backend_user_id() != credentials.id())
1194            {
1195                let id = mapping.backend_user_id();
1196
1197                if authentication_key_infos
1198                    .iter()
1199                    .any(|info| info.object_id == id)
1200                {
1201                    warn!("Replacing the existing authentication key {id}...");
1202                    commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(id)));
1203                    ids.push(id);
1204                }
1205
1206                commands.push(Command::PutAuthenticationKey {
1207                    info: KeyInfo {
1208                        key_id: creds.id(),
1209                        domains: mapping.domains(),
1210                        caps: mapping.capabilities(),
1211                        label: mapping.label(),
1212                    },
1213                    delegated_caps: mapping.capabilities(),
1214                    authentication_key: AuthenticationKey::try_from(creds.passphrase())?,
1215                });
1216                ids.push(id);
1217            }
1218
1219            // Remove the default administrative authentication key, if an authentication key with
1220            // its ID is available in the backend, but the ID is not used in the Signstar
1221            // configuration.
1222            if authentication_key_infos
1223                .iter()
1224                .any(|info| info.object_id == YubiHsm2AdminCredentials::DEFAULT_ID)
1225                && !self
1226                    .admin_user_mappings_and_creds
1227                    .as_ref()
1228                    .iter()
1229                    .any(|(mapping, _)| {
1230                        mapping.backend_user_id() == YubiHsm2AdminCredentials::DEFAULT_ID
1231                    })
1232            {
1233                warn!(
1234                    "Removing the default authentication key {}...",
1235                    YubiHsm2AdminCredentials::DEFAULT_ID
1236                );
1237                commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(
1238                    YubiHsm2AdminCredentials::DEFAULT_ID,
1239                )));
1240                ids.push(YubiHsm2AdminCredentials::DEFAULT_ID)
1241            }
1242
1243            // Change the currently used authentication key, if it is setup in the administrative
1244            // credentials/ configuration.
1245            if let Some((_, current_credentials)) = self
1246                .admin_user_mappings_and_creds
1247                .as_ref()
1248                .iter()
1249                .find(|(mapping, ..)| mapping.backend_user_id() == credentials.id())
1250            {
1251                warn!(
1252                    "Changing the authentication key {}, which is currently used...",
1253                    current_credentials.id()
1254                );
1255                commands.push(Command::ChangeAuthenticationKey {
1256                    key_id: current_credentials.id(),
1257                    authentication_key: AuthenticationKey::try_from(
1258                        current_credentials.passphrase(),
1259                    )?,
1260                });
1261                ids.push(current_credentials.id());
1262            }
1263
1264            (commands, ids)
1265        };
1266
1267        // If there is nothing to do, exit early.
1268        if commands.is_empty() {
1269            return Ok(());
1270        }
1271
1272        let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
1273        let scenario_result = self.runner.run(&scenario)?;
1274
1275        // Validate the return values against the acclaimed actions.
1276        let Some(command_return_values) = scenario_result.chains().first() else {
1277            return Err(Error::ScenarioLogic {
1278                context: format!(
1279                    "there are no command return values when adding the administrative users {}",
1280                    ids.iter()
1281                        .map(ToString::to_string)
1282                        .collect::<Vec<_>>()
1283                        .join(", ")
1284                ),
1285            }
1286            .into());
1287        };
1288        if command_return_values.len() != ids.len() {
1289            return Err(Error::ScenarioLogic {
1290                context: format!("the number of command return values ({}) does not match the number of requested commands ({}) when removing and/or adding the administrative users {}",
1291                    command_return_values.len(),
1292                    ids.len(),
1293                    ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1294                ),
1295            }
1296            .into());
1297        };
1298
1299        // Ensure that the requested and returned authentication key IDs match.
1300        for (command_return_value, requested_id) in command_return_values.iter().zip(ids.iter()) {
1301            match command_return_value {
1302                CommandReturnValue::PutAuthenticationKey(returned_id) => {
1303                    if returned_id != requested_id {
1304                        return Err(Error::ScenarioLogic {
1305                        context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({requested_id}) when adding the administrative user"),
1306                    }
1307                    .into());
1308                    }
1309                }
1310                CommandReturnValue::DeleteObject => {
1311                    // NOTE: There is nothing to match here.
1312                }
1313                CommandReturnValue::ChangeAuthenticationKey(returned_id) => {
1314                    if returned_id != requested_id {
1315                        return Err(Error::ScenarioLogic {
1316                        context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({requested_id}) when changing the administrative user"),
1317                    }
1318                    .into());
1319                    }
1320                }
1321                _ => return Err(Error::ScenarioLogic {
1322                    context: format!("the command return value is not for adding, removing or changing the administrative user {requested_id}: {command_return_value:?}"),
1323                }
1324                .into()),
1325            }
1326        }
1327
1328        info!("Successfully added all admins");
1329
1330        // Check whether the default credentials are still in use (they shouldn't be at this
1331        // point) and set the internal state accordingly.
1332        if self.check_set_default_credentials() {
1333            return Err(Error::DefaultAdminStillUsable.into());
1334        }
1335
1336        Ok(())
1337    }
1338
1339    /// Returns the currently usable credentials for an administrative user.
1340    ///
1341    /// # Errors
1342    ///
1343    /// Returns an error, if no usable administrative credentials are found.
1344    fn usable_admin_creds(&self) -> Result<Credentials, crate::Error> {
1345        info!("Finding usable administrative credentials...");
1346
1347        let credentials = if self.default_credentials_in_use() {
1348            YubiHsm2AdminCredentials::default_credentials()
1349        } else {
1350            let Some(credentials) = self
1351                .admin_credentials
1352                .administrators()
1353                .iter()
1354                .find(|credentials| are_admin_creds_usable(&self.runner, credentials))
1355            else {
1356                return Err(Error::NoUsableAdmin.into());
1357            };
1358
1359            credentials.clone()
1360        };
1361
1362        info!(
1363            "Using administrative credentials with authentication ID {}",
1364            credentials.id()
1365        );
1366
1367        Ok(credentials)
1368    }
1369
1370    /// Returns the list of available authentication key objects in the backend.
1371    ///
1372    /// # Errors
1373    ///
1374    /// Returns an error, if
1375    /// - no usable administrative credentials can be found
1376    /// - retrieving the information on authentication key objects fails.
1377    pub(crate) fn user_states(&self) -> Result<Vec<YubiHsm2BackendUserData>, crate::Error> {
1378        let credentials = self.usable_admin_creds()?;
1379        // Get the list of all authentication key IDs.
1380        let infos = get_object_infos_for_object_types(
1381            &self.runner,
1382            &credentials,
1383            &[ObjectType::AuthenticationKey],
1384        )?;
1385
1386        let user_states = infos
1387            .iter()
1388            .map(|info| YubiHsm2BackendUserData {
1389                id: info.object_id,
1390                capabilities: info.capabilities.into(),
1391                domains: info.domains.into(),
1392            })
1393            .collect::<Vec<_>>();
1394
1395        Ok(user_states)
1396    }
1397
1398    /// Returns the list of available non-authentication key objects in the backend.
1399    ///
1400    /// # Errors
1401    ///
1402    /// Returns an error if
1403    ///
1404    /// - no usable administrative credentials can be found
1405    /// - the fetching of one or more object infos fails
1406    pub(crate) fn key_states(&self) -> Result<Vec<YubiHsm2BackendUserKeyData>, crate::Error> {
1407        let credentials = self.usable_admin_creds()?;
1408        // Get the list of IDs for all objects that are not authentication keys.
1409        let object_types = [
1410            ObjectType::AsymmetricKey,
1411            ObjectType::HmacKey,
1412            ObjectType::Opaque,
1413            ObjectType::OtpAeakey,
1414            ObjectType::Template,
1415            ObjectType::WrapKey,
1416        ];
1417        let infos = get_object_infos_for_object_types(&self.runner, &credentials, &object_types)?;
1418
1419        let key_states = infos
1420            .into_iter()
1421            .filter_map(|info| {
1422                // NOTE: Ignore object IDs reserved by the vendor (<https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-intro-core-concepts.html#object-id>).
1423                // This needs fixing for the mockhsm integration and can then be removed, as these objects should never be listed in the first place: https://gitlab.archlinux.org/dvzrv/yubihsm2/-/work_items/11
1424                if info.object_id == 0 || info.object_id == Id::MAX {
1425                    None
1426                } else {
1427                    Some(YubiHsm2BackendUserKeyData {
1428                        id: info.object_id,
1429                        object_type: info.object_type.into(),
1430                        capabilities: info.capabilities.into(),
1431                        domains: info.domains.into(),
1432                        algorithm: info.algorithm.into(),
1433                        label: info.label.into(),
1434                        length: info.length,
1435                    })
1436                }
1437            })
1438            .collect::<Vec<_>>();
1439
1440        Ok(key_states)
1441    }
1442}
1443
1444#[cfg(all(test, feature = "_yubihsm2-mockhsm"))]
1445mod tests {
1446    use std::collections::BTreeSet;
1447
1448    use log::LevelFilter;
1449    use rstest::{fixture, rstest};
1450    use signstar_common::logging::setup_logging;
1451    use signstar_crypto::{
1452        AdministrativeSecretHandling,
1453        NonAdministrativeSecretHandling,
1454        key::{
1455            CryptographicKeyContext,
1456
1457            SigningKeySetup,
1458            base::{KeyMechanism, KeyType, SignatureType},
1459        },
1460        openpgp::OpenPgpUserIdList,
1461        passphrase::Passphrase,
1462    };
1463    use signstar_yubihsm2::{Connection, object::Domain};
1464    use testresult::TestResult;
1465
1466    use super::*;
1467    use crate::{
1468        config::{ConfigBuilder, SystemConfig},
1469        yubihsm2::{YubiHsm2Config, YubiHsm2UserMapping, state::YubiHsm2BackendState},
1470    };
1471
1472    /// Creates a MockHSM [`Connector`].
1473    #[fixture]
1474    fn connector() -> Connector {
1475        Connector::mockhsm()
1476    }
1477
1478    /// Creates a default [`YubiHsm2Config`] for testing purposes.
1479    #[fixture]
1480    fn yubihsm2_config() -> TestResult<YubiHsm2Config> {
1481        Ok(YubiHsm2Config::new(
1482            BTreeSet::from_iter([
1483                Connection::Mock
1484            ]),
1485            BTreeSet::from_iter([
1486                YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1487                YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1488                YubiHsm2UserMapping::AuditLog {
1489                    authentication_key_id: 3,
1490                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1491                    system_user: "yubihsm2-metrics-user".parse()?,
1492                },
1493                YubiHsm2UserMapping::Backup{
1494                    authentication_key_id: 2,
1495                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1496                    system_user: "yubihsm2-backup-user".parse()?,
1497                    wrapping_key_id: 1,
1498                },
1499                YubiHsm2UserMapping::HermeticAuditLog {
1500                    authentication_key_id: 4,
1501                    system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1502                },
1503                YubiHsm2UserMapping::Signing {
1504                    authentication_key_id: 5,
1505                    signing_key_id: 1,
1506                    key_setup: SigningKeySetup::new(
1507                        KeyType::Curve25519,
1508                        vec![KeyMechanism::EdDsaSignature],
1509                        None,
1510                        SignatureType::EdDsa,
1511                        CryptographicKeyContext::OpenPgp {
1512                            notations: Default::default(),
1513                            user_ids: OpenPgpUserIdList::new(vec![
1514                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1515                            ])?,
1516                            version: "v4".parse()?,
1517                        },
1518                    )?,
1519                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1520                    system_user: "yubihsm2-signing-user".parse()?,
1521                    domain: Domain::One,
1522                }
1523            ]),
1524        )?)
1525    }
1526
1527    /// Creates a simple [`Config`] with [`YubiHsm2Config`].
1528    #[fixture]
1529    fn signstar_config(yubihsm2_config: TestResult<YubiHsm2Config>) -> TestResult<Config> {
1530        let yubihsm2_config = yubihsm2_config?;
1531        let config = ConfigBuilder::new(SystemConfig::new(
1532            1,
1533            signstar_crypto::AdministrativeSecretHandling::Plaintext,
1534            signstar_crypto::NonAdministrativeSecretHandling::Plaintext,
1535            BTreeSet::from_iter([]),
1536        )?)
1537        .set_yubihsm2_config(yubihsm2_config)
1538        .finish()?;
1539
1540        Ok(config)
1541    }
1542
1543    #[fixture]
1544    fn admin_credentials() -> TestResult<YubiHsm2AdminCredentials> {
1545        let admin_creds = YubiHsm2AdminCredentials::new(
1546            1,
1547            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1548            vec![
1549                Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1550                Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1551            ]
1552        )?;
1553        Ok(admin_creds)
1554    }
1555
1556    /// Creates a new [`YubiHsm2Backend`].
1557    ///
1558    /// # Errors
1559    ///
1560    /// Returns an error, if [`YubiHsm2Backend::new`] fails.
1561    ///
1562    /// # Panics
1563    ///
1564    /// Panics if [`YubiHsm2Backend::new`] returns [`Option::None`].
1565    fn create_yubihsm2_backend<'admin_creds, 'config>(
1566        connector: Connector,
1567        admin_credentials: &'admin_creds YubiHsm2AdminCredentials,
1568        signstar_config: &'config Config,
1569    ) -> TestResult<YubiHsm2Backend<'admin_creds, 'config>> {
1570        setup_logging(LevelFilter::Debug)?;
1571        let Some(backend) = YubiHsm2Backend::new(connector, admin_credentials, signstar_config)?
1572        else {
1573            panic!("The Config did not contain a YubiHsm2Config.");
1574        };
1575
1576        Ok(backend)
1577    }
1578
1579    /// Ensures, that [`YubiHsm2Backend::sync`] can be executed successfully (against a MockHSM
1580    /// backend).
1581    #[rstest]
1582    #[case::single_admin_uses_default_id(
1583        YubiHsm2AdminCredentials::new(
1584            1,
1585            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1586            vec![
1587                Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1588            ]
1589        )?,
1590        YubiHsm2Config::new(
1591            BTreeSet::from_iter([
1592                Connection::Mock
1593            ]),
1594            BTreeSet::from_iter([
1595                YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1596                YubiHsm2UserMapping::AuditLog {
1597                    authentication_key_id: 3,
1598                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1599                    system_user: "yubihsm2-metrics-user".parse()?,
1600                },
1601                YubiHsm2UserMapping::Backup{
1602                    authentication_key_id: 2,
1603                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1604                    system_user: "yubihsm2-backup-user".parse()?,
1605                    wrapping_key_id: 1,
1606                },
1607                YubiHsm2UserMapping::HermeticAuditLog {
1608                    authentication_key_id: 4,
1609                    system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1610                },
1611                YubiHsm2UserMapping::Signing {
1612                    authentication_key_id: 5,
1613                    signing_key_id: 1,
1614                    key_setup: SigningKeySetup::new(
1615                        KeyType::Curve25519,
1616                        vec![KeyMechanism::EdDsaSignature],
1617                        None,
1618                        SignatureType::EdDsa,
1619                        CryptographicKeyContext::OpenPgp {
1620                            notations: Default::default(),
1621                            user_ids: OpenPgpUserIdList::new(vec![
1622                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1623                            ])?,
1624                            version: "v4".parse()?,
1625                        },
1626                    )?,
1627                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1628                    system_user: "yubihsm2-signing-user".parse()?,
1629                    domain: Domain::One,
1630                }
1631            ]),
1632        )?
1633    )]
1634    #[case::single_admin_does_not_use_default_id(
1635        YubiHsm2AdminCredentials::new(
1636            1,
1637            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1638            vec![
1639                Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1640            ]
1641        )?,
1642        YubiHsm2Config::new(
1643            BTreeSet::from_iter([
1644                Connection::Mock
1645            ]),
1646            BTreeSet::from_iter([
1647                YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1648                YubiHsm2UserMapping::AuditLog {
1649                    authentication_key_id: 3,
1650                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1651                    system_user: "yubihsm2-metrics-user".parse()?,
1652                },
1653                YubiHsm2UserMapping::Backup{
1654                    authentication_key_id: 2,
1655                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1656                    system_user: "yubihsm2-backup-user".parse()?,
1657                    wrapping_key_id: 1,
1658                },
1659                YubiHsm2UserMapping::HermeticAuditLog {
1660                    authentication_key_id: 4,
1661                    system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1662                },
1663                YubiHsm2UserMapping::Signing {
1664                    authentication_key_id: 5,
1665                    signing_key_id: 1,
1666                    key_setup: SigningKeySetup::new(
1667                        KeyType::Curve25519,
1668                        vec![KeyMechanism::EdDsaSignature],
1669                        None,
1670                        SignatureType::EdDsa,
1671                        CryptographicKeyContext::OpenPgp {
1672                            notations: Default::default(),
1673                            user_ids: OpenPgpUserIdList::new(vec![
1674                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1675                            ])?,
1676                            version: "v4".parse()?,
1677                        },
1678                    )?,
1679                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1680                    system_user: "yubihsm2-signing-user".parse()?,
1681                    domain: Domain::One,
1682                }
1683            ]),
1684        )?
1685    )]
1686    #[case::multiple_admins_do_not_use_default_id(
1687        YubiHsm2AdminCredentials::new(
1688            1,
1689            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1690            vec![
1691                Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1692                Credentials::new(7, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1693            ]
1694        )?,
1695        YubiHsm2Config::new(
1696            BTreeSet::from_iter([
1697                Connection::Mock
1698            ]),
1699            BTreeSet::from_iter([
1700                YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1701                YubiHsm2UserMapping::Admin { authentication_key_id: 7 },
1702                YubiHsm2UserMapping::AuditLog {
1703                    authentication_key_id: 3,
1704                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1705                    system_user: "yubihsm2-metrics-user".parse()?,
1706                },
1707                YubiHsm2UserMapping::Backup{
1708                    authentication_key_id: 2,
1709                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1710                    system_user: "yubihsm2-backup-user".parse()?,
1711                    wrapping_key_id: 1,
1712                },
1713                YubiHsm2UserMapping::HermeticAuditLog {
1714                    authentication_key_id: 4,
1715                    system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1716                },
1717                YubiHsm2UserMapping::Signing {
1718                    authentication_key_id: 5,
1719                    signing_key_id: 1,
1720                    key_setup: SigningKeySetup::new(
1721                        KeyType::Curve25519,
1722                        vec![KeyMechanism::EdDsaSignature],
1723                        None,
1724                        SignatureType::EdDsa,
1725                        CryptographicKeyContext::OpenPgp {
1726                            notations: Default::default(),
1727                            user_ids: OpenPgpUserIdList::new(vec![
1728                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1729                            ])?,
1730                            version: "v4".parse()?,
1731                        },
1732                    )?,
1733                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1734                    system_user: "yubihsm2-signing-user".parse()?,
1735                    domain: Domain::One,
1736                }
1737            ]),
1738        )?
1739    )]
1740    #[case::multiple_admins_one_uses_default_id(
1741        YubiHsm2AdminCredentials::new(
1742            1,
1743            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1744            vec![
1745                Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1746                Credentials::new(7, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1747            ]
1748        )?,
1749        YubiHsm2Config::new(
1750            BTreeSet::from_iter([
1751                Connection::Mock
1752            ]),
1753            BTreeSet::from_iter([
1754                YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1755                YubiHsm2UserMapping::Admin { authentication_key_id: 7 },
1756                YubiHsm2UserMapping::AuditLog {
1757                    authentication_key_id: 3,
1758                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1759                    system_user: "yubihsm2-metrics-user".parse()?,
1760                },
1761                YubiHsm2UserMapping::Backup{
1762                    authentication_key_id: 2,
1763                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1764                    system_user: "yubihsm2-backup-user".parse()?,
1765                    wrapping_key_id: 1,
1766                },
1767                YubiHsm2UserMapping::HermeticAuditLog {
1768                    authentication_key_id: 4,
1769                    system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1770                },
1771                YubiHsm2UserMapping::Signing {
1772                    authentication_key_id: 5,
1773                    signing_key_id: 1,
1774                    key_setup: SigningKeySetup::new(
1775                        KeyType::Curve25519,
1776                        vec![KeyMechanism::EdDsaSignature],
1777                        None,
1778                        SignatureType::EdDsa,
1779                        CryptographicKeyContext::OpenPgp {
1780                            notations: Default::default(),
1781                            user_ids: OpenPgpUserIdList::new(vec![
1782                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1783                            ])?,
1784                            version: "v4".parse()?,
1785                        },
1786                    )?,
1787                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1788                    system_user: "yubihsm2-signing-user".parse()?,
1789                    domain: Domain::One,
1790                }
1791            ]),
1792        )?
1793    )]
1794    fn yubihsm2_backend_sync_succeeds(
1795        connector: Connector,
1796        #[case] admin_credentials: YubiHsm2AdminCredentials,
1797        #[case] yubihsm2_config: YubiHsm2Config,
1798    ) -> TestResult {
1799        setup_logging(LevelFilter::Debug)?;
1800
1801        let config = ConfigBuilder::new(SystemConfig::new(
1802            1,
1803            AdministrativeSecretHandling::Plaintext,
1804            NonAdministrativeSecretHandling::Plaintext,
1805            BTreeSet::from_iter([]),
1806        )?)
1807        .set_yubihsm2_config(yubihsm2_config)
1808        .finish()?;
1809        let Some(backend) = YubiHsm2Backend::new(connector, &admin_credentials, &config)? else {
1810            panic!("No YubiHsm2Config in the provided Signstar config");
1811        };
1812        let credentials = vec![
1813            Credentials::new("2".parse()?, Passphrase::generate(Some(50))),
1814            Credentials::new("3".parse()?, Passphrase::generate(Some(50))),
1815            Credentials::new("4".parse()?, Passphrase::generate(Some(50))),
1816            Credentials::new("5".parse()?, Passphrase::generate(Some(50))),
1817        ];
1818
1819        backend.sync(&credentials)?;
1820
1821        // Re-run the sync
1822        backend.sync(&credentials)?;
1823
1824        Ok(())
1825    }
1826
1827    /// Ensures, that creating a `UserMappingsAndCredentials` fails on duplicate credentials.
1828    #[test]
1829    fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_duplicate_creds()
1830    -> TestResult {
1831        let user_mappings = HashSet::new();
1832        let creds = vec![
1833            Credentials::new(1, Passphrase::generate(Some(50))),
1834            Credentials::new(1, Passphrase::generate(Some(50))),
1835        ];
1836        match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1837            Err(crate::Error::YubiHsm2Backend(crate::yubihsm2::Error::DuplicateCredentials {
1838                ..
1839            })) => {}
1840            Err(error) => panic!(
1841                "Expected to fail with Error::DuplicateCredentials but failed with a different error: {error}"
1842            ),
1843            Ok(_) => {
1844                panic!("Expected to fail with Error::DuplicateCredentials but succeeded instead!")
1845            }
1846        }
1847
1848        Ok(())
1849    }
1850
1851    /// Ensures, that creating a `UserMappingsAndCredentials` fails on missing credentials for
1852    /// mappings.
1853    #[test]
1854    fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_missing_creds_for_mappings()
1855    -> TestResult {
1856        let user_mappings_list = [
1857            YubiHsm2UserMapping::Admin {
1858                authentication_key_id: 1,
1859            },
1860            YubiHsm2UserMapping::Admin {
1861                authentication_key_id: 2,
1862            },
1863        ];
1864        let user_mappings = HashSet::from_iter(user_mappings_list.iter());
1865        let creds = vec![Credentials::new(1, Passphrase::generate(Some(50)))];
1866        match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1867            Err(crate::Error::YubiHsm2Backend(
1868                crate::yubihsm2::Error::UserMappingWithoutCredentials { .. },
1869            )) => {}
1870            Err(error) => panic!(
1871                "Expected to fail with Error::UserMappingWithoutCredentials but failed with a different error: {error}"
1872            ),
1873            Ok(_) => {
1874                panic!(
1875                    "Expected to fail with Error::UserMappingWithoutCredentials but succeeded instead!"
1876                )
1877            }
1878        }
1879
1880        Ok(())
1881    }
1882
1883    /// Ensures, that creating a `UserMappingsAndCredentials` fails on missing mappings for
1884    /// credentials.
1885    #[test]
1886    fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_missing_mappings_for_creds()
1887    -> TestResult {
1888        let user_mappings_list = [YubiHsm2UserMapping::Admin {
1889            authentication_key_id: 1,
1890        }];
1891        let user_mappings = HashSet::from_iter(user_mappings_list.iter());
1892        let creds = vec![
1893            Credentials::new(1, Passphrase::generate(Some(50))),
1894            Credentials::new(2, Passphrase::generate(Some(50))),
1895        ];
1896        match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1897            Err(crate::Error::YubiHsm2Backend(
1898                crate::yubihsm2::Error::CredentialsWithoutUserMapping { .. },
1899            )) => {}
1900            Err(error) => panic!(
1901                "Expected to fail with Error::CredentialsWithoutUserMapping but failed with a different error: {error}"
1902            ),
1903            Ok(_) => {
1904                panic!(
1905                    "Expected to fail with Error::CredentialsWithoutUserMapping but succeeded instead!"
1906                )
1907            }
1908        }
1909
1910        Ok(())
1911    }
1912
1913    /// Ensures that creating a [`YubiHsm2BackendState`] from the default [`YubiHsm2Backend`]
1914    /// succeeds.
1915    #[rstest]
1916    fn yubihsm2_backend_state_try_from_yubihsm2_backend_succeeds(
1917        connector: Connector,
1918        admin_credentials: TestResult<YubiHsm2AdminCredentials>,
1919        signstar_config: TestResult<Config>,
1920    ) -> TestResult {
1921        let signstar_config = signstar_config?;
1922        let admin_credentials = admin_credentials?;
1923        let backend = create_yubihsm2_backend(connector, &admin_credentials, &signstar_config)?;
1924
1925        let _state = YubiHsm2BackendState::try_from(&backend)?;
1926
1927        Ok(())
1928    }
1929
1930    /// Ensures that creating a [`YubiHsm2Backend`] fails if the iterations of
1931    /// [`YubiHsm2AdminCredentials`] and [`Config`] do not match.
1932    #[rstest]
1933    fn yubihsm2_backend_new_fails_on_mismatching_iterations(
1934        connector: Connector,
1935        signstar_config: TestResult<Config>,
1936    ) -> TestResult {
1937        let signstar_config = signstar_config?;
1938        let admin_credentials = YubiHsm2AdminCredentials::new(
1939            2,
1940            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1941            vec![
1942                Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1943                Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1944            ]
1945        )?;
1946        match YubiHsm2Backend::new(connector, &admin_credentials, &signstar_config) {
1947            Err(crate::Error::IterationMismatch { .. }) => {}
1948            Err(error) => panic!(
1949                "Expected to fail with Error::IterationMismatch, but failed differently instead: {error}"
1950            ),
1951            Ok(_) => {
1952                panic!("Expected to fail with Error::IterationMismatch, but succeeded instead")
1953            }
1954        }
1955
1956        Ok(())
1957    }
1958
1959    /// Ensures that creating a [`YubiHsm2Backend`] fails if iteration of
1960    /// [`YubiHsm2AdminCredentials`] and [`Config`] do not match.
1961    #[rstest]
1962    fn yubihsm2_backend_new_fails_on_missing_yubihsm2_config(
1963        connector: Connector,
1964        admin_credentials: TestResult<YubiHsm2AdminCredentials>,
1965    ) -> TestResult {
1966        let admin_credentials = admin_credentials?;
1967        let signstar_config = ConfigBuilder::new(SystemConfig::new(
1968            1,
1969            signstar_crypto::AdministrativeSecretHandling::Plaintext,
1970            signstar_crypto::NonAdministrativeSecretHandling::Plaintext,
1971            BTreeSet::from_iter([]),
1972        )?)
1973        .finish()?;
1974
1975        match YubiHsm2Backend::new(connector, &admin_credentials, &signstar_config) {
1976            Ok(None) => {}
1977            Ok(Some(_)) => {
1978                panic!("Expected to succeed with None, but succeeded with Some instead")
1979            }
1980            Err(error) => panic!("Expected to succeed with None, but failed instead: {error}"),
1981        }
1982
1983        Ok(())
1984    }
1985}