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