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, time::Duration};
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    /// Resets the backend device to its defaults and reconnects to it.
588    ///
589    /// # Errors
590    ///
591    /// Returns an error, if
592    ///
593    /// - resetting the device and reconnecting to it fails
594    /// - the number of scenario return values does not match the number of requested actions
595    /// - the scenario return value matches the expected one for resetting and reconnecting
596    pub fn reset(&self) -> Result<(), crate::Error> {
597        info!("Reset the backend {:?} to its defaults...", self.connector);
598
599        let credentials = self.usable_admin_creds()?;
600
601        let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(
602            credentials,
603            vec![Command::ResetDeviceAndReconnect {
604                // NOTE: Some USB controllers are sloooooooooooooow.
605                timeout: Duration::from_secs(5),
606            }],
607        )]);
608        let scenario_result = self.runner.run(&scenario)?;
609
610        // Ensure that the correct return value is received.
611        if scenario_result.chains().len() > 1 {
612            return Err(Error::ScenarioLogic {
613                context: format!("there are more return values ({}) than requested actions (1), when resetting the backend device {:?}",
614                        scenario_result.chains().len(),
615                        self.connector
616                ),
617            }
618            .into());
619        }
620        let Some(command_return_values) = scenario_result.chains().first() else {
621            return Err(Error::ScenarioLogic {
622                context: format!(
623                    "there are no command return values when resetting the backend device {:?}",
624                    self.connector
625                ),
626            }
627            .into());
628        };
629        if command_return_values.len() != 1 {
630            return Err(Error::ScenarioLogic {
631            context: format!("the number of command return values ({}) does not match the number of requested actions (1) when resetting the backend device {:?}",
632                command_return_values.len(),
633                self.connector
634            ),
635        }
636        .into());
637        };
638        match command_return_values.first() {
639            Some(CommandReturnValue::ResetDeviceAndReconnect) => {}
640            Some(_) => {
641                return Err(Error::ScenarioLogic {
642                            context: format!("a different return value from the one expected when resetting the backend device {:?} was returned", self.connector)
643                        }
644                        .into());
645            }
646            _ => {
647                return Err(Error::ScenarioLogic {
648                    context: format!(
649                        "no return value returned when resetting the backend device {:?}",
650                        self.connector
651                    ),
652                }
653                .into());
654            }
655        }
656
657        Ok(())
658    }
659
660    /// Syncs the state of a Signstar configuration with the backend using credentials for users in
661    /// non-administrative roles.
662    pub fn sync(&self, user_credentials: &[Credentials]) -> Result<(), crate::Error> {
663        info!("Syncing the YubiHSM2 backend with the Signstar configuration...");
664
665        let non_admin_users_and_creds =
666            UserMappingsAndCredentials::new_from_non_admin(self.yubihsm2_config, user_credentials)?;
667        debug!(
668            "valid mappings and their creds: {}",
669            non_admin_users_and_creds
670                .as_ref()
671                .iter()
672                .map(|(mapping, creds)| format!("{mapping:?}: {creds:?}"))
673                .collect::<Vec<_>>()
674                .join(", ")
675        );
676
677        // NOTE: Here, we reset the backend, if it can still be accessed with the default
678        // credentials.
679        //
680        // This ensures, that no rogue keys are left on the device, before we start synchronizing
681        // its state with that of the configuration.
682        if self.check_set_default_credentials() {
683            self.reset()?;
684        }
685
686        self.add_admin_users()?;
687        self.add_wrap_key()?;
688        self.add_non_admin_users(&non_admin_users_and_creds)?;
689        self.add_signing_keys()?;
690        self.add_openpgp_certificates(&non_admin_users_and_creds)?;
691
692        Ok(())
693    }
694
695    /// Adds the OpenPGP certificates for keys that use them.
696    ///
697    /// # Note
698    ///
699    /// Does **not** overwrite existing data!
700    ///
701    /// # Errors
702    ///
703    /// Returns an error, if
704    ///
705    /// - a usable administrative authentication key cannot be found
706    /// - retrieving of opaque data info fails
707    /// - creating an OpenPGP certificate for a signgin key fails
708    /// - the scenario of adding OpenPGP certificates as opaque data fails
709    /// - the return values of the scenario do not match the requested actions
710    fn add_openpgp_certificates(
711        &self,
712        non_admin_user_mappings_and_creds: &UserMappingsAndCredentials,
713    ) -> Result<(), crate::Error> {
714        info!("Adding OpenPGP certificates for signing keys...");
715
716        let credentials = self.usable_admin_creds()?;
717
718        let (commands, ids) = {
719            let opaque_data_infos = get_object_infos_for_object_types(
720                &self.runner,
721                &credentials,
722                &[ObjectType::Opaque],
723            )?;
724            let mut commands = Vec::new();
725            let mut ids = Vec::new();
726
727            for (mapping, credentials) in non_admin_user_mappings_and_creds.as_ref() {
728                // NOTE: We are disregarding the key information here, because we currently only
729                // consider ed25519 keys.
730                if let YubiHsm2UserMapping::Signing {
731                    domain,
732                    signing_key_id,
733                    key_setup,
734                    ..
735                } = mapping
736                {
737                    let CryptographicKeyContext::OpenPgp {
738                        user_ids, version, ..
739                    } = key_setup.key_context()
740                    else {
741                        debug!(
742                            "Skipping the generation of an OpenPGP certificate for signing key {signing_key_id}, because it is not setup for use with OpenPGP..."
743                        );
744                        continue;
745                    };
746
747                    if opaque_data_infos
748                        .iter()
749                        .any(|info| info.object_id == *signing_key_id)
750                    {
751                        warn!(
752                            "Skipping the generation of an OpenPGP certificate for signing key {signing_key_id}, because data exists already..."
753                        );
754                        continue;
755                    }
756
757                    let client = Client::create(self.connector.clone(), (*credentials).into())
758                        .map_err(|source| signstar_yubihsm2::Error::Client { context: "creating a client for adding OpenPGP certificates for a signing key", source })?;
759                    let signer = YubiHsm2SigningKey::new(client, *signing_key_id);
760                    let flags = {
761                        let mut flags = OpenPgpKeyUsageFlags::default();
762                        flags.set_sign();
763                        flags
764                    };
765                    let certificate = generate_certificate(
766                        &signer,
767                        flags,
768                        user_ids.as_ref(),
769                        Default::default(),
770                        Timestamp::now(),
771                        *version,
772                    )?;
773                    signer.close_session()?;
774
775                    commands.push(Command::PutOpaque {
776                        id: *signing_key_id,
777                        label: YubiHsm2Config::openpgp_certificate_label(),
778                        domains: Domains::from(*domain),
779                        capabilities: OpaqueDataCapabilities::ExportableUnderWrap,
780                        algorithm: OpaqueDataAlgorithm::OpaqueData,
781                        data: OpaqueData::new(certificate)?,
782                    });
783                    ids.push(*signing_key_id);
784                }
785            }
786
787            (commands, ids)
788        };
789
790        // If there is nothing to do, exit early.
791        if commands.is_empty() {
792            return Ok(());
793        }
794
795        let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
796        let scenario_result = self.runner.run(&scenario)?;
797
798        // Ensure that the requested and returned authentication key IDs match.
799        let Some(command_return_values) = scenario_result.chains().first() else {
800            return Err(Error::ScenarioLogic {
801                context: format!(
802                    "there are no command return values when adding OpenPGP certificates for signing keys {}",
803                    ids.iter()
804                        .map(ToString::to_string)
805                        .collect::<Vec<_>>()
806                        .join(", ")
807                ),
808            }
809            .into());
810        };
811        if command_return_values.len() != ids.len() {
812            return Err(Error::ScenarioLogic {
813            context: format!("the number of command return values ({}) does not match the number of requested IDs ({}) when adding OpenPGP certificates for signing keys {}",
814                command_return_values.len(),
815                ids.len(),
816                ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
817            ),
818        }
819        .into());
820        };
821
822        for (command_return_value, requested_id) in command_return_values.iter().zip(ids.iter()) {
823            let returned_id = match command_return_value {
824                CommandReturnValue::PutOpaque(returned_id) => *returned_id,
825                command => {
826                    let command_name = CommandName::from(command);
827                    return Err(Error::ScenarioLogic {
828                        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"),
829                    }
830                    .into());
831                }
832            };
833
834            if *requested_id != returned_id {
835                return Err(Error::ScenarioLogic {
836                    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 {}",
837                        ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
838                    ),
839                }
840                .into());
841            }
842        }
843
844        Ok(())
845    }
846
847    /// Adds the asymmetric signing keys.
848    ///
849    /// # Note
850    ///
851    /// Does **not** overwrite existing signing keys!
852    ///
853    /// # Errors
854    ///
855    /// Returns an error, if
856    ///
857    /// - a usable administrative authentication key cannot be found
858    /// - retrieving of signing key info fails
859    /// - the scenario of generating asymmetric signging keys fails
860    /// - the return values of the scenario do not match the requested actions
861    fn add_signing_keys(&self) -> Result<(), crate::Error> {
862        info!("Adding asymmetric signing keys...");
863
864        let credentials = self.usable_admin_creds()?;
865        let (commands, ids) = {
866            let signing_key_infos = get_object_infos_for_object_types(
867                &self.runner,
868                &credentials,
869                &[ObjectType::AsymmetricKey],
870            )?;
871            let mut commands = Vec::new();
872            let mut ids = Vec::new();
873
874            for mapping in self.yubihsm2_config.mappings() {
875                // NOTE: We are disregarding the key information here, because we only ever consider
876                // ed25519 keys.
877                if let YubiHsm2UserMapping::Signing {
878                    domain,
879                    signing_key_id,
880                    ..
881                } = mapping
882                {
883                    // Do not replace already existing signing keys.
884                    if signing_key_infos
885                        .iter()
886                        .any(|info| info.object_id == *signing_key_id)
887                    {
888                        warn!("Not adding signing key {signing_key_id}, as it exists already...");
889                        continue;
890                    }
891
892                    commands.push(Command::GenerateAsymmetricKey {
893                        info: KeyInfo {
894                            key_id: *signing_key_id,
895                            domains: Domains::from(*domain),
896                            caps: mapping.capabilities(),
897                            label: mapping.label(),
898                        },
899                    });
900                    ids.push(*signing_key_id);
901                }
902            }
903
904            (commands, ids)
905        };
906
907        // If there is nothing to do, exit early.
908        if commands.is_empty() {
909            return Ok(());
910        }
911
912        let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
913        let scenario_result = self.runner.run(&scenario)?;
914
915        let Some(command_return_values) = scenario_result.chains().first() else {
916            return Err(Error::ScenarioLogic {
917                context: format!(
918                    "there are no command return values when adding the signing keys {}",
919                    ids.iter()
920                        .map(ToString::to_string)
921                        .collect::<Vec<_>>()
922                        .join(", ")
923                ),
924            }
925            .into());
926        };
927        if command_return_values.len() != ids.len() {
928            return Err(Error::ScenarioLogic {
929            context: format!("the number of command return values ({}) does not match the number of requested IDs ({}) when adding the signing keys {}",
930                command_return_values.len(),
931                ids.len(),
932                ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
933            ),
934        }
935        .into());
936        };
937
938        for (number, command_return_value) in command_return_values.iter().enumerate() {
939            let Some(id) = ids.get(number) else {
940                return Err(Error::ScenarioLogic {
941                    context: format!("the signing key ID number {number} does not exist in the list of signing keys {}",
942                        ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
943                    ),
944                }
945                .into());
946            };
947            let CommandReturnValue::GenerateAsymmetricKey(returned_id) = command_return_value
948            else {
949                return Err(Error::ScenarioLogic {
950                    context: format!("the command return value when adding the signing key {id} is that of a different action"),
951                }
952                .into());
953            };
954            if id != returned_id {
955                return Err(Error::ScenarioLogic {
956                    context: format!("the returned signing key ID ({returned_id}) does not match the requested one ({id}) when adding the signing keys {}",
957                        ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
958                    ),
959                }
960                .into());
961            }
962        }
963
964        Ok(())
965    }
966
967    /// Adds non-administrative users.
968    ///
969    /// # Note
970    ///
971    /// Existing authentication keys are replaced!
972    ///
973    /// # Errors
974    ///
975    /// Returns an error, if
976    ///
977    /// - a usable administrative authentication key cannot be found
978    /// - the list of authentication key objects cannot be retrieved from the backend
979    /// - a new authentication key cannot be created
980    /// - the scenario cannot be run successfully
981    /// - one or more return values of the scenario do not match the requested actions
982    fn add_non_admin_users(
983        &self,
984        non_admin_user_mappings_and_creds: &UserMappingsAndCredentials,
985    ) -> Result<(), crate::Error> {
986        info!("Setting up non-administrative users...");
987
988        let credentials = self.usable_admin_creds()?;
989
990        let (commands, ids) = {
991            let authentication_key_infos = get_object_infos_for_object_types(
992                &self.runner,
993                &credentials,
994                &[ObjectType::AuthenticationKey],
995            )?;
996            let mut commands = Vec::new();
997            let mut ids = Vec::new();
998
999            for (mapping, creds) in non_admin_user_mappings_and_creds.as_ref() {
1000                let id = mapping.backend_user_id();
1001
1002                if authentication_key_infos
1003                    .iter()
1004                    .any(|info| info.object_id == id)
1005                {
1006                    warn!("The existing authentication key {id} will be replaced...");
1007                    commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(id)));
1008                }
1009
1010                commands.push(Command::PutAuthenticationKey {
1011                    info: mapping.authentication_key_info(),
1012                    delegated_caps: Capabilities::from(vec![].as_slice()),
1013                    authentication_key: AuthenticationKey::try_from(creds.passphrase())
1014                        .map_err(crate::Error::SignstarYubiHsm2)?,
1015                });
1016                ids.push(id);
1017            }
1018
1019            (commands, ids)
1020        };
1021
1022        // If there is nothing to do, exit early.
1023        if commands.is_empty() {
1024            return Ok(());
1025        }
1026
1027        let number_of_commands = commands.len();
1028        let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
1029        let scenario_result = self.runner.run(&scenario)?;
1030
1031        // Validate the return values against the acclaimed actions.
1032        let Some(command_return_values) = scenario_result.chains().first() else {
1033            return Err(Error::ScenarioLogic {
1034                context: format!("there are no command return values when adding the non-administrative users {}",
1035                    ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1036                ),
1037            }
1038            .into());
1039        };
1040        if command_return_values.len() != number_of_commands {
1041            return Err(Error::ScenarioLogic {
1042            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 {}",
1043                command_return_values.len(),
1044                ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1045            ),
1046        }
1047        .into());
1048        };
1049
1050        // Ensure that the requested and returned authentication key IDs match.
1051        for (number, command_return_value) in command_return_values
1052            .iter()
1053            .filter(|command_return_value| {
1054                matches!(
1055                    command_return_value,
1056                    CommandReturnValue::PutAuthenticationKey(_)
1057                )
1058            })
1059            .enumerate()
1060        {
1061            let Some(id) = ids.get(number) else {
1062                return Err(Error::ScenarioLogic {
1063                    context: format!("the authentication key ID number {number} does not exist in the list of non-administrative users {}",
1064                        ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1065                    ),
1066                }
1067                .into());
1068            };
1069            let CommandReturnValue::PutAuthenticationKey(returned_id) = command_return_value else {
1070                return Err(Error::ScenarioLogic {
1071                    context: format!("the command return value when adding the non-administrative user {id} is that of a different action"),
1072                }
1073                .into());
1074            };
1075            if id != returned_id {
1076                return Err(Error::ScenarioLogic {
1077                    context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({id}) when adding the non-administrative users {}",
1078                        ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1079                    ),
1080                }
1081                .into());
1082            }
1083        }
1084
1085        Ok(())
1086    }
1087
1088    /// Adds a wrap key based on the backup passphrase.
1089    ///
1090    /// # Note
1091    ///
1092    /// Existing wrap keys are replaced!
1093    ///
1094    /// # Errors
1095    ///
1096    /// Returns an error, if
1097    ///
1098    /// - usable administrative credentials cannot be found
1099    /// - infos about wrap keys cannot be retrieved from the backend
1100    /// - a wrap key cannot be created from the backup passphrase
1101    /// - running the scenario against the backend fails
1102    /// - the scenario's return values do not match the request
1103    fn add_wrap_key(&self) -> Result<(), crate::Error> {
1104        info!("Setting up wrapping key...");
1105
1106        let credentials = self.usable_admin_creds()?;
1107
1108        let commands = {
1109            let wrap_key_infos = get_object_infos_for_object_types(
1110                &self.runner,
1111                &credentials,
1112                &[ObjectType::WrapKey],
1113            )?;
1114            let mut commands = Vec::new();
1115
1116            // If the wrap key exists already, delete it first.
1117            if wrap_key_infos
1118                .iter()
1119                .any(|info| info.object_id == YubiHsm2Config::WRAP_KEY_ID)
1120            {
1121                warn!(
1122                    "The existing wrapping key ({}) will be replaced...",
1123                    YubiHsm2Config::WRAP_KEY_ID
1124                );
1125                commands.push(Command::DeleteObject(ObjectId::WrappingKey(
1126                    YubiHsm2Config::WRAP_KEY_ID,
1127                )))
1128            }
1129
1130            let passphrase = self.admin_credentials.backup_passphrase();
1131            let wrapping_key: WrapKey =
1132                WrapKeyFromPassphrase::new(passphrase, WrapKeyKind::Aes256)?.try_into()?;
1133            commands.push(Command::PutWrapKey {
1134                info: KeyInfo {
1135                    key_id: YubiHsm2Config::WRAP_KEY_ID,
1136                    domains: Domains::all(),
1137                    caps: Capabilities::from(YubiHsm2UserMapping::CAP_BACKUP),
1138                    label: YubiHsm2Config::wrap_key_label(),
1139                },
1140                delegated_caps: Capabilities::from(YubiHsm2UserMapping::CAP_BACKUP),
1141                wrapping_key,
1142            });
1143            commands
1144        };
1145        let number_of_commands = commands.len();
1146
1147        let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(
1148            credentials.clone(),
1149            commands,
1150        )]);
1151        let scenario_return_value = self.runner.run(&scenario)?;
1152
1153        // Validate the return values against the acclaimed actions.
1154        let command_return_values = {
1155            let return_values: Vec<Vec<CommandReturnValue>> = scenario_return_value.into();
1156            let return_values_len = return_values.len();
1157            let mut return_values_iter = return_values.into_iter();
1158
1159            let Some(command_return_values) = return_values_iter.next() else {
1160                return Err(Error::ScenarioLogic {
1161                    context: format!(
1162                        "there are no command return values when adding the wrap key {}",
1163                        YubiHsm2Config::WRAP_KEY_ID
1164                    ),
1165                }
1166                .into());
1167            };
1168            if return_values_iter.next().is_some() {
1169                return Err(Error::ScenarioLogic {
1170                    context: format!("there are {} return value lists when adding the wrap key {}, but we expected exactly 1",
1171                        return_values_len,
1172                        YubiHsm2Config::WRAP_KEY_ID
1173                    ),
1174                }
1175                .into());
1176            }
1177            command_return_values
1178        };
1179        let returned_id = {
1180            match command_return_values.len() {
1181                2 => {
1182                    let Some(CommandReturnValue::DeleteObject) = command_return_values.first()
1183                    else {
1184                        return Err(Error::ScenarioLogic {
1185                            context: format!(
1186                                "there is no return value for removing a previously existing wrap key {}",
1187                                YubiHsm2Config::WRAP_KEY_ID
1188                            ),
1189                        }
1190                        .into());
1191                    };
1192                    let Some(CommandReturnValue::PutWrapKey(id)) = command_return_values.get(1)
1193                    else {
1194                        return Err(Error::ScenarioLogic {
1195                            context: format!(
1196                                "there is no return value for adding the wrap key {}",
1197                                YubiHsm2Config::WRAP_KEY_ID
1198                            ),
1199                        }
1200                        .into());
1201                    };
1202                    *id
1203                }
1204                1 => {
1205                    let Some(CommandReturnValue::PutWrapKey(id)) = command_return_values.first()
1206                    else {
1207                        return Err(Error::ScenarioLogic {
1208                            context: format!(
1209                                "there is no return value for adding the wrap key {}",
1210                                YubiHsm2Config::WRAP_KEY_ID
1211                            ),
1212                        }
1213                        .into());
1214                    };
1215                    *id
1216                }
1217                _ => {
1218                    return Err(Error::ScenarioLogic {
1219                        context: format!("there are {} return values when removing and/or adding the wrap key {}, but we expected exactly {number_of_commands}",
1220                            command_return_values.len(),
1221                            YubiHsm2Config::WRAP_KEY_ID
1222                        ),
1223                    }
1224                    .into());
1225                }
1226            }
1227        };
1228        if returned_id != YubiHsm2Config::WRAP_KEY_ID {
1229            return Err(Error::ScenarioLogic {
1230                context: format!("the returned key ID ({returned_id}) does not match the requested one when adding the wrap key {}",
1231                    YubiHsm2Config::WRAP_KEY_ID
1232                ),
1233            }
1234            .into());
1235        }
1236
1237        Ok(())
1238    }
1239
1240    /// Sets up all admin users in the backend.
1241    ///
1242    /// # Note
1243    ///
1244    /// Existing authentication keys are replaced!
1245    ///
1246    /// # Errors
1247    ///
1248    /// Returns an error, if
1249    ///
1250    /// - no usable administrative credentials can be found
1251    /// - currently used authentication keys cannot be retrieved from the backend
1252    /// - the currently used credentials cannot be found in the set of available credentials
1253    /// - the scenario for removing and/or adding all relevant administrative authentication keys
1254    ///   fails
1255    /// - the return values for the scenario do not match the requested actions
1256    /// - the default admin credentials are still usable at the end of this function
1257    fn add_admin_users(&self) -> Result<(), crate::Error> {
1258        info!("Setting up administrative users...");
1259
1260        let credentials = self.usable_admin_creds()?;
1261        let (commands, ids) = {
1262            let authentication_key_infos = get_object_infos_for_object_types(
1263                &self.runner,
1264                &credentials,
1265                &[ObjectType::AuthenticationKey],
1266            )?;
1267            let mut commands = Vec::new();
1268            let mut ids = Vec::new();
1269
1270            // Delete and/or put all administrative credentials, except the one currently in use.
1271            for (mapping, creds) in self
1272                .admin_user_mappings_and_creds
1273                .as_ref()
1274                .iter()
1275                .filter(|(mapping, ..)| mapping.backend_user_id() != credentials.id())
1276            {
1277                let id = mapping.backend_user_id();
1278
1279                if authentication_key_infos
1280                    .iter()
1281                    .any(|info| info.object_id == id)
1282                {
1283                    warn!("Replacing the existing authentication key {id}...");
1284                    commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(id)));
1285                    ids.push(id);
1286                }
1287
1288                commands.push(Command::PutAuthenticationKey {
1289                    info: KeyInfo {
1290                        key_id: creds.id(),
1291                        domains: mapping.domains(),
1292                        caps: mapping.capabilities(),
1293                        label: mapping.label(),
1294                    },
1295                    delegated_caps: mapping.capabilities(),
1296                    authentication_key: AuthenticationKey::try_from(creds.passphrase())?,
1297                });
1298                ids.push(id);
1299            }
1300
1301            // Remove the default administrative authentication key, if an authentication key with
1302            // its ID is available in the backend, but the ID is not used in the Signstar
1303            // configuration.
1304            if authentication_key_infos
1305                .iter()
1306                .any(|info| info.object_id == YubiHsm2AdminCredentials::DEFAULT_ID)
1307                && !self
1308                    .admin_user_mappings_and_creds
1309                    .as_ref()
1310                    .iter()
1311                    .any(|(mapping, _)| {
1312                        mapping.backend_user_id() == YubiHsm2AdminCredentials::DEFAULT_ID
1313                    })
1314            {
1315                warn!(
1316                    "Removing the default authentication key {}...",
1317                    YubiHsm2AdminCredentials::DEFAULT_ID
1318                );
1319                commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(
1320                    YubiHsm2AdminCredentials::DEFAULT_ID,
1321                )));
1322                ids.push(YubiHsm2AdminCredentials::DEFAULT_ID)
1323            }
1324
1325            // Change the currently used authentication key, if it is setup in the administrative
1326            // credentials/ configuration.
1327            if let Some((_, current_credentials)) = self
1328                .admin_user_mappings_and_creds
1329                .as_ref()
1330                .iter()
1331                .find(|(mapping, ..)| mapping.backend_user_id() == credentials.id())
1332            {
1333                warn!(
1334                    "Changing the authentication key {}, which is currently used...",
1335                    current_credentials.id()
1336                );
1337                commands.push(Command::ChangeAuthenticationKey {
1338                    key_id: current_credentials.id(),
1339                    authentication_key: AuthenticationKey::try_from(
1340                        current_credentials.passphrase(),
1341                    )?,
1342                });
1343                ids.push(current_credentials.id());
1344            }
1345
1346            (commands, ids)
1347        };
1348
1349        // If there is nothing to do, exit early.
1350        if commands.is_empty() {
1351            return Ok(());
1352        }
1353
1354        let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
1355        let scenario_result = self.runner.run(&scenario)?;
1356
1357        // Validate the return values against the acclaimed actions.
1358        let Some(command_return_values) = scenario_result.chains().first() else {
1359            return Err(Error::ScenarioLogic {
1360                context: format!(
1361                    "there are no command return values when adding the administrative users {}",
1362                    ids.iter()
1363                        .map(ToString::to_string)
1364                        .collect::<Vec<_>>()
1365                        .join(", ")
1366                ),
1367            }
1368            .into());
1369        };
1370        if command_return_values.len() != ids.len() {
1371            return Err(Error::ScenarioLogic {
1372                context: format!("the number of command return values ({}) does not match the number of requested commands ({}) when removing and/or adding the administrative users {}",
1373                    command_return_values.len(),
1374                    ids.len(),
1375                    ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1376                ),
1377            }
1378            .into());
1379        };
1380
1381        // Ensure that the requested and returned authentication key IDs match.
1382        for (command_return_value, requested_id) in command_return_values.iter().zip(ids.iter()) {
1383            match command_return_value {
1384                CommandReturnValue::PutAuthenticationKey(returned_id) => {
1385                    if returned_id != requested_id {
1386                        return Err(Error::ScenarioLogic {
1387                        context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({requested_id}) when adding the administrative user"),
1388                    }
1389                    .into());
1390                    }
1391                }
1392                CommandReturnValue::DeleteObject => {
1393                    // NOTE: There is nothing to match here.
1394                }
1395                CommandReturnValue::ChangeAuthenticationKey(returned_id) => {
1396                    if returned_id != requested_id {
1397                        return Err(Error::ScenarioLogic {
1398                        context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({requested_id}) when changing the administrative user"),
1399                    }
1400                    .into());
1401                    }
1402                }
1403                _ => return Err(Error::ScenarioLogic {
1404                    context: format!("the command return value is not for adding, removing or changing the administrative user {requested_id}: {command_return_value:?}"),
1405                }
1406                .into()),
1407            }
1408        }
1409
1410        info!("Successfully added all admins");
1411
1412        // Check whether the default credentials are still in use (they shouldn't be at this
1413        // point) and set the internal state accordingly.
1414        if self.check_set_default_credentials() {
1415            return Err(Error::DefaultAdminStillUsable.into());
1416        }
1417
1418        Ok(())
1419    }
1420
1421    /// Returns the currently usable credentials for an administrative user.
1422    ///
1423    /// # Errors
1424    ///
1425    /// Returns an error, if no usable administrative credentials are found.
1426    fn usable_admin_creds(&self) -> Result<Credentials, crate::Error> {
1427        info!("Finding usable administrative credentials...");
1428
1429        let credentials = if self.default_credentials_in_use() {
1430            YubiHsm2AdminCredentials::default_credentials()
1431        } else {
1432            let Some(credentials) = self
1433                .admin_credentials
1434                .administrators()
1435                .iter()
1436                .find(|credentials| are_admin_creds_usable(&self.runner, credentials))
1437            else {
1438                return Err(Error::NoUsableAdmin.into());
1439            };
1440
1441            credentials.clone()
1442        };
1443
1444        info!(
1445            "Using administrative credentials with authentication ID {}",
1446            credentials.id()
1447        );
1448
1449        Ok(credentials)
1450    }
1451
1452    /// Returns the list of available authentication key objects in the backend.
1453    ///
1454    /// # Errors
1455    ///
1456    /// Returns an error, if
1457    /// - no usable administrative credentials can be found
1458    /// - retrieving the information on authentication key objects fails.
1459    pub(crate) fn user_states(&self) -> Result<Vec<YubiHsm2BackendUserData>, crate::Error> {
1460        let credentials = self.usable_admin_creds()?;
1461        // Get the list of all authentication key IDs.
1462        let infos = get_object_infos_for_object_types(
1463            &self.runner,
1464            &credentials,
1465            &[ObjectType::AuthenticationKey],
1466        )?;
1467
1468        let user_states = infos
1469            .iter()
1470            .map(|info| YubiHsm2BackendUserData {
1471                id: info.object_id,
1472                capabilities: info.capabilities.into(),
1473                domains: info.domains.into(),
1474            })
1475            .collect::<Vec<_>>();
1476
1477        Ok(user_states)
1478    }
1479
1480    /// Returns the list of available non-authentication key objects in the backend.
1481    ///
1482    /// # Errors
1483    ///
1484    /// Returns an error if
1485    ///
1486    /// - no usable administrative credentials can be found
1487    /// - the fetching of one or more object infos fails
1488    pub(crate) fn key_states(&self) -> Result<Vec<YubiHsm2BackendUserKeyData>, crate::Error> {
1489        let credentials = self.usable_admin_creds()?;
1490        // Get the list of IDs for all objects that are not authentication keys.
1491        let object_types = [
1492            ObjectType::AsymmetricKey,
1493            ObjectType::HmacKey,
1494            ObjectType::Opaque,
1495            ObjectType::OtpAeakey,
1496            ObjectType::Template,
1497            ObjectType::WrapKey,
1498        ];
1499        let infos = get_object_infos_for_object_types(&self.runner, &credentials, &object_types)?;
1500
1501        let key_states = infos
1502            .into_iter()
1503            .map(|info| YubiHsm2BackendUserKeyData {
1504                id: info.object_id,
1505                object_type: info.object_type.into(),
1506                capabilities: info.capabilities.into(),
1507                domains: info.domains.into(),
1508                algorithm: info.algorithm.into(),
1509                label: info.label.into(),
1510                length: info.length,
1511            })
1512            .collect::<Vec<_>>();
1513
1514        Ok(key_states)
1515    }
1516}
1517
1518#[cfg(all(test, feature = "_yubihsm2-mockhsm"))]
1519mod tests {
1520    use std::collections::BTreeSet;
1521
1522    use log::LevelFilter;
1523    use rstest::{fixture, rstest};
1524    use signstar_common::logging::setup_terminal_logging;
1525    use signstar_crypto::{
1526        AdministrativeSecretHandling,
1527        NonAdministrativeSecretHandling,
1528        key::{
1529            CryptographicKeyContext,
1530
1531            SigningKeySetup,
1532            base::{KeyMechanism, KeyType, SignatureType},
1533        },
1534        openpgp::OpenPgpUserIdList,
1535        passphrase::Passphrase,
1536    };
1537    use signstar_yubihsm2::{Connection, object::Domain};
1538    use testresult::TestResult;
1539
1540    use super::*;
1541    use crate::{
1542        config::{ConfigBuilder, SystemConfig},
1543        yubihsm2::{YubiHsm2Config, YubiHsm2UserMapping, state::YubiHsm2BackendState},
1544    };
1545
1546    /// Creates a MockHSM [`Connector`].
1547    #[fixture]
1548    fn connector() -> Connector {
1549        Connector::mockhsm()
1550    }
1551
1552    /// Creates a default [`YubiHsm2Config`] for testing purposes.
1553    #[fixture]
1554    fn yubihsm2_config() -> TestResult<YubiHsm2Config> {
1555        Ok(YubiHsm2Config::new(
1556            BTreeSet::from_iter([
1557                Connection::Mock
1558            ]),
1559            BTreeSet::from_iter([
1560                YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1561                YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1562                YubiHsm2UserMapping::AuditLog {
1563                    authentication_key_id: 3,
1564                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1565                    system_user: "yubihsm2-audit-log".parse()?,
1566                },
1567                YubiHsm2UserMapping::Backup{
1568                    authentication_key_id: 2,
1569                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1570                    system_user: "yubihsm2-backup".parse()?,
1571                },
1572                YubiHsm2UserMapping::HermeticAuditLog {
1573                    authentication_key_id: 4,
1574                    system_user: "yubihsm2-hermetic-audit-log".parse()?,
1575                },
1576                YubiHsm2UserMapping::Signing {
1577                    authentication_key_id: 5,
1578                    signing_key_id: 1,
1579                    key_setup: SigningKeySetup::new(
1580                        KeyType::Curve25519,
1581                        vec![KeyMechanism::EdDsaSignature],
1582                        None,
1583                        SignatureType::EdDsa,
1584                        CryptographicKeyContext::OpenPgp {
1585                            notations: Default::default(),
1586                            user_ids: OpenPgpUserIdList::new(vec![
1587                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1588                            ])?,
1589                            version: "v4".parse()?,
1590                        },
1591                    )?,
1592                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1593                    system_user: "yubihsm2-signing".parse()?,
1594                    domain: Domain::One,
1595                }
1596            ]),
1597        )?)
1598    }
1599
1600    /// Creates a simple [`Config`] with [`YubiHsm2Config`].
1601    #[fixture]
1602    fn signstar_config(yubihsm2_config: TestResult<YubiHsm2Config>) -> TestResult<Config> {
1603        let yubihsm2_config = yubihsm2_config?;
1604        let config = ConfigBuilder::new(SystemConfig::new(
1605            1,
1606            signstar_crypto::AdministrativeSecretHandling::Plaintext,
1607            signstar_crypto::NonAdministrativeSecretHandling::Plaintext,
1608            BTreeSet::from_iter([]),
1609        )?)
1610        .set_yubihsm2_config(yubihsm2_config)
1611        .finish()?;
1612
1613        Ok(config)
1614    }
1615
1616    #[fixture]
1617    fn admin_credentials() -> TestResult<YubiHsm2AdminCredentials> {
1618        let admin_creds = YubiHsm2AdminCredentials::new(
1619            1,
1620            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1621            vec![
1622                Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1623                Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1624            ]
1625        )?;
1626        Ok(admin_creds)
1627    }
1628
1629    /// Creates a new [`YubiHsm2Backend`].
1630    ///
1631    /// # Errors
1632    ///
1633    /// Returns an error, if [`YubiHsm2Backend::new`] fails.
1634    ///
1635    /// # Panics
1636    ///
1637    /// Panics if [`YubiHsm2Backend::new`] returns [`Option::None`].
1638    fn create_yubihsm2_backend<'admin_creds, 'config>(
1639        connector: Connector,
1640        admin_credentials: &'admin_creds YubiHsm2AdminCredentials,
1641        signstar_config: &'config Config,
1642    ) -> TestResult<YubiHsm2Backend<'admin_creds, 'config>> {
1643        setup_terminal_logging(LevelFilter::Debug)?;
1644        let Some(backend) = YubiHsm2Backend::new(connector, admin_credentials, signstar_config)?
1645        else {
1646            panic!("The Config did not contain a YubiHsm2Config.");
1647        };
1648
1649        Ok(backend)
1650    }
1651
1652    /// Ensures, that [`YubiHsm2Backend::sync`] can be executed successfully (against a MockHSM
1653    /// backend).
1654    #[rstest]
1655    #[case::single_admin_uses_default_id(
1656        YubiHsm2AdminCredentials::new(
1657            1,
1658            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1659            vec![
1660                Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1661            ]
1662        )?,
1663        YubiHsm2Config::new(
1664            BTreeSet::from_iter([
1665                Connection::Mock
1666            ]),
1667            BTreeSet::from_iter([
1668                YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1669                YubiHsm2UserMapping::AuditLog {
1670                    authentication_key_id: 3,
1671                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1672                    system_user: "yubihsm2-audit-log".parse()?,
1673                },
1674                YubiHsm2UserMapping::Backup{
1675                    authentication_key_id: 2,
1676                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1677                    system_user: "yubihsm2-backup".parse()?,
1678                },
1679                YubiHsm2UserMapping::HermeticAuditLog {
1680                    authentication_key_id: 4,
1681                    system_user: "yubihsm2-hermetic-audit-log".parse()?,
1682                },
1683                YubiHsm2UserMapping::Signing {
1684                    authentication_key_id: 5,
1685                    signing_key_id: 1,
1686                    key_setup: SigningKeySetup::new(
1687                        KeyType::Curve25519,
1688                        vec![KeyMechanism::EdDsaSignature],
1689                        None,
1690                        SignatureType::EdDsa,
1691                        CryptographicKeyContext::OpenPgp {
1692                            notations: Default::default(),
1693                            user_ids: OpenPgpUserIdList::new(vec![
1694                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1695                            ])?,
1696                            version: "v4".parse()?,
1697                        },
1698                    )?,
1699                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1700                    system_user: "yubihsm2-signing".parse()?,
1701                    domain: Domain::One,
1702                }
1703            ]),
1704        )?
1705    )]
1706    #[case::single_admin_does_not_use_default_id(
1707        YubiHsm2AdminCredentials::new(
1708            1,
1709            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1710            vec![
1711                Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1712            ]
1713        )?,
1714        YubiHsm2Config::new(
1715            BTreeSet::from_iter([
1716                Connection::Mock
1717            ]),
1718            BTreeSet::from_iter([
1719                YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1720                YubiHsm2UserMapping::AuditLog {
1721                    authentication_key_id: 3,
1722                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1723                    system_user: "yubihsm2-audit-log".parse()?,
1724                },
1725                YubiHsm2UserMapping::Backup{
1726                    authentication_key_id: 2,
1727                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1728                    system_user: "yubihsm2-backup".parse()?,
1729                },
1730                YubiHsm2UserMapping::HermeticAuditLog {
1731                    authentication_key_id: 4,
1732                    system_user: "yubihsm2-hermetic-audit-log".parse()?,
1733                },
1734                YubiHsm2UserMapping::Signing {
1735                    authentication_key_id: 5,
1736                    signing_key_id: 1,
1737                    key_setup: SigningKeySetup::new(
1738                        KeyType::Curve25519,
1739                        vec![KeyMechanism::EdDsaSignature],
1740                        None,
1741                        SignatureType::EdDsa,
1742                        CryptographicKeyContext::OpenPgp {
1743                            notations: Default::default(),
1744                            user_ids: OpenPgpUserIdList::new(vec![
1745                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1746                            ])?,
1747                            version: "v4".parse()?,
1748                        },
1749                    )?,
1750                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1751                    system_user: "yubihsm2-signing".parse()?,
1752                    domain: Domain::One,
1753                }
1754            ]),
1755        )?
1756    )]
1757    #[case::multiple_admins_do_not_use_default_id(
1758        YubiHsm2AdminCredentials::new(
1759            1,
1760            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1761            vec![
1762                Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1763                Credentials::new(7, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1764            ]
1765        )?,
1766        YubiHsm2Config::new(
1767            BTreeSet::from_iter([
1768                Connection::Mock
1769            ]),
1770            BTreeSet::from_iter([
1771                YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1772                YubiHsm2UserMapping::Admin { authentication_key_id: 7 },
1773                YubiHsm2UserMapping::AuditLog {
1774                    authentication_key_id: 3,
1775                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1776                    system_user: "yubihsm2-metrics".parse()?,
1777                },
1778                YubiHsm2UserMapping::Backup{
1779                    authentication_key_id: 2,
1780                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1781                    system_user: "yubihsm2-backup".parse()?,
1782                },
1783                YubiHsm2UserMapping::HermeticAuditLog {
1784                    authentication_key_id: 4,
1785                    system_user: "yubihsm2-hermetic-audit-log".parse()?,
1786                },
1787                YubiHsm2UserMapping::Signing {
1788                    authentication_key_id: 5,
1789                    signing_key_id: 1,
1790                    key_setup: SigningKeySetup::new(
1791                        KeyType::Curve25519,
1792                        vec![KeyMechanism::EdDsaSignature],
1793                        None,
1794                        SignatureType::EdDsa,
1795                        CryptographicKeyContext::OpenPgp {
1796                            notations: Default::default(),
1797                            user_ids: OpenPgpUserIdList::new(vec![
1798                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1799                            ])?,
1800                            version: "v4".parse()?,
1801                        },
1802                    )?,
1803                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1804                    system_user: "yubihsm2-signing".parse()?,
1805                    domain: Domain::One,
1806                }
1807            ]),
1808        )?
1809    )]
1810    #[case::multiple_admins_one_uses_default_id(
1811        YubiHsm2AdminCredentials::new(
1812            1,
1813            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1814            vec![
1815                Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1816                Credentials::new(7, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1817            ]
1818        )?,
1819        YubiHsm2Config::new(
1820            BTreeSet::from_iter([
1821                Connection::Mock
1822            ]),
1823            BTreeSet::from_iter([
1824                YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1825                YubiHsm2UserMapping::Admin { authentication_key_id: 7 },
1826                YubiHsm2UserMapping::AuditLog {
1827                    authentication_key_id: 3,
1828                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1829                    system_user: "yubihsm2-audit-log".parse()?,
1830                },
1831                YubiHsm2UserMapping::Backup{
1832                    authentication_key_id: 2,
1833                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1834                    system_user: "yubihsm2-backup".parse()?,
1835                },
1836                YubiHsm2UserMapping::HermeticAuditLog {
1837                    authentication_key_id: 4,
1838                    system_user: "yubihsm2-hermetic-audit-log".parse()?,
1839                },
1840                YubiHsm2UserMapping::Signing {
1841                    authentication_key_id: 5,
1842                    signing_key_id: 1,
1843                    key_setup: SigningKeySetup::new(
1844                        KeyType::Curve25519,
1845                        vec![KeyMechanism::EdDsaSignature],
1846                        None,
1847                        SignatureType::EdDsa,
1848                        CryptographicKeyContext::OpenPgp {
1849                            notations: Default::default(),
1850                            user_ids: OpenPgpUserIdList::new(vec![
1851                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1852                            ])?,
1853                            version: "v4".parse()?,
1854                        },
1855                    )?,
1856                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1857                    system_user: "yubihsm2-signing".parse()?,
1858                    domain: Domain::One,
1859                }
1860            ]),
1861        )?
1862    )]
1863    fn yubihsm2_backend_sync_succeeds(
1864        connector: Connector,
1865        #[case] admin_credentials: YubiHsm2AdminCredentials,
1866        #[case] yubihsm2_config: YubiHsm2Config,
1867    ) -> TestResult {
1868        setup_terminal_logging(LevelFilter::Debug)?;
1869
1870        let config = ConfigBuilder::new(SystemConfig::new(
1871            1,
1872            AdministrativeSecretHandling::Plaintext,
1873            NonAdministrativeSecretHandling::Plaintext,
1874            BTreeSet::from_iter([]),
1875        )?)
1876        .set_yubihsm2_config(yubihsm2_config)
1877        .finish()?;
1878        let Some(backend) = YubiHsm2Backend::new(connector, &admin_credentials, &config)? else {
1879            panic!("No YubiHsm2Config in the provided Signstar config");
1880        };
1881        let credentials = vec![
1882            Credentials::new("2".parse()?, Passphrase::generate(Some(50))),
1883            Credentials::new("3".parse()?, Passphrase::generate(Some(50))),
1884            Credentials::new("4".parse()?, Passphrase::generate(Some(50))),
1885            Credentials::new("5".parse()?, Passphrase::generate(Some(50))),
1886        ];
1887
1888        backend.sync(&credentials)?;
1889
1890        // Re-run the sync
1891        backend.sync(&credentials)?;
1892
1893        Ok(())
1894    }
1895
1896    /// Ensures, that creating a `UserMappingsAndCredentials` fails on duplicate credentials.
1897    #[test]
1898    fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_duplicate_creds()
1899    -> TestResult {
1900        let user_mappings = HashSet::new();
1901        let creds = vec![
1902            Credentials::new(1, Passphrase::generate(Some(50))),
1903            Credentials::new(1, Passphrase::generate(Some(50))),
1904        ];
1905        match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1906            Err(crate::Error::YubiHsm2Backend(crate::yubihsm2::Error::DuplicateCredentials {
1907                ..
1908            })) => {}
1909            Err(error) => panic!(
1910                "Expected to fail with Error::DuplicateCredentials but failed with a different error: {error}"
1911            ),
1912            Ok(_) => {
1913                panic!("Expected to fail with Error::DuplicateCredentials but succeeded instead!")
1914            }
1915        }
1916
1917        Ok(())
1918    }
1919
1920    /// Ensures, that creating a `UserMappingsAndCredentials` fails on missing credentials for
1921    /// mappings.
1922    #[test]
1923    fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_missing_creds_for_mappings()
1924    -> TestResult {
1925        let user_mappings_list = [
1926            YubiHsm2UserMapping::Admin {
1927                authentication_key_id: 1,
1928            },
1929            YubiHsm2UserMapping::Admin {
1930                authentication_key_id: 2,
1931            },
1932        ];
1933        let user_mappings = HashSet::from_iter(user_mappings_list.iter());
1934        let creds = vec![Credentials::new(1, Passphrase::generate(Some(50)))];
1935        match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1936            Err(crate::Error::YubiHsm2Backend(
1937                crate::yubihsm2::Error::UserMappingWithoutCredentials { .. },
1938            )) => {}
1939            Err(error) => panic!(
1940                "Expected to fail with Error::UserMappingWithoutCredentials but failed with a different error: {error}"
1941            ),
1942            Ok(_) => {
1943                panic!(
1944                    "Expected to fail with Error::UserMappingWithoutCredentials but succeeded instead!"
1945                )
1946            }
1947        }
1948
1949        Ok(())
1950    }
1951
1952    /// Ensures, that creating a `UserMappingsAndCredentials` fails on missing mappings for
1953    /// credentials.
1954    #[test]
1955    fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_missing_mappings_for_creds()
1956    -> TestResult {
1957        let user_mappings_list = [YubiHsm2UserMapping::Admin {
1958            authentication_key_id: 1,
1959        }];
1960        let user_mappings = HashSet::from_iter(user_mappings_list.iter());
1961        let creds = vec![
1962            Credentials::new(1, Passphrase::generate(Some(50))),
1963            Credentials::new(2, Passphrase::generate(Some(50))),
1964        ];
1965        match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1966            Err(crate::Error::YubiHsm2Backend(
1967                crate::yubihsm2::Error::CredentialsWithoutUserMapping { .. },
1968            )) => {}
1969            Err(error) => panic!(
1970                "Expected to fail with Error::CredentialsWithoutUserMapping but failed with a different error: {error}"
1971            ),
1972            Ok(_) => {
1973                panic!(
1974                    "Expected to fail with Error::CredentialsWithoutUserMapping but succeeded instead!"
1975                )
1976            }
1977        }
1978
1979        Ok(())
1980    }
1981
1982    /// Ensures that creating a [`YubiHsm2BackendState`] from the default [`YubiHsm2Backend`]
1983    /// succeeds.
1984    #[rstest]
1985    fn yubihsm2_backend_state_try_from_yubihsm2_backend_succeeds(
1986        connector: Connector,
1987        admin_credentials: TestResult<YubiHsm2AdminCredentials>,
1988        signstar_config: TestResult<Config>,
1989    ) -> TestResult {
1990        let signstar_config = signstar_config?;
1991        let admin_credentials = admin_credentials?;
1992        let backend = create_yubihsm2_backend(connector, &admin_credentials, &signstar_config)?;
1993
1994        let _state = YubiHsm2BackendState::try_from(&backend)?;
1995
1996        Ok(())
1997    }
1998
1999    /// Ensures that creating a [`YubiHsm2Backend`] fails if the iterations of
2000    /// [`YubiHsm2AdminCredentials`] and [`Config`] do not match.
2001    #[rstest]
2002    fn yubihsm2_backend_new_fails_on_mismatching_iterations(
2003        connector: Connector,
2004        signstar_config: TestResult<Config>,
2005    ) -> TestResult {
2006        let signstar_config = signstar_config?;
2007        let admin_credentials = YubiHsm2AdminCredentials::new(
2008            2,
2009            Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
2010            vec![
2011                Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
2012                Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
2013            ]
2014        )?;
2015        match YubiHsm2Backend::new(connector, &admin_credentials, &signstar_config) {
2016            Err(crate::Error::IterationMismatch { .. }) => {}
2017            Err(error) => panic!(
2018                "Expected to fail with Error::IterationMismatch, but failed differently instead: {error}"
2019            ),
2020            Ok(_) => {
2021                panic!("Expected to fail with Error::IterationMismatch, but succeeded instead")
2022            }
2023        }
2024
2025        Ok(())
2026    }
2027
2028    /// Ensures that creating a [`YubiHsm2Backend`] fails if iteration of
2029    /// [`YubiHsm2AdminCredentials`] and [`Config`] do not match.
2030    #[rstest]
2031    fn yubihsm2_backend_new_fails_on_missing_yubihsm2_config(
2032        connector: Connector,
2033        admin_credentials: TestResult<YubiHsm2AdminCredentials>,
2034    ) -> TestResult {
2035        let admin_credentials = admin_credentials?;
2036        let signstar_config = ConfigBuilder::new(SystemConfig::new(
2037            1,
2038            signstar_crypto::AdministrativeSecretHandling::Plaintext,
2039            signstar_crypto::NonAdministrativeSecretHandling::Plaintext,
2040            BTreeSet::from_iter([]),
2041        )?)
2042        .finish()?;
2043
2044        match YubiHsm2Backend::new(connector, &admin_credentials, &signstar_config) {
2045            Ok(None) => {}
2046            Ok(Some(_)) => {
2047                panic!("Expected to succeed with None, but succeeded with Some instead")
2048            }
2049            Err(error) => panic!("Expected to succeed with None, but failed instead: {error}"),
2050        }
2051
2052        Ok(())
2053    }
2054}