Skip to main content

signstar_config/config/file/
mod.rs

1//! Configuration file handling.
2
3#[cfg(all(feature = "nethsm", feature = "yubihsm2"))]
4pub mod impl_all;
5#[cfg(all(feature = "nethsm", not(feature = "yubihsm2")))]
6pub mod impl_nethsm;
7#[cfg(not(any(feature = "nethsm", feature = "yubihsm2")))]
8pub mod impl_none;
9#[cfg(all(feature = "yubihsm2", not(feature = "nethsm")))]
10pub mod impl_yubihsm2;
11
12#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
13use std::collections::{BTreeSet, HashSet};
14use std::{
15    fs::read_to_string,
16    path::{Path, PathBuf},
17    str::FromStr,
18};
19
20use garde::Validate;
21use log::info;
22#[cfg(feature = "nethsm")]
23use nethsm::Connection;
24use serde::{Deserialize, Serialize};
25#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
26use signstar_crypto::{AdministrativeSecretHandling, NonAdministrativeSecretHandling};
27#[cfg(feature = "yubihsm2")]
28use signstar_yubihsm2::Connection as YubiHsm2Connection;
29use strum::{AsRefStr, VariantNames};
30
31use crate::config::SystemConfig;
32#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
33use crate::config::{ConfigAuthorizedKeyEntries, ConfigSystemUserIds};
34#[cfg(feature = "nethsm")]
35use crate::nethsm::{NetHsmConfig, NetHsmUserMapping};
36#[cfg(feature = "yubihsm2")]
37use crate::yubihsm2::{YubiHsm2Config, YubiHsm2UserMapping};
38
39/// Backend specific data for a user mapping.
40#[derive(Clone, Debug, Eq, PartialEq)]
41pub enum UserBackendConnection {
42    /// The connection configuration for a user of a NetHSM backend.
43    ///
44    /// # Note
45    ///
46    /// Only supported when using the `nethsm` feature.
47    #[cfg(feature = "nethsm")]
48    NetHsm {
49        /// Administrative credentials handling.
50        admin_secret_handling: AdministrativeSecretHandling,
51
52        /// Non-administrative credentials handling.
53        non_admin_secret_handling: NonAdministrativeSecretHandling,
54
55        /// The available connections to the NetHSM backend.
56        connections: BTreeSet<Connection>,
57
58        /// A specific NetHSM user mapping.
59        mapping: NetHsmUserMapping,
60    },
61
62    /// The connection configuration for a user of a YubiHSM2 backend.
63    ///
64    /// # Note
65    ///
66    /// Only supported when using the `yubihsm2` feature.
67    #[cfg(feature = "yubihsm2")]
68    YubiHsm2 {
69        /// Administrative credentials handling.
70        admin_secret_handling: AdministrativeSecretHandling,
71
72        /// Non-administrative credentials handling.
73        non_admin_secret_handling: NonAdministrativeSecretHandling,
74
75        /// The available connections to the YubiHSM2 backend.
76        connections: BTreeSet<YubiHsm2Connection>,
77
78        /// A specific YubiHSM2 user mapping.
79        mapping: YubiHsm2UserMapping,
80    },
81}
82
83/// A filter for the retrieval of lists of [`UserBackendConnection`] from a [`Config`].
84#[derive(Clone, Copy, Debug)]
85pub enum UserBackendConnectionFilter {
86    /// Target all backend users.
87    All,
88
89    /// Only target administrative backend users.
90    Admin,
91
92    /// Only target non-administrative backend users.
93    NonAdmin,
94}
95
96/// Validates overlapping assumptions of two configuration objects.
97///
98/// Ensures that `config_a` and `config_b` have no overlapping system user IDs or SSH
99/// authorized_keys.
100///
101/// # Errors
102///
103/// Returns an error if there are
104///
105/// - duplicate system users
106/// - duplicate SSH authorized keys (by comparing the actual SSH public keys)
107#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
108fn validate_confs<T, U>(config_a: &T, config_b: &U) -> garde::Result
109where
110    T: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
111    U: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
112{
113    // Collect duplicate system user IDs.
114    let duplicate_system_user_ids = {
115        let system_config_user_ids = config_a.system_user_ids();
116        let config_user_ids = config_b.system_user_ids();
117        let duplicates = system_config_user_ids
118            .intersection(&config_user_ids)
119            .map(|system_user_id| system_user_id.to_string())
120            .collect::<HashSet<_>>();
121
122        if duplicates.is_empty() {
123            None
124        } else {
125            let mut duplicates = Vec::from_iter(duplicates);
126            duplicates.sort();
127            Some(format!(
128                "the duplicate system user ID{} {}",
129                if duplicates.len() > 1 { "s" } else { "" },
130                duplicates.join(", ")
131            ))
132        }
133    };
134
135    // Collect all duplicate SSH public keys in authorized_keys.
136    let duplicate_public_keys = {
137        let system_config_public_keys: HashSet<_> = config_a
138            .authorized_key_entries()
139            .iter()
140            .cloned()
141            .map(|authorized_key| authorized_key.as_ref().public_key())
142            .collect();
143        let config_public_keys: HashSet<_> = config_b
144            .authorized_key_entries()
145            .iter()
146            .cloned()
147            .map(|authorized_key| authorized_key.as_ref().public_key())
148            .collect();
149        let duplicates: HashSet<_> = system_config_public_keys
150            .intersection(&config_public_keys)
151            .cloned()
152            .map(|public_key| {
153                let mut public_key = public_key.clone();
154                // Unset the comment as it may be set to different values.
155                public_key.set_comment("");
156                format!("\"{}\"", public_key.to_string())
157            })
158            .collect();
159
160        if duplicates.is_empty() {
161            None
162        } else {
163            let mut duplicates = Vec::from_iter(duplicates);
164            duplicates.sort();
165            Some(format!(
166                "the duplicate SSH public key{} {}",
167                if duplicates.len() > 1 { "s" } else { "" },
168                duplicates.join(", ")
169            ))
170        }
171    };
172
173    let messages = [duplicate_system_user_ids, duplicate_public_keys];
174    let error_messages = {
175        let mut error_messages = Vec::new();
176
177        for message in messages.iter().flatten() {
178            error_messages.push(message.as_str());
179        }
180
181        error_messages
182    };
183
184    match error_messages.len() {
185        0 => Ok(()),
186        1 => Err(garde::Error::new(format!(
187            "contains {}",
188            error_messages.join("\n")
189        ))),
190        _ => Err(garde::Error::new(format!(
191            "contains multiple issues:\n⤷ {}",
192            error_messages.join("\n⤷ ")
193        ))),
194    }
195}
196
197/// Validates a required config object against an optional one.
198///
199/// Ensures that the the two configuration objects have no overlapping system user IDs or SSH
200/// authorized_keys.
201///
202/// # Errors
203///
204/// Returns an error if there are
205///
206/// - duplicate system users
207/// - duplicate SSH authorized keys (by comparing the actual SSH public keys)
208#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
209fn validate_config_against_optional_config<T, U>(
210    config_a: &Option<T>,
211) -> impl FnOnce(&U, &()) -> garde::Result + '_
212where
213    T: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
214    U: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
215{
216    move |config_b, _| {
217        let Some(config_a) = config_a else {
218            return Ok(());
219        };
220
221        validate_confs(config_a, config_b)
222    }
223}
224
225/// Validates two optional config objects against each other.
226///
227/// Ensures that - if both config objects are present - they have no overlapping system user IDs or
228/// SSH authorized_keys.
229///
230/// # Errors
231///
232/// Returns an error if there are
233///
234/// - duplicate system users
235/// - duplicate SSH authorized keys (by comparing the actual SSH public keys)
236#[cfg(all(feature = "nethsm", feature = "yubihsm2"))]
237fn validate_two_optional_configs<T, U>(
238    backend_config_a: &Option<T>,
239) -> impl FnOnce(&Option<U>, &()) -> garde::Result + '_
240where
241    T: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
242    U: ConfigAuthorizedKeyEntries + ConfigSystemUserIds,
243{
244    move |backend_config_b, _| {
245        if let Some(backend_config_a) = backend_config_a
246            && let Some(backend_config_b) = backend_config_b
247        {
248            validate_confs(backend_config_a, backend_config_b)?;
249        }
250
251        Ok(())
252    }
253}
254
255/// The supported configuration file formats.
256#[derive(AsRefStr, Clone, Copy, Debug, Default, strum::Display, VariantNames)]
257#[strum(serialize_all = "lowercase")]
258enum ConfigFileFormat {
259    #[default]
260    Yaml,
261}
262
263/// The configuration of a Signstar system.
264///
265/// Tracks system-wide configuration items, as well as configurations for specific backends.
266#[derive(Clone, Debug, Default, Deserialize, Serialize, Validate)]
267#[serde(rename_all = "snake_case")]
268pub struct Config {
269    /// System configuration object.
270    // Validate against NetHsmConfig if support is compiled in.
271    #[cfg_attr(
272        feature = "nethsm",
273        garde(custom(validate_config_against_optional_config(&self.nethsm)))
274    )]
275    // Validate against YubiHsm2Config if support is compiled in.
276    #[cfg_attr(
277        feature = "yubihsm2",
278        garde(custom(validate_config_against_optional_config(&self.yubihsm2)))
279    )]
280    #[garde(dive)]
281    system: SystemConfig,
282
283    /// Optional configuration object for NetHSM backends.
284    ///
285    /// # Note
286    ///
287    /// Only supported when using the `nethsm` feature.
288    #[cfg(feature = "nethsm")]
289    // Validate against YubiHsm2Config if support is compiled in.
290    #[cfg_attr(
291        all(feature = "nethsm", feature = "yubihsm2"),
292        garde(custom(validate_two_optional_configs(&self.yubihsm2)))
293    )]
294    #[garde(dive)]
295    #[serde(skip_serializing_if = "Option::is_none")]
296    nethsm: Option<NetHsmConfig>,
297
298    /// Optional configuration object for YubiHSM2 backends.
299    ///
300    /// # Note
301    ///
302    /// Only supported when using the `yubihsm2` feature.
303    #[cfg(feature = "yubihsm2")]
304    // Validate against NetHsmConfig if support is compiled in.
305    #[cfg_attr(
306        all(feature = "nethsm", feature = "yubihsm2"),
307        garde(custom(validate_two_optional_configs(&self.nethsm)))
308    )]
309    #[garde(dive)]
310    #[serde(skip_serializing_if = "Option::is_none")]
311    yubihsm2: Option<YubiHsm2Config>,
312}
313
314impl Config {
315    /// The default config directory below "/usr/".
316    pub const DEFAULT_CONFIG_DIR: &str = "/usr/share/signstar/";
317
318    /// The override config directory below "/run/".
319    pub const RUN_OVERRIDE_CONFIG_DIR: &str = "/run/signstar/";
320
321    /// The override config directory below "/etc/".
322    pub const ETC_OVERRIDE_CONFIG_DIR: &str = "/etc/signstar/";
323
324    /// The configuration file name (without file type suffix).
325    pub const CONFIG_NAME: &str = "config";
326
327    /// Returns the default location of the Signstar configuration file on a system.
328    pub fn default_system_path() -> PathBuf {
329        PathBuf::from(Self::DEFAULT_CONFIG_DIR).join(PathBuf::from(format!(
330            "{}.{}",
331            Self::CONFIG_NAME,
332            ConfigFileFormat::default()
333        )))
334    }
335
336    /// Returns the first found path of a Signstar configuratino on the system.
337    ///
338    /// # Errors
339    ///
340    /// Returns an error if no configuration file is found.
341    pub fn first_existing_system_path() -> Result<PathBuf, crate::Error> {
342        let path = Self::list_config_file_paths()
343            .into_iter()
344            .find(|path| path.is_file());
345        path.ok_or(crate::ConfigError::ConfigIsMissing.into())
346    }
347
348    /// Returns the list of supported directory paths in which configuration files may reside.
349    ///
350    /// The returned list of paths is sorted in increasing precedence.
351    pub fn list_config_dirs() -> Vec<PathBuf> {
352        [
353            Self::DEFAULT_CONFIG_DIR,
354            Self::RUN_OVERRIDE_CONFIG_DIR,
355            Self::ETC_OVERRIDE_CONFIG_DIR,
356        ]
357        .iter()
358        .map(PathBuf::from)
359        .collect()
360    }
361
362    /// Returns the list of supported configuration file paths.
363    ///
364    /// The returned list of paths is sorted in increasing precedence.
365    pub fn list_config_file_paths() -> Vec<PathBuf> {
366        Self::list_config_dirs()
367            .into_iter()
368            .map(|dir| {
369                dir.join(
370                    PathBuf::from(Self::CONFIG_NAME)
371                        .with_added_extension(ConfigFileFormat::default().as_ref()),
372                )
373            })
374            .collect()
375    }
376
377    /// Creates a new [`Config`] from a string slice containing YAML data.
378    ///
379    /// # Errors
380    ///
381    /// Returns an error if deserialization or validation fails.
382    fn from_yaml_str(s: &str) -> Result<Self, crate::Error> {
383        let config: Self =
384            serde_saphyr::from_str(s).map_err(|source| crate::ConfigError::YamlDeserialize {
385                context: "creating a Signstar configuration object".to_string(),
386                source,
387            })?;
388
389        config
390            .validate()
391            .map_err(|source| crate::Error::Validation {
392                context: "validating a Signstar configuration object".to_string(),
393                source,
394            })?;
395
396        Ok(config)
397    }
398
399    /// Creates a new [`Config`] from a file containing YAML data.
400    ///
401    /// # Errors
402    ///
403    /// Returns an error if
404    /// - the file does not exist
405    /// - deserialization or validation fails.
406    fn from_yaml_file(path: impl AsRef<Path>) -> Result<Self, crate::Error> {
407        let path = path.as_ref();
408        info!("Reading Signstar configuration file {path:?}");
409
410        let config_data = read_to_string(path).map_err(|source| crate::Error::IoPath {
411            path: path.to_path_buf(),
412            context: "reading it to string",
413            source,
414        })?;
415        Self::from_yaml_str(&config_data)
416    }
417
418    /// Creates a new [`Config`] from a file `path`.
419    ///
420    /// # Errors
421    ///
422    /// Returns an error if
423    ///
424    /// - `path` has no file extension
425    /// - `path` does not use one of the supported file extensions
426    /// - creating a [`Config`] from the data fails
427    /// - validating a [`Config`] created from the data fails
428    pub fn from_file_path(path: impl AsRef<Path>) -> Result<Self, crate::Error> {
429        let path = path.as_ref();
430        let extension = {
431            let Some(extension) = path.extension() else {
432                return Err(crate::ConfigError::MissingFileExtension {
433                    path: path.to_path_buf(),
434                }
435                .into());
436            };
437            extension.to_string_lossy().to_string()
438        };
439
440        if !ConfigFileFormat::VARIANTS.contains(&extension.as_ref()) {
441            return Err(crate::ConfigError::UnsupportedFileExtension {
442                path: path.to_path_buf(),
443                extension,
444            }
445            .into());
446        }
447
448        Self::from_yaml_file(path)
449    }
450
451    /// Creates a new [`Config`] from the first found Signstar configuration file path on the
452    /// system.
453    ///
454    /// # Note
455    ///
456    /// Uses [`Config::first_existing_system_path`] to determine the first existing Signstar
457    /// configuration file path.
458    ///
459    /// # Errors
460    ///
461    /// Returns an error if [`Config`] creation from the found path fails.
462    pub fn from_system_path() -> Result<Self, crate::Error> {
463        Self::from_yaml_file(Self::first_existing_system_path()?)
464    }
465
466    /// Serializes `self` as a YAML string.
467    ///
468    /// # Errors
469    ///
470    /// Returns an error if serialization fails.
471    pub fn to_yaml_string(&self) -> Result<String, crate::Error> {
472        serde_saphyr::to_string(&self).map_err(|source| {
473            crate::ConfigError::YamlSerialize {
474                context: "serializing Signstar config",
475                source,
476            }
477            .into()
478        })
479    }
480
481    /// Returns a reference to the [`SystemConfig`].
482    pub fn system(&self) -> &SystemConfig {
483        &self.system
484    }
485
486    /// Returns a reference to the [`NetHsmConfig`].
487    #[cfg(feature = "nethsm")]
488    pub fn nethsm(&self) -> Option<&NetHsmConfig> {
489        self.nethsm.as_ref()
490    }
491
492    /// Returns a reference to the [`YubiHsm2Config`].
493    #[cfg(feature = "yubihsm2")]
494    pub fn yubihsm2(&self) -> Option<&YubiHsm2Config> {
495        self.yubihsm2.as_ref()
496    }
497}
498
499impl FromStr for Config {
500    type Err = crate::Error;
501
502    /// Creates a new [`Config`] from a string slice containing valid YAML.
503    ///
504    /// # Errors
505    ///
506    /// Returns an error if no [`Config`] can be created from `s`.
507    fn from_str(s: &str) -> Result<Self, Self::Err> {
508        Config::from_yaml_str(s)
509    }
510}
511
512/// A builder for [`Config`].
513#[derive(Clone, Debug)]
514pub struct ConfigBuilder(Config);
515
516impl ConfigBuilder {
517    /// Adds a [`NetHsmConfig`] to the builder.
518    #[cfg(feature = "nethsm")]
519    pub fn set_nethsm_config(mut self, nethsm: NetHsmConfig) -> Self {
520        self.0.nethsm = Some(nethsm);
521        self
522    }
523
524    /// Adds a [`YubiHsm2Config`] to the builder.
525    #[cfg(feature = "yubihsm2")]
526    pub fn set_yubihsm2_config(mut self, yubihsm2: YubiHsm2Config) -> Self {
527        self.0.yubihsm2 = Some(yubihsm2);
528        self
529    }
530
531    /// Creates a [`Config`] from the builder.
532    ///
533    /// # Errors
534    ///
535    /// Returns an error if validation for the [`Config`] fails.
536    pub fn finish(self) -> Result<Config, crate::Error> {
537        self.0
538            .validate()
539            .map_err(|source| crate::Error::Validation {
540                context: "validating a configuration object".to_string(),
541                source,
542            })?;
543
544        Ok(self.0)
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use std::{collections::BTreeSet, num::NonZeroUsize, thread::current};
551
552    use insta::{assert_snapshot, with_settings};
553    #[cfg(feature = "nethsm")]
554    use nethsm::ConnectionSecurity;
555    use pretty_assertions::assert_eq;
556    use rstest::{fixture, rstest};
557    use signstar_crypto::{AdministrativeSecretHandling, NonAdministrativeSecretHandling};
558    #[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
559    use signstar_crypto::{
560        key::{CryptographicKeyContext, KeyMechanism, KeyType, SignatureType, SigningKeySetup},
561        openpgp::OpenPgpUserIdList,
562    };
563    #[cfg(feature = "yubihsm2")]
564    use signstar_yubihsm2::object::Domain;
565    use tempfile::{NamedTempFile, TempDir};
566    use testresult::TestResult;
567
568    use super::*;
569    #[cfg(feature = "nethsm")]
570    use crate::NetHsmMetricsUsers;
571    use crate::{AuthorizedKeyEntry, SystemUserId};
572    use crate::{ConfigError, config::SystemUserMapping};
573
574    const SNAPSHOT_PATH: &str = "fixtures/file/";
575
576    /// Creates a default [`SystemConfig`] for testing purposes.
577    #[fixture]
578    fn default_system_config() -> TestResult<SystemConfig> {
579        Ok(SystemConfig::new(
580            1,
581            AdministrativeSecretHandling::ShamirsSecretSharing {
582                number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
583                threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
584            },
585            NonAdministrativeSecretHandling::SystemdCreds,
586            BTreeSet::from_iter([
587                SystemUserMapping::ShareHolder {
588                    system_user: "share-holder1".parse()?,
589                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
590                },
591                SystemUserMapping::ShareHolder {
592                    system_user: "share-holder2".parse()?,
593                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
594                },
595                SystemUserMapping::ShareHolder {
596                    system_user: "share-holder3".parse()?,
597                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
598                },
599                SystemUserMapping::WireGuardDownload {
600                    system_user: "wireguard-downloader".parse()?,
601                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
602                },
603            ]),
604        )?)
605    }
606
607    /// Creates a default [`NetHsmConfig`] for testing purposes.
608    #[cfg(feature = "nethsm")]
609    #[fixture]
610    fn default_nethsm_config() -> TestResult<NetHsmConfig> {
611        Ok(NetHsmConfig::new(
612            BTreeSet::from_iter([
613                Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
614                Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
615            ]),
616            BTreeSet::from_iter([
617                NetHsmUserMapping::Admin("admin".parse()?),
618                NetHsmUserMapping::Backup{
619                    backend_user: "backup".parse()?,
620                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
621                    system_user: "nethsm-backup-user".parse()?,
622                },
623                NetHsmUserMapping::HermeticMetrics {
624                    backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
625                    system_user: "nethsm-hermetic-metrics-user".parse()?,
626                },
627                NetHsmUserMapping::Metrics {
628                    backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
629                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
630                    system_user: "nethsm-metrics-user".parse()?,
631                },
632                NetHsmUserMapping::Signing {
633                    backend_user: "signing".parse()?,
634                    signing_key_id: "signing1".parse()?,
635                    key_setup: SigningKeySetup::new(
636                        KeyType::Curve25519,
637                        vec![KeyMechanism::EdDsaSignature],
638                        None,
639                        SignatureType::EdDsa,
640                        CryptographicKeyContext::OpenPgp {
641                            user_ids: OpenPgpUserIdList::new(vec![
642                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
643                            ])?,
644                            version: "v4".parse()?,
645                        },
646                    )?,
647                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
648                    system_user: "nethsm-signing-user".parse()?,
649                    tag: "signing1".to_string(),
650                }
651            ]),
652        )?)
653    }
654
655    /// Creates a default [`YubiHsm2Config`] for testing purposes.
656    #[cfg(feature = "yubihsm2")]
657    #[fixture]
658    fn default_yubihsm2_config() -> TestResult<YubiHsm2Config> {
659        Ok(YubiHsm2Config::new(
660            BTreeSet::from_iter([
661                YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
662                YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
663            ]),
664            BTreeSet::from_iter([
665                YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
666                YubiHsm2UserMapping::AuditLog {
667                    authentication_key_id: "3".parse()?,
668                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
669                    system_user: "yubihsm2-metrics-user".parse()?,
670                },
671                YubiHsm2UserMapping::Backup{
672                    authentication_key_id: "2".parse()?,
673                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
674                    system_user: "yubihsm2-backup-user".parse()?,
675                    wrapping_key_id: "1".parse()?,
676                },
677                YubiHsm2UserMapping::HermeticAuditLog {
678                    authentication_key_id: "4".parse()?,
679                    system_user: "yubihsm2-hermetic-metrics-user".parse()?,
680                },
681                YubiHsm2UserMapping::Signing {
682                    authentication_key_id: "5".parse()?,
683                    signing_key_id: "1".parse()?,
684                    key_setup: SigningKeySetup::new(
685                        KeyType::Curve25519,
686                        vec![KeyMechanism::EdDsaSignature],
687                        None,
688                        SignatureType::EdDsa,
689                        CryptographicKeyContext::OpenPgp {
690                            user_ids: OpenPgpUserIdList::new(vec![
691                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
692                            ])?,
693                            version: "v4".parse()?,
694                        },
695                    )?,
696                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
697                    system_user: "yubihsm2-signing-user".parse()?,
698                    domain: Domain::One,
699                }
700            ]),
701        )?)
702    }
703
704    /// Ensures, that [`Config::default_system_path`] always returns the same path.
705    #[test]
706    fn config_default_system_path() {
707        assert_eq!(
708            Config::default_system_path(),
709            PathBuf::from("/usr/share/signstar/config.yaml")
710        )
711    }
712
713    /// Ensures, that [`Config::list_config_file_paths`] always returns the same list of paths.
714    #[test]
715    fn config_list_config_file_paths() {
716        assert_eq!(
717            Config::list_config_file_paths(),
718            vec![
719                PathBuf::from("/usr/share/signstar/config.yaml"),
720                PathBuf::from("/run/signstar/config.yaml"),
721                PathBuf::from("/etc/signstar/config.yaml"),
722            ]
723        )
724    }
725
726    /// Ensures, that [`Config::from_file_path`] fails on missing file extensions.
727    #[rstest]
728    fn config_from_file_path_fails_on_missing_file_extension() -> TestResult {
729        let temp_dir = TempDir::new()?;
730
731        match Config::from_file_path(temp_dir.path().join("config")) {
732            Ok(config) => panic!(
733                "Should have failed to create a Config object, but succeeded instead: {config:?}"
734            ),
735            Err(crate::Error::Config(ConfigError::MissingFileExtension { .. })) => {}
736            Err(error) => panic!(
737                "Should have failed with a ConfigError::MissingFileExtension, but failed with a different error instead: {error}"
738            ),
739        }
740
741        Ok(())
742    }
743
744    /// Ensures, that [`Config::from_file_path`] fails on unsupported file extensions.
745    #[rstest]
746    fn config_from_file_path_fails_on_unsupported_file_extension() -> TestResult {
747        let temp_file = NamedTempFile::with_suffix(".toml")?;
748
749        match Config::from_file_path(temp_file.path()) {
750            Ok(config) => panic!(
751                "Should have failed to create a Config object, but succeeded instead: {config:?}"
752            ),
753            Err(crate::Error::Config(ConfigError::UnsupportedFileExtension { .. })) => {}
754            Err(error) => panic!(
755                "Should have failed with a ConfigError::UnsupportedFileExtension, but failed with a different error instead: {error}"
756            ),
757        }
758
759        Ok(())
760    }
761
762    /// Tests, that are only available when using no backend.
763    #[cfg(not(any(feature = "nethsm", feature = "yubihsm2")))]
764    mod no_backend {
765        use std::collections::HashSet;
766
767        use pretty_assertions::assert_eq;
768
769        use super::*;
770        use crate::config::{ConfigAuthorizedKeyEntries, ConfigSystemUserIds};
771
772        /// Creates a default [`Config`] for testing purposes.
773        #[fixture]
774        fn default_config(default_system_config: TestResult<SystemConfig>) -> TestResult<Config> {
775            Ok(ConfigBuilder::new(default_system_config?).finish()?)
776        }
777
778        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
779        /// correctly.
780        #[rstest]
781        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
782            let config = default_config?;
783            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
784                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
785                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
786                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
787                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
788            ]);
789
790            assert_eq!(
791                config.authorized_key_entries(),
792                expected.iter().collect::<HashSet<_>>()
793            );
794            Ok(())
795        }
796
797        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
798        #[rstest]
799        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
800            let config = default_config?;
801            let expected: HashSet<SystemUserId> = HashSet::from_iter([
802                "share-holder1".parse()?,
803                "share-holder2".parse()?,
804                "share-holder3".parse()?,
805                "wireguard-downloader".parse()?,
806            ]);
807
808            assert_eq!(
809                config.system_user_ids(),
810                expected.iter().collect::<HashSet<_>>()
811            );
812            Ok(())
813        }
814    }
815
816    /// Tests, that are only available when using the NetHSM (and no other) backend.
817    #[cfg(all(feature = "nethsm", not(feature = "yubihsm2")))]
818    mod nethsm_backend {
819        use pretty_assertions::assert_eq;
820
821        use super::*;
822
823        /// Creates a default [`Config`] for testing purposes.
824        #[fixture]
825        fn default_config(
826            default_system_config: TestResult<SystemConfig>,
827            default_nethsm_config: TestResult<NetHsmConfig>,
828        ) -> TestResult<Config> {
829            Ok(ConfigBuilder::new(default_system_config?)
830                .set_nethsm_config(default_nethsm_config?)
831                .finish()?)
832        }
833
834        /// Ensures, that [`ConfigBuilder::finish`] fails on issues with overlapping data in
835        /// configuration components.
836        ///
837        /// Here, a custom [`NetHsmConfig`] is staged together with a default [`SystemConfig`]
838        /// (created by [`default_system_config`]) to create a failure scenario.
839        #[rstest]
840        #[case::two_duplicate_system_users_two_duplicate_ssh_public_keys(
841            "Configuration with system-wide and NetHSM configuration has two duplicate system users and two duplicate SSH public keys",
842            NetHsmConfig::new(
843                BTreeSet::from_iter([
844                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
845                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
846                ]),
847                BTreeSet::from_iter([
848                    NetHsmUserMapping::Admin("admin".parse()?),
849                    NetHsmUserMapping::Backup{
850                        backend_user: "backup".parse()?,
851                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
852                        system_user: "share-holder1".parse()?,
853                    },
854                    NetHsmUserMapping::HermeticMetrics {
855                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
856                        system_user: "nethsm-hermetic-metrics-user".parse()?,
857                    },
858                    NetHsmUserMapping::Metrics {
859                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
860                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
861                        system_user: "share-holder2".parse()?,
862                    },
863                    NetHsmUserMapping::Signing {
864                        backend_user: "signing".parse()?,
865                        signing_key_id: "signing1".parse()?,
866                        key_setup: SigningKeySetup::new(
867                            KeyType::Curve25519,
868                            vec![KeyMechanism::EdDsaSignature],
869                            None,
870                            SignatureType::EdDsa,
871                            CryptographicKeyContext::OpenPgp {
872                                user_ids: OpenPgpUserIdList::new(vec![
873                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
874                                ])?,
875                                version: "v4".parse()?,
876                            },
877                        )?,
878                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
879                        system_user: "nethsm-signing-user".parse()?,
880                        tag: "signing1".to_string(),
881                    }
882                ]),
883            )?
884        )]
885        #[case::one_duplicate_system_user_two_duplicate_ssh_public_keys(
886            "Configuration with system-wide and NetHSM configuration has one duplicate system user and two duplicate SSH public keys",
887            NetHsmConfig::new(
888                BTreeSet::from_iter([
889                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
890                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
891                ]),
892                BTreeSet::from_iter([
893                    NetHsmUserMapping::Admin("admin".parse()?),
894                    NetHsmUserMapping::Backup{
895                        backend_user: "backup".parse()?,
896                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
897                        system_user: "share-holder1".parse()?,
898                    },
899                    NetHsmUserMapping::HermeticMetrics {
900                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
901                        system_user: "nethsm-hermetic-metrics-user".parse()?,
902                    },
903                    NetHsmUserMapping::Metrics {
904                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
905                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
906                        system_user: "nethsm-metrics-user".parse()?,
907                    },
908                    NetHsmUserMapping::Signing {
909                        backend_user: "signing".parse()?,
910                        signing_key_id: "signing1".parse()?,
911                        key_setup: SigningKeySetup::new(
912                            KeyType::Curve25519,
913                            vec![KeyMechanism::EdDsaSignature],
914                            None,
915                            SignatureType::EdDsa,
916                            CryptographicKeyContext::OpenPgp {
917                                user_ids: OpenPgpUserIdList::new(vec![
918                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
919                                ])?,
920                                version: "v4".parse()?,
921                            },
922                        )?,
923                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
924                        system_user: "nethsm-signing-user".parse()?,
925                        tag: "signing1".to_string(),
926                    }
927                ]),
928            )?
929        )]
930        #[case::one_duplicate_system_user_one_duplicate_ssh_public_key(
931            "Configuration with system-wide and NetHSM configuration has one duplicate system user and one duplicate SSH public key",
932            NetHsmConfig::new(
933                BTreeSet::from_iter([
934                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
935                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
936                ]),
937                BTreeSet::from_iter([
938                    NetHsmUserMapping::Admin("admin".parse()?),
939                    NetHsmUserMapping::Backup{
940                        backend_user: "backup".parse()?,
941                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
942                        system_user: "share-holder1".parse()?,
943                    },
944                    NetHsmUserMapping::HermeticMetrics {
945                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
946                        system_user: "nethsm-hermetic-metrics-user".parse()?,
947                    },
948                    NetHsmUserMapping::Metrics {
949                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
950                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
951                        system_user: "nethsm-metrics-user".parse()?,
952                    },
953                    NetHsmUserMapping::Signing {
954                        backend_user: "signing".parse()?,
955                        signing_key_id: "signing1".parse()?,
956                        key_setup: SigningKeySetup::new(
957                            KeyType::Curve25519,
958                            vec![KeyMechanism::EdDsaSignature],
959                            None,
960                            SignatureType::EdDsa,
961                            CryptographicKeyContext::OpenPgp {
962                                user_ids: OpenPgpUserIdList::new(vec![
963                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
964                                ])?,
965                                version: "v4".parse()?,
966                            },
967                        )?,
968                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
969                        system_user: "nethsm-signing-user".parse()?,
970                        tag: "signing1".to_string(),
971                    }
972                ]),
973            )?
974        )]
975        #[case::one_duplicate_ssh_public_key(
976            "Configuration with system-wide and NetHSM configuration has one duplicate SSH public key",
977            NetHsmConfig::new(
978                BTreeSet::from_iter([
979                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
980                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
981                ]),
982                BTreeSet::from_iter([
983                    NetHsmUserMapping::Admin("admin".parse()?),
984                    NetHsmUserMapping::Backup{
985                        backend_user: "backup".parse()?,
986                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
987                        system_user: "nethsm-backup-user".parse()?,
988                    },
989                    NetHsmUserMapping::HermeticMetrics {
990                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
991                        system_user: "nethsm-hermetic-metrics-user".parse()?,
992                    },
993                    NetHsmUserMapping::Metrics {
994                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
995                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
996                        system_user: "nethsm-metrics-user".parse()?,
997                    },
998                    NetHsmUserMapping::Signing {
999                        backend_user: "signing".parse()?,
1000                        signing_key_id: "signing1".parse()?,
1001                        key_setup: SigningKeySetup::new(
1002                            KeyType::Curve25519,
1003                            vec![KeyMechanism::EdDsaSignature],
1004                            None,
1005                            SignatureType::EdDsa,
1006                            CryptographicKeyContext::OpenPgp {
1007                                user_ids: OpenPgpUserIdList::new(vec![
1008                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1009                                ])?,
1010                                version: "v4".parse()?,
1011                            },
1012                        )?,
1013                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1014                        system_user: "nethsm-signing-user".parse()?,
1015                        tag: "signing1".to_string(),
1016                    }
1017                ]),
1018            )?
1019        )]
1020        #[case::one_duplicate_system_user(
1021            "Configuration with system-wide and NetHSM configuration has one duplicate system user",
1022            NetHsmConfig::new(
1023                BTreeSet::from_iter([
1024                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1025                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1026                ]),
1027                BTreeSet::from_iter([
1028                    NetHsmUserMapping::Admin("admin".parse()?),
1029                    NetHsmUserMapping::Backup{
1030                        backend_user: "backup".parse()?,
1031                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1032                        system_user: "share-holder1".parse()?,
1033                    },
1034                    NetHsmUserMapping::HermeticMetrics {
1035                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1036                        system_user: "nethsm-hermetic-metrics-user".parse()?,
1037                    },
1038                    NetHsmUserMapping::Metrics {
1039                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1040                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1041                        system_user: "nethsm-metrics-user".parse()?,
1042                    },
1043                    NetHsmUserMapping::Signing {
1044                        backend_user: "signing".parse()?,
1045                        signing_key_id: "signing1".parse()?,
1046                        key_setup: SigningKeySetup::new(
1047                            KeyType::Curve25519,
1048                            vec![KeyMechanism::EdDsaSignature],
1049                            None,
1050                            SignatureType::EdDsa,
1051                            CryptographicKeyContext::OpenPgp {
1052                                user_ids: OpenPgpUserIdList::new(vec![
1053                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1054                                ])?,
1055                                version: "v4".parse()?,
1056                            },
1057                        )?,
1058                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1059                        system_user: "nethsm-signing-user".parse()?,
1060                        tag: "signing1".to_string(),
1061                    }
1062                ]),
1063            )?
1064        )]
1065        fn config_builder_fails_validation(
1066            default_system_config: TestResult<SystemConfig>,
1067            #[case] description: &str,
1068            #[case] nethsm_config: NetHsmConfig,
1069        ) -> TestResult {
1070            let error_message = match ConfigBuilder::new(default_system_config?)
1071                .set_nethsm_config(nethsm_config)
1072                .finish()
1073            {
1074                Err(error) => error.to_string(),
1075                Ok(config) => panic!(
1076                    "Expected to fail with Error::Validation, but succeeded instead: {}",
1077                    config.to_yaml_string()?
1078                ),
1079            };
1080
1081            with_settings!({
1082                description => description,
1083                snapshot_path => SNAPSHOT_PATH,
1084                prepend_module_to_snapshot => false,
1085            }, {
1086                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_message);
1087            });
1088
1089            Ok(())
1090        }
1091
1092        /// Ensures, that [`Config::nethsm`] returns the original input.
1093        #[rstest]
1094        fn config_nethsm(
1095            default_system_config: TestResult<SystemConfig>,
1096            default_nethsm_config: TestResult<NetHsmConfig>,
1097        ) -> TestResult {
1098            let nethsm_config = default_nethsm_config?;
1099
1100            let config = ConfigBuilder::new(default_system_config?)
1101                .set_nethsm_config(nethsm_config.clone())
1102                .finish()?;
1103
1104            assert_eq!(
1105                &nethsm_config,
1106                config.nethsm().expect("a NetHsmConfig reference")
1107            );
1108
1109            Ok(())
1110        }
1111
1112        /// Ensures, that an optional [`UserBackendConnection`] can be retrieved from a [`Config`].
1113        #[rstest]
1114        #[case::nethsm_signing(
1115            "nethsm-signing-user",
1116            Some(UserBackendConnection::NetHsm {
1117                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1118                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1119                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1120                },
1121                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1122                connections: BTreeSet::from_iter([
1123                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1124                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1125                ]),
1126                mapping: NetHsmUserMapping::Signing {
1127                    backend_user: "signing".parse()?,
1128                    signing_key_id: "signing1".parse()?,
1129                    key_setup: SigningKeySetup::new(
1130                        KeyType::Curve25519,
1131                        vec![KeyMechanism::EdDsaSignature],
1132                        None,
1133                        SignatureType::EdDsa,
1134                        CryptographicKeyContext::OpenPgp {
1135                            user_ids: OpenPgpUserIdList::new(vec![
1136                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1137                            ])?,
1138                            version: "v4".parse()?,
1139                        },
1140                    )?,
1141                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1142                    system_user: "nethsm-signing-user".parse()?,
1143                    tag: "signing1".to_string(),
1144                }
1145            })
1146        )]
1147        #[case::none("foo", None)]
1148        fn config_user_backend_connection(
1149            default_config: TestResult<Config>,
1150            #[case] system_user: &str,
1151            #[case] expected_connection: Option<UserBackendConnection>,
1152        ) -> TestResult {
1153            let config = default_config?;
1154            assert_eq!(
1155                expected_connection,
1156                config.user_backend_connection(&system_user.parse()?)
1157            );
1158
1159            Ok(())
1160        }
1161
1162        /// Ensures, that [`Config::user_backend_connections`] returns the correct list of
1163        /// [`UserBackendConnection`] items according to a [`UserBackendConnectionFilter`].
1164        #[rstest]
1165        #[case::filter_all(
1166            UserBackendConnectionFilter::All,
1167            vec![
1168                UserBackendConnection::NetHsm {
1169                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1170                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1171                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1172                    },
1173                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1174                    connections: BTreeSet::from_iter([
1175                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1176                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1177                    ]),
1178                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
1179                },
1180                UserBackendConnection::NetHsm {
1181                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1182                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1183                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1184                    },
1185                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1186                    connections: BTreeSet::from_iter([
1187                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1188                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1189                    ]),
1190                    mapping: NetHsmUserMapping::Backup{
1191                        backend_user: "backup".parse()?,
1192                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1193                        system_user: "nethsm-backup-user".parse()?,
1194                    }
1195                },
1196                UserBackendConnection::NetHsm {
1197                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1198                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1199                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1200                    },
1201                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1202                    connections: BTreeSet::from_iter([
1203                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1204                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1205                    ]),
1206                    mapping: NetHsmUserMapping::HermeticMetrics {
1207                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1208                        system_user: "nethsm-hermetic-metrics-user".parse()?,
1209                    }
1210                },
1211                UserBackendConnection::NetHsm {
1212                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1213                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1214                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1215                    },
1216                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1217                    connections: BTreeSet::from_iter([
1218                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1219                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1220                    ]),
1221                    mapping: NetHsmUserMapping::Metrics {
1222                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1223                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1224                        system_user: "nethsm-metrics-user".parse()?,
1225                    }
1226                },
1227                UserBackendConnection::NetHsm {
1228                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1229                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1230                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1231                    },
1232                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1233                    connections: BTreeSet::from_iter([
1234                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1235                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1236                    ]),
1237                    mapping: NetHsmUserMapping::Signing {
1238                        backend_user: "signing".parse()?,
1239                        signing_key_id: "signing1".parse()?,
1240                        key_setup: SigningKeySetup::new(
1241                            KeyType::Curve25519,
1242                            vec![KeyMechanism::EdDsaSignature],
1243                            None,
1244                            SignatureType::EdDsa,
1245                            CryptographicKeyContext::OpenPgp {
1246                                user_ids: OpenPgpUserIdList::new(vec![
1247                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1248                                ])?,
1249                                version: "v4".parse()?,
1250                            },
1251                        )?,
1252                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1253                        system_user: "nethsm-signing-user".parse()?,
1254                        tag: "signing1".to_string(),
1255                    }
1256                },
1257            ],
1258        )]
1259        #[case::filter_admin(
1260            UserBackendConnectionFilter::Admin,
1261            vec![
1262                UserBackendConnection::NetHsm {
1263                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1264                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1265                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1266                    },
1267                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1268                    connections: BTreeSet::from_iter([
1269                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1270                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1271                    ]),
1272                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
1273                },
1274            ],
1275        )]
1276        #[case::filter_non_admin(
1277            UserBackendConnectionFilter::NonAdmin,
1278            vec![
1279                UserBackendConnection::NetHsm {
1280                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1281                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1282                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1283                    },
1284                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1285                    connections: BTreeSet::from_iter([
1286                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1287                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1288                    ]),
1289                    mapping: NetHsmUserMapping::Backup{
1290                        backend_user: "backup".parse()?,
1291                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1292                        system_user: "nethsm-backup-user".parse()?,
1293                    }
1294                },
1295                UserBackendConnection::NetHsm {
1296                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1297                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1298                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1299                    },
1300                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1301                    connections: BTreeSet::from_iter([
1302                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1303                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1304                    ]),
1305                    mapping: NetHsmUserMapping::HermeticMetrics {
1306                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1307                        system_user: "nethsm-hermetic-metrics-user".parse()?,
1308                    }
1309                },
1310                UserBackendConnection::NetHsm {
1311                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1312                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1313                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1314                    },
1315                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1316                    connections: BTreeSet::from_iter([
1317                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1318                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1319                    ]),
1320                    mapping: NetHsmUserMapping::Metrics {
1321                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1322                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1323                        system_user: "nethsm-metrics-user".parse()?,
1324                    }
1325                },
1326                UserBackendConnection::NetHsm {
1327                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1328                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1329                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1330                    },
1331                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1332                    connections: BTreeSet::from_iter([
1333                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1334                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1335                    ]),
1336                    mapping: NetHsmUserMapping::Signing {
1337                        backend_user: "signing".parse()?,
1338                        signing_key_id: "signing1".parse()?,
1339                        key_setup: SigningKeySetup::new(
1340                            KeyType::Curve25519,
1341                            vec![KeyMechanism::EdDsaSignature],
1342                            None,
1343                            SignatureType::EdDsa,
1344                            CryptographicKeyContext::OpenPgp {
1345                                user_ids: OpenPgpUserIdList::new(vec![
1346                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1347                                ])?,
1348                                version: "v4".parse()?,
1349                            },
1350                        )?,
1351                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1352                        system_user: "nethsm-signing-user".parse()?,
1353                        tag: "signing1".to_string(),
1354                    }
1355                },
1356            ],
1357        )]
1358        fn config_user_backend_connections(
1359            default_config: TestResult<Config>,
1360            #[case] filter: UserBackendConnectionFilter,
1361            #[case] expected_connections: Vec<UserBackendConnection>,
1362        ) -> TestResult {
1363            let config = default_config?;
1364
1365            assert_eq!(
1366                expected_connections,
1367                config.user_backend_connections(filter)
1368            );
1369
1370            Ok(())
1371        }
1372
1373        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
1374        /// correctly.
1375        #[rstest]
1376        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
1377            let config = default_config?;
1378            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
1379                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
1380                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
1381                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1382                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1383                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1384                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1385                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1386            ]);
1387
1388            assert_eq!(
1389                config.authorized_key_entries(),
1390                expected.iter().collect::<HashSet<_>>()
1391            );
1392            Ok(())
1393        }
1394
1395        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
1396        #[rstest]
1397        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
1398            let config = default_config?;
1399            let expected: HashSet<SystemUserId> = HashSet::from_iter([
1400                "share-holder1".parse()?,
1401                "share-holder2".parse()?,
1402                "share-holder3".parse()?,
1403                "wireguard-downloader".parse()?,
1404                "nethsm-backup-user".parse()?,
1405                "nethsm-hermetic-metrics-user".parse()?,
1406                "nethsm-metrics-user".parse()?,
1407                "nethsm-signing-user".parse()?,
1408            ]);
1409
1410            assert_eq!(
1411                config.system_user_ids(),
1412                expected.iter().collect::<HashSet<_>>()
1413            );
1414            Ok(())
1415        }
1416
1417        /// Ensures, that a [`Config`] object leads to a specific YAML output.
1418        ///
1419        /// In this particular case, a [`SystemConfig`] and a [`NetHsmConfig`] object are present.
1420        #[rstest]
1421        fn config_to_yaml_string(
1422            default_system_config: TestResult<SystemConfig>,
1423            default_nethsm_config: TestResult<NetHsmConfig>,
1424        ) -> TestResult {
1425            let config = ConfigBuilder::new(default_system_config?)
1426                .set_nethsm_config(default_nethsm_config?)
1427                .finish()?;
1428            let config_str = config.to_yaml_string()?;
1429
1430            with_settings!({
1431                description => "Configuration with system-wide and NetHSM configuration",
1432                snapshot_path => SNAPSHOT_PATH,
1433                prepend_module_to_snapshot => false,
1434            }, {
1435                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
1436            });
1437
1438            Ok(())
1439        }
1440
1441        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
1442        /// the same YAML string.
1443        ///
1444        /// The configuration file describes a [`SystemConfig`] and a [`NetHsmConfig`] object.
1445        #[rstest]
1446        fn roundtrip_yaml_config(
1447            #[files("../fixtures/config/nethsm_backend/*.yaml")] path: PathBuf,
1448        ) -> TestResult {
1449            let config_string = read_to_string(&path)?;
1450            let config = Config::from_file_path(&path)?;
1451
1452            assert_eq!(config.to_yaml_string()?, config_string);
1453
1454            Ok(())
1455        }
1456
1457        /// Ensures, that [`AdministrativeSecretHandling`] and
1458        /// [`NonAdministrativeSecretHandling`]can be retrieved from a
1459        /// [`UserBackendConnection`].
1460        #[rstest]
1461        fn user_backend_connection_secret_handling(
1462            default_config: TestResult<Config>,
1463        ) -> TestResult {
1464            let config = default_config?;
1465            let admin_secret_handling = AdministrativeSecretHandling::ShamirsSecretSharing {
1466                number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1467                threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1468            };
1469            let non_admin_secret_handling = NonAdministrativeSecretHandling::SystemdCreds;
1470
1471            let user_backend_connection = config
1472                .user_backend_connection(&"nethsm-signing-user".parse()?)
1473                .expect("there to be a mapping of the requested name");
1474
1475            assert_eq!(
1476                user_backend_connection.admin_secret_handling(),
1477                admin_secret_handling
1478            );
1479            assert_eq!(
1480                user_backend_connection.non_admin_secret_handling(),
1481                non_admin_secret_handling
1482            );
1483
1484            Ok(())
1485        }
1486    }
1487
1488    /// Tests, that are only available when using the YubiHSM2 (and no other) backend.
1489    #[cfg(all(feature = "yubihsm2", not(feature = "nethsm")))]
1490    mod yubihsm2_backend {
1491        use pretty_assertions::assert_eq;
1492
1493        use super::*;
1494
1495        /// Creates a default [`Config`] for testing purposes.
1496        #[fixture]
1497        fn default_config(
1498            default_system_config: TestResult<SystemConfig>,
1499            default_yubihsm2_config: TestResult<YubiHsm2Config>,
1500        ) -> TestResult<Config> {
1501            Ok(ConfigBuilder::new(default_system_config?)
1502                .set_yubihsm2_config(default_yubihsm2_config?)
1503                .finish()?)
1504        }
1505
1506        /// Ensures, that [`ConfigBuilder::finish`] fails on issues with overlapping data in
1507        /// configuration components.
1508        ///
1509        /// Here, a custom [`YubiHsm2Config`] is staged together with a default [`SystemConfig`]
1510        /// (created by [`default_system_config`]) to create a failure scenario.
1511        #[rstest]
1512        #[case::two_duplicate_system_users_two_duplicate_ssh_public_keys(
1513            "Configuration with system-wide and YubiHSM2 configuration has two duplicate system users and two duplicate SSH public keys",
1514            YubiHsm2Config::new(
1515                BTreeSet::from_iter([
1516                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1517                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1518                ]),
1519                BTreeSet::from_iter([
1520                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1521                    YubiHsm2UserMapping::AuditLog {
1522                        authentication_key_id: "3".parse()?,
1523                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1524                        system_user: "share-holder2".parse()?,
1525                    },
1526                    YubiHsm2UserMapping::Backup{
1527                        authentication_key_id: "2".parse()?,
1528                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1529                        system_user: "share-holder1".parse()?,
1530                        wrapping_key_id: "1".parse()?,
1531                    },
1532                    YubiHsm2UserMapping::HermeticAuditLog {
1533                        authentication_key_id: "4".parse()?,
1534                        system_user: "yubihsm2-hermetic-metrics".parse()?,
1535                    },
1536                    YubiHsm2UserMapping::Signing {
1537                        authentication_key_id: "5".parse()?,
1538                        signing_key_id: "1".parse()?,
1539                        key_setup: SigningKeySetup::new(
1540                            KeyType::Curve25519,
1541                            vec![KeyMechanism::EdDsaSignature],
1542                            None,
1543                            SignatureType::EdDsa,
1544                            CryptographicKeyContext::OpenPgp {
1545                                user_ids: OpenPgpUserIdList::new(vec![
1546                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1547                                ])?,
1548                                version: "v4".parse()?,
1549                            },
1550                        )?,
1551                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1552                        system_user: "yubihsm2-signing-user".parse()?,
1553                        domain: Domain::One,
1554                    }
1555                ]),
1556            )?
1557         )]
1558        #[case::one_duplicate_system_user_two_duplicate_ssh_public_keys(
1559            "Configuration with system-wide and YubiHSM2 configuration has one duplicate system user and two duplicate SSH public keys",
1560            YubiHsm2Config::new(
1561                BTreeSet::from_iter([
1562                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1563                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1564                ]),
1565                BTreeSet::from_iter([
1566                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1567                    YubiHsm2UserMapping::AuditLog {
1568                        authentication_key_id: "3".parse()?,
1569                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1570                        system_user: "yubihsm2-metrics-user".parse()?,
1571                    },
1572                    YubiHsm2UserMapping::Backup{
1573                        authentication_key_id: "2".parse()?,
1574                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1575                        system_user: "share-holder1".parse()?,
1576                        wrapping_key_id: "1".parse()?,
1577                    },
1578                    YubiHsm2UserMapping::HermeticAuditLog {
1579                        authentication_key_id: "4".parse()?,
1580                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1581                    },
1582                    YubiHsm2UserMapping::Signing {
1583                        authentication_key_id: "5".parse()?,
1584                        signing_key_id: "1".parse()?,
1585                        key_setup: SigningKeySetup::new(
1586                            KeyType::Curve25519,
1587                            vec![KeyMechanism::EdDsaSignature],
1588                            None,
1589                            SignatureType::EdDsa,
1590                            CryptographicKeyContext::OpenPgp {
1591                                user_ids: OpenPgpUserIdList::new(vec![
1592                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1593                                ])?,
1594                                version: "v4".parse()?,
1595                            },
1596                        )?,
1597                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1598                        system_user: "yubihsm2-signing-user".parse()?,
1599                        domain: Domain::One,
1600                    }
1601                ]),
1602            )?
1603         )]
1604        #[case::one_duplicate_system_user_one_duplicate_ssh_public_key(
1605            "Configuration with system-wide and YubiHSM2 configuration has one duplicate system user and one duplicate SSH public key",
1606            YubiHsm2Config::new(
1607                BTreeSet::from_iter([
1608                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1609                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1610                ]),
1611                BTreeSet::from_iter([
1612                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1613                    YubiHsm2UserMapping::AuditLog {
1614                        authentication_key_id: "3".parse()?,
1615                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1616                        system_user: "yubihsm2-metrics-user".parse()?,
1617                    },
1618                    YubiHsm2UserMapping::Backup{
1619                        authentication_key_id: "2".parse()?,
1620                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1621                        system_user: "share-holder1".parse()?,
1622                        wrapping_key_id: "1".parse()?,
1623                    },
1624                    YubiHsm2UserMapping::HermeticAuditLog {
1625                        authentication_key_id: "4".parse()?,
1626                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1627                    },
1628                    YubiHsm2UserMapping::Signing {
1629                        authentication_key_id: "5".parse()?,
1630                        signing_key_id: "1".parse()?,
1631                        key_setup: SigningKeySetup::new(
1632                            KeyType::Curve25519,
1633                            vec![KeyMechanism::EdDsaSignature],
1634                            None,
1635                            SignatureType::EdDsa,
1636                            CryptographicKeyContext::OpenPgp {
1637                                user_ids: OpenPgpUserIdList::new(vec![
1638                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1639                                ])?,
1640                                version: "v4".parse()?,
1641                            },
1642                        )?,
1643                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1644                        system_user: "yubihsm2-signing-user".parse()?,
1645                        domain: Domain::One,
1646                    }
1647                ]),
1648            )?
1649         )]
1650        #[case::one_duplicate_ssh_public_key(
1651            "Configuration with system-wide and YubiHSM2 configuration has one duplicate SSH public key",
1652            YubiHsm2Config::new(
1653                BTreeSet::from_iter([
1654                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1655                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1656                ]),
1657                BTreeSet::from_iter([
1658                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1659                    YubiHsm2UserMapping::AuditLog {
1660                        authentication_key_id: "3".parse()?,
1661                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1662                        system_user: "yubihsm2-metrics-user".parse()?,
1663                    },
1664                    YubiHsm2UserMapping::Backup{
1665                        authentication_key_id: "2".parse()?,
1666                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1667                        system_user: "yubihsm2-backup-user".parse()?,
1668                        wrapping_key_id: "1".parse()?,
1669                    },
1670                    YubiHsm2UserMapping::HermeticAuditLog {
1671                        authentication_key_id: "4".parse()?,
1672                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1673                    },
1674                    YubiHsm2UserMapping::Signing {
1675                        authentication_key_id: "5".parse()?,
1676                        signing_key_id: "1".parse()?,
1677                        key_setup: SigningKeySetup::new(
1678                            KeyType::Curve25519,
1679                            vec![KeyMechanism::EdDsaSignature],
1680                            None,
1681                            SignatureType::EdDsa,
1682                            CryptographicKeyContext::OpenPgp {
1683                                user_ids: OpenPgpUserIdList::new(vec![
1684                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1685                                ])?,
1686                                version: "v4".parse()?,
1687                            },
1688                        )?,
1689                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1690                        system_user: "yubihsm2-signing-user".parse()?,
1691                        domain: Domain::One,
1692                    }
1693                ]),
1694            )?
1695         )]
1696        #[case::one_duplicate_system_user(
1697            "Configuration with system-wide and YubiHSM2 configuration has one duplicate system user",
1698            YubiHsm2Config::new(
1699                BTreeSet::from_iter([
1700                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1701                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1702                ]),
1703                BTreeSet::from_iter([
1704                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1705                    YubiHsm2UserMapping::AuditLog {
1706                        authentication_key_id: "3".parse()?,
1707                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1708                        system_user: "yubihsm2-metrics-user".parse()?,
1709                    },
1710                    YubiHsm2UserMapping::Backup{
1711                        authentication_key_id: "2".parse()?,
1712                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1713                        system_user: "share-holder1".parse()?,
1714                        wrapping_key_id: "1".parse()?,
1715                    },
1716                    YubiHsm2UserMapping::HermeticAuditLog {
1717                        authentication_key_id: "4".parse()?,
1718                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1719                    },
1720                    YubiHsm2UserMapping::Signing {
1721                        authentication_key_id: "5".parse()?,
1722                        signing_key_id: "1".parse()?,
1723                        key_setup: SigningKeySetup::new(
1724                            KeyType::Curve25519,
1725                            vec![KeyMechanism::EdDsaSignature],
1726                            None,
1727                            SignatureType::EdDsa,
1728                            CryptographicKeyContext::OpenPgp {
1729                                user_ids: OpenPgpUserIdList::new(vec![
1730                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1731                                ])?,
1732                                version: "v4".parse()?,
1733                            },
1734                        )?,
1735                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1736                        system_user: "yubihsm2-signing-user".parse()?,
1737                        domain: Domain::One,
1738                    }
1739                ]),
1740            )?
1741         )]
1742        fn config_builder_fails_validation(
1743            default_system_config: TestResult<SystemConfig>,
1744            #[case] description: &str,
1745            #[case] yubihsm2_config: YubiHsm2Config,
1746        ) -> TestResult {
1747            let error_message = match ConfigBuilder::new(default_system_config?)
1748                .set_yubihsm2_config(yubihsm2_config)
1749                .finish()
1750            {
1751                Err(error) => error.to_string(),
1752                Ok(config) => panic!(
1753                    "Expected to fail with Error::Validation, but succeeded instead: {}",
1754                    config.to_yaml_string()?
1755                ),
1756            };
1757
1758            with_settings!({
1759                description => description,
1760                snapshot_path => SNAPSHOT_PATH,
1761                prepend_module_to_snapshot => false,
1762            }, {
1763                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_message);
1764            });
1765
1766            Ok(())
1767        }
1768
1769        /// Ensures, that [`Config::yubihsm2`] returns the original input.
1770        #[rstest]
1771        fn config_yubihsm2(
1772            default_system_config: TestResult<SystemConfig>,
1773            default_yubihsm2_config: TestResult<YubiHsm2Config>,
1774        ) -> TestResult {
1775            let yubihsm2_config = default_yubihsm2_config?;
1776
1777            let config = ConfigBuilder::new(default_system_config?)
1778                .set_yubihsm2_config(yubihsm2_config.clone())
1779                .finish()?;
1780
1781            assert_eq!(
1782                &yubihsm2_config,
1783                config.yubihsm2().expect("a YubiHsm2Config reference")
1784            );
1785
1786            Ok(())
1787        }
1788
1789        /// Ensures, that an optional [`UserBackendConnection`] can be retrieved from a [`Config`].
1790        #[rstest]
1791        #[case::yubihsm2_signing(
1792            "yubihsm2-signing-user",
1793            Some(UserBackendConnection::YubiHsm2 {
1794                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1795                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1796                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1797                },
1798                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1799                connections: BTreeSet::from_iter([
1800                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1801                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1802                ]),
1803                mapping: YubiHsm2UserMapping::Signing {
1804                    authentication_key_id: "5".parse()?,
1805                    signing_key_id: "1".parse()?,
1806                    key_setup: SigningKeySetup::new(
1807                        KeyType::Curve25519,
1808                        vec![KeyMechanism::EdDsaSignature],
1809                        None,
1810                        SignatureType::EdDsa,
1811                        CryptographicKeyContext::OpenPgp {
1812                            user_ids: OpenPgpUserIdList::new(vec![
1813                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1814                            ])?,
1815                            version: "v4".parse()?,
1816                        },
1817                    )?,
1818                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1819                    system_user: "yubihsm2-signing-user".parse()?,
1820                    domain: Domain::One,
1821                }
1822            })
1823        )]
1824        #[case::none("foo", None)]
1825        fn config_user_backend_connection(
1826            default_config: TestResult<Config>,
1827            #[case] system_user: &str,
1828            #[case] expected_connection: Option<UserBackendConnection>,
1829        ) -> TestResult {
1830            let config = default_config?;
1831            assert_eq!(
1832                expected_connection,
1833                config.user_backend_connection(&system_user.parse()?)
1834            );
1835
1836            Ok(())
1837        }
1838
1839        /// Ensures, that [`Config::user_backend_connections`] returns the correct list of
1840        /// [`UserBackendConnection`] items according to a [`UserBackendConnectionFilter`].
1841        #[rstest]
1842        #[case::filter_all(
1843            UserBackendConnectionFilter::All,
1844            vec![
1845                UserBackendConnection::YubiHsm2 {
1846                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1847                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1848                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1849                    },
1850                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1851                    connections: BTreeSet::from_iter([
1852                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1853                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1854                    ]),
1855                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1856                },
1857                UserBackendConnection::YubiHsm2 {
1858                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1859                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1860                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1861                    },
1862                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1863                    connections: BTreeSet::from_iter([
1864                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1865                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1866                    ]),
1867                    mapping: YubiHsm2UserMapping::AuditLog {
1868                        authentication_key_id: "3".parse()?,
1869                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1870                        system_user: "yubihsm2-metrics-user".parse()?,
1871                    },
1872                },
1873                UserBackendConnection::YubiHsm2 {
1874                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1875                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1876                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1877                    },
1878                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1879                    connections: BTreeSet::from_iter([
1880                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1881                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1882                    ]),
1883                    mapping: YubiHsm2UserMapping::Backup{
1884                        authentication_key_id: "2".parse()?,
1885                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1886                        system_user: "yubihsm2-backup-user".parse()?,
1887                        wrapping_key_id: "1".parse()?,
1888                    },
1889                },
1890                UserBackendConnection::YubiHsm2 {
1891                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1892                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1893                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1894                    },
1895                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1896                    connections: BTreeSet::from_iter([
1897                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1898                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1899                    ]),
1900                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
1901                        authentication_key_id: "4".parse()?,
1902                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1903                    },
1904                },
1905                UserBackendConnection::YubiHsm2 {
1906                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1907                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1908                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1909                    },
1910                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1911                    connections: BTreeSet::from_iter([
1912                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1913                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1914                    ]),
1915                    mapping: YubiHsm2UserMapping::Signing {
1916                        authentication_key_id: "5".parse()?,
1917                        signing_key_id: "1".parse()?,
1918                        key_setup: SigningKeySetup::new(
1919                            KeyType::Curve25519,
1920                            vec![KeyMechanism::EdDsaSignature],
1921                            None,
1922                            SignatureType::EdDsa,
1923                            CryptographicKeyContext::OpenPgp {
1924                                user_ids: OpenPgpUserIdList::new(vec![
1925                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1926                                ])?,
1927                                version: "v4".parse()?,
1928                            },
1929                        )?,
1930                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1931                        system_user: "yubihsm2-signing-user".parse()?,
1932                        domain: Domain::One,
1933                    }
1934                },
1935            ],
1936        )]
1937        #[case::filter_admin(
1938            UserBackendConnectionFilter::Admin,
1939            vec![
1940                UserBackendConnection::YubiHsm2 {
1941                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1942                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1943                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1944                    },
1945                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1946                    connections: BTreeSet::from_iter([
1947                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1948                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1949                    ]),
1950                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1951                },
1952            ],
1953        )]
1954        #[case::filter_non_admin(
1955            UserBackendConnectionFilter::NonAdmin,
1956            vec![
1957                UserBackendConnection::YubiHsm2 {
1958                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1959                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1960                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1961                    },
1962                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1963                    connections: BTreeSet::from_iter([
1964                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1965                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1966                    ]),
1967                    mapping: YubiHsm2UserMapping::AuditLog {
1968                        authentication_key_id: "3".parse()?,
1969                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1970                        system_user: "yubihsm2-metrics-user".parse()?,
1971                    },
1972                },
1973                UserBackendConnection::YubiHsm2 {
1974                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1975                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1976                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1977                    },
1978                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1979                    connections: BTreeSet::from_iter([
1980                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1981                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1982                    ]),
1983                    mapping: YubiHsm2UserMapping::Backup{
1984                        authentication_key_id: "2".parse()?,
1985                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1986                        system_user: "yubihsm2-backup-user".parse()?,
1987                        wrapping_key_id: "1".parse()?,
1988                    },
1989                },
1990                UserBackendConnection::YubiHsm2 {
1991                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1992                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1993                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1994                    },
1995                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1996                    connections: BTreeSet::from_iter([
1997                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1998                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1999                    ]),
2000                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
2001                        authentication_key_id: "4".parse()?,
2002                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2003                    },
2004                },
2005                UserBackendConnection::YubiHsm2 {
2006                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2007                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2008                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2009                    },
2010                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2011                    connections: BTreeSet::from_iter([
2012                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2013                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2014                    ]),
2015                    mapping: YubiHsm2UserMapping::Signing {
2016                        authentication_key_id: "5".parse()?,
2017                        signing_key_id: "1".parse()?,
2018                        key_setup: SigningKeySetup::new(
2019                            KeyType::Curve25519,
2020                            vec![KeyMechanism::EdDsaSignature],
2021                            None,
2022                            SignatureType::EdDsa,
2023                            CryptographicKeyContext::OpenPgp {
2024                                user_ids: OpenPgpUserIdList::new(vec![
2025                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2026                                ])?,
2027                                version: "v4".parse()?,
2028                            },
2029                        )?,
2030                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2031                        system_user: "yubihsm2-signing-user".parse()?,
2032                        domain: Domain::One,
2033                    }
2034                },
2035            ],
2036        )]
2037        fn config_user_backend_connections(
2038            default_config: TestResult<Config>,
2039            #[case] filter: UserBackendConnectionFilter,
2040            #[case] expected_connections: Vec<UserBackendConnection>,
2041        ) -> TestResult {
2042            let config = default_config?;
2043
2044            assert_eq!(
2045                expected_connections,
2046                config.user_backend_connections(filter)
2047            );
2048
2049            Ok(())
2050        }
2051
2052        /// Ensures, that a [`Config`] object leads to a specific YAML output.
2053        ///
2054        /// In this particular case, a [`SystemConfig`] and a [`YubiHsm2Config`] object are present.
2055        #[rstest]
2056        fn config_to_yaml_string(
2057            default_system_config: TestResult<SystemConfig>,
2058            default_yubihsm2_config: TestResult<YubiHsm2Config>,
2059        ) -> TestResult {
2060            let config = ConfigBuilder::new(default_system_config?)
2061                .set_yubihsm2_config(default_yubihsm2_config?)
2062                .finish()?;
2063            let config_str = config.to_yaml_string()?;
2064
2065            with_settings!({
2066                description => "Configuration with system-wide and YubiHSM2 configuration",
2067                snapshot_path => SNAPSHOT_PATH,
2068                prepend_module_to_snapshot => false,
2069            }, {
2070                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
2071            });
2072
2073            Ok(())
2074        }
2075
2076        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
2077        /// correctly.
2078        #[rstest]
2079        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
2080            let config = default_config?;
2081            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
2082                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
2083                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2084                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
2085                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2086                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2087                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2088                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2089            ]);
2090
2091            assert_eq!(
2092                config.authorized_key_entries(),
2093                expected.iter().collect::<HashSet<_>>()
2094            );
2095            Ok(())
2096        }
2097
2098        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
2099        #[rstest]
2100        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
2101            let config = default_config?;
2102            let expected: HashSet<SystemUserId> = HashSet::from_iter([
2103                "share-holder1".parse()?,
2104                "share-holder2".parse()?,
2105                "share-holder3".parse()?,
2106                "wireguard-downloader".parse()?,
2107                "yubihsm2-metrics-user".parse()?,
2108                "yubihsm2-backup-user".parse()?,
2109                "yubihsm2-hermetic-metrics-user".parse()?,
2110                "yubihsm2-signing-user".parse()?,
2111            ]);
2112
2113            assert_eq!(
2114                config.system_user_ids(),
2115                expected.iter().collect::<HashSet<_>>()
2116            );
2117            Ok(())
2118        }
2119
2120        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
2121        /// the same YAML string.
2122        ///
2123        /// The configuration file describes a [`SystemConfig`] and a [`YubiHsm2Config`] object.
2124        #[rstest]
2125        #[cfg(not(feature = "_yubihsm2-mockhsm"))]
2126        fn roundtrip_yaml_config(
2127            #[files("../fixtures/config/yubihsm2_backend/*.yaml")] path: PathBuf,
2128        ) -> TestResult {
2129            let config_string = read_to_string(&path)?;
2130            let config = Config::from_file_path(&path)?;
2131
2132            assert_eq!(config.to_yaml_string()?, config_string);
2133
2134            Ok(())
2135        }
2136
2137        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
2138        /// the same YAML string.
2139        ///
2140        /// The configuration file describes a [`SystemConfig`] and a [`YubiHsm2Config`] object.
2141        #[rstest]
2142        #[cfg(feature = "_yubihsm2-mockhsm")]
2143        fn roundtrip_yaml_config_mockhsm(
2144            #[files("../fixtures/config/yubihsm2_mockhsm_backend/*.yaml")] path: PathBuf,
2145        ) -> TestResult {
2146            let config_string = read_to_string(&path)?;
2147            let config = Config::from_file_path(&path)?;
2148
2149            assert_eq!(config.to_yaml_string()?, config_string);
2150
2151            Ok(())
2152        }
2153
2154        /// Ensures, that [`AdministrativeSecretHandling`] and
2155        /// [`NonAdministrativeSecretHandling`]can be retrieved from a
2156        /// [`UserBackendConnection`].
2157        #[rstest]
2158        fn user_backend_connection_secret_handling(
2159            default_config: TestResult<Config>,
2160        ) -> TestResult {
2161            let config = default_config?;
2162            let admin_secret_handling = AdministrativeSecretHandling::ShamirsSecretSharing {
2163                number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2164                threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2165            };
2166            let non_admin_secret_handling = NonAdministrativeSecretHandling::SystemdCreds;
2167
2168            let user_backend_connection = config
2169                .user_backend_connection(&"yubihsm2-signing-user".parse()?)
2170                .expect("there to be a mapping of the requested name");
2171
2172            assert_eq!(
2173                user_backend_connection.admin_secret_handling(),
2174                admin_secret_handling
2175            );
2176            assert_eq!(
2177                user_backend_connection.non_admin_secret_handling(),
2178                non_admin_secret_handling
2179            );
2180
2181            Ok(())
2182        }
2183    }
2184
2185    /// Tests, that are only available when using all available backends.
2186    #[cfg(all(feature = "nethsm", feature = "yubihsm2"))]
2187    mod all_backends {
2188        use pretty_assertions::assert_eq;
2189
2190        use super::*;
2191
2192        /// Creates a default [`Config`] for testing purposes.
2193        #[fixture]
2194        fn default_config(
2195            default_system_config: TestResult<SystemConfig>,
2196            default_nethsm_config: TestResult<NetHsmConfig>,
2197            default_yubihsm2_config: TestResult<YubiHsm2Config>,
2198        ) -> TestResult<Config> {
2199            Ok(ConfigBuilder::new(default_system_config?)
2200                .set_nethsm_config(default_nethsm_config?)
2201                .set_yubihsm2_config(default_yubihsm2_config?)
2202                .finish()?)
2203        }
2204
2205        /// Ensures, that [`ConfigBuilder::finish`] fails on issues with overlapping data in
2206        /// configuration components.
2207        ///
2208        /// Here, custom [`NetHsmConfig`] and [`YubiHsm2Config`] objects are staged together with a
2209        /// default [`SystemConfig`] (created by [`default_system_config`]) to create a failure
2210        /// scenario.
2211        #[rstest]
2212        #[case::backend_overlap_duplicate_system_users_two_duplicate_ssh_public_keys(
2213            "Configuration with system-wide, NetHSM and YubiHSM2 configuration has two duplicate system users and two duplicate SSH public keys in the backends",
2214            NetHsmConfig::new(
2215                BTreeSet::from_iter([
2216                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2217                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2218                ]),
2219                BTreeSet::from_iter([
2220                    NetHsmUserMapping::Admin("admin".parse()?),
2221                    NetHsmUserMapping::Backup{
2222                        backend_user: "backup".parse()?,
2223                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
2224                        system_user: "backup-user".parse()?,
2225                    },
2226                    NetHsmUserMapping::HermeticMetrics {
2227                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2228                        system_user: "nethsm-hermetic-metrics-user".parse()?,
2229                    },
2230                    NetHsmUserMapping::Metrics {
2231                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2232                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
2233                        system_user: "metrics-user".parse()?,
2234                    },
2235                    NetHsmUserMapping::Signing {
2236                        backend_user: "signing".parse()?,
2237                        signing_key_id: "signing1".parse()?,
2238                        key_setup: SigningKeySetup::new(
2239                            KeyType::Curve25519,
2240                            vec![KeyMechanism::EdDsaSignature],
2241                            None,
2242                            SignatureType::EdDsa,
2243                            CryptographicKeyContext::OpenPgp {
2244                                user_ids: OpenPgpUserIdList::new(vec![
2245                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2246                                ])?,
2247                                version: "v4".parse()?,
2248                            },
2249                        )?,
2250                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
2251                        system_user: "nethsm-signing-user".parse()?,
2252                        tag: "nethsm-signing1".to_string(),
2253                    }
2254                ]),
2255            )?,
2256            YubiHsm2Config::new(
2257                BTreeSet::from_iter([
2258                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2259                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2260                ]),
2261                BTreeSet::from_iter([
2262                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2263                    YubiHsm2UserMapping::AuditLog {
2264                        authentication_key_id: "3".parse()?,
2265                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
2266                        system_user: "metrics-user".parse()?,
2267                    },
2268                    YubiHsm2UserMapping::Backup {
2269                        authentication_key_id: "2".parse()?,
2270                        wrapping_key_id: "2".parse()?,
2271                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
2272                        system_user: "backup-user".parse()?,
2273                    },
2274                    YubiHsm2UserMapping::HermeticAuditLog {
2275                        authentication_key_id: "4".parse()?,
2276                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2277                    },
2278                    YubiHsm2UserMapping::Signing {
2279                        authentication_key_id: "5".parse()?,
2280                        key_setup: SigningKeySetup::new(
2281                            KeyType::Curve25519,
2282                            vec![KeyMechanism::EdDsaSignature],
2283                            None,
2284                            SignatureType::EdDsa,
2285                            CryptographicKeyContext::OpenPgp {
2286                                user_ids: OpenPgpUserIdList::new(vec![
2287                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2288                                ])?,
2289                                version: "v4".parse()?,
2290                            },
2291                        )?,
2292                        signing_key_id: "1".parse()?,
2293                        domain: Domain::One,
2294                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2295                        system_user: "yubihsm2-signing-user".parse()? }
2296                ]),
2297            )?,
2298        )]
2299        #[case::backend_overlap_one_duplicate_system_user(
2300            "Configuration with system-wide, NetHSM and YubiHSM2 configuration has one duplicate system user in the backends",
2301            NetHsmConfig::new(
2302                BTreeSet::from_iter([
2303                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2304                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2305                ]),
2306                BTreeSet::from_iter([
2307                    NetHsmUserMapping::Admin("admin".parse()?),
2308                    NetHsmUserMapping::Backup{
2309                        backend_user: "backup".parse()?,
2310                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
2311                        system_user: "backup-user".parse()?,
2312                    },
2313                    NetHsmUserMapping::HermeticMetrics {
2314                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2315                        system_user: "nethsm-hermetic-metrics-user".parse()?,
2316                    },
2317                    NetHsmUserMapping::Metrics {
2318                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2319                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
2320                        system_user: "nethsm-metrics-user".parse()?,
2321                    },
2322                    NetHsmUserMapping::Signing {
2323                        backend_user: "signing".parse()?,
2324                        signing_key_id: "signing1".parse()?,
2325                        key_setup: SigningKeySetup::new(
2326                            KeyType::Curve25519,
2327                            vec![KeyMechanism::EdDsaSignature],
2328                            None,
2329                            SignatureType::EdDsa,
2330                            CryptographicKeyContext::OpenPgp {
2331                                user_ids: OpenPgpUserIdList::new(vec![
2332                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2333                                ])?,
2334                                version: "v4".parse()?,
2335                            },
2336                        )?,
2337                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
2338                        system_user: "nethsm-signing-user".parse()?,
2339                        tag: "nethsm-signing1".to_string(),
2340                    }
2341                ]),
2342            )?,
2343            YubiHsm2Config::new(
2344                BTreeSet::from_iter([
2345                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2346                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2347                ]),
2348                BTreeSet::from_iter([
2349                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2350                    YubiHsm2UserMapping::AuditLog {
2351                        authentication_key_id: "3".parse()?,
2352                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2353                        system_user: "yubihsm2-metrics-user".parse()?,
2354                    },
2355                    YubiHsm2UserMapping::Backup {
2356                        authentication_key_id: "2".parse()?,
2357                        wrapping_key_id: "2".parse()?,
2358                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2359                        system_user: "backup-user".parse()?,
2360                    },
2361                    YubiHsm2UserMapping::HermeticAuditLog {
2362                        authentication_key_id: "4".parse()?,
2363                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2364                    },
2365                    YubiHsm2UserMapping::Signing {
2366                        authentication_key_id: "5".parse()?,
2367                        key_setup: SigningKeySetup::new(
2368                            KeyType::Curve25519,
2369                            vec![KeyMechanism::EdDsaSignature],
2370                            None,
2371                            SignatureType::EdDsa,
2372                            CryptographicKeyContext::OpenPgp {
2373                                user_ids: OpenPgpUserIdList::new(vec![
2374                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2375                                ])?,
2376                                version: "v4".parse()?,
2377                            },
2378                        )?,
2379                        signing_key_id: "1".parse()?,
2380                        domain: Domain::One,
2381                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2382                        system_user: "yubihsm2-signing-user".parse()? }
2383                ]),
2384            )?,
2385        )]
2386        #[case::system_overlap_duplicate_system_users_two_duplicate_ssh_public_keys(
2387            "Configuration with system-wide, NetHSM and YubiHSM2 configuration has two duplicate system users and two duplicate SSH public keys in the system and the backends",
2388            NetHsmConfig::new(
2389                BTreeSet::from_iter([
2390                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2391                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2392                ]),
2393                BTreeSet::from_iter([
2394                    NetHsmUserMapping::Admin("admin".parse()?),
2395                    NetHsmUserMapping::Backup{
2396                        backend_user: "backup".parse()?,
2397                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
2398                        system_user: "share-holder1".parse()?,
2399                    },
2400                    NetHsmUserMapping::Metrics {
2401                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2402                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2403                        system_user: "share-holder2".parse()?,
2404                    },
2405                    NetHsmUserMapping::HermeticMetrics {
2406                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2407                        system_user: "nethsm-hermetic-metrics-user".parse()?,
2408                    },
2409                    NetHsmUserMapping::Signing {
2410                        backend_user: "signing".parse()?,
2411                        signing_key_id: "signing1".parse()?,
2412                        key_setup: SigningKeySetup::new(
2413                            KeyType::Curve25519,
2414                            vec![KeyMechanism::EdDsaSignature],
2415                            None,
2416                            SignatureType::EdDsa,
2417                            CryptographicKeyContext::OpenPgp {
2418                                user_ids: OpenPgpUserIdList::new(vec![
2419                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2420                                ])?,
2421                                version: "v4".parse()?,
2422                            },
2423                        )?,
2424                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
2425                        system_user: "nethsm-signing-user".parse()?,
2426                        tag: "nethsm-signing1".to_string(),
2427                    }
2428                ]),
2429            )?,
2430            YubiHsm2Config::new(
2431                BTreeSet::from_iter([
2432                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2433                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2434                ]),
2435                BTreeSet::from_iter([
2436                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2437                    YubiHsm2UserMapping::Backup {
2438                        authentication_key_id: "2".parse()?,
2439                        wrapping_key_id: "2".parse()?,
2440                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
2441                        system_user: "share-holder1".parse()?,
2442                    },
2443                    YubiHsm2UserMapping::AuditLog {
2444                        authentication_key_id: "3".parse()?,
2445                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2446                        system_user: "share-holder2".parse()?,
2447                    },
2448                    YubiHsm2UserMapping::HermeticAuditLog {
2449                        authentication_key_id: "4".parse()?,
2450                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2451                    },
2452                    YubiHsm2UserMapping::Signing {
2453                        authentication_key_id: "5".parse()?,
2454                        key_setup: SigningKeySetup::new(
2455                            KeyType::Curve25519,
2456                            vec![KeyMechanism::EdDsaSignature],
2457                            None,
2458                            SignatureType::EdDsa,
2459                            CryptographicKeyContext::OpenPgp {
2460                                user_ids: OpenPgpUserIdList::new(vec![
2461                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2462                                ])?,
2463                                version: "v4".parse()?,
2464                            },
2465                        )?,
2466                        signing_key_id: "1".parse()?,
2467                        domain: Domain::One,
2468                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2469                        system_user: "yubihsm2-signing-user".parse()? }
2470                ]),
2471            )?,
2472        )]
2473        fn config_fails_validation(
2474            default_system_config: TestResult<SystemConfig>,
2475            #[case] description: &str,
2476            #[case] nethsm_config: NetHsmConfig,
2477            #[case] yubihsm2_config: YubiHsm2Config,
2478        ) -> TestResult {
2479            let error_message = match ConfigBuilder::new(default_system_config?)
2480                .set_nethsm_config(nethsm_config)
2481                .set_yubihsm2_config(yubihsm2_config)
2482                .finish()
2483            {
2484                Err(error) => error.to_string(),
2485                Ok(config) => panic!(
2486                    "Expected to fail with Error::Validation, but succeeded instead: {}",
2487                    config.to_yaml_string()?
2488                ),
2489            };
2490
2491            with_settings!({
2492                description => description,
2493                snapshot_path => SNAPSHOT_PATH,
2494                prepend_module_to_snapshot => false,
2495            }, {
2496                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_message);
2497            });
2498
2499            Ok(())
2500        }
2501
2502        /// Ensures, that an optional [`UserBackendConnection`] can be retrieved from a [`Config`].
2503        #[rstest]
2504        #[case::nethsm_signing(
2505            "nethsm-signing-user",
2506            Some(UserBackendConnection::NetHsm {
2507                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2508                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2509                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2510                },
2511                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2512                connections: BTreeSet::from_iter([
2513                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2514                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2515                ]),
2516                mapping: NetHsmUserMapping::Signing {
2517                    backend_user: "signing".parse()?,
2518                    signing_key_id: "signing1".parse()?,
2519                    key_setup: SigningKeySetup::new(
2520                        KeyType::Curve25519,
2521                        vec![KeyMechanism::EdDsaSignature],
2522                        None,
2523                        SignatureType::EdDsa,
2524                        CryptographicKeyContext::OpenPgp {
2525                            user_ids: OpenPgpUserIdList::new(vec![
2526                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2527                            ])?,
2528                            version: "v4".parse()?,
2529                        },
2530                    )?,
2531                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
2532                    system_user: "nethsm-signing-user".parse()?,
2533                    tag: "signing1".to_string(),
2534                }
2535            })
2536        )]
2537        #[case::yubihsm2_signing(
2538            "yubihsm2-signing-user",
2539            Some(UserBackendConnection::YubiHsm2 {
2540                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2541                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2542                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2543                },
2544                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2545                connections: BTreeSet::from_iter([
2546                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2547                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2548                ]),
2549                mapping: YubiHsm2UserMapping::Signing {
2550                    authentication_key_id: "5".parse()?,
2551                    signing_key_id: "1".parse()?,
2552                    key_setup: SigningKeySetup::new(
2553                        KeyType::Curve25519,
2554                        vec![KeyMechanism::EdDsaSignature],
2555                        None,
2556                        SignatureType::EdDsa,
2557                        CryptographicKeyContext::OpenPgp {
2558                            user_ids: OpenPgpUserIdList::new(vec![
2559                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2560                            ])?,
2561                            version: "v4".parse()?,
2562                        },
2563                    )?,
2564                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2565                    system_user: "yubihsm2-signing-user".parse()?,
2566                    domain: Domain::One,
2567                }
2568            })
2569        )]
2570        #[case::none("foo", None)]
2571        fn config_user_backend_connection(
2572            default_config: TestResult<Config>,
2573            #[case] system_user: &str,
2574            #[case] expected_connection: Option<UserBackendConnection>,
2575        ) -> TestResult {
2576            let config = default_config?;
2577            assert_eq!(
2578                expected_connection,
2579                config.user_backend_connection(&system_user.parse()?)
2580            );
2581
2582            Ok(())
2583        }
2584
2585        /// Ensures, that [`Config::user_backend_connections`] returns the correct list of
2586        /// [`UserBackendConnection`] items according to a [`UserBackendConnectionFilter`].
2587        #[rstest]
2588        #[case::filter_all(
2589            UserBackendConnectionFilter::All,
2590            vec![
2591                UserBackendConnection::NetHsm {
2592                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2593                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2594                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2595                    },
2596                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2597                    connections: BTreeSet::from_iter([
2598                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2599                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2600                    ]),
2601                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
2602                },
2603                UserBackendConnection::NetHsm {
2604                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2605                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2606                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2607                    },
2608                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2609                    connections: BTreeSet::from_iter([
2610                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2611                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2612                    ]),
2613                    mapping: NetHsmUserMapping::Backup{
2614                        backend_user: "backup".parse()?,
2615                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
2616                        system_user: "nethsm-backup-user".parse()?,
2617                    }
2618                },
2619                UserBackendConnection::NetHsm {
2620                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2621                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2622                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2623                    },
2624                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2625                    connections: BTreeSet::from_iter([
2626                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2627                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2628                    ]),
2629                    mapping: NetHsmUserMapping::HermeticMetrics {
2630                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2631                        system_user: "nethsm-hermetic-metrics-user".parse()?,
2632                    }
2633                },
2634                UserBackendConnection::NetHsm {
2635                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2636                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2637                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2638                    },
2639                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2640                    connections: BTreeSet::from_iter([
2641                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2642                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2643                    ]),
2644                    mapping: NetHsmUserMapping::Metrics {
2645                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2646                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
2647                        system_user: "nethsm-metrics-user".parse()?,
2648                    }
2649                },
2650                UserBackendConnection::NetHsm {
2651                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2652                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2653                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2654                    },
2655                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2656                    connections: BTreeSet::from_iter([
2657                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2658                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2659                    ]),
2660                    mapping: NetHsmUserMapping::Signing {
2661                        backend_user: "signing".parse()?,
2662                        signing_key_id: "signing1".parse()?,
2663                        key_setup: SigningKeySetup::new(
2664                            KeyType::Curve25519,
2665                            vec![KeyMechanism::EdDsaSignature],
2666                            None,
2667                            SignatureType::EdDsa,
2668                            CryptographicKeyContext::OpenPgp {
2669                                user_ids: OpenPgpUserIdList::new(vec![
2670                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2671                                ])?,
2672                                version: "v4".parse()?,
2673                            },
2674                        )?,
2675                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
2676                        system_user: "nethsm-signing-user".parse()?,
2677                        tag: "signing1".to_string(),
2678                    }
2679                },
2680                UserBackendConnection::YubiHsm2 {
2681                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2682                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2683                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2684                    },
2685                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2686                    connections: BTreeSet::from_iter([
2687                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2688                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2689                    ]),
2690                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2691                },
2692                UserBackendConnection::YubiHsm2 {
2693                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2694                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2695                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2696                    },
2697                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2698                    connections: BTreeSet::from_iter([
2699                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2700                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2701                    ]),
2702                    mapping: YubiHsm2UserMapping::AuditLog {
2703                        authentication_key_id: "3".parse()?,
2704                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2705                        system_user: "yubihsm2-metrics-user".parse()?,
2706                    },
2707                },
2708                UserBackendConnection::YubiHsm2 {
2709                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2710                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2711                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2712                    },
2713                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2714                    connections: BTreeSet::from_iter([
2715                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2716                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2717                    ]),
2718                    mapping: YubiHsm2UserMapping::Backup{
2719                        authentication_key_id: "2".parse()?,
2720                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2721                        system_user: "yubihsm2-backup-user".parse()?,
2722                        wrapping_key_id: "1".parse()?,
2723                    },
2724                },
2725                UserBackendConnection::YubiHsm2 {
2726                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2727                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2728                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2729                    },
2730                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2731                    connections: BTreeSet::from_iter([
2732                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2733                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2734                    ]),
2735                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
2736                        authentication_key_id: "4".parse()?,
2737                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2738                    },
2739                },
2740                UserBackendConnection::YubiHsm2 {
2741                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2742                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2743                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2744                    },
2745                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2746                    connections: BTreeSet::from_iter([
2747                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2748                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2749                    ]),
2750                    mapping: YubiHsm2UserMapping::Signing {
2751                        authentication_key_id: "5".parse()?,
2752                        signing_key_id: "1".parse()?,
2753                        key_setup: SigningKeySetup::new(
2754                            KeyType::Curve25519,
2755                            vec![KeyMechanism::EdDsaSignature],
2756                            None,
2757                            SignatureType::EdDsa,
2758                            CryptographicKeyContext::OpenPgp {
2759                                user_ids: OpenPgpUserIdList::new(vec![
2760                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2761                                ])?,
2762                                version: "v4".parse()?,
2763                            },
2764                        )?,
2765                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2766                        system_user: "yubihsm2-signing-user".parse()?,
2767                        domain: Domain::One,
2768                    }
2769                },
2770            ],
2771        )]
2772        #[case::filter_admin(
2773            UserBackendConnectionFilter::Admin,
2774            vec![
2775                UserBackendConnection::NetHsm {
2776                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2777                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2778                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2779                    },
2780                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2781                    connections: BTreeSet::from_iter([
2782                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2783                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2784                    ]),
2785                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
2786                },
2787                UserBackendConnection::YubiHsm2 {
2788                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2789                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2790                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2791                    },
2792                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2793                    connections: BTreeSet::from_iter([
2794                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2795                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2796                    ]),
2797                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2798                },
2799            ],
2800        )]
2801        #[case::filter_non_admin(
2802            UserBackendConnectionFilter::NonAdmin,
2803            vec![
2804                UserBackendConnection::NetHsm {
2805                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2806                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2807                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2808                    },
2809                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2810                    connections: BTreeSet::from_iter([
2811                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2812                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2813                    ]),
2814                    mapping: NetHsmUserMapping::Backup{
2815                        backend_user: "backup".parse()?,
2816                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
2817                        system_user: "nethsm-backup-user".parse()?,
2818                    }
2819                },
2820                UserBackendConnection::NetHsm {
2821                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2822                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2823                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2824                    },
2825                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2826                    connections: BTreeSet::from_iter([
2827                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2828                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2829                    ]),
2830                    mapping: NetHsmUserMapping::HermeticMetrics {
2831                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2832                        system_user: "nethsm-hermetic-metrics-user".parse()?,
2833                    }
2834                },
2835                UserBackendConnection::NetHsm {
2836                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2837                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2838                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2839                    },
2840                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2841                    connections: BTreeSet::from_iter([
2842                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2843                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2844                    ]),
2845                    mapping: NetHsmUserMapping::Metrics {
2846                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2847                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
2848                        system_user: "nethsm-metrics-user".parse()?,
2849                    }
2850                },
2851                UserBackendConnection::NetHsm {
2852                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2853                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2854                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2855                    },
2856                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2857                    connections: BTreeSet::from_iter([
2858                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2859                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2860                    ]),
2861                    mapping: NetHsmUserMapping::Signing {
2862                        backend_user: "signing".parse()?,
2863                        signing_key_id: "signing1".parse()?,
2864                        key_setup: SigningKeySetup::new(
2865                            KeyType::Curve25519,
2866                            vec![KeyMechanism::EdDsaSignature],
2867                            None,
2868                            SignatureType::EdDsa,
2869                            CryptographicKeyContext::OpenPgp {
2870                                user_ids: OpenPgpUserIdList::new(vec![
2871                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2872                                ])?,
2873                                version: "v4".parse()?,
2874                            },
2875                        )?,
2876                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
2877                        system_user: "nethsm-signing-user".parse()?,
2878                        tag: "signing1".to_string(),
2879                    }
2880                },
2881                UserBackendConnection::YubiHsm2 {
2882                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2883                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2884                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2885                    },
2886                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2887                    connections: BTreeSet::from_iter([
2888                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2889                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2890                    ]),
2891                    mapping: YubiHsm2UserMapping::AuditLog {
2892                        authentication_key_id: "3".parse()?,
2893                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2894                        system_user: "yubihsm2-metrics-user".parse()?,
2895                    },
2896                },
2897                UserBackendConnection::YubiHsm2 {
2898                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2899                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2900                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2901                    },
2902                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2903                    connections: BTreeSet::from_iter([
2904                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2905                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2906                    ]),
2907                    mapping: YubiHsm2UserMapping::Backup{
2908                        authentication_key_id: "2".parse()?,
2909                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2910                        system_user: "yubihsm2-backup-user".parse()?,
2911                        wrapping_key_id: "1".parse()?,
2912                    },
2913                },
2914                UserBackendConnection::YubiHsm2 {
2915                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2916                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2917                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2918                    },
2919                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2920                    connections: BTreeSet::from_iter([
2921                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2922                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2923                    ]),
2924                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
2925                        authentication_key_id: "4".parse()?,
2926                        system_user: "yubihsm2-hermetic-metrics-user".parse()?,
2927                    },
2928                },
2929                UserBackendConnection::YubiHsm2 {
2930                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2931                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2932                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2933                    },
2934                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2935                    connections: BTreeSet::from_iter([
2936                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2937                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2938                    ]),
2939                    mapping: YubiHsm2UserMapping::Signing {
2940                        authentication_key_id: "5".parse()?,
2941                        signing_key_id: "1".parse()?,
2942                        key_setup: SigningKeySetup::new(
2943                            KeyType::Curve25519,
2944                            vec![KeyMechanism::EdDsaSignature],
2945                            None,
2946                            SignatureType::EdDsa,
2947                            CryptographicKeyContext::OpenPgp {
2948                                user_ids: OpenPgpUserIdList::new(vec![
2949                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2950                                ])?,
2951                                version: "v4".parse()?,
2952                            },
2953                        )?,
2954                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2955                        system_user: "yubihsm2-signing-user".parse()?,
2956                        domain: Domain::One,
2957                    }
2958                },
2959            ],
2960        )]
2961        fn config_user_backend_connections(
2962            default_config: TestResult<Config>,
2963            #[case] filter: UserBackendConnectionFilter,
2964            #[case] expected_connections: Vec<UserBackendConnection>,
2965        ) -> TestResult {
2966            let config = default_config?;
2967
2968            assert_eq!(
2969                expected_connections,
2970                config.user_backend_connections(filter)
2971            );
2972
2973            Ok(())
2974        }
2975
2976        /// Ensures, that a [`Config`] object leads to a specific YAML output.
2977        ///
2978        /// In this particular case, a [`SystemConfig`], a [`NetHsmConfig`] and a [`YubiHsm2Config`]
2979        /// object are present.
2980        #[rstest]
2981        fn config_to_yaml_string(
2982            default_system_config: TestResult<SystemConfig>,
2983            default_nethsm_config: TestResult<NetHsmConfig>,
2984            default_yubihsm2_config: TestResult<YubiHsm2Config>,
2985        ) -> TestResult {
2986            let config = ConfigBuilder::new(default_system_config?)
2987                .set_nethsm_config(default_nethsm_config?)
2988                .set_yubihsm2_config(default_yubihsm2_config?)
2989                .finish()?;
2990            let config_str = config.to_yaml_string()?;
2991
2992            with_settings!({
2993                description => "Configuration with system-wide, NetHSM and YubiHSM2 configuration",
2994                snapshot_path => SNAPSHOT_PATH,
2995                prepend_module_to_snapshot => false,
2996            }, {
2997                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
2998            });
2999
3000            Ok(())
3001        }
3002
3003        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
3004        /// correctly.
3005        #[rstest]
3006        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
3007            let config = default_config?;
3008            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
3009                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
3010                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
3011                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
3012                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
3013                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
3014                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
3015                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3016                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
3017                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
3018                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3019            ]);
3020
3021            assert_eq!(
3022                config.authorized_key_entries(),
3023                expected.iter().collect::<HashSet<_>>()
3024            );
3025            Ok(())
3026        }
3027
3028        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
3029        #[rstest]
3030        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
3031            let config = default_config?;
3032            let expected: HashSet<SystemUserId> = HashSet::from_iter([
3033                "share-holder1".parse()?,
3034                "share-holder2".parse()?,
3035                "share-holder3".parse()?,
3036                "wireguard-downloader".parse()?,
3037                "nethsm-backup-user".parse()?,
3038                "nethsm-hermetic-metrics-user".parse()?,
3039                "nethsm-metrics-user".parse()?,
3040                "nethsm-signing-user".parse()?,
3041                "yubihsm2-metrics-user".parse()?,
3042                "yubihsm2-backup-user".parse()?,
3043                "yubihsm2-hermetic-metrics-user".parse()?,
3044                "yubihsm2-signing-user".parse()?,
3045            ]);
3046
3047            assert_eq!(
3048                config.system_user_ids(),
3049                expected.iter().collect::<HashSet<_>>()
3050            );
3051            Ok(())
3052        }
3053
3054        /// Create a [`Config`] using [`ConfigBuilder`].
3055        #[rstest]
3056        fn config_builder_new(
3057            default_system_config: TestResult<SystemConfig>,
3058            default_nethsm_config: TestResult<NetHsmConfig>,
3059            default_yubihsm2_config: TestResult<YubiHsm2Config>,
3060        ) -> TestResult {
3061            let _config = ConfigBuilder::new(default_system_config?)
3062                .set_nethsm_config(default_nethsm_config?)
3063                .set_yubihsm2_config(default_yubihsm2_config?)
3064                .finish()?;
3065
3066            Ok(())
3067        }
3068
3069        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
3070        /// the same YAML string.
3071        ///
3072        /// The configuration file describes a [`SystemConfig`], [`NetHsmConfig`] and a
3073        /// [`YubiHsm2Config`] object.
3074        #[rstest]
3075        fn roundtrip_yaml_config(
3076            #[files("../fixtures/config/all_backends/*.yaml")] path: PathBuf,
3077        ) -> TestResult {
3078            let config_string = read_to_string(&path)?;
3079            let config = Config::from_file_path(&path)?;
3080
3081            assert_eq!(config.to_yaml_string()?, config_string);
3082
3083            Ok(())
3084        }
3085
3086        /// Ensures, that [`AdministrativeSecretHandling`] and
3087        /// [`NonAdministrativeSecretHandling`]can be retrieved from a
3088        /// [`UserBackendConnection`].
3089        #[rstest]
3090        fn user_backend_connection_secret_handling(
3091            default_config: TestResult<Config>,
3092        ) -> TestResult {
3093            let config = default_config?;
3094            let admin_secret_handling = AdministrativeSecretHandling::ShamirsSecretSharing {
3095                number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3096                threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3097            };
3098            let non_admin_secret_handling = NonAdministrativeSecretHandling::SystemdCreds;
3099
3100            for user in ["nethsm-signing-user", "yubihsm2-signing-user"] {
3101                let user_backend_connection = config
3102                    .user_backend_connection(&user.parse()?)
3103                    .expect("there to be a mapping of the requested name");
3104
3105                assert_eq!(
3106                    user_backend_connection.admin_secret_handling(),
3107                    admin_secret_handling
3108                );
3109                assert_eq!(
3110                    user_backend_connection.non_admin_secret_handling(),
3111                    non_admin_secret_handling
3112                );
3113            }
3114
3115            Ok(())
3116        }
3117    }
3118
3119    /// Tests, that are only available when using no backends.
3120    #[cfg(not(all(feature = "nethsm", feature = "yubihsm2")))]
3121    mod no_backends {
3122        use pretty_assertions::assert_eq;
3123
3124        use super::*;
3125
3126        /// Create a [`Config`] using [`ConfigBuilder`].
3127        #[rstest]
3128        fn config_builder_new(default_system_config: TestResult<SystemConfig>) -> TestResult {
3129            let _config = ConfigBuilder::new(default_system_config?).finish()?;
3130
3131            Ok(())
3132        }
3133
3134        /// Ensures that a reference to the [`SystemConfig`] can be retrieved from [`Config`].
3135        #[rstest]
3136        fn config_system(default_system_config: TestResult<SystemConfig>) -> TestResult {
3137            let system_config = default_system_config?;
3138            let config = ConfigBuilder::new(system_config.clone()).finish()?;
3139            assert_eq!(config.system(), &system_config);
3140
3141            Ok(())
3142        }
3143
3144        /// Ensures, that a [`Config`] object leads to a specific YAML output.
3145        ///
3146        /// In this particular case, only a [`SystemConfig`] object are present.
3147        #[rstest]
3148        fn config_to_yaml_string(default_system_config: TestResult<SystemConfig>) -> TestResult {
3149            let config = ConfigBuilder::new(default_system_config?).finish()?;
3150            let config_str = config.to_yaml_string()?;
3151
3152            with_settings!({
3153                description => "Configuration with system-wide, NetHSM and YubiHSM2 configuration",
3154                snapshot_path => SNAPSHOT_PATH,
3155                prepend_module_to_snapshot => false,
3156            }, {
3157                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
3158            });
3159
3160            Ok(())
3161        }
3162
3163        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
3164        /// the same YAML string.
3165        ///
3166        /// The configuration file only describes a [`SystemConfig`] object.
3167        #[rstest]
3168        fn roundtrip_yaml_config(
3169            #[files("../fixtures/config/no_backend/*.yaml")] path: PathBuf,
3170        ) -> TestResult {
3171            let config_string = read_to_string(&path)?;
3172            let config = Config::from_file_path(&path)?;
3173
3174            assert_eq!(config.to_yaml_string()?, config_string);
3175
3176            Ok(())
3177        }
3178    }
3179}