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: "signstar-share-holder1".parse()?,
629                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
630                },
631                SystemUserMapping::ShareHolder {
632                    system_user: "signstar-share-holder2".parse()?,
633                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
634                },
635                SystemUserMapping::ShareHolder {
636                    system_user: "signstar-share-holder3".parse()?,
637                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?
638                },
639                SystemUserMapping::WireguardDownload {
640                    system_user: "signstar-wireguard-download".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                    "signstar-share-holder1".parse()?,
654                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?),
655                ),
656                (
657                    "signstar-share-holder2".parse()?,
658                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?),
659                ),
660                (
661                    "signstar-share-holder3".parse()?,
662                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?),
663                ),
664                (
665                    "signstar-wireguard-download".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".parse()?,
686                },
687                NetHsmUserMapping::HermeticMetrics {
688                    backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
689                    system_user: "nethsm-hermetic-metrics".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".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".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".parse()?,
732                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?),
733                ),
734                (
735                    "nethsm-hermetic-metrics".parse()?,
736                    None,
737                ),
738                (
739                    "nethsm-metrics".parse()?,
740                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?),
741                ),
742                (
743                    "nethsm-signing".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-audit-log".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".parse()?,
769                },
770                YubiHsm2UserMapping::HermeticAuditLog {
771                    authentication_key_id: "4".parse()?,
772                    system_user: "yubihsm2-hermetic-audit-log".parse()?,
773                },
774                YubiHsm2UserMapping::Signing {
775                    authentication_key_id: "5".parse()?,
776                    signing_key_id: "1".parse()?,
777                    key_setup: SigningKeySetup::new(
778                        KeyType::Curve25519,
779                        vec![KeyMechanism::EdDsaSignature],
780                        None,
781                        SignatureType::EdDsa,
782                        CryptographicKeyContext::OpenPgp {
783                            user_ids: OpenPgpUserIdList::new(vec![
784                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
785                            ])?,
786                            version: "v4".parse()?,
787                            notations: Default::default(),
788                        },
789                    )?,
790                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
791                    system_user: "yubihsm2-signing".parse()?,
792                    domain: Domain::One,
793                }
794            ]),
795        )?)
796    }
797
798    /// List of raw data required to create [`SystemUserData`] for each item in the default
799    /// [`YubiHsm2Config`].
800    #[cfg(feature = "yubihsm2")]
801    #[fixture]
802    fn raw_user_data_yubihsm2() -> TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>> {
803        Ok(vec![
804                (
805                    SystemUserId::root(),
806                    None,
807                ),
808                (
809                    "yubihsm2-audit-log".parse()?,
810                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?),
811                ),
812                (
813                    "yubihsm2-backup".parse()?,
814                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?),
815                ),
816                (
817                    "yubihsm2-hermetic-audit-log".parse()?,
818                    None,
819                ),
820                (
821                    "yubihsm2-signing".parse()?,
822                    Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?),
823                ),
824            ])
825    }
826
827    /// Ensures, that [`Config::default_system_path`] always returns the same path.
828    #[test]
829    fn config_default_system_path() {
830        assert_eq!(
831            Config::default_system_path(),
832            PathBuf::from("/usr/share/signstar/config.yaml")
833        )
834    }
835
836    /// Ensures, that [`Config::list_config_file_paths`] always returns the same list of paths.
837    #[test]
838    fn config_list_config_file_paths() {
839        assert_eq!(
840            Config::list_config_file_paths(),
841            vec![
842                PathBuf::from("/usr/share/signstar/config.yaml"),
843                PathBuf::from("/run/signstar/config.yaml"),
844                PathBuf::from("/etc/signstar/config.yaml"),
845            ]
846        )
847    }
848
849    /// Ensures, that [`Config::from_file_path`] fails on missing file extensions.
850    #[rstest]
851    fn config_from_file_path_fails_on_missing_file_extension() -> TestResult {
852        let temp_dir = TempDir::new()?;
853
854        match Config::from_file_path(temp_dir.path().join("config")) {
855            Ok(config) => panic!(
856                "Should have failed to create a Config object, but succeeded instead: {config:?}"
857            ),
858            Err(crate::Error::Config(Error::MissingFileExtension { .. })) => {}
859            Err(error) => panic!(
860                "Should have failed with a ConfigError::MissingFileExtension, but failed with a different error instead: {error}"
861            ),
862        }
863
864        Ok(())
865    }
866
867    /// Ensures, that [`Config::from_file_path`] fails on unsupported file extensions.
868    #[rstest]
869    fn config_from_file_path_fails_on_unsupported_file_extension() -> TestResult {
870        let temp_file = NamedTempFile::with_suffix(".toml")?;
871
872        match Config::from_file_path(temp_file.path()) {
873            Ok(config) => panic!(
874                "Should have failed to create a Config object, but succeeded instead: {config:?}"
875            ),
876            Err(crate::Error::Config(Error::UnsupportedFileExtension { .. })) => {}
877            Err(error) => panic!(
878                "Should have failed with a ConfigError::UnsupportedFileExtension, but failed with a different error instead: {error}"
879            ),
880        }
881
882        Ok(())
883    }
884
885    /// Tests, that are only available when using no backend.
886    #[cfg(not(any(feature = "nethsm", feature = "yubihsm2")))]
887    mod no_backend {
888        use std::collections::HashSet;
889
890        use pretty_assertions::assert_eq;
891
892        use super::*;
893        use crate::config::{
894            ConfigAuthorizedKeyEntries,
895            ConfigSystemUserIds,
896            SystemUserData,
897            traits::ConfigSystemUserData,
898        };
899
900        /// Creates a default [`Config`] for testing purposes.
901        #[fixture]
902        fn default_config(default_system_config: TestResult<SystemConfig>) -> TestResult<Config> {
903            Ok(ConfigBuilder::new(default_system_config?).finish()?)
904        }
905
906        /// Create a [`Config`] using [`ConfigBuilder`].
907        #[rstest]
908        fn config_builder_new(default_system_config: TestResult<SystemConfig>) -> TestResult {
909            let _config = ConfigBuilder::new(default_system_config?).finish()?;
910
911            Ok(())
912        }
913
914        /// Ensures that a reference to the [`SystemConfig`] can be retrieved from [`Config`].
915        #[rstest]
916        fn config_system(default_system_config: TestResult<SystemConfig>) -> TestResult {
917            let system_config = default_system_config?;
918            let config = ConfigBuilder::new(system_config.clone()).finish()?;
919            assert_eq!(config.system(), &system_config);
920
921            Ok(())
922        }
923
924        /// Ensures, that a [`Config`] object leads to a specific YAML output.
925        ///
926        /// In this particular case, only a [`SystemConfig`] object are present.
927        #[rstest]
928        fn config_to_yaml_string(default_system_config: TestResult<SystemConfig>) -> TestResult {
929            let config = ConfigBuilder::new(default_system_config?).finish()?;
930            let config_str = config.to_yaml_string()?;
931
932            with_settings!({
933                description => "Configuration with only system-wide configuration",
934                snapshot_path => SNAPSHOT_PATH,
935                prepend_module_to_snapshot => false,
936            }, {
937                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
938            });
939
940            Ok(())
941        }
942
943        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
944        /// the same YAML string.
945        ///
946        /// The configuration file only describes a [`SystemConfig`] object.
947        #[rstest]
948        fn roundtrip_yaml_config(
949            #[files("../fixtures/config/no_backend/*.yaml")] path: PathBuf,
950        ) -> TestResult {
951            let config_string = read_to_string(&path)?;
952            let config = Config::from_file_path(&path)?;
953
954            assert_eq!(config.to_yaml_string()?, config_string);
955
956            Ok(())
957        }
958
959        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
960        /// correctly.
961        #[rstest]
962        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
963            let config = default_config?;
964            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
965                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
966                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
967                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
968                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
969            ]);
970
971            assert_eq!(
972                config.authorized_key_entries(),
973                expected.iter().collect::<HashSet<_>>()
974            );
975            Ok(())
976        }
977
978        /// Ensures, that [`Config::system_user_data`] returns [`SystemUserData`] entries correctly.
979        #[rstest]
980        fn config_system_user_data(
981            default_config: TestResult<Config>,
982            raw_user_data_system: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
983        ) -> TestResult {
984            let config = default_config?;
985            let raw_user_data = raw_user_data_system?;
986            let expected: HashSet<SystemUserData> = HashSet::from_iter([
987                SystemUserData::HostShareholder {
988                    system_user: &raw_user_data[0].0,
989                    ssh_authorized_key: raw_user_data[0]
990                        .1
991                        .as_ref()
992                        .expect("to have SSH authorized key"),
993                },
994                SystemUserData::HostShareholder {
995                    system_user: &raw_user_data[1].0,
996                    ssh_authorized_key: raw_user_data[1]
997                        .1
998                        .as_ref()
999                        .expect("to have SSH authorized key"),
1000                },
1001                SystemUserData::HostShareholder {
1002                    system_user: &raw_user_data[2].0,
1003                    ssh_authorized_key: raw_user_data[2]
1004                        .1
1005                        .as_ref()
1006                        .expect("to have SSH authorized key"),
1007                },
1008                SystemUserData::HostDownloadNetworkConfig {
1009                    system_user: &raw_user_data[3].0,
1010                    ssh_authorized_key: raw_user_data[3]
1011                        .1
1012                        .as_ref()
1013                        .expect("to have SSH authorized key"),
1014                },
1015            ]);
1016
1017            assert_eq!(config.system_user_data(), expected);
1018            Ok(())
1019        }
1020
1021        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
1022        #[rstest]
1023        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
1024            let config = default_config?;
1025            let expected: HashSet<SystemUserId> = HashSet::from_iter([
1026                "signstar-share-holder1".parse()?,
1027                "signstar-share-holder2".parse()?,
1028                "signstar-share-holder3".parse()?,
1029                "signstar-wireguard-download".parse()?,
1030            ]);
1031
1032            assert_eq!(
1033                config.system_user_ids(),
1034                expected.iter().collect::<HashSet<_>>()
1035            );
1036            Ok(())
1037        }
1038
1039        /// Ensures, that [`SystemUserConfigState`] can be created from [`Config`].
1040        #[rstest]
1041        fn system_user_config_state_from_config(default_config: TestResult<Config>) -> TestResult {
1042            let config = default_config?;
1043            let state = SystemUserConfigState::from(&config);
1044
1045            assert_eq!(state.system_user_data, config.system_user_data(),);
1046            Ok(())
1047        }
1048    }
1049
1050    /// Tests, that are only available when using the NetHSM (and no other) backend.
1051    #[cfg(all(feature = "nethsm", not(feature = "yubihsm2")))]
1052    mod nethsm_backend {
1053        use pretty_assertions::assert_eq;
1054
1055        use super::*;
1056        use crate::config::{
1057            SystemUserData,
1058            traits::{ConfigSystemUserData, MappingAuthorizedKeyEntry, MappingSystemUserId},
1059        };
1060
1061        /// Creates a default [`Config`] for testing purposes.
1062        #[fixture]
1063        fn default_config(
1064            default_system_config: TestResult<SystemConfig>,
1065            default_nethsm_config: TestResult<NetHsmConfig>,
1066        ) -> TestResult<Config> {
1067            Ok(ConfigBuilder::new(default_system_config?)
1068                .set_nethsm_config(default_nethsm_config?)
1069                .finish()?)
1070        }
1071
1072        /// List of raw data required to create [`SystemUserData`] for each item in the default
1073        /// [`Config`].
1074        #[fixture]
1075        fn raw_user_data(
1076            raw_user_data_system: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1077            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1078        ) -> TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>> {
1079            let mut data = raw_user_data_system?;
1080            data.extend(raw_user_data_nethsm?);
1081            Ok(data)
1082        }
1083
1084        /// Ensures that [`MappingSystemUserId`] for [`UserBackendConnection`] works as intended.
1085        #[rstest]
1086        fn user_backend_connection_system_user_id(
1087            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1088        ) -> TestResult {
1089            let raw_user_data_nethsm = raw_user_data_nethsm?;
1090            let data = UserBackendConnection::NetHsm {
1091                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
1092                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
1093                connections: BTreeSet::from_iter([Connection::new(
1094                    "https://nethsm1.example.org/".parse()?,
1095                    ConnectionSecurity::Unsafe,
1096                )]),
1097                mapping: NetHsmUserMapping::Backup {
1098                    backend_user: "backup".parse()?,
1099                    ssh_authorized_key: raw_user_data_nethsm[1]
1100                        .1
1101                        .clone()
1102                        .expect("to have an SSH authorized key"),
1103                    system_user: raw_user_data_nethsm[1].0.clone(),
1104                },
1105            };
1106            assert_eq!(data.system_user_id(), Some(&raw_user_data_nethsm[1].0));
1107
1108            Ok(())
1109        }
1110
1111        /// Ensures that [`MappingAuthorizedKeyEntry`] for [`UserBackendConnection`] works as
1112        /// intended.
1113        #[rstest]
1114        fn user_backend_connection_authorized_key_entry(
1115            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1116        ) -> TestResult {
1117            let raw_user_data_nethsm = raw_user_data_nethsm?;
1118            let data = UserBackendConnection::NetHsm {
1119                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
1120                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
1121                connections: BTreeSet::from_iter([Connection::new(
1122                    "https://nethsm1.example.org/".parse()?,
1123                    ConnectionSecurity::Unsafe,
1124                )]),
1125                mapping: NetHsmUserMapping::Backup {
1126                    backend_user: "backup".parse()?,
1127                    ssh_authorized_key: raw_user_data_nethsm[1]
1128                        .1
1129                        .clone()
1130                        .expect("to have an SSH authorized key"),
1131                    system_user: raw_user_data_nethsm[1].0.clone(),
1132                },
1133            };
1134            assert_eq!(
1135                data.authorized_key_entry(),
1136                Some(
1137                    raw_user_data_nethsm[1]
1138                        .1
1139                        .as_ref()
1140                        .expect("to have an SSH authorized key")
1141                )
1142            );
1143
1144            Ok(())
1145        }
1146
1147        /// Ensures, that [`ConfigBuilder::finish`] fails on issues with overlapping data in
1148        /// configuration components.
1149        ///
1150        /// Here, a custom [`NetHsmConfig`] is staged together with a default [`SystemConfig`]
1151        /// (created by [`default_system_config`]) to create a failure scenario.
1152        #[rstest]
1153        #[case::two_duplicate_system_users_two_duplicate_ssh_public_keys(
1154            "Configuration with system-wide and NetHSM configuration has two duplicate system users and two duplicate SSH public keys",
1155            NetHsmConfig::new(
1156                BTreeSet::from_iter([
1157                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1158                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1159                ]),
1160                BTreeSet::from_iter([
1161                    NetHsmUserMapping::Admin("admin".parse()?),
1162                    NetHsmUserMapping::Backup{
1163                        backend_user: "backup".parse()?,
1164                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1165                        system_user: "signstar-share-holder1".parse()?,
1166                    },
1167                    NetHsmUserMapping::HermeticMetrics {
1168                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1169                        system_user: "nethsm-hermetic-metrics".parse()?,
1170                    },
1171                    NetHsmUserMapping::Metrics {
1172                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1173                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
1174                        system_user: "signstar-share-holder2".parse()?,
1175                    },
1176                    NetHsmUserMapping::Signing {
1177                        backend_user: "signing".parse()?,
1178                        signing_key_id: "signing1".parse()?,
1179                        key_setup: SigningKeySetup::new(
1180                            KeyType::Curve25519,
1181                            vec![KeyMechanism::EdDsaSignature],
1182                            None,
1183                            SignatureType::EdDsa,
1184                            CryptographicKeyContext::OpenPgp {
1185                                user_ids: OpenPgpUserIdList::new(vec![
1186                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1187                                ])?,
1188                                notations: Default::default(),
1189                                version: "v4".parse()?,
1190                            },
1191                        )?,
1192                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1193                        system_user: "nethsm-signing".parse()?,
1194                        tag: "signing1".to_string(),
1195                    }
1196                ]),
1197            )?
1198        )]
1199        #[case::one_duplicate_system_user_two_duplicate_ssh_public_keys(
1200            "Configuration with system-wide and NetHSM configuration has one duplicate system user and two duplicate SSH public keys",
1201            NetHsmConfig::new(
1202                BTreeSet::from_iter([
1203                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1204                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1205                ]),
1206                BTreeSet::from_iter([
1207                    NetHsmUserMapping::Admin("admin".parse()?),
1208                    NetHsmUserMapping::Backup{
1209                        backend_user: "backup".parse()?,
1210                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1211                        system_user: "signstar-share-holder1".parse()?,
1212                    },
1213                    NetHsmUserMapping::HermeticMetrics {
1214                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1215                        system_user: "nethsm-hermetic-metrics".parse()?,
1216                    },
1217                    NetHsmUserMapping::Metrics {
1218                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1219                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
1220                        system_user: "nethsm-metrics".parse()?,
1221                    },
1222                    NetHsmUserMapping::Signing {
1223                        backend_user: "signing".parse()?,
1224                        signing_key_id: "signing1".parse()?,
1225                        key_setup: SigningKeySetup::new(
1226                            KeyType::Curve25519,
1227                            vec![KeyMechanism::EdDsaSignature],
1228                            None,
1229                            SignatureType::EdDsa,
1230                            CryptographicKeyContext::OpenPgp {
1231                                user_ids: OpenPgpUserIdList::new(vec![
1232                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1233                                ])?,
1234                                notations: Default::default(),
1235                                version: "v4".parse()?,
1236                            },
1237                        )?,
1238                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1239                        system_user: "nethsm-signing".parse()?,
1240                        tag: "signing1".to_string(),
1241                    }
1242                ]),
1243            )?
1244        )]
1245        #[case::one_duplicate_system_user_one_duplicate_ssh_public_key(
1246            "Configuration with system-wide and NetHSM configuration has one duplicate system user and one duplicate SSH public key",
1247            NetHsmConfig::new(
1248                BTreeSet::from_iter([
1249                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1250                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1251                ]),
1252                BTreeSet::from_iter([
1253                    NetHsmUserMapping::Admin("admin".parse()?),
1254                    NetHsmUserMapping::Backup{
1255                        backend_user: "backup".parse()?,
1256                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1257                        system_user: "signstar-share-holder1".parse()?,
1258                    },
1259                    NetHsmUserMapping::HermeticMetrics {
1260                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1261                        system_user: "nethsm-hermetic-metrics".parse()?,
1262                    },
1263                    NetHsmUserMapping::Metrics {
1264                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1265                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1266                        system_user: "nethsm-metrics".parse()?,
1267                    },
1268                    NetHsmUserMapping::Signing {
1269                        backend_user: "signing".parse()?,
1270                        signing_key_id: "signing1".parse()?,
1271                        key_setup: SigningKeySetup::new(
1272                            KeyType::Curve25519,
1273                            vec![KeyMechanism::EdDsaSignature],
1274                            None,
1275                            SignatureType::EdDsa,
1276                            CryptographicKeyContext::OpenPgp {
1277                                user_ids: OpenPgpUserIdList::new(vec![
1278                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1279                                ])?,
1280                                notations: Default::default(),
1281                                version: "v4".parse()?,
1282                            },
1283                        )?,
1284                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1285                        system_user: "nethsm-signing".parse()?,
1286                        tag: "signing1".to_string(),
1287                    }
1288                ]),
1289            )?
1290        )]
1291        #[case::one_duplicate_ssh_public_key(
1292            "Configuration with system-wide and NetHSM configuration has one duplicate SSH public key",
1293            NetHsmConfig::new(
1294                BTreeSet::from_iter([
1295                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1296                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1297                ]),
1298                BTreeSet::from_iter([
1299                    NetHsmUserMapping::Admin("admin".parse()?),
1300                    NetHsmUserMapping::Backup{
1301                        backend_user: "backup".parse()?,
1302                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1303                        system_user: "nethsm-backup".parse()?,
1304                    },
1305                    NetHsmUserMapping::HermeticMetrics {
1306                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1307                        system_user: "nethsm-hermetic-metrics".parse()?,
1308                    },
1309                    NetHsmUserMapping::Metrics {
1310                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1311                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1312                        system_user: "nethsm-metrics".parse()?,
1313                    },
1314                    NetHsmUserMapping::Signing {
1315                        backend_user: "signing".parse()?,
1316                        signing_key_id: "signing1".parse()?,
1317                        key_setup: SigningKeySetup::new(
1318                            KeyType::Curve25519,
1319                            vec![KeyMechanism::EdDsaSignature],
1320                            None,
1321                            SignatureType::EdDsa,
1322                            CryptographicKeyContext::OpenPgp {
1323                                user_ids: OpenPgpUserIdList::new(vec![
1324                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1325                                ])?,
1326                                notations: Default::default(),
1327                                version: "v4".parse()?,
1328                            },
1329                        )?,
1330                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1331                        system_user: "nethsm-signing".parse()?,
1332                        tag: "signing1".to_string(),
1333                    }
1334                ]),
1335            )?
1336        )]
1337        #[case::one_duplicate_system_user(
1338            "Configuration with system-wide and NetHSM configuration has one duplicate system user",
1339            NetHsmConfig::new(
1340                BTreeSet::from_iter([
1341                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1342                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1343                ]),
1344                BTreeSet::from_iter([
1345                    NetHsmUserMapping::Admin("admin".parse()?),
1346                    NetHsmUserMapping::Backup{
1347                        backend_user: "backup".parse()?,
1348                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1349                        system_user: "signstar-share-holder1".parse()?,
1350                    },
1351                    NetHsmUserMapping::HermeticMetrics {
1352                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1353                        system_user: "nethsm-hermetic-metrics".parse()?,
1354                    },
1355                    NetHsmUserMapping::Metrics {
1356                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1357                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1358                        system_user: "nethsm-metrics".parse()?,
1359                    },
1360                    NetHsmUserMapping::Signing {
1361                        backend_user: "signing".parse()?,
1362                        signing_key_id: "signing1".parse()?,
1363                        key_setup: SigningKeySetup::new(
1364                            KeyType::Curve25519,
1365                            vec![KeyMechanism::EdDsaSignature],
1366                            None,
1367                            SignatureType::EdDsa,
1368                            CryptographicKeyContext::OpenPgp {
1369                                user_ids: OpenPgpUserIdList::new(vec![
1370                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1371                                ])?,
1372                                notations: Default::default(),
1373                                version: "v4".parse()?,
1374                            },
1375                        )?,
1376                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1377                        system_user: "nethsm-signing".parse()?,
1378                        tag: "signing1".to_string(),
1379                    }
1380                ]),
1381            )?
1382        )]
1383        fn config_builder_fails_validation(
1384            default_system_config: TestResult<SystemConfig>,
1385            #[case] description: &str,
1386            #[case] nethsm_config: NetHsmConfig,
1387        ) -> TestResult {
1388            let error_message = match ConfigBuilder::new(default_system_config?)
1389                .set_nethsm_config(nethsm_config)
1390                .finish()
1391            {
1392                Err(error) => error.to_string(),
1393                Ok(config) => panic!(
1394                    "Expected to fail with Error::Validation, but succeeded instead: {}",
1395                    config.to_yaml_string()?
1396                ),
1397            };
1398
1399            with_settings!({
1400                description => description,
1401                snapshot_path => SNAPSHOT_PATH,
1402                prepend_module_to_snapshot => false,
1403            }, {
1404                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_message);
1405            });
1406
1407            Ok(())
1408        }
1409
1410        /// Ensures, that [`Config::nethsm`] returns the original input.
1411        #[rstest]
1412        fn config_nethsm(
1413            default_system_config: TestResult<SystemConfig>,
1414            default_nethsm_config: TestResult<NetHsmConfig>,
1415        ) -> TestResult {
1416            let nethsm_config = default_nethsm_config?;
1417
1418            let config = ConfigBuilder::new(default_system_config?)
1419                .set_nethsm_config(nethsm_config.clone())
1420                .finish()?;
1421
1422            assert_eq!(
1423                &nethsm_config,
1424                config.nethsm().expect("a NetHsmConfig reference")
1425            );
1426
1427            Ok(())
1428        }
1429
1430        /// Ensures, that an optional [`UserBackendConnection`] can be retrieved from a [`Config`].
1431        #[rstest]
1432        #[case::nethsm_signing(
1433            "nethsm-signing",
1434            Some(UserBackendConnection::NetHsm {
1435                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1436                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1437                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1438                },
1439                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1440                connections: BTreeSet::from_iter([
1441                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1442                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1443                ]),
1444                mapping: NetHsmUserMapping::Signing {
1445                    backend_user: "signing".parse()?,
1446                    signing_key_id: "signing1".parse()?,
1447                    key_setup: SigningKeySetup::new(
1448                        KeyType::Curve25519,
1449                        vec![KeyMechanism::EdDsaSignature],
1450                        None,
1451                        SignatureType::EdDsa,
1452                        CryptographicKeyContext::OpenPgp {
1453                            user_ids: OpenPgpUserIdList::new(vec![
1454                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1455                            ])?,
1456                            notations: Default::default(),
1457                            version: "v4".parse()?,
1458                        },
1459                    )?,
1460                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1461                    system_user: "nethsm-signing".parse()?,
1462                    tag: "signing1".to_string(),
1463                }
1464            })
1465        )]
1466        #[case::none("foo", None)]
1467        fn config_user_backend_connection(
1468            default_config: TestResult<Config>,
1469            #[case] system_user: &str,
1470            #[case] expected_connection: Option<UserBackendConnection>,
1471        ) -> TestResult {
1472            let config = default_config?;
1473            assert_eq!(
1474                expected_connection,
1475                config.user_backend_connection(&system_user.parse()?)
1476            );
1477
1478            Ok(())
1479        }
1480
1481        /// Ensures, that [`Config::user_backend_connections`] returns the correct list of
1482        /// [`UserBackendConnection`] items according to a [`UserBackendConnectionFilter`].
1483        #[rstest]
1484        #[case::no_filter(
1485            &[],
1486            vec![
1487                UserBackendConnection::NetHsm {
1488                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1489                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1490                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1491                    },
1492                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1493                    connections: BTreeSet::from_iter([
1494                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1495                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1496                    ]),
1497                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
1498                },
1499                UserBackendConnection::NetHsm {
1500                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1501                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1502                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1503                    },
1504                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1505                    connections: BTreeSet::from_iter([
1506                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1507                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1508                    ]),
1509                    mapping: NetHsmUserMapping::Backup{
1510                        backend_user: "backup".parse()?,
1511                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1512                        system_user: "nethsm-backup".parse()?,
1513                    }
1514                },
1515                UserBackendConnection::NetHsm {
1516                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1517                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1518                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1519                    },
1520                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1521                    connections: BTreeSet::from_iter([
1522                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1523                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1524                    ]),
1525                    mapping: NetHsmUserMapping::HermeticMetrics {
1526                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1527                        system_user: "nethsm-hermetic-metrics".parse()?,
1528                    }
1529                },
1530                UserBackendConnection::NetHsm {
1531                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1532                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1533                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1534                    },
1535                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1536                    connections: BTreeSet::from_iter([
1537                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1538                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1539                    ]),
1540                    mapping: NetHsmUserMapping::Metrics {
1541                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1542                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1543                        system_user: "nethsm-metrics".parse()?,
1544                    }
1545                },
1546                UserBackendConnection::NetHsm {
1547                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1548                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1549                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1550                    },
1551                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1552                    connections: BTreeSet::from_iter([
1553                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1554                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1555                    ]),
1556                    mapping: NetHsmUserMapping::Signing {
1557                        backend_user: "signing".parse()?,
1558                        signing_key_id: "signing1".parse()?,
1559                        key_setup: SigningKeySetup::new(
1560                            KeyType::Curve25519,
1561                            vec![KeyMechanism::EdDsaSignature],
1562                            None,
1563                            SignatureType::EdDsa,
1564                            CryptographicKeyContext::OpenPgp {
1565                                user_ids: OpenPgpUserIdList::new(vec![
1566                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1567                                ])?,
1568                                notations: Default::default(),
1569                                version: "v4".parse()?,
1570                            },
1571                        )?,
1572                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1573                        system_user: "nethsm-signing".parse()?,
1574                        tag: "signing1".to_string(),
1575                    }
1576                },
1577            ],
1578        )]
1579        #[case::filter_admin(
1580            &[UserBackendConnectionFilter::Admin],
1581            vec![
1582                UserBackendConnection::NetHsm {
1583                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1584                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1585                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1586                    },
1587                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1588                    connections: BTreeSet::from_iter([
1589                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1590                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1591                    ]),
1592                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
1593                },
1594            ],
1595        )]
1596        #[case::filter_non_admin(
1597            &[UserBackendConnectionFilter::NonAdmin],
1598            vec![
1599                UserBackendConnection::NetHsm {
1600                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1601                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1602                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1603                    },
1604                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1605                    connections: BTreeSet::from_iter([
1606                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1607                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1608                    ]),
1609                    mapping: NetHsmUserMapping::Backup{
1610                        backend_user: "backup".parse()?,
1611                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1612                        system_user: "nethsm-backup".parse()?,
1613                    }
1614                },
1615                UserBackendConnection::NetHsm {
1616                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1617                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1618                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1619                    },
1620                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1621                    connections: BTreeSet::from_iter([
1622                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1623                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1624                    ]),
1625                    mapping: NetHsmUserMapping::HermeticMetrics {
1626                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
1627                        system_user: "nethsm-hermetic-metrics".parse()?,
1628                    }
1629                },
1630                UserBackendConnection::NetHsm {
1631                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1632                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1633                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1634                    },
1635                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1636                    connections: BTreeSet::from_iter([
1637                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1638                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1639                    ]),
1640                    mapping: NetHsmUserMapping::Metrics {
1641                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
1642                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1643                        system_user: "nethsm-metrics".parse()?,
1644                    }
1645                },
1646                UserBackendConnection::NetHsm {
1647                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
1648                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1649                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1650                    },
1651                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
1652                    connections: BTreeSet::from_iter([
1653                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
1654                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
1655                    ]),
1656                    mapping: NetHsmUserMapping::Signing {
1657                        backend_user: "signing".parse()?,
1658                        signing_key_id: "signing1".parse()?,
1659                        key_setup: SigningKeySetup::new(
1660                            KeyType::Curve25519,
1661                            vec![KeyMechanism::EdDsaSignature],
1662                            None,
1663                            SignatureType::EdDsa,
1664                            CryptographicKeyContext::OpenPgp {
1665                                user_ids: OpenPgpUserIdList::new(vec![
1666                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1667                                ])?,
1668                                notations: Default::default(),
1669                                version: "v4".parse()?,
1670                            },
1671                        )?,
1672                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1673                        system_user: "nethsm-signing".parse()?,
1674                        tag: "signing1".to_string(),
1675                    }
1676                },
1677            ],
1678        )]
1679        fn config_user_backend_connections(
1680            default_config: TestResult<Config>,
1681            #[case] filters: &[UserBackendConnectionFilter],
1682            #[case] expected_connections: Vec<UserBackendConnection>,
1683        ) -> TestResult {
1684            let config = default_config?;
1685
1686            assert_eq!(
1687                expected_connections,
1688                config.user_backend_connections(filters)
1689            );
1690
1691            Ok(())
1692        }
1693
1694        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
1695        /// correctly.
1696        #[rstest]
1697        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
1698            let config = default_config?;
1699            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
1700                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
1701                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
1702                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
1703                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1704                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
1705                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
1706                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
1707            ]);
1708
1709            assert_eq!(
1710                config.authorized_key_entries(),
1711                expected.iter().collect::<HashSet<_>>()
1712            );
1713            Ok(())
1714        }
1715
1716        /// Ensures, that [`Config::system_user_data`] returns [`SystemUserData`] entries correctly.
1717        #[rstest]
1718        fn config_system_user_data(
1719            default_config: TestResult<Config>,
1720            raw_user_data: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1721        ) -> TestResult {
1722            let config = default_config?;
1723            let raw_user_data = raw_user_data?;
1724            let expected: HashSet<SystemUserData> = HashSet::from_iter([
1725                SystemUserData::HostShareholder {
1726                    system_user: &raw_user_data[0].0,
1727                    ssh_authorized_key: raw_user_data[0]
1728                        .1
1729                        .as_ref()
1730                        .expect("to have SSH authorized key"),
1731                },
1732                SystemUserData::HostShareholder {
1733                    system_user: &raw_user_data[1].0,
1734                    ssh_authorized_key: raw_user_data[1]
1735                        .1
1736                        .as_ref()
1737                        .expect("to have SSH authorized key"),
1738                },
1739                SystemUserData::HostShareholder {
1740                    system_user: &raw_user_data[2].0,
1741                    ssh_authorized_key: raw_user_data[2]
1742                        .1
1743                        .as_ref()
1744                        .expect("to have SSH authorized key"),
1745                },
1746                SystemUserData::HostDownloadNetworkConfig {
1747                    system_user: &raw_user_data[3].0,
1748                    ssh_authorized_key: raw_user_data[3]
1749                        .1
1750                        .as_ref()
1751                        .expect("to have SSH authorized key"),
1752                },
1753                SystemUserData::BackendAdmin {
1754                    system_user: raw_user_data[4].0.clone(),
1755                },
1756                SystemUserData::BackendBackup {
1757                    system_user: &raw_user_data[5].0,
1758                    ssh_authorized_key: raw_user_data[5]
1759                        .1
1760                        .as_ref()
1761                        .expect("to have SSH authorized key"),
1762                },
1763                SystemUserData::BackendHermeticMetrics {
1764                    system_user: &raw_user_data[6].0,
1765                },
1766                SystemUserData::BackendMetrics {
1767                    system_user: &raw_user_data[7].0,
1768                    ssh_authorized_key: raw_user_data[7]
1769                        .1
1770                        .as_ref()
1771                        .expect("to have SSH authorized key"),
1772                },
1773                SystemUserData::BackendSign {
1774                    system_user: &raw_user_data[8].0,
1775                    ssh_authorized_key: raw_user_data[8]
1776                        .1
1777                        .as_ref()
1778                        .expect("to have SSH authorized key"),
1779                },
1780            ]);
1781
1782            assert_eq!(config.system_user_data(), expected);
1783            Ok(())
1784        }
1785
1786        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
1787        #[rstest]
1788        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
1789            let config = default_config?;
1790            let expected: HashSet<SystemUserId> = HashSet::from_iter([
1791                "signstar-share-holder1".parse()?,
1792                "signstar-share-holder2".parse()?,
1793                "signstar-share-holder3".parse()?,
1794                "signstar-wireguard-download".parse()?,
1795                "nethsm-backup".parse()?,
1796                "nethsm-hermetic-metrics".parse()?,
1797                "nethsm-metrics".parse()?,
1798                "nethsm-signing".parse()?,
1799            ]);
1800
1801            assert_eq!(
1802                config.system_user_ids(),
1803                expected.iter().collect::<HashSet<_>>()
1804            );
1805            Ok(())
1806        }
1807
1808        /// Ensures, that a [`Config`] object leads to a specific YAML output.
1809        ///
1810        /// In this particular case, a [`SystemConfig`] and a [`NetHsmConfig`] object are present.
1811        #[rstest]
1812        fn config_to_yaml_string(
1813            default_system_config: TestResult<SystemConfig>,
1814            default_nethsm_config: TestResult<NetHsmConfig>,
1815        ) -> TestResult {
1816            let config = ConfigBuilder::new(default_system_config?)
1817                .set_nethsm_config(default_nethsm_config?)
1818                .finish()?;
1819            let config_str = config.to_yaml_string()?;
1820
1821            with_settings!({
1822                description => "Configuration with system-wide and NetHSM configuration",
1823                snapshot_path => SNAPSHOT_PATH,
1824                prepend_module_to_snapshot => false,
1825            }, {
1826                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
1827            });
1828
1829            Ok(())
1830        }
1831
1832        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
1833        /// the same YAML string.
1834        ///
1835        /// The configuration file describes a [`SystemConfig`] and a [`NetHsmConfig`] object.
1836        #[rstest]
1837        fn roundtrip_yaml_config(
1838            #[files("../fixtures/config/nethsm_backend/*.yaml")] path: PathBuf,
1839        ) -> TestResult {
1840            let config_string = read_to_string(&path)?;
1841            let config = Config::from_file_path(&path)?;
1842
1843            assert_eq!(config.to_yaml_string()?, config_string);
1844
1845            Ok(())
1846        }
1847
1848        /// Ensures, that [`AdministrativeSecretHandling`] and
1849        /// [`NonAdministrativeSecretHandling`]can be retrieved from a
1850        /// [`UserBackendConnection`].
1851        #[rstest]
1852        fn user_backend_connection_secret_handling(
1853            default_config: TestResult<Config>,
1854        ) -> TestResult {
1855            let config = default_config?;
1856            let admin_secret_handling = AdministrativeSecretHandling::ShamirsSecretSharing {
1857                number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
1858                threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
1859            };
1860            let non_admin_secret_handling = NonAdministrativeSecretHandling::SystemdCreds;
1861
1862            let user_backend_connection = config
1863                .user_backend_connection(&"nethsm-signing".parse()?)
1864                .expect("there to be a mapping of the requested name");
1865
1866            assert_eq!(
1867                user_backend_connection.admin_secret_handling(),
1868                admin_secret_handling
1869            );
1870            assert_eq!(
1871                user_backend_connection.non_admin_secret_handling(),
1872                non_admin_secret_handling
1873            );
1874
1875            Ok(())
1876        }
1877
1878        /// Ensures, that [`SystemUserConfigState`] can be created from [`Config`].
1879        #[rstest]
1880        fn system_user_config_state_from_config(default_config: TestResult<Config>) -> TestResult {
1881            let config = default_config?;
1882            let state = SystemUserConfigState::from(&config);
1883
1884            assert_eq!(state.system_user_data, config.system_user_data(),);
1885            Ok(())
1886        }
1887    }
1888
1889    /// Tests, that are only available when using the YubiHSM2 (and no other) backend.
1890    #[cfg(all(feature = "yubihsm2", not(feature = "nethsm")))]
1891    mod yubihsm2_backend {
1892        use pretty_assertions::assert_eq;
1893
1894        use super::*;
1895        use crate::config::{
1896            SystemUserData,
1897            traits::{ConfigSystemUserData, MappingAuthorizedKeyEntry, MappingSystemUserId},
1898        };
1899
1900        /// Creates a default [`Config`] for testing purposes.
1901        #[fixture]
1902        fn default_config(
1903            default_system_config: TestResult<SystemConfig>,
1904            default_yubihsm2_config: TestResult<YubiHsm2Config>,
1905        ) -> TestResult<Config> {
1906            Ok(ConfigBuilder::new(default_system_config?)
1907                .set_yubihsm2_config(default_yubihsm2_config?)
1908                .finish()?)
1909        }
1910
1911        /// List of raw data required to create [`SystemUserData`] for each item in the default
1912        /// [`Config`].
1913        #[fixture]
1914        fn raw_user_data(
1915            raw_user_data_system: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1916            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1917        ) -> TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>> {
1918            let mut data = raw_user_data_system?;
1919            data.extend(raw_user_data_yubihsm2?);
1920            Ok(data)
1921        }
1922
1923        /// Ensures that [`MappingSystemUserId`] for [`UserBackendConnection`] works as intended.
1924        #[rstest]
1925        fn user_backend_connection_system_user_id(
1926            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1927        ) -> TestResult {
1928            let raw_user_data_yubihsm2 = raw_user_data_yubihsm2?;
1929            let data = UserBackendConnection::YubiHsm2 {
1930                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
1931                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
1932                connections: BTreeSet::from_iter([YubiHsm2Connection::Usb {
1933                    serial_number: "0123456789".parse()?,
1934                }]),
1935                mapping: YubiHsm2UserMapping::AuditLog {
1936                    authentication_key_id: "1".parse()?,
1937                    ssh_authorized_key: raw_user_data_yubihsm2[1]
1938                        .1
1939                        .clone()
1940                        .expect("to have an SSH authorized key"),
1941                    system_user: raw_user_data_yubihsm2[1].0.clone(),
1942                },
1943            };
1944            assert_eq!(data.system_user_id(), Some(&raw_user_data_yubihsm2[1].0));
1945
1946            Ok(())
1947        }
1948
1949        /// Ensures that [`MappingAuthorizedKeyEntry`] for [`UserBackendConnection`] works as
1950        /// intended.
1951        #[rstest]
1952        fn user_backend_connection_authorized_key_entry(
1953            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
1954        ) -> TestResult {
1955            let raw_user_data_yubihsm2 = raw_user_data_yubihsm2?;
1956            let data = UserBackendConnection::YubiHsm2 {
1957                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
1958                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
1959                connections: BTreeSet::from_iter([YubiHsm2Connection::Usb {
1960                    serial_number: "0123456789".parse()?,
1961                }]),
1962                mapping: YubiHsm2UserMapping::AuditLog {
1963                    authentication_key_id: "1".parse()?,
1964                    ssh_authorized_key: raw_user_data_yubihsm2[1]
1965                        .1
1966                        .clone()
1967                        .expect("to have an SSH authorized key"),
1968                    system_user: raw_user_data_yubihsm2[1].0.clone(),
1969                },
1970            };
1971            assert_eq!(
1972                data.authorized_key_entry(),
1973                Some(
1974                    raw_user_data_yubihsm2[1]
1975                        .1
1976                        .as_ref()
1977                        .expect("to have an SSH authorized key")
1978                )
1979            );
1980
1981            Ok(())
1982        }
1983
1984        /// Ensures, that [`ConfigBuilder::finish`] fails on issues with overlapping data in
1985        /// configuration components.
1986        ///
1987        /// Here, a custom [`YubiHsm2Config`] is staged together with a default [`SystemConfig`]
1988        /// (created by [`default_system_config`]) to create a failure scenario.
1989        #[rstest]
1990        #[case::two_duplicate_system_users_two_duplicate_ssh_public_keys(
1991            "Configuration with system-wide and YubiHSM2 configuration has two duplicate system users and two duplicate SSH public keys",
1992            YubiHsm2Config::new(
1993                BTreeSet::from_iter([
1994                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
1995                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
1996                ]),
1997                BTreeSet::from_iter([
1998                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1999                    YubiHsm2UserMapping::AuditLog {
2000                        authentication_key_id: "3".parse()?,
2001                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
2002                        system_user: "signstar-share-holder2".parse()?,
2003                    },
2004                    YubiHsm2UserMapping::Backup{
2005                        authentication_key_id: "2".parse()?,
2006                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2007                        system_user: "signstar-share-holder1".parse()?,
2008                    },
2009                    YubiHsm2UserMapping::HermeticAuditLog {
2010                        authentication_key_id: "4".parse()?,
2011                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
2012                    },
2013                    YubiHsm2UserMapping::Signing {
2014                        authentication_key_id: "5".parse()?,
2015                        signing_key_id: "1".parse()?,
2016                        key_setup: SigningKeySetup::new(
2017                            KeyType::Curve25519,
2018                            vec![KeyMechanism::EdDsaSignature],
2019                            None,
2020                            SignatureType::EdDsa,
2021                            CryptographicKeyContext::OpenPgp {
2022                                user_ids: OpenPgpUserIdList::new(vec![
2023                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2024                                ])?,
2025                                notations: Default::default(),
2026                                version: "v4".parse()?,
2027                            },
2028                        )?,
2029                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2030                        system_user: "yubihsm2-signing".parse()?,
2031                        domain: Domain::One,
2032                    }
2033                ]),
2034            )?
2035         )]
2036        #[case::one_duplicate_system_user_two_duplicate_ssh_public_keys(
2037            "Configuration with system-wide and YubiHSM2 configuration has one duplicate system user and two duplicate SSH public keys",
2038            YubiHsm2Config::new(
2039                BTreeSet::from_iter([
2040                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2041                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2042                ]),
2043                BTreeSet::from_iter([
2044                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2045                    YubiHsm2UserMapping::AuditLog {
2046                        authentication_key_id: "3".parse()?,
2047                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
2048                        system_user: "yubihsm2-audit-log".parse()?,
2049                    },
2050                    YubiHsm2UserMapping::Backup{
2051                        authentication_key_id: "2".parse()?,
2052                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2053                        system_user: "signstar-share-holder1".parse()?,
2054                    },
2055                    YubiHsm2UserMapping::HermeticAuditLog {
2056                        authentication_key_id: "4".parse()?,
2057                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
2058                    },
2059                    YubiHsm2UserMapping::Signing {
2060                        authentication_key_id: "5".parse()?,
2061                        signing_key_id: "1".parse()?,
2062                        key_setup: SigningKeySetup::new(
2063                            KeyType::Curve25519,
2064                            vec![KeyMechanism::EdDsaSignature],
2065                            None,
2066                            SignatureType::EdDsa,
2067                            CryptographicKeyContext::OpenPgp {
2068                                user_ids: OpenPgpUserIdList::new(vec![
2069                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2070                                ])?,
2071                                notations: Default::default(),
2072                                version: "v4".parse()?,
2073                            },
2074                        )?,
2075                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2076                        system_user: "yubihsm2-signing".parse()?,
2077                        domain: Domain::One,
2078                    }
2079                ]),
2080            )?
2081         )]
2082        #[case::one_duplicate_system_user_one_duplicate_ssh_public_key(
2083            "Configuration with system-wide and YubiHSM2 configuration has one duplicate system user and one duplicate SSH public key",
2084            YubiHsm2Config::new(
2085                BTreeSet::from_iter([
2086                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2087                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2088                ]),
2089                BTreeSet::from_iter([
2090                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2091                    YubiHsm2UserMapping::AuditLog {
2092                        authentication_key_id: "3".parse()?,
2093                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
2094                        system_user: "yubihsm2-audit-log".parse()?,
2095                    },
2096                    YubiHsm2UserMapping::Backup{
2097                        authentication_key_id: "2".parse()?,
2098                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2099                        system_user: "signstar-share-holder1".parse()?,
2100                    },
2101                    YubiHsm2UserMapping::HermeticAuditLog {
2102                        authentication_key_id: "4".parse()?,
2103                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
2104                    },
2105                    YubiHsm2UserMapping::Signing {
2106                        authentication_key_id: "5".parse()?,
2107                        signing_key_id: "1".parse()?,
2108                        key_setup: SigningKeySetup::new(
2109                            KeyType::Curve25519,
2110                            vec![KeyMechanism::EdDsaSignature],
2111                            None,
2112                            SignatureType::EdDsa,
2113                            CryptographicKeyContext::OpenPgp {
2114                                user_ids: OpenPgpUserIdList::new(vec![
2115                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2116                                ])?,
2117                                notations: Default::default(),
2118                                version: "v4".parse()?,
2119                            },
2120                        )?,
2121                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2122                        system_user: "yubihsm2-signing".parse()?,
2123                        domain: Domain::One,
2124                    }
2125                ]),
2126            )?
2127         )]
2128        #[case::one_duplicate_ssh_public_key(
2129            "Configuration with system-wide and YubiHSM2 configuration has one duplicate SSH public key",
2130            YubiHsm2Config::new(
2131                BTreeSet::from_iter([
2132                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2133                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2134                ]),
2135                BTreeSet::from_iter([
2136                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2137                    YubiHsm2UserMapping::AuditLog {
2138                        authentication_key_id: "3".parse()?,
2139                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
2140                        system_user: "yubihsm2-audit-log".parse()?,
2141                    },
2142                    YubiHsm2UserMapping::Backup{
2143                        authentication_key_id: "2".parse()?,
2144                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2145                        system_user: "yubihsm2-backup".parse()?,
2146                    },
2147                    YubiHsm2UserMapping::HermeticAuditLog {
2148                        authentication_key_id: "4".parse()?,
2149                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
2150                    },
2151                    YubiHsm2UserMapping::Signing {
2152                        authentication_key_id: "5".parse()?,
2153                        signing_key_id: "1".parse()?,
2154                        key_setup: SigningKeySetup::new(
2155                            KeyType::Curve25519,
2156                            vec![KeyMechanism::EdDsaSignature],
2157                            None,
2158                            SignatureType::EdDsa,
2159                            CryptographicKeyContext::OpenPgp {
2160                                user_ids: OpenPgpUserIdList::new(vec![
2161                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2162                                ])?,
2163                                notations: Default::default(),
2164                                version: "v4".parse()?,
2165                            },
2166                        )?,
2167                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2168                        system_user: "yubihsm2-signing".parse()?,
2169                        domain: Domain::One,
2170                    }
2171                ]),
2172            )?
2173         )]
2174        #[case::one_duplicate_system_user(
2175            "Configuration with system-wide and YubiHSM2 configuration has one duplicate system user",
2176            YubiHsm2Config::new(
2177                BTreeSet::from_iter([
2178                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2179                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2180                ]),
2181                BTreeSet::from_iter([
2182                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2183                    YubiHsm2UserMapping::AuditLog {
2184                        authentication_key_id: "3".parse()?,
2185                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2186                        system_user: "yubihsm2-audit-log".parse()?,
2187                    },
2188                    YubiHsm2UserMapping::Backup{
2189                        authentication_key_id: "2".parse()?,
2190                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2191                        system_user: "signstar-share-holder1".parse()?,
2192                    },
2193                    YubiHsm2UserMapping::HermeticAuditLog {
2194                        authentication_key_id: "4".parse()?,
2195                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
2196                    },
2197                    YubiHsm2UserMapping::Signing {
2198                        authentication_key_id: "5".parse()?,
2199                        signing_key_id: "1".parse()?,
2200                        key_setup: SigningKeySetup::new(
2201                            KeyType::Curve25519,
2202                            vec![KeyMechanism::EdDsaSignature],
2203                            None,
2204                            SignatureType::EdDsa,
2205                            CryptographicKeyContext::OpenPgp {
2206                                user_ids: OpenPgpUserIdList::new(vec![
2207                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2208                                ])?,
2209                                notations: Default::default(),
2210                                version: "v4".parse()?,
2211                            },
2212                        )?,
2213                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2214                        system_user: "yubihsm2-signing".parse()?,
2215                        domain: Domain::One,
2216                    }
2217                ]),
2218            )?
2219         )]
2220        fn config_builder_fails_validation(
2221            default_system_config: TestResult<SystemConfig>,
2222            #[case] description: &str,
2223            #[case] yubihsm2_config: YubiHsm2Config,
2224        ) -> TestResult {
2225            let error_message = match ConfigBuilder::new(default_system_config?)
2226                .set_yubihsm2_config(yubihsm2_config)
2227                .finish()
2228            {
2229                Err(error) => error.to_string(),
2230                Ok(config) => panic!(
2231                    "Expected to fail with Error::Validation, but succeeded instead: {}",
2232                    config.to_yaml_string()?
2233                ),
2234            };
2235
2236            with_settings!({
2237                description => description,
2238                snapshot_path => SNAPSHOT_PATH,
2239                prepend_module_to_snapshot => false,
2240            }, {
2241                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_message);
2242            });
2243
2244            Ok(())
2245        }
2246
2247        /// Ensures, that [`Config::yubihsm2`] returns the original input.
2248        #[rstest]
2249        fn config_yubihsm2(
2250            default_system_config: TestResult<SystemConfig>,
2251            default_yubihsm2_config: TestResult<YubiHsm2Config>,
2252        ) -> TestResult {
2253            let yubihsm2_config = default_yubihsm2_config?;
2254
2255            let config = ConfigBuilder::new(default_system_config?)
2256                .set_yubihsm2_config(yubihsm2_config.clone())
2257                .finish()?;
2258
2259            assert_eq!(
2260                &yubihsm2_config,
2261                config.yubihsm2().expect("a YubiHsm2Config reference")
2262            );
2263
2264            Ok(())
2265        }
2266
2267        /// Ensures, that an optional [`UserBackendConnection`] can be retrieved from a [`Config`].
2268        #[rstest]
2269        #[case::yubihsm2_signing(
2270            "yubihsm2-signing",
2271            Some(UserBackendConnection::YubiHsm2 {
2272                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2273                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2274                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2275                },
2276                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2277                connections: BTreeSet::from_iter([
2278                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2279                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2280                ]),
2281                mapping: YubiHsm2UserMapping::Signing {
2282                    authentication_key_id: "5".parse()?,
2283                    signing_key_id: "1".parse()?,
2284                    key_setup: SigningKeySetup::new(
2285                        KeyType::Curve25519,
2286                        vec![KeyMechanism::EdDsaSignature],
2287                        None,
2288                        SignatureType::EdDsa,
2289                        CryptographicKeyContext::OpenPgp {
2290                            user_ids: OpenPgpUserIdList::new(vec![
2291                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2292                            ])?,
2293                            notations: Default::default(),
2294                            version: "v4".parse()?,
2295                        },
2296                    )?,
2297                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2298                    system_user: "yubihsm2-signing".parse()?,
2299                    domain: Domain::One,
2300                }
2301            })
2302        )]
2303        #[case::none("foo", None)]
2304        fn config_user_backend_connection(
2305            default_config: TestResult<Config>,
2306            #[case] system_user: &str,
2307            #[case] expected_connection: Option<UserBackendConnection>,
2308        ) -> TestResult {
2309            let config = default_config?;
2310            assert_eq!(
2311                expected_connection,
2312                config.user_backend_connection(&system_user.parse()?)
2313            );
2314
2315            Ok(())
2316        }
2317
2318        /// Ensures, that [`Config::user_backend_connections`] returns the correct list of
2319        /// [`UserBackendConnection`] items according to a [`UserBackendConnectionFilter`].
2320        #[rstest]
2321        #[case::no_filter(
2322            &[],
2323            vec![
2324                UserBackendConnection::YubiHsm2 {
2325                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2326                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2327                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2328                    },
2329                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2330                    connections: BTreeSet::from_iter([
2331                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2332                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2333                    ]),
2334                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2335                },
2336                UserBackendConnection::YubiHsm2 {
2337                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2338                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2339                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2340                    },
2341                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2342                    connections: BTreeSet::from_iter([
2343                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2344                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2345                    ]),
2346                    mapping: YubiHsm2UserMapping::AuditLog {
2347                        authentication_key_id: "3".parse()?,
2348                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2349                        system_user: "yubihsm2-audit-log".parse()?,
2350                    },
2351                },
2352                UserBackendConnection::YubiHsm2 {
2353                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2354                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2355                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2356                    },
2357                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2358                    connections: BTreeSet::from_iter([
2359                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2360                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2361                    ]),
2362                    mapping: YubiHsm2UserMapping::Backup{
2363                        authentication_key_id: "2".parse()?,
2364                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2365                        system_user: "yubihsm2-backup".parse()?,
2366                    },
2367                },
2368                UserBackendConnection::YubiHsm2 {
2369                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2370                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2371                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2372                    },
2373                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2374                    connections: BTreeSet::from_iter([
2375                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2376                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2377                    ]),
2378                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
2379                        authentication_key_id: "4".parse()?,
2380                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
2381                    },
2382                },
2383                UserBackendConnection::YubiHsm2 {
2384                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2385                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2386                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2387                    },
2388                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2389                    connections: BTreeSet::from_iter([
2390                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2391                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2392                    ]),
2393                    mapping: YubiHsm2UserMapping::Signing {
2394                        authentication_key_id: "5".parse()?,
2395                        signing_key_id: "1".parse()?,
2396                        key_setup: SigningKeySetup::new(
2397                            KeyType::Curve25519,
2398                            vec![KeyMechanism::EdDsaSignature],
2399                            None,
2400                            SignatureType::EdDsa,
2401                            CryptographicKeyContext::OpenPgp {
2402                                user_ids: OpenPgpUserIdList::new(vec![
2403                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2404                                ])?,
2405                                notations: Default::default(),
2406                                version: "v4".parse()?,
2407                            },
2408                        )?,
2409                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2410                        system_user: "yubihsm2-signing".parse()?,
2411                        domain: Domain::One,
2412                    }
2413                },
2414            ],
2415        )]
2416        #[case::filter_admin(
2417            &[UserBackendConnectionFilter::Admin],
2418            vec![
2419                UserBackendConnection::YubiHsm2 {
2420                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2421                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2422                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2423                    },
2424                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2425                    connections: BTreeSet::from_iter([
2426                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2427                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2428                    ]),
2429                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2430                },
2431            ],
2432        )]
2433        #[case::filter_non_admin(
2434            &[UserBackendConnectionFilter::NonAdmin],
2435            vec![
2436                UserBackendConnection::YubiHsm2 {
2437                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2438                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2439                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2440                    },
2441                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2442                    connections: BTreeSet::from_iter([
2443                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2444                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2445                    ]),
2446                    mapping: YubiHsm2UserMapping::AuditLog {
2447                        authentication_key_id: "3".parse()?,
2448                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2449                        system_user: "yubihsm2-audit-log".parse()?,
2450                    },
2451                },
2452                UserBackendConnection::YubiHsm2 {
2453                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2454                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2455                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2456                    },
2457                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2458                    connections: BTreeSet::from_iter([
2459                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2460                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2461                    ]),
2462                    mapping: YubiHsm2UserMapping::Backup{
2463                        authentication_key_id: "2".parse()?,
2464                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2465                        system_user: "yubihsm2-backup".parse()?,
2466                    },
2467                },
2468                UserBackendConnection::YubiHsm2 {
2469                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2470                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2471                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2472                    },
2473                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2474                    connections: BTreeSet::from_iter([
2475                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2476                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2477                    ]),
2478                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
2479                        authentication_key_id: "4".parse()?,
2480                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
2481                    },
2482                },
2483                UserBackendConnection::YubiHsm2 {
2484                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
2485                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2486                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2487                    },
2488                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
2489                    connections: BTreeSet::from_iter([
2490                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2491                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2492                    ]),
2493                    mapping: YubiHsm2UserMapping::Signing {
2494                        authentication_key_id: "5".parse()?,
2495                        signing_key_id: "1".parse()?,
2496                        key_setup: SigningKeySetup::new(
2497                            KeyType::Curve25519,
2498                            vec![KeyMechanism::EdDsaSignature],
2499                            None,
2500                            SignatureType::EdDsa,
2501                            CryptographicKeyContext::OpenPgp {
2502                                user_ids: OpenPgpUserIdList::new(vec![
2503                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2504                                ])?,
2505                                notations: Default::default(),
2506                                version: "v4".parse()?,
2507                            },
2508                        )?,
2509                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2510                        system_user: "yubihsm2-signing".parse()?,
2511                        domain: Domain::One,
2512                    }
2513                },
2514            ],
2515        )]
2516        fn config_user_backend_connections(
2517            default_config: TestResult<Config>,
2518            #[case] filters: &[UserBackendConnectionFilter],
2519            #[case] expected_connections: Vec<UserBackendConnection>,
2520        ) -> TestResult {
2521            let config = default_config?;
2522
2523            assert_eq!(
2524                expected_connections,
2525                config.user_backend_connections(filters)
2526            );
2527
2528            Ok(())
2529        }
2530
2531        /// Ensures, that a [`Config`] object leads to a specific YAML output.
2532        ///
2533        /// In this particular case, a [`SystemConfig`] and a [`YubiHsm2Config`] object are present.
2534        #[rstest]
2535        fn config_to_yaml_string(
2536            default_system_config: TestResult<SystemConfig>,
2537            default_yubihsm2_config: TestResult<YubiHsm2Config>,
2538        ) -> TestResult {
2539            let config = ConfigBuilder::new(default_system_config?)
2540                .set_yubihsm2_config(default_yubihsm2_config?)
2541                .finish()?;
2542            let config_str = config.to_yaml_string()?;
2543
2544            with_settings!({
2545                description => "Configuration with system-wide and YubiHSM2 configuration",
2546                snapshot_path => SNAPSHOT_PATH,
2547                prepend_module_to_snapshot => false,
2548            }, {
2549                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
2550            });
2551
2552            Ok(())
2553        }
2554
2555        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
2556        /// correctly.
2557        #[rstest]
2558        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
2559            let config = default_config?;
2560            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
2561                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
2562                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
2563                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
2564                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
2565                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
2566                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
2567                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2568            ]);
2569
2570            assert_eq!(
2571                config.authorized_key_entries(),
2572                expected.iter().collect::<HashSet<_>>()
2573            );
2574            Ok(())
2575        }
2576
2577        /// Ensures, that [`Config::system_user_data`] returns [`SystemUserData`] entries correctly.
2578        #[rstest]
2579        fn config_system_user_data(
2580            default_config: TestResult<Config>,
2581            raw_user_data: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2582        ) -> TestResult {
2583            let config = default_config?;
2584            let raw_user_data = raw_user_data?;
2585            let expected: HashSet<SystemUserData> = HashSet::from_iter([
2586                SystemUserData::HostShareholder {
2587                    system_user: &raw_user_data[0].0,
2588                    ssh_authorized_key: raw_user_data[0]
2589                        .1
2590                        .as_ref()
2591                        .expect("to have SSH authorized key"),
2592                },
2593                SystemUserData::HostShareholder {
2594                    system_user: &raw_user_data[1].0,
2595                    ssh_authorized_key: raw_user_data[1]
2596                        .1
2597                        .as_ref()
2598                        .expect("to have SSH authorized key"),
2599                },
2600                SystemUserData::HostShareholder {
2601                    system_user: &raw_user_data[2].0,
2602                    ssh_authorized_key: raw_user_data[2]
2603                        .1
2604                        .as_ref()
2605                        .expect("to have SSH authorized key"),
2606                },
2607                SystemUserData::HostDownloadNetworkConfig {
2608                    system_user: &raw_user_data[3].0,
2609                    ssh_authorized_key: raw_user_data[3]
2610                        .1
2611                        .as_ref()
2612                        .expect("to have SSH authorized key"),
2613                },
2614                SystemUserData::BackendAdmin {
2615                    system_user: raw_user_data[4].0.clone(),
2616                },
2617                SystemUserData::BackendMetrics {
2618                    system_user: &raw_user_data[5].0,
2619                    ssh_authorized_key: raw_user_data[5]
2620                        .1
2621                        .as_ref()
2622                        .expect("to have SSH authorized key"),
2623                },
2624                SystemUserData::BackendBackup {
2625                    system_user: &raw_user_data[6].0,
2626                    ssh_authorized_key: raw_user_data[6]
2627                        .1
2628                        .as_ref()
2629                        .expect("to have SSH authorized key"),
2630                },
2631                SystemUserData::BackendHermeticMetrics {
2632                    system_user: &raw_user_data[7].0,
2633                },
2634                SystemUserData::BackendSign {
2635                    system_user: &raw_user_data[8].0,
2636                    ssh_authorized_key: raw_user_data[8]
2637                        .1
2638                        .as_ref()
2639                        .expect("to have SSH authorized key"),
2640                },
2641            ]);
2642
2643            assert_eq!(config.system_user_data(), expected);
2644            Ok(())
2645        }
2646
2647        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
2648        #[rstest]
2649        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
2650            let config = default_config?;
2651            let expected: HashSet<SystemUserId> = HashSet::from_iter([
2652                "signstar-share-holder1".parse()?,
2653                "signstar-share-holder2".parse()?,
2654                "signstar-share-holder3".parse()?,
2655                "signstar-wireguard-download".parse()?,
2656                "yubihsm2-audit-log".parse()?,
2657                "yubihsm2-backup".parse()?,
2658                "yubihsm2-hermetic-audit-log".parse()?,
2659                "yubihsm2-signing".parse()?,
2660            ]);
2661
2662            assert_eq!(
2663                config.system_user_ids(),
2664                expected.iter().collect::<HashSet<_>>()
2665            );
2666            Ok(())
2667        }
2668
2669        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
2670        /// the same YAML string.
2671        ///
2672        /// The configuration file describes a [`SystemConfig`] and a [`YubiHsm2Config`] object.
2673        #[rstest]
2674        #[cfg(not(feature = "_yubihsm2-mockhsm"))]
2675        fn roundtrip_yaml_config(
2676            #[files("../fixtures/config/yubihsm2_backend/*.yaml")] path: PathBuf,
2677        ) -> TestResult {
2678            let config_string = read_to_string(&path)?;
2679            let config = Config::from_file_path(&path)?;
2680
2681            assert_eq!(config.to_yaml_string()?, config_string);
2682
2683            Ok(())
2684        }
2685
2686        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
2687        /// the same YAML string.
2688        ///
2689        /// The configuration file describes a [`SystemConfig`] and a [`YubiHsm2Config`] object.
2690        #[rstest]
2691        #[cfg(feature = "_yubihsm2-mockhsm")]
2692        fn roundtrip_yaml_config_mockhsm(
2693            #[files("../fixtures/config/yubihsm2_mockhsm_backend/*.yaml")] path: PathBuf,
2694        ) -> TestResult {
2695            let config_string = read_to_string(&path)?;
2696            let config = Config::from_file_path(&path)?;
2697
2698            assert_eq!(config.to_yaml_string()?, config_string);
2699
2700            Ok(())
2701        }
2702
2703        /// Ensures, that [`AdministrativeSecretHandling`] and
2704        /// [`NonAdministrativeSecretHandling`]can be retrieved from a
2705        /// [`UserBackendConnection`].
2706        #[rstest]
2707        fn user_backend_connection_secret_handling(
2708            default_config: TestResult<Config>,
2709        ) -> TestResult {
2710            let config = default_config?;
2711            let admin_secret_handling = AdministrativeSecretHandling::ShamirsSecretSharing {
2712                number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
2713                threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
2714            };
2715            let non_admin_secret_handling = NonAdministrativeSecretHandling::SystemdCreds;
2716
2717            let user_backend_connection = config
2718                .user_backend_connection(&"yubihsm2-signing".parse()?)
2719                .expect("there to be a mapping of the requested name");
2720
2721            assert_eq!(
2722                user_backend_connection.admin_secret_handling(),
2723                admin_secret_handling
2724            );
2725            assert_eq!(
2726                user_backend_connection.non_admin_secret_handling(),
2727                non_admin_secret_handling
2728            );
2729
2730            Ok(())
2731        }
2732
2733        /// Ensures, that [`SystemUserConfigState`] can be created from [`Config`].
2734        #[rstest]
2735        fn system_user_config_state_from_config(default_config: TestResult<Config>) -> TestResult {
2736            let config = default_config?;
2737            let state = SystemUserConfigState::from(&config);
2738
2739            assert_eq!(state.system_user_data, config.system_user_data(),);
2740            Ok(())
2741        }
2742    }
2743
2744    /// Tests, that are only available when using all available backends.
2745    #[cfg(all(feature = "nethsm", feature = "yubihsm2"))]
2746    mod all_backends {
2747        use log::LevelFilter;
2748        use pretty_assertions::assert_eq;
2749        use signstar_common::logging::setup_terminal_logging;
2750
2751        use super::*;
2752        use crate::config::{
2753            MappingAuthorizedKeyEntry,
2754            MappingSystemUserId,
2755            SystemUserData,
2756            traits::ConfigSystemUserData,
2757        };
2758
2759        /// Creates a default [`Config`] for testing purposes.
2760        #[fixture]
2761        fn default_config(
2762            default_system_config: TestResult<SystemConfig>,
2763            default_nethsm_config: TestResult<NetHsmConfig>,
2764            default_yubihsm2_config: TestResult<YubiHsm2Config>,
2765        ) -> TestResult<Config> {
2766            Ok(ConfigBuilder::new(default_system_config?)
2767                .set_nethsm_config(default_nethsm_config?)
2768                .set_yubihsm2_config(default_yubihsm2_config?)
2769                .finish()?)
2770        }
2771
2772        /// List of raw data required to create [`SystemUserData`] for each item in the default
2773        /// [`Config`].
2774        #[fixture]
2775        fn raw_user_data(
2776            raw_user_data_system: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2777            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2778            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2779        ) -> TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>> {
2780            let mut data = raw_user_data_system?;
2781            data.extend(raw_user_data_nethsm?);
2782            data.extend(raw_user_data_yubihsm2?);
2783            Ok(data)
2784        }
2785
2786        /// Ensures that [`MappingSystemUserId`] for [`UserBackendConnection`] works as intended.
2787        #[rstest]
2788        fn user_backend_connection_system_user_id(
2789            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2790            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2791        ) -> TestResult {
2792            let raw_user_data_nethsm = raw_user_data_nethsm?;
2793            let data = UserBackendConnection::NetHsm {
2794                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
2795                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
2796                connections: BTreeSet::from_iter([Connection::new(
2797                    "https://nethsm1.example.org/".parse()?,
2798                    ConnectionSecurity::Unsafe,
2799                )]),
2800                mapping: NetHsmUserMapping::Backup {
2801                    backend_user: "backup".parse()?,
2802                    ssh_authorized_key: raw_user_data_nethsm[1]
2803                        .1
2804                        .clone()
2805                        .expect("to have an SSH authorized key"),
2806                    system_user: raw_user_data_nethsm[1].0.clone(),
2807                },
2808            };
2809            assert_eq!(data.system_user_id(), Some(&raw_user_data_nethsm[1].0));
2810
2811            let raw_user_data_yubihsm2 = raw_user_data_yubihsm2?;
2812            let data = UserBackendConnection::YubiHsm2 {
2813                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
2814                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
2815                connections: BTreeSet::from_iter([YubiHsm2Connection::Usb {
2816                    serial_number: "0123456789".parse()?,
2817                }]),
2818                mapping: YubiHsm2UserMapping::AuditLog {
2819                    authentication_key_id: "1".parse()?,
2820                    ssh_authorized_key: raw_user_data_yubihsm2[1]
2821                        .1
2822                        .clone()
2823                        .expect("to have an SSH authorized key"),
2824                    system_user: raw_user_data_yubihsm2[1].0.clone(),
2825                },
2826            };
2827            assert_eq!(data.system_user_id(), Some(&raw_user_data_yubihsm2[1].0));
2828
2829            Ok(())
2830        }
2831
2832        /// Ensures that [`MappingAuthorizedKeyEntry`] for [`UserBackendConnection`] works as
2833        /// intended.
2834        #[rstest]
2835        fn user_backend_connection_authorized_key_entry(
2836            raw_user_data_nethsm: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2837            raw_user_data_yubihsm2: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
2838        ) -> TestResult {
2839            let raw_user_data_nethsm = raw_user_data_nethsm?;
2840            let data = UserBackendConnection::NetHsm {
2841                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
2842                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
2843                connections: BTreeSet::from_iter([Connection::new(
2844                    "https://nethsm1.example.org/".parse()?,
2845                    ConnectionSecurity::Unsafe,
2846                )]),
2847                mapping: NetHsmUserMapping::Backup {
2848                    backend_user: "backup".parse()?,
2849                    ssh_authorized_key: raw_user_data_nethsm[1]
2850                        .1
2851                        .clone()
2852                        .expect("to have an SSH authorized key"),
2853                    system_user: raw_user_data_nethsm[1].0.clone(),
2854                },
2855            };
2856            assert_eq!(
2857                data.authorized_key_entry(),
2858                Some(
2859                    raw_user_data_nethsm[1]
2860                        .1
2861                        .as_ref()
2862                        .expect("to have an SSH authorized key")
2863                )
2864            );
2865
2866            let raw_user_data_yubihsm2 = raw_user_data_yubihsm2?;
2867            let data = UserBackendConnection::YubiHsm2 {
2868                admin_secret_handling: AdministrativeSecretHandling::Plaintext,
2869                non_admin_secret_handling: NonAdministrativeSecretHandling::Plaintext,
2870                connections: BTreeSet::from_iter([YubiHsm2Connection::Usb {
2871                    serial_number: "0123456789".parse()?,
2872                }]),
2873                mapping: YubiHsm2UserMapping::AuditLog {
2874                    authentication_key_id: "1".parse()?,
2875                    ssh_authorized_key: raw_user_data_yubihsm2[1]
2876                        .1
2877                        .clone()
2878                        .expect("to have an SSH authorized key"),
2879                    system_user: raw_user_data_yubihsm2[1].0.clone(),
2880                },
2881            };
2882            assert_eq!(
2883                data.authorized_key_entry(),
2884                Some(
2885                    raw_user_data_yubihsm2[1]
2886                        .1
2887                        .as_ref()
2888                        .expect("to have an SSH authorized key")
2889                )
2890            );
2891
2892            Ok(())
2893        }
2894
2895        /// Ensures, that [`ConfigBuilder::finish`] fails on issues with overlapping data in
2896        /// configuration components.
2897        ///
2898        /// Here, custom [`NetHsmConfig`] and [`YubiHsm2Config`] objects are staged together with a
2899        /// default [`SystemConfig`] (created by [`default_system_config`]) to create a failure
2900        /// scenario.
2901        #[rstest]
2902        #[case::backend_overlap_duplicate_system_users_two_duplicate_ssh_public_keys(
2903            "Configuration with system-wide, NetHSM and YubiHSM2 configuration has two duplicate system users and two duplicate SSH public keys in the backends",
2904            NetHsmConfig::new(
2905                BTreeSet::from_iter([
2906                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2907                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2908                ]),
2909                BTreeSet::from_iter([
2910                    NetHsmUserMapping::Admin("admin".parse()?),
2911                    NetHsmUserMapping::Backup{
2912                        backend_user: "backup".parse()?,
2913                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
2914                        system_user: "duplicate-backup".parse()?,
2915                    },
2916                    NetHsmUserMapping::HermeticMetrics {
2917                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
2918                        system_user: "nethsm-hermetic-metrics".parse()?,
2919                    },
2920                    NetHsmUserMapping::Metrics {
2921                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
2922                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
2923                        system_user: "duplicate-metrics".parse()?,
2924                    },
2925                    NetHsmUserMapping::Signing {
2926                        backend_user: "signing".parse()?,
2927                        signing_key_id: "signing1".parse()?,
2928                        key_setup: SigningKeySetup::new(
2929                            KeyType::Curve25519,
2930                            vec![KeyMechanism::EdDsaSignature],
2931                            None,
2932                            SignatureType::EdDsa,
2933                            CryptographicKeyContext::OpenPgp {
2934                                user_ids: OpenPgpUserIdList::new(vec![
2935                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2936                                ])?,
2937                                version: "v4".parse()?,
2938                                notations: Default::default(),
2939                            },
2940                        )?,
2941                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
2942                        system_user: "nethsm-signing".parse()?,
2943                        tag: "nethsm-signing1".to_string(),
2944                    }
2945                ]),
2946            )?,
2947            YubiHsm2Config::new(
2948                BTreeSet::from_iter([
2949                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
2950                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
2951                ]),
2952                BTreeSet::from_iter([
2953                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
2954                    YubiHsm2UserMapping::AuditLog {
2955                        authentication_key_id: "3".parse()?,
2956                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
2957                        system_user: "duplicate-metrics".parse()?,
2958                    },
2959                    YubiHsm2UserMapping::Backup {
2960                        authentication_key_id: "2".parse()?,
2961                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
2962                        system_user: "duplicate-backup".parse()?,
2963                    },
2964                    YubiHsm2UserMapping::HermeticAuditLog {
2965                        authentication_key_id: "4".parse()?,
2966                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
2967                    },
2968                    YubiHsm2UserMapping::Signing {
2969                        authentication_key_id: "5".parse()?,
2970                        key_setup: SigningKeySetup::new(
2971                            KeyType::Curve25519,
2972                            vec![KeyMechanism::EdDsaSignature],
2973                            None,
2974                            SignatureType::EdDsa,
2975                            CryptographicKeyContext::OpenPgp {
2976                                user_ids: OpenPgpUserIdList::new(vec![
2977                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
2978                                ])?,
2979                                version: "v4".parse()?,
2980                                notations: Default::default(),
2981                            },
2982                        )?,
2983                        signing_key_id: "1".parse()?,
2984                        domain: Domain::One,
2985                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
2986                        system_user: "yubihsm2-signing".parse()? }
2987                ]),
2988            )?,
2989        )]
2990        #[case::backend_overlap_one_duplicate_system_user(
2991            "Configuration with system-wide, NetHSM and YubiHSM2 configuration has one duplicate system user in the backends",
2992            NetHsmConfig::new(
2993                BTreeSet::from_iter([
2994                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
2995                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
2996                ]),
2997                BTreeSet::from_iter([
2998                    NetHsmUserMapping::Admin("admin".parse()?),
2999                    NetHsmUserMapping::Backup{
3000                        backend_user: "backup".parse()?,
3001                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
3002                        system_user: "duplicate-backup".parse()?,
3003                    },
3004                    NetHsmUserMapping::HermeticMetrics {
3005                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
3006                        system_user: "nethsm-hermetic-metrics".parse()?,
3007                    },
3008                    NetHsmUserMapping::Metrics {
3009                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
3010                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
3011                        system_user: "nethsm-metrics".parse()?,
3012                    },
3013                    NetHsmUserMapping::Signing {
3014                        backend_user: "signing".parse()?,
3015                        signing_key_id: "signing1".parse()?,
3016                        key_setup: SigningKeySetup::new(
3017                            KeyType::Curve25519,
3018                            vec![KeyMechanism::EdDsaSignature],
3019                            None,
3020                            SignatureType::EdDsa,
3021                            CryptographicKeyContext::OpenPgp {
3022                                user_ids: OpenPgpUserIdList::new(vec![
3023                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3024                                ])?,
3025                                version: "v4".parse()?,
3026                                notations: Default::default(),
3027                            },
3028                        )?,
3029                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3030                        system_user: "nethsm-signing".parse()?,
3031                        tag: "nethsm-signing1".to_string(),
3032                    }
3033                ]),
3034            )?,
3035            YubiHsm2Config::new(
3036                BTreeSet::from_iter([
3037                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3038                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3039                ]),
3040                BTreeSet::from_iter([
3041                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
3042                    YubiHsm2UserMapping::AuditLog {
3043                        authentication_key_id: "3".parse()?,
3044                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
3045                        system_user: "yubihsm2-audit-log".parse()?,
3046                    },
3047                    YubiHsm2UserMapping::Backup {
3048                        authentication_key_id: "2".parse()?,
3049                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
3050                        system_user: "duplicate-backup".parse()?,
3051                    },
3052                    YubiHsm2UserMapping::HermeticAuditLog {
3053                        authentication_key_id: "4".parse()?,
3054                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
3055                    },
3056                    YubiHsm2UserMapping::Signing {
3057                        authentication_key_id: "5".parse()?,
3058                        key_setup: SigningKeySetup::new(
3059                            KeyType::Curve25519,
3060                            vec![KeyMechanism::EdDsaSignature],
3061                            None,
3062                            SignatureType::EdDsa,
3063                            CryptographicKeyContext::OpenPgp {
3064                                user_ids: OpenPgpUserIdList::new(vec![
3065                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3066                                ])?,
3067                                version: "v4".parse()?,
3068                                notations: Default::default(),
3069                            },
3070                        )?,
3071                        signing_key_id: "1".parse()?,
3072                        domain: Domain::One,
3073                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3074                        system_user: "yubihsm2-signing".parse()? }
3075                ]),
3076            )?,
3077        )]
3078        #[case::system_overlap_duplicate_system_users_two_duplicate_ssh_public_keys(
3079            "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",
3080            NetHsmConfig::new(
3081                BTreeSet::from_iter([
3082                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3083                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3084                ]),
3085                BTreeSet::from_iter([
3086                    NetHsmUserMapping::Admin("admin".parse()?),
3087                    NetHsmUserMapping::Backup{
3088                        backend_user: "backup".parse()?,
3089                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
3090                        system_user: "duplicate-backup".parse()?,
3091                    },
3092                    NetHsmUserMapping::Metrics {
3093                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
3094                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
3095                        system_user: "duplicate-metrics".parse()?,
3096                    },
3097                    NetHsmUserMapping::HermeticMetrics {
3098                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
3099                        system_user: "nethsm-hermetic-metrics".parse()?,
3100                    },
3101                    NetHsmUserMapping::Signing {
3102                        backend_user: "signing".parse()?,
3103                        signing_key_id: "signing1".parse()?,
3104                        key_setup: SigningKeySetup::new(
3105                            KeyType::Curve25519,
3106                            vec![KeyMechanism::EdDsaSignature],
3107                            None,
3108                            SignatureType::EdDsa,
3109                            CryptographicKeyContext::OpenPgp {
3110                                user_ids: OpenPgpUserIdList::new(vec![
3111                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3112                                ])?,
3113                                version: "v4".parse()?,
3114                                notations: Default::default(),
3115                            },
3116                        )?,
3117                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3118                        system_user: "nethsm-signing".parse()?,
3119                        tag: "nethsm-signing1".to_string(),
3120                    }
3121                ]),
3122            )?,
3123            YubiHsm2Config::new(
3124                BTreeSet::from_iter([
3125                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3126                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3127                ]),
3128                BTreeSet::from_iter([
3129                    YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
3130                    YubiHsm2UserMapping::Backup {
3131                        authentication_key_id: "2".parse()?,
3132                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
3133                        system_user: "duplicate-backup".parse()?,
3134                    },
3135                    YubiHsm2UserMapping::AuditLog {
3136                        authentication_key_id: "3".parse()?,
3137                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
3138                        system_user: "duplicate-metrics".parse()?,
3139                    },
3140                    YubiHsm2UserMapping::HermeticAuditLog {
3141                        authentication_key_id: "4".parse()?,
3142                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
3143                    },
3144                    YubiHsm2UserMapping::Signing {
3145                        authentication_key_id: "5".parse()?,
3146                        key_setup: SigningKeySetup::new(
3147                            KeyType::Curve25519,
3148                            vec![KeyMechanism::EdDsaSignature],
3149                            None,
3150                            SignatureType::EdDsa,
3151                            CryptographicKeyContext::OpenPgp {
3152                                user_ids: OpenPgpUserIdList::new(vec![
3153                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3154                                ])?,
3155                                version: "v4".parse()?,
3156                                notations: Default::default(),
3157                            },
3158                        )?,
3159                        signing_key_id: "1".parse()?,
3160                        domain: Domain::One,
3161                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3162                        system_user: "yubihsm2-signing".parse()? }
3163                ]),
3164            )?,
3165        )]
3166        fn config_fails_validation(
3167            default_system_config: TestResult<SystemConfig>,
3168            #[case] description: &str,
3169            #[case] nethsm_config: NetHsmConfig,
3170            #[case] yubihsm2_config: YubiHsm2Config,
3171        ) -> TestResult {
3172            let error_message = match ConfigBuilder::new(default_system_config?)
3173                .set_nethsm_config(nethsm_config)
3174                .set_yubihsm2_config(yubihsm2_config)
3175                .finish()
3176            {
3177                Err(error) => error.to_string(),
3178                Ok(config) => panic!(
3179                    "Expected to fail with Error::Validation, but succeeded instead: {}",
3180                    config.to_yaml_string()?
3181                ),
3182            };
3183
3184            with_settings!({
3185                description => description,
3186                snapshot_path => SNAPSHOT_PATH,
3187                prepend_module_to_snapshot => false,
3188            }, {
3189                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), error_message);
3190            });
3191
3192            Ok(())
3193        }
3194
3195        /// Ensures, that an optional [`UserBackendConnection`] can be retrieved from a [`Config`].
3196        #[rstest]
3197        #[case::nethsm_signing(
3198            "nethsm-signing",
3199            Some(UserBackendConnection::NetHsm {
3200                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3201                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3202                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3203                },
3204                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3205                connections: BTreeSet::from_iter([
3206                    Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3207                    Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3208                ]),
3209                mapping: NetHsmUserMapping::Signing {
3210                    backend_user: "signing".parse()?,
3211                    signing_key_id: "signing1".parse()?,
3212                    key_setup: SigningKeySetup::new(
3213                        KeyType::Curve25519,
3214                        vec![KeyMechanism::EdDsaSignature],
3215                        None,
3216                        SignatureType::EdDsa,
3217                        CryptographicKeyContext::OpenPgp {
3218                            user_ids: OpenPgpUserIdList::new(vec![
3219                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3220                            ])?,
3221                            version: "v4".parse()?,
3222                            notations: Default::default(),
3223                        },
3224                    )?,
3225                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3226                    system_user: "nethsm-signing".parse()?,
3227                    tag: "signing1".to_string(),
3228                }
3229            })
3230        )]
3231        #[case::yubihsm2_signing(
3232            "yubihsm2-signing",
3233            Some(UserBackendConnection::YubiHsm2 {
3234                admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3235                    number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3236                    threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3237                },
3238                non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3239                connections: BTreeSet::from_iter([
3240                    YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3241                    YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3242                ]),
3243                mapping: YubiHsm2UserMapping::Signing {
3244                    authentication_key_id: "5".parse()?,
3245                    signing_key_id: "1".parse()?,
3246                    key_setup: SigningKeySetup::new(
3247                        KeyType::Curve25519,
3248                        vec![KeyMechanism::EdDsaSignature],
3249                        None,
3250                        SignatureType::EdDsa,
3251                        CryptographicKeyContext::OpenPgp {
3252                            user_ids: OpenPgpUserIdList::new(vec![
3253                                "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3254                            ])?,
3255                            version: "v4".parse()?,
3256                            notations: Default::default(),
3257                        },
3258                    )?,
3259                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3260                    system_user: "yubihsm2-signing".parse()?,
3261                    domain: Domain::One,
3262                }
3263            })
3264        )]
3265        #[case::none("foo", None)]
3266        fn config_user_backend_connection(
3267            default_config: TestResult<Config>,
3268            #[case] system_user: &str,
3269            #[case] expected_connection: Option<UserBackendConnection>,
3270        ) -> TestResult {
3271            let config = default_config?;
3272            assert_eq!(
3273                expected_connection,
3274                config.user_backend_connection(&system_user.parse()?)
3275            );
3276
3277            Ok(())
3278        }
3279
3280        /// Ensures, that [`Config::user_backend_connections`] returns the correct list of
3281        /// [`UserBackendConnection`] items according to a set of [`UserBackendConnectionFilter`].
3282        #[rstest]
3283        #[case::no_filter(
3284            &[],
3285            vec![
3286                UserBackendConnection::NetHsm {
3287                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3288                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3289                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3290                    },
3291                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3292                    connections: BTreeSet::from_iter([
3293                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3294                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3295                    ]),
3296                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
3297                },
3298                UserBackendConnection::NetHsm {
3299                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3300                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3301                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3302                    },
3303                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3304                    connections: BTreeSet::from_iter([
3305                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3306                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3307                    ]),
3308                    mapping: NetHsmUserMapping::Backup{
3309                        backend_user: "backup".parse()?,
3310                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
3311                        system_user: "nethsm-backup".parse()?,
3312                    }
3313                },
3314                UserBackendConnection::NetHsm {
3315                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3316                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3317                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3318                    },
3319                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3320                    connections: BTreeSet::from_iter([
3321                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3322                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3323                    ]),
3324                    mapping: NetHsmUserMapping::HermeticMetrics {
3325                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
3326                        system_user: "nethsm-hermetic-metrics".parse()?,
3327                    }
3328                },
3329                UserBackendConnection::NetHsm {
3330                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3331                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3332                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3333                    },
3334                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3335                    connections: BTreeSet::from_iter([
3336                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3337                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3338                    ]),
3339                    mapping: NetHsmUserMapping::Metrics {
3340                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
3341                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
3342                        system_user: "nethsm-metrics".parse()?,
3343                    }
3344                },
3345                UserBackendConnection::NetHsm {
3346                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3347                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3348                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3349                    },
3350                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3351                    connections: BTreeSet::from_iter([
3352                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3353                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3354                    ]),
3355                    mapping: NetHsmUserMapping::Signing {
3356                        backend_user: "signing".parse()?,
3357                        signing_key_id: "signing1".parse()?,
3358                        key_setup: SigningKeySetup::new(
3359                            KeyType::Curve25519,
3360                            vec![KeyMechanism::EdDsaSignature],
3361                            None,
3362                            SignatureType::EdDsa,
3363                            CryptographicKeyContext::OpenPgp {
3364                                user_ids: OpenPgpUserIdList::new(vec![
3365                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3366                                ])?,
3367                                version: "v4".parse()?,
3368                                notations: Default::default(),
3369                            },
3370                        )?,
3371                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3372                        system_user: "nethsm-signing".parse()?,
3373                        tag: "signing1".to_string(),
3374                    }
3375                },
3376                UserBackendConnection::YubiHsm2 {
3377                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3378                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3379                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3380                    },
3381                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3382                    connections: BTreeSet::from_iter([
3383                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3384                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3385                    ]),
3386                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
3387                },
3388                UserBackendConnection::YubiHsm2 {
3389                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3390                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3391                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3392                    },
3393                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3394                    connections: BTreeSet::from_iter([
3395                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3396                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3397                    ]),
3398                    mapping: YubiHsm2UserMapping::AuditLog {
3399                        authentication_key_id: "3".parse()?,
3400                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
3401                        system_user: "yubihsm2-audit-log".parse()?,
3402                    },
3403                },
3404                UserBackendConnection::YubiHsm2 {
3405                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3406                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3407                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3408                    },
3409                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3410                    connections: BTreeSet::from_iter([
3411                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3412                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3413                    ]),
3414                    mapping: YubiHsm2UserMapping::Backup{
3415                        authentication_key_id: "2".parse()?,
3416                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
3417                        system_user: "yubihsm2-backup".parse()?,
3418                    },
3419                },
3420                UserBackendConnection::YubiHsm2 {
3421                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3422                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3423                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3424                    },
3425                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3426                    connections: BTreeSet::from_iter([
3427                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3428                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3429                    ]),
3430                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
3431                        authentication_key_id: "4".parse()?,
3432                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
3433                    },
3434                },
3435                UserBackendConnection::YubiHsm2 {
3436                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3437                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3438                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3439                    },
3440                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3441                    connections: BTreeSet::from_iter([
3442                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3443                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3444                    ]),
3445                    mapping: YubiHsm2UserMapping::Signing {
3446                        authentication_key_id: "5".parse()?,
3447                        signing_key_id: "1".parse()?,
3448                        key_setup: SigningKeySetup::new(
3449                            KeyType::Curve25519,
3450                            vec![KeyMechanism::EdDsaSignature],
3451                            None,
3452                            SignatureType::EdDsa,
3453                            CryptographicKeyContext::OpenPgp {
3454                                user_ids: OpenPgpUserIdList::new(vec![
3455                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3456                                ])?,
3457                                version: "v4".parse()?,
3458                                notations: Default::default(),
3459                            },
3460                        )?,
3461                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3462                        system_user: "yubihsm2-signing".parse()?,
3463                        domain: Domain::One,
3464                    }
3465                },
3466            ],
3467        )]
3468        #[case::filter_admin(
3469            &[UserBackendConnectionFilter::Admin],
3470            vec![
3471                UserBackendConnection::NetHsm {
3472                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3473                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3474                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3475                    },
3476                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3477                    connections: BTreeSet::from_iter([
3478                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3479                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3480                    ]),
3481                    mapping: NetHsmUserMapping::Admin("admin".parse()?)
3482                },
3483                UserBackendConnection::YubiHsm2 {
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                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3491                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3492                    ]),
3493                    mapping: YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
3494                },
3495            ],
3496        )]
3497        #[case::filter_non_admin(
3498            &[UserBackendConnectionFilter::NonAdmin],
3499            vec![
3500                UserBackendConnection::NetHsm {
3501                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3502                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3503                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3504                    },
3505                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3506                    connections: BTreeSet::from_iter([
3507                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3508                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3509                    ]),
3510                    mapping: NetHsmUserMapping::Backup{
3511                        backend_user: "backup".parse()?,
3512                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
3513                        system_user: "nethsm-backup".parse()?,
3514                    }
3515                },
3516                UserBackendConnection::NetHsm {
3517                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3518                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3519                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3520                    },
3521                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3522                    connections: BTreeSet::from_iter([
3523                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3524                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3525                    ]),
3526                    mapping: NetHsmUserMapping::HermeticMetrics {
3527                        backend_users: NetHsmMetricsUsers::new("hermeticmetrics".parse()?, vec!["hermetickeymetrics".parse()?])?,
3528                        system_user: "nethsm-hermetic-metrics".parse()?,
3529                    }
3530                },
3531                UserBackendConnection::NetHsm {
3532                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3533                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3534                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3535                    },
3536                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3537                    connections: BTreeSet::from_iter([
3538                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3539                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3540                    ]),
3541                    mapping: NetHsmUserMapping::Metrics {
3542                        backend_users: NetHsmMetricsUsers::new("metrics".parse()?, vec!["keymetrics".parse()?])?,
3543                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
3544                        system_user: "nethsm-metrics".parse()?,
3545                    }
3546                },
3547                UserBackendConnection::NetHsm {
3548                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3549                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3550                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3551                    },
3552                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3553                    connections: BTreeSet::from_iter([
3554                        Connection::new("https://nethsm1.example.org/".parse()?, ConnectionSecurity::Unsafe),
3555                        Connection::new("https://nethsm2.example.org/".parse()?, ConnectionSecurity::Unsafe),
3556                    ]),
3557                    mapping: NetHsmUserMapping::Signing {
3558                        backend_user: "signing".parse()?,
3559                        signing_key_id: "signing1".parse()?,
3560                        key_setup: SigningKeySetup::new(
3561                            KeyType::Curve25519,
3562                            vec![KeyMechanism::EdDsaSignature],
3563                            None,
3564                            SignatureType::EdDsa,
3565                            CryptographicKeyContext::OpenPgp {
3566                                user_ids: OpenPgpUserIdList::new(vec![
3567                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3568                                ])?,
3569                                version: "v4".parse()?,
3570                                notations: Default::default(),
3571                            },
3572                        )?,
3573                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3574                        system_user: "nethsm-signing".parse()?,
3575                        tag: "signing1".to_string(),
3576                    }
3577                },
3578                UserBackendConnection::YubiHsm2 {
3579                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3580                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3581                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3582                    },
3583                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3584                    connections: BTreeSet::from_iter([
3585                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3586                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3587                    ]),
3588                    mapping: YubiHsm2UserMapping::AuditLog {
3589                        authentication_key_id: "3".parse()?,
3590                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
3591                        system_user: "yubihsm2-audit-log".parse()?,
3592                    },
3593                },
3594                UserBackendConnection::YubiHsm2 {
3595                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3596                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3597                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3598                    },
3599                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3600                    connections: BTreeSet::from_iter([
3601                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3602                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3603                    ]),
3604                    mapping: YubiHsm2UserMapping::Backup{
3605                        authentication_key_id: "2".parse()?,
3606                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
3607                        system_user: "yubihsm2-backup".parse()?,
3608                    },
3609                },
3610                UserBackendConnection::YubiHsm2 {
3611                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3612                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3613                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3614                    },
3615                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3616                    connections: BTreeSet::from_iter([
3617                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3618                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3619                    ]),
3620                    mapping: YubiHsm2UserMapping::HermeticAuditLog {
3621                        authentication_key_id: "4".parse()?,
3622                        system_user: "yubihsm2-hermetic-audit-log".parse()?,
3623                    },
3624                },
3625                UserBackendConnection::YubiHsm2 {
3626                    admin_secret_handling: AdministrativeSecretHandling::ShamirsSecretSharing {
3627                        number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3628                        threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3629                    },
3630                    non_admin_secret_handling: NonAdministrativeSecretHandling::SystemdCreds,
3631                    connections: BTreeSet::from_iter([
3632                        YubiHsm2Connection::Usb {serial_number: "0012345678".parse()? },
3633                        YubiHsm2Connection::Usb {serial_number: "0087654321".parse()? },
3634                    ]),
3635                    mapping: YubiHsm2UserMapping::Signing {
3636                        authentication_key_id: "5".parse()?,
3637                        signing_key_id: "1".parse()?,
3638                        key_setup: SigningKeySetup::new(
3639                            KeyType::Curve25519,
3640                            vec![KeyMechanism::EdDsaSignature],
3641                            None,
3642                            SignatureType::EdDsa,
3643                            CryptographicKeyContext::OpenPgp {
3644                                user_ids: OpenPgpUserIdList::new(vec![
3645                                    "Foobar McFooface <foobar@mcfooface.org>".parse()?,
3646                                ])?,
3647                                version: "v4".parse()?,
3648                                notations: Default::default(),
3649                            },
3650                        )?,
3651                        ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3652                        system_user: "yubihsm2-signing".parse()?,
3653                        domain: Domain::One,
3654                    }
3655                },
3656            ],
3657        )]
3658        fn config_user_backend_connections(
3659            default_config: TestResult<Config>,
3660            #[case] filters: &[UserBackendConnectionFilter],
3661            #[case] expected_connections: Vec<UserBackendConnection>,
3662        ) -> TestResult {
3663            setup_terminal_logging(LevelFilter::Debug)?;
3664            let config = default_config?;
3665
3666            assert_eq!(
3667                expected_connections,
3668                config.user_backend_connections(filters)
3669            );
3670
3671            Ok(())
3672        }
3673
3674        /// Ensures, that a [`Config`] object leads to a specific YAML output.
3675        ///
3676        /// In this particular case, a [`SystemConfig`], a [`NetHsmConfig`] and a [`YubiHsm2Config`]
3677        /// object are present.
3678        #[rstest]
3679        fn config_to_yaml_string(
3680            default_system_config: TestResult<SystemConfig>,
3681            default_nethsm_config: TestResult<NetHsmConfig>,
3682            default_yubihsm2_config: TestResult<YubiHsm2Config>,
3683        ) -> TestResult {
3684            let config = ConfigBuilder::new(default_system_config?)
3685                .set_nethsm_config(default_nethsm_config?)
3686                .set_yubihsm2_config(default_yubihsm2_config?)
3687                .finish()?;
3688            let config_str = config.to_yaml_string()?;
3689
3690            with_settings!({
3691                description => "Configuration with system-wide, NetHSM and YubiHSM2 configuration",
3692                snapshot_path => SNAPSHOT_PATH,
3693                prepend_module_to_snapshot => false,
3694            }, {
3695                assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
3696            });
3697
3698            Ok(())
3699        }
3700
3701        /// Ensures, that [`Config::authorized_key_entries`] returns SSH authorized key entries
3702        /// correctly.
3703        #[rstest]
3704        fn config_authorized_key_entries(default_config: TestResult<Config>) -> TestResult {
3705            let config = default_config?;
3706            let expected: HashSet<AuthorizedKeyEntry> = HashSet::from_iter([
3707                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host".parse()?,
3708                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host".parse()?,
3709                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host".parse()?,
3710                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
3711                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxR0Oc+SWXkEvvZPitc6NvjvykgiKc9iauRI7tLYvcp user@host".parse()?,
3712                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETxhCqeZhfzFLfH0KFyw3u/w/dkRBUrft8tQm7DEVzY user@host".parse()?,
3713                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host".parse()?,
3714                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
3715                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
3716                "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
3717            ]);
3718
3719            assert_eq!(
3720                config.authorized_key_entries(),
3721                expected.iter().collect::<HashSet<_>>()
3722            );
3723            Ok(())
3724        }
3725
3726        /// Ensures, that [`Config::system_user_data`] returns [`SystemUserData`] entries correctly.
3727        #[rstest]
3728        fn config_system_user_data(
3729            default_config: TestResult<Config>,
3730            raw_user_data: TestResult<Vec<(SystemUserId, Option<AuthorizedKeyEntry>)>>,
3731        ) -> TestResult {
3732            let config = default_config?;
3733            let raw_user_data = raw_user_data?;
3734            let expected: HashSet<SystemUserData> = HashSet::from_iter([
3735                SystemUserData::HostShareholder {
3736                    system_user: &raw_user_data[0].0,
3737                    ssh_authorized_key: raw_user_data[0]
3738                        .1
3739                        .as_ref()
3740                        .expect("to have SSH authorized key"),
3741                },
3742                SystemUserData::HostShareholder {
3743                    system_user: &raw_user_data[1].0,
3744                    ssh_authorized_key: raw_user_data[1]
3745                        .1
3746                        .as_ref()
3747                        .expect("to have SSH authorized key"),
3748                },
3749                SystemUserData::HostShareholder {
3750                    system_user: &raw_user_data[2].0,
3751                    ssh_authorized_key: raw_user_data[2]
3752                        .1
3753                        .as_ref()
3754                        .expect("to have SSH authorized key"),
3755                },
3756                SystemUserData::HostDownloadNetworkConfig {
3757                    system_user: &raw_user_data[3].0,
3758                    ssh_authorized_key: raw_user_data[3]
3759                        .1
3760                        .as_ref()
3761                        .expect("to have SSH authorized key"),
3762                },
3763                SystemUserData::BackendAdmin {
3764                    system_user: raw_user_data[4].0.clone(),
3765                },
3766                SystemUserData::BackendBackup {
3767                    system_user: &raw_user_data[5].0,
3768                    ssh_authorized_key: raw_user_data[5]
3769                        .1
3770                        .as_ref()
3771                        .expect("to have SSH authorized key"),
3772                },
3773                SystemUserData::BackendHermeticMetrics {
3774                    system_user: &raw_user_data[6].0,
3775                },
3776                SystemUserData::BackendMetrics {
3777                    system_user: &raw_user_data[7].0,
3778                    ssh_authorized_key: raw_user_data[7]
3779                        .1
3780                        .as_ref()
3781                        .expect("to have SSH authorized key"),
3782                },
3783                SystemUserData::BackendSign {
3784                    system_user: &raw_user_data[8].0,
3785                    ssh_authorized_key: raw_user_data[8]
3786                        .1
3787                        .as_ref()
3788                        .expect("to have SSH authorized key"),
3789                },
3790                SystemUserData::BackendMetrics {
3791                    system_user: &raw_user_data[10].0,
3792                    ssh_authorized_key: raw_user_data[10]
3793                        .1
3794                        .as_ref()
3795                        .expect("to have SSH authorized key"),
3796                },
3797                SystemUserData::BackendBackup {
3798                    system_user: &raw_user_data[11].0,
3799                    ssh_authorized_key: raw_user_data[11]
3800                        .1
3801                        .as_ref()
3802                        .expect("to have SSH authorized key"),
3803                },
3804                SystemUserData::BackendHermeticMetrics {
3805                    system_user: &raw_user_data[12].0,
3806                },
3807                SystemUserData::BackendSign {
3808                    system_user: &raw_user_data[13].0,
3809                    ssh_authorized_key: raw_user_data[13]
3810                        .1
3811                        .as_ref()
3812                        .expect("to have SSH authorized key"),
3813                },
3814            ]);
3815
3816            assert_eq!(config.system_user_data(), expected);
3817            Ok(())
3818        }
3819
3820        /// Ensures, that [`Config::system_user_ids`] returns system user IDs correctly.
3821        #[rstest]
3822        fn config_system_user_ids(default_config: TestResult<Config>) -> TestResult {
3823            let config = default_config?;
3824            let expected: HashSet<SystemUserId> = HashSet::from_iter([
3825                "signstar-share-holder1".parse()?,
3826                "signstar-share-holder2".parse()?,
3827                "signstar-share-holder3".parse()?,
3828                "signstar-wireguard-download".parse()?,
3829                "nethsm-backup".parse()?,
3830                "nethsm-hermetic-metrics".parse()?,
3831                "nethsm-metrics".parse()?,
3832                "nethsm-signing".parse()?,
3833                "yubihsm2-audit-log".parse()?,
3834                "yubihsm2-backup".parse()?,
3835                "yubihsm2-hermetic-audit-log".parse()?,
3836                "yubihsm2-signing".parse()?,
3837            ]);
3838
3839            assert_eq!(
3840                config.system_user_ids(),
3841                expected.iter().collect::<HashSet<_>>()
3842            );
3843            Ok(())
3844        }
3845
3846        /// Create a [`Config`] using [`ConfigBuilder`].
3847        #[rstest]
3848        fn config_builder_new(
3849            default_system_config: TestResult<SystemConfig>,
3850            default_nethsm_config: TestResult<NetHsmConfig>,
3851            default_yubihsm2_config: TestResult<YubiHsm2Config>,
3852        ) -> TestResult {
3853            let _config = ConfigBuilder::new(default_system_config?)
3854                .set_nethsm_config(default_nethsm_config?)
3855                .set_yubihsm2_config(default_yubihsm2_config?)
3856                .finish()?;
3857
3858            Ok(())
3859        }
3860
3861        /// Ensures, that a valid [`Config`] can be created from a YAML file and turned back into
3862        /// the same YAML string.
3863        ///
3864        /// The configuration file describes a [`SystemConfig`], [`NetHsmConfig`] and a
3865        /// [`YubiHsm2Config`] object.
3866        #[rstest]
3867        fn roundtrip_yaml_config(
3868            #[files("../fixtures/config/all_backends/*.yaml")] path: PathBuf,
3869        ) -> TestResult {
3870            let config_string = read_to_string(&path)?;
3871            let config = Config::from_file_path(&path)?;
3872
3873            assert_eq!(config.to_yaml_string()?, config_string);
3874
3875            Ok(())
3876        }
3877
3878        /// Ensures, that [`AdministrativeSecretHandling`] and
3879        /// [`NonAdministrativeSecretHandling`]can be retrieved from a
3880        /// [`UserBackendConnection`].
3881        #[rstest]
3882        fn user_backend_connection_secret_handling(
3883            default_config: TestResult<Config>,
3884        ) -> TestResult {
3885            let config = default_config?;
3886            let admin_secret_handling = AdministrativeSecretHandling::ShamirsSecretSharing {
3887                number_of_shares: NonZeroUsize::new(3).expect("3 is larger than 0"),
3888                threshold: NonZeroUsize::new(2).expect("2 is larger than 0"),
3889            };
3890            let non_admin_secret_handling = NonAdministrativeSecretHandling::SystemdCreds;
3891
3892            for user in ["nethsm-signing", "yubihsm2-signing"] {
3893                let user_backend_connection = config
3894                    .user_backend_connection(&user.parse()?)
3895                    .expect("there to be a mapping of the requested name");
3896
3897                assert_eq!(
3898                    user_backend_connection.admin_secret_handling(),
3899                    admin_secret_handling
3900                );
3901                assert_eq!(
3902                    user_backend_connection.non_admin_secret_handling(),
3903                    non_admin_secret_handling
3904                );
3905            }
3906
3907            Ok(())
3908        }
3909    }
3910}