Skip to main content

signstar_config/config/state/
nethsm.rs

1//! State representation of Signstar configuration items for a NetHSM backend.
2
3use std::any::Any;
4use std::fmt::Display;
5
6use log::{trace, warn};
7#[cfg(doc)]
8use nethsm::NetHsm;
9use nethsm::{KeyId, NamespaceId, UserId, UserRole};
10use signstar_crypto::key::{CryptographicKeyContext, KeyMechanism, KeyType};
11
12use crate::{
13    config::state::KeyCertificateState,
14    nethsm::{NetHsmConfig, NetHsmUserKeysFilter, state::NetHsmState},
15    state::{StateComparisonReport, StateHandling, StateType},
16};
17
18/// The state of a user.
19///
20/// State may be derived e.g. from a [`NetHsm`] backend or a Signstar configuration file.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct UserState {
23    /// The name of the user.
24    pub name: UserId,
25    /// The role of the user.
26    pub role: UserRole,
27    /// The zero or more tags assigned to the user.
28    pub tags: Vec<String>,
29}
30
31impl Display for UserState {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        write!(f, "{} (role: {}", self.name, self.role)?;
34        if !self.tags.is_empty() {
35            write!(f, "; tags: {}", self.tags.join(", "))?;
36        }
37        write!(f, ")")?;
38
39        Ok(())
40    }
41}
42
43/// A failure that can occur when comparing two sets of [`UserState`].
44#[derive(Debug)]
45pub enum UserStateComparisonFailure {
46    /// A [`UserState`] is present in the left hand side but not in the right hand side.
47    Unmatched {
48        /// The type of state of the left hand side of the comparison.
49        state_type: StateType,
50
51        /// The type of state of the right hand side of the comparison.
52        other_state_type: StateType,
53
54        /// The user state is present in `state_type`, but not in `other_state_type`.
55        user_state: UserState,
56    },
57
58    /// One [`UserState`] does not match another.
59    Mismatch {
60        /// The user state of the left hand side of the comparison.
61        user: UserState,
62
63        /// The type of state of the left hand side of the comparison.
64        state_type: StateType,
65
66        /// The user state of the right hand side of the comparison.
67        other_user: UserState,
68
69        /// The type of state of the right hand side of the comparison.
70        other_state_type: StateType,
71    },
72}
73
74impl Display for UserStateComparisonFailure {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        match self {
77            Self::Unmatched {
78                user_state,
79                state_type,
80                other_state_type,
81            } => {
82                writeln!(
83                    f,
84                    "User state present in {state_type}, but not in {other_state_type}:\n{user_state}"
85                )?;
86            }
87            Self::Mismatch {
88                user,
89                state_type,
90                other_user,
91                other_state_type,
92            } => {
93                writeln!(
94                    f,
95                    "Differing user state between {state_type} (A) and {other_state_type} (B):"
96                )?;
97                writeln!(f, "A: {user}")?;
98                writeln!(f, "B: {other_user}")?;
99            }
100        }
101        Ok(())
102    }
103}
104
105/// A set of [`UserState`].
106#[derive(Debug)]
107pub struct UserStates<'a> {
108    /// The type of state the users are used in.
109    pub state_type: StateType,
110    /// The user states.
111    pub users: &'a [UserState],
112}
113
114impl<'a> UserStates<'a> {
115    /// Compares this [`UserStates`] with another.
116    pub fn compare(&self, other: &UserStates) -> Vec<UserStateComparisonFailure> {
117        let mut failures = Vec::new();
118
119        // Create a list of all unmatched user states in `self.users` and the matched ones in
120        // `other.users`.
121        let (unmatched_self_users, matched_other_users) = {
122            let mut unmatched_self_users = Vec::new();
123            let mut matched_other_users = Vec::new();
124
125            // Get all user states in `self.users` that are also in `other.users` and compare them.
126            // Create a `StateComparisonError::UserStateMismatch` for all mismatching user states.
127            for self_user in self.users.iter() {
128                let Some(other_user) = other.users.iter().find(|user| user.name == self_user.name)
129                else {
130                    unmatched_self_users.push(self_user);
131                    continue;
132                };
133
134                matched_other_users.push(other_user);
135                if self_user != other_user {
136                    failures.push(UserStateComparisonFailure::Mismatch {
137                        user: self_user.clone(),
138                        state_type: self.state_type,
139                        other_user: other_user.clone(),
140                        other_state_type: other.state_type,
141                    });
142                    continue;
143                }
144            }
145
146            (unmatched_self_users, matched_other_users)
147        };
148
149        // If there are unmatched user states `self.users`, add a dedicated error for them.
150        if !unmatched_self_users.is_empty() {
151            for user_state in unmatched_self_users {
152                failures.push(UserStateComparisonFailure::Unmatched {
153                    state_type: self.state_type,
154                    other_state_type: other.state_type,
155                    user_state: user_state.clone(),
156                });
157            }
158        }
159
160        {
161            // If there are unmatched user states for `other.users`, add a dedicated error for them.
162            let mut unmatched_other_users = Vec::new();
163            if matched_other_users.len() != other.users.len() {
164                for other_user in other.users.iter() {
165                    if !matched_other_users.contains(&other_user) {
166                        unmatched_other_users.push(other_user);
167                    };
168                }
169            }
170            if !unmatched_other_users.is_empty() {
171                for user_state in unmatched_other_users {
172                    failures.push(UserStateComparisonFailure::Unmatched {
173                        state_type: other.state_type,
174                        other_state_type: self.state_type,
175                        user_state: user_state.clone(),
176                    });
177                }
178            }
179        }
180
181        failures
182    }
183}
184
185impl<'a> Display for UserStates<'a> {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        writeln!(f, "{} users:", self.state_type)?;
188        for key in self.users.iter() {
189            writeln!(f, "{key}")?;
190        }
191
192        Ok(())
193    }
194}
195
196/// A failure that can occur when comparing two sets of [`UserState`].
197#[derive(Debug)]
198pub enum KeyStateComparisonFailure {
199    /// A [`KeyState`] is present in the left hand side but not in the right hand side.
200    Unmatched {
201        /// The type of state of the left hand side of the comparison.
202        state_type: StateType,
203
204        /// The type of state of the right hand side of the comparison.
205        other_state_type: StateType,
206
207        /// The key states that are present in `state_type`, but not in `other_state_type`.
208        key_state: KeyState,
209    },
210
211    /// One [`KeyState`] does not match another.
212    Mismatch {
213        /// The key state of the left hand side of the comparison.
214        key: KeyState,
215
216        /// The type of state of the left hand side of the comparison.
217        state_type: StateType,
218
219        /// The key state of the right hand side of the comparison.
220        other_key: KeyState,
221
222        /// The type of state of the right hand side of the comparison.
223        other_state_type: StateType,
224    },
225}
226
227impl Display for KeyStateComparisonFailure {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        match self {
230            Self::Unmatched {
231                key_state,
232                state_type,
233                other_state_type,
234            } => {
235                writeln!(
236                    f,
237                    "Key state present in {state_type}, but not in {other_state_type}:\n{key_state}"
238                )?;
239            }
240            Self::Mismatch {
241                key,
242                state_type,
243                other_key,
244                other_state_type,
245            } => {
246                writeln!(
247                    f,
248                    "Differing key state between {state_type} (A) and {other_state_type} (B):"
249                )?;
250                writeln!(f, "A: {key}")?;
251                writeln!(f, "B: {other_key}")?;
252            }
253        }
254        Ok(())
255    }
256}
257
258/// The state of a key.
259///
260/// State may be derived e.g. from a [`NetHsm`] backend or a Signstar configuration file.
261#[derive(Clone, Debug, Eq, PartialEq)]
262pub struct KeyState {
263    /// The name of the key.
264    pub name: KeyId,
265    /// The optional namespace the key is used in.
266    pub namespace: Option<NamespaceId>,
267    /// The zero or more tags assigned to the key.
268    pub tags: Vec<String>,
269    /// The key type of the key.
270    pub key_type: KeyType,
271    /// The mechanisms supported by the key.
272    pub mechanisms: Vec<KeyMechanism>,
273    /// The context in which the key is used.
274    pub key_cert_state: KeyCertificateState,
275}
276
277impl Display for KeyState {
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        write!(f, "{} (", self.name)?;
280        if let Some(namespace) = self.namespace.as_ref() {
281            write!(f, "namespace: {namespace}; ")?;
282        }
283        if !self.tags.is_empty() {
284            write!(f, "tags: {}; ", self.tags.join(", "))?;
285        }
286        write!(f, "type: {}; ", self.key_type)?;
287        write!(
288            f,
289            "mechanisms: {}; ",
290            self.mechanisms
291                .iter()
292                .map(|mechanism| mechanism.to_string())
293                .collect::<Vec<String>>()
294                .join(", ")
295        )?;
296        write!(f, "context: {}", self.key_cert_state)?;
297        write!(f, ")")?;
298
299        Ok(())
300    }
301}
302
303/// A set of [`KeyState`]s used in the same [`StateType`].
304#[derive(Debug)]
305pub struct KeyStates<'a> {
306    /// The type of state the keys are used in.
307    pub state_type: StateType,
308    /// The key states.
309    pub keys: &'a [KeyState],
310}
311
312impl<'a> KeyStates<'a> {
313    /// Compares this [`UserStates`] with another.
314    pub fn compare(&self, other: &KeyStates) -> Vec<KeyStateComparisonFailure> {
315        let mut failures = Vec::new();
316
317        // Create a list of all unmatched key states in `self.keys` and the matched ones in
318        // `other.keys`.
319        let (unmatched_self_keys, matched_other_keys) =
320            {
321                let mut unmatched_self_keys = Vec::new();
322                let mut matched_other_keys = Vec::new();
323
324                // Get all key states in `self.keys` that are also in `other.keys` and compare them.
325                // Create a `StateComparisonError::KeyStateMismatch` for all mismatching key states.
326                for self_key in self.keys.iter() {
327                    let Some(other_key) = other.keys.iter().find(|key| {
328                        key.name == self_key.name && key.namespace == self_key.namespace
329                    }) else {
330                        unmatched_self_keys.push(self_key);
331                        continue;
332                    };
333
334                    matched_other_keys.push(other_key);
335
336                    // Note: If `self_key.key_cert_state` or `other_key.key_cert_state` has a
337                    // `CryptographicKeyContext::Raw`, it cannot have a `KeyCertificateState` other
338                    // than `KeyCertificateState::Empty`, as "raw" keys do not have a certificate.
339                    if (matches!(self_key.key_cert_state, KeyCertificateState::Empty)
340                        && matches!(
341                            other_key.key_cert_state,
342                            KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
343                        ))
344                        || (matches!(
345                            self_key.key_cert_state,
346                            KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
347                        ) && matches!(other_key.key_cert_state, KeyCertificateState::Empty))
348                    {
349                        continue;
350                    }
351
352                    if self_key != other_key {
353                        failures.push(KeyStateComparisonFailure::Mismatch {
354                            state_type: self.state_type,
355                            key: self_key.clone(),
356                            other_state_type: other.state_type,
357                            other_key: other_key.clone(),
358                        })
359                    }
360                }
361
362                (unmatched_self_keys, matched_other_keys)
363            };
364
365        // If there are unmatched key states in `self.keys`, add a dedicated error for them.
366        if !unmatched_self_keys.is_empty() {
367            for key_state in unmatched_self_keys {
368                failures.push(KeyStateComparisonFailure::Unmatched {
369                    state_type: self.state_type,
370                    other_state_type: other.state_type,
371                    key_state: key_state.clone(),
372                });
373            }
374        }
375
376        {
377            // If there are unmatched key states in `other.keys`, add a dedicated error for them.
378            let mut unmatched_other_keys = Vec::new();
379            if matched_other_keys.len() != other.keys.len() {
380                for other_key in other.keys.iter() {
381                    if !matched_other_keys.contains(&other_key) {
382                        unmatched_other_keys.push(other_key);
383                        continue;
384                    };
385                }
386            }
387            if !unmatched_other_keys.is_empty() {
388                for key_state in unmatched_other_keys {
389                    failures.push(KeyStateComparisonFailure::Unmatched {
390                        state_type: other.state_type,
391                        other_state_type: self.state_type,
392                        key_state: key_state.clone(),
393                    });
394                }
395            }
396        }
397
398        failures
399    }
400}
401
402impl<'a> Display for KeyStates<'a> {
403    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404        writeln!(f, "{} keys:", self.state_type)?;
405        for key in self.keys.iter() {
406            writeln!(f, "{key}")?;
407        }
408
409        Ok(())
410    }
411}
412
413/// The state of configuration items for a NetHSM backend in a Signstar configuration.
414#[derive(Debug, Eq, PartialEq)]
415pub struct SignstarConfigNetHsmState {
416    /// The user states.
417    pub(crate) user_states: Vec<UserState>,
418    /// The key states.
419    pub(crate) key_states: Vec<KeyState>,
420}
421
422impl SignstarConfigNetHsmState {
423    /// The specific [`StateType`] of this state.
424    const STATE_TYPE: StateType = StateType::SignstarConfigNetHsm;
425}
426
427impl StateHandling for SignstarConfigNetHsmState {
428    fn state_type(&self) -> StateType {
429        Self::STATE_TYPE
430    }
431
432    fn as_any(&self) -> &dyn Any {
433        self
434    }
435
436    fn compare(&self, other: &dyn StateHandling) -> StateComparisonReport {
437        if !self.is_comparable(other) {
438            trace!(
439                "{} is not compatible with {}",
440                self.state_type(),
441                other.state_type()
442            );
443            return StateComparisonReport::Incompatible {
444                self_state: self.state_type(),
445                other_state: other.state_type(),
446            };
447        }
448
449        let (user_failures, key_failures) = {
450            let (self_user_states, other_user_states, self_key_states, other_key_states) =
451                match other.state_type() {
452                    StateType::SignstarConfigNetHsm => {
453                        let Some(other) =
454                            other.as_any().downcast_ref::<SignstarConfigNetHsmState>()
455                        else {
456                            warn!("Unexpectedly unable to find a {}", other.state_type());
457                            return StateComparisonReport::Incompatible {
458                                self_state: self.state_type(),
459                                other_state: other.state_type(),
460                            };
461                        };
462                        (
463                            UserStates {
464                                state_type: self.state_type(),
465                                users: &self.user_states,
466                            },
467                            UserStates {
468                                state_type: other.state_type(),
469                                users: &other.user_states,
470                            },
471                            KeyStates {
472                                state_type: self.state_type(),
473                                keys: &self.key_states,
474                            },
475                            KeyStates {
476                                state_type: other.state_type(),
477                                keys: &other.key_states,
478                            },
479                        )
480                    }
481                    StateType::NetHsm => {
482                        let Some(other) = other.as_any().downcast_ref::<NetHsmState>() else {
483                            warn!("Unexpectedly unable to find a {}", other.state_type());
484                            return StateComparisonReport::Incompatible {
485                                self_state: self.state_type(),
486                                other_state: other.state_type(),
487                            };
488                        };
489                        (
490                            UserStates {
491                                state_type: self.state_type(),
492                                users: &self.user_states,
493                            },
494                            UserStates {
495                                state_type: other.state_type(),
496                                users: &other.user_states,
497                            },
498                            KeyStates {
499                                state_type: self.state_type(),
500                                keys: &self.key_states,
501                            },
502                            KeyStates {
503                                state_type: other.state_type(),
504                                keys: &other.key_states,
505                            },
506                        )
507                    }
508                    StateType::SignstarConfigYubiHsm2 | StateType::YubiHsm2 => {
509                        return StateComparisonReport::Incompatible {
510                            self_state: self.state_type(),
511                            other_state: other.state_type(),
512                        };
513                    }
514                };
515
516            let user_failures = self_user_states.compare(&other_user_states);
517            let key_failures = self_key_states.compare(&other_key_states);
518
519            (user_failures, key_failures)
520        };
521
522        let failures = {
523            let mut failures: Vec<String> = Vec::new();
524
525            for user_failure in user_failures.iter() {
526                failures.push(user_failure.to_string());
527            }
528            for key_failure in key_failures.iter() {
529                failures.push(key_failure.to_string());
530            }
531
532            failures
533        };
534
535        if !failures.is_empty() {
536            return StateComparisonReport::Failure(failures);
537        }
538
539        StateComparisonReport::Success
540    }
541}
542
543impl From<&NetHsmConfig> for SignstarConfigNetHsmState {
544    fn from(value: &NetHsmConfig) -> Self {
545        let mut key_states: Vec<KeyState> = Vec::new();
546        let mut user_states: Vec<UserState> = Vec::new();
547
548        for mapping in value.mappings() {
549            if let Some(user_key_data) = mapping.nethsm_user_key_data(NetHsmUserKeysFilter::All) {
550                key_states.push(KeyState {
551                    name: user_key_data.key_id.clone(),
552                    namespace: user_key_data.user.namespace().cloned(),
553                    tags: vec![user_key_data.tag.to_string()],
554                    key_type: user_key_data.key_setup.key_type(),
555                    mechanisms: user_key_data.key_setup.key_mechanisms().to_vec(),
556                    key_cert_state: KeyCertificateState::KeyContext(
557                        user_key_data.key_setup.key_context().clone(),
558                    ),
559                })
560            }
561            for user_data in mapping.nethsm_user_data() {
562                user_states.push(UserState {
563                    name: user_data.user.clone(),
564                    role: user_data.role,
565                    tags: user_data
566                        .tag
567                        .map(|tag| vec![tag.to_string()])
568                        .unwrap_or_else(Vec::new),
569                })
570            }
571        }
572
573        SignstarConfigNetHsmState {
574            user_states,
575            key_states,
576        }
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use std::collections::BTreeSet;
583
584    use log::LevelFilter;
585    use nethsm::{
586        Connection,
587        ConnectionSecurity,
588        OpenPgpUserIdList,
589        OpenPgpVersion,
590        SignatureType,
591    };
592    use rstest::rstest;
593    use signstar_common::logging::setup_logging;
594    use signstar_crypto::key::SigningKeySetup;
595    use testresult::TestResult;
596
597    use super::*;
598    use crate::nethsm::NetHsmUserMapping;
599
600    /// Ensures, that a [`SignstarConfigNetHsmState`] can be reliable created from a
601    /// [`NetHsmConfig`].
602    #[rstest]
603    #[case::with_signing_user(
604        NetHsmConfig::new(
605            BTreeSet::from_iter([Connection::new(
606                "https://nethsm.example.org".parse()?,
607                ConnectionSecurity::Unsafe,
608            )]),
609            BTreeSet::from_iter([
610                NetHsmUserMapping::Admin("admin".parse()?),
611                NetHsmUserMapping::Signing {
612                    backend_user: "signing1".parse()?,
613                    signing_key_id: "signing1".parse()?,
614                    key_setup: SigningKeySetup::new(
615                        KeyType::Curve25519,
616                        vec![KeyMechanism::EdDsaSignature],
617                        None,
618                        SignatureType::EdDsa,
619                        CryptographicKeyContext::OpenPgp {
620                            user_ids: OpenPgpUserIdList::new(vec![
621                                "John Doe <john@example.org>".parse()?,
622                            ])?,
623                            version: OpenPgpVersion::V4,
624                        },
625                    )?,
626                    ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
627                    system_user: "nethsm-signing1".parse()?,
628                    tag: "tag1".to_string(),
629                },
630            ]),
631        )?,
632        SignstarConfigNetHsmState {
633            user_states: vec![
634                UserState {
635                    name: "admin".parse()?,
636                    role: UserRole::Administrator,
637                    tags: Vec::new(),
638                },
639                UserState {
640                    name: "signing1".parse()?,
641                    role: UserRole::Operator,
642                    tags: vec!["tag1".to_string()],
643                },
644            ],
645            key_states: vec![KeyState {
646                name: "signing1".parse()?,
647                namespace: None,
648                key_type: KeyType::Curve25519,
649                tags: vec!["tag1".to_string()],
650                mechanisms: vec![KeyMechanism::EdDsaSignature],
651                key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::OpenPgp {
652                    user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
653                    version: OpenPgpVersion::V4,
654                }),
655            }],
656        }
657    )]
658    #[case::without_signing_user(
659        NetHsmConfig::new(
660            BTreeSet::from_iter([Connection::new(
661                "https://nethsm.example.org".parse()?,
662                ConnectionSecurity::Unsafe,
663            )]),
664            BTreeSet::from_iter([
665                NetHsmUserMapping::Admin("admin".parse()?),
666            ]),
667        )?,
668        SignstarConfigNetHsmState {
669            user_states: vec![
670                UserState {
671                    name: "admin".parse()?,
672                    role: UserRole::Administrator,
673                    tags: Vec::new(),
674                },
675            ],
676            key_states: Vec::new(),
677        }
678    )]
679    fn signstar_state_nethsm_from_nethsm_config(
680        #[case] input: NetHsmConfig,
681        #[case] expected: SignstarConfigNetHsmState,
682    ) -> TestResult {
683        assert_eq!(expected, SignstarConfigNetHsmState::from(&input));
684
685        Ok(())
686    }
687
688    /// Ensures that [`UserState::to_string`] shows correctly.
689    #[rstest]
690    #[case(
691        UserState{
692            name: "testuser".parse()?,
693            role: UserRole::Operator,
694            tags: vec!["tag1".to_string(), "tag2".to_string()]
695        },
696        "testuser (role: Operator; tags: tag1, tag2)",
697    )]
698    #[case(
699        UserState{
700            name: "testuser".parse()?,
701            role: UserRole::Operator,
702            tags: Vec::new(),
703        },
704        "testuser (role: Operator)",
705    )]
706    #[case(
707        UserState{
708            name: "testuser".parse()?,
709            role: UserRole::Metrics,
710            tags: Vec::new(),
711        },
712        "testuser (role: Metrics)",
713    )]
714    #[case(
715        UserState{
716            name: "testuser".parse()?,
717            role: UserRole::Backup,
718            tags: Vec::new(),
719        },
720        "testuser (role: Backup)",
721    )]
722    #[case(
723        UserState{name:
724            "testuser".parse()?,
725            role: UserRole::Administrator,
726            tags: Vec::new(),
727        },
728        "testuser (role: Administrator)",
729    )]
730    fn user_state_to_string(#[case] user_state: UserState, #[case] expected: &str) -> TestResult {
731        setup_logging(LevelFilter::Debug)?;
732
733        assert_eq!(user_state.to_string(), expected);
734        Ok(())
735    }
736
737    /// Ensures that [`KeyState::to_string`] shows correctly.
738    #[rstest]
739    #[case::namespaced_key_with_openpgp_v4_cert(
740        KeyState{
741            name: "key1".parse()?,
742            namespace: Some("ns1".parse()?),
743            tags: vec!["tag1".to_string(), "tag2".to_string()],
744            key_type: KeyType::Curve25519,
745            mechanisms: vec![KeyMechanism::EdDsaSignature],
746            key_cert_state: KeyCertificateState::KeyContext(
747                CryptographicKeyContext::OpenPgp {
748                    user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
749                    version: OpenPgpVersion::V4,
750                })
751        },
752        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: \"John Doe <john@example.org>\"))",
753    )]
754    #[case::namespaced_key_with_raw_cert(
755        KeyState{
756            name: "key1".parse()?,
757            namespace: Some("ns1".parse()?),
758            tags: vec!["tag1".to_string(), "tag2".to_string()],
759            key_type: KeyType::Curve25519,
760            mechanisms: vec![KeyMechanism::EdDsaSignature],
761            key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
762        },
763        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Raw)",
764    )]
765    #[case::namespaced_key_with_no_cert(
766        KeyState{
767            name: "key1".parse()?,
768            namespace: Some("ns1".parse()?),
769            tags: vec!["tag1".to_string(), "tag2".to_string()],
770            key_type: KeyType::Curve25519,
771            mechanisms: vec![KeyMechanism::EdDsaSignature],
772            key_cert_state: KeyCertificateState::Empty
773        },
774        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Empty)",
775    )]
776    #[case::namespaced_key_with_cert_error(
777        KeyState{
778            name: "key1".parse()?,
779            namespace: Some("ns1".parse()?),
780            tags: vec!["tag1".to_string(), "tag2".to_string()],
781            key_type: KeyType::Curve25519,
782            mechanisms: vec![KeyMechanism::EdDsaSignature],
783            key_cert_state: KeyCertificateState::Error { message: "the dog ate it".to_string() }
784        },
785        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Error retrieving key certificate - the dog ate it)",
786    )]
787    #[case::namespaced_key_with_not_a_cert_context(
788        KeyState{
789            name: "key1".parse()?,
790            namespace: Some("ns1".parse()?),
791            tags: vec!["tag1".to_string(), "tag2".to_string()],
792            key_type: KeyType::Curve25519,
793            mechanisms: vec![KeyMechanism::EdDsaSignature],
794            key_cert_state: KeyCertificateState::NotACryptographicKeyContext { message: "failed to convert".to_string() }
795        },
796        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Not a cryptographic key context - \"failed to convert\")",
797    )]
798    #[case::namespaced_key_with_not_an_openpgp_cert(
799        KeyState{
800            name: "key1".parse()?,
801            namespace: Some("ns1".parse()?),
802            tags: vec!["tag1".to_string(), "tag2".to_string()],
803            key_type: KeyType::Curve25519,
804            mechanisms: vec![KeyMechanism::EdDsaSignature],
805            key_cert_state: KeyCertificateState::NotAnOpenPgpCertificate { message: "it's a blob".to_string() }
806        },
807        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Not an OpenPGP certificate - \"it's a blob\")",
808    )]
809    #[case::system_wide_key_with_no_cert_and_no_tags_and_raw_cert(
810        KeyState{
811            name: "key1".parse()?,
812            namespace: None,
813            tags: Vec::new(),
814            key_type: KeyType::Curve25519,
815            mechanisms: vec![KeyMechanism::EdDsaSignature],
816            key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
817        },
818        "key1 (type: Curve25519; mechanisms: EdDsaSignature; context: Raw)",
819    )]
820    fn key_state_to_string(#[case] key_state: KeyState, #[case] expected: &str) -> TestResult {
821        setup_logging(LevelFilter::Debug)?;
822
823        assert_eq!(key_state.to_string(), expected);
824        Ok(())
825    }
826
827    /// Ensures that [`KeyStates::to_string`] shows correctly.
828    #[rstest]
829    #[case(
830        KeyStates{
831            state_type: StateType::NetHsm,
832            keys: &[KeyState{
833                name: "key1".parse()?,
834                namespace: None,
835                tags: Vec::new(),
836                key_type: KeyType::Curve25519,
837                mechanisms: vec![KeyMechanism::EdDsaSignature],
838                key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
839            }],
840        },
841        "NetHSM keys:\nkey1 (type: Curve25519; mechanisms: EdDsaSignature; context: Raw)\n",
842    )]
843    #[case(
844        KeyStates{
845            state_type: StateType::SignstarConfigNetHsm,
846            keys: &[KeyState{
847                name: "key1".parse()?,
848                namespace: None,
849                tags: Vec::new(),
850                key_type: KeyType::Curve25519,
851                mechanisms: vec![KeyMechanism::EdDsaSignature],
852                key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
853            }],
854        },
855        "Signstar configuration (NetHSM) keys:\nkey1 (type: Curve25519; mechanisms: EdDsaSignature; context: Raw)\n",
856    )]
857    fn key_state_type_display(#[case] key_states: KeyStates, #[case] expected: &str) -> TestResult {
858        setup_logging(LevelFilter::Debug)?;
859
860        assert_eq!(key_states.to_string(), expected);
861        Ok(())
862    }
863
864    /// Ensures that [`UserStates::to_string`] shows correctly.
865    #[rstest]
866    #[case(
867        UserStates{
868            state_type: StateType::NetHsm,
869            users: &[UserState{
870                name: "testuser".parse()?,
871                role: UserRole::Administrator,
872                tags: Vec::new(),
873            }]
874        },
875        "NetHSM users:\ntestuser (role: Administrator)\n",
876    )]
877    #[case(
878        UserStates{
879            state_type: StateType::SignstarConfigNetHsm,
880            users: &[UserState{
881                name: "testuser".parse()?,
882                role: UserRole::Administrator,
883                tags: Vec::new(),
884            }],
885        },
886        "Signstar configuration (NetHSM) users:\ntestuser (role: Administrator)\n",
887    )]
888    fn user_state_type_display(
889        #[case] user_states: UserStates,
890        #[case] expected: &str,
891    ) -> TestResult {
892        setup_logging(LevelFilter::Debug)?;
893
894        assert_eq!(user_states.to_string(), expected);
895        Ok(())
896    }
897
898    /// Ensures that [`StateHandling::compare`] successfully compares [`NetHsmState`] and
899    /// [`SignstarConfigNetHsmState`] containing the same data.
900    #[rstest]
901    #[case::nethsm_vs_config_empty(
902        NetHsmState {
903            user_states: Vec::new(),
904            key_states: Vec::new(),
905        },
906        SignstarConfigNetHsmState {
907            user_states: Vec::new(),
908            key_states: Vec::new(),
909        },
910    )]
911    #[case::config_vs_config_empty(
912        SignstarConfigNetHsmState {
913            user_states: Vec::new(),
914            key_states: Vec::new(),
915        },
916        SignstarConfigNetHsmState {
917            user_states: Vec::new(),
918            key_states: Vec::new(),
919        },
920    )]
921    #[case::nethsm_vs_nethsm_empty(
922        NetHsmState {
923            user_states: Vec::new(),
924            key_states: Vec::new(),
925        },
926        NetHsmState {
927            user_states: Vec::new(),
928            key_states: Vec::new(),
929        },
930    )]
931    #[case::nethsm_vs_config_with_users_and_keys(
932        NetHsmState {
933            user_states: vec![
934                UserState{
935                    name: "operator1".parse()?,
936                    role: UserRole::Operator,
937                    tags: vec!["tag1".to_string()]
938                },
939                UserState{
940                    name: "admin".parse()?,
941                    role: UserRole::Administrator,
942                    tags: Vec::new(),
943                },
944            ],
945            key_states: vec![
946                KeyState{
947                    name: "key1".parse()?,
948                    namespace: None,
949                    tags: vec!["tag1".to_string()],
950                    key_type: KeyType::Curve25519,
951                    mechanisms: vec![KeyMechanism::EdDsaSignature],
952                    key_cert_state: KeyCertificateState::KeyContext(
953                        CryptographicKeyContext::OpenPgp {
954                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
955                            version: OpenPgpVersion::V4,
956                        }
957                    )
958                },
959            ],
960        },
961        SignstarConfigNetHsmState {
962            user_states: vec![
963                UserState{
964                    name: "operator1".parse()?,
965                    role: UserRole::Operator,
966                    tags: vec!["tag1".to_string()]
967                },
968                UserState{
969                    name: "admin".parse()?,
970                    role: UserRole::Administrator,
971                    tags: Vec::new(),
972                },
973            ],
974            key_states: vec![
975                KeyState{
976                    name: "key1".parse()?,
977                    namespace: None,
978                    tags: vec!["tag1".to_string()],
979                    key_type: KeyType::Curve25519,
980                    mechanisms: vec![KeyMechanism::EdDsaSignature],
981                    key_cert_state: KeyCertificateState::KeyContext(
982                        CryptographicKeyContext::OpenPgp {
983                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
984                            version: OpenPgpVersion::V4,
985                        }
986                    )
987                },
988            ],
989        },
990    )]
991    #[case::config_vs_config_with_users_and_keys(
992        SignstarConfigNetHsmState {
993            user_states: vec![
994                UserState{
995                    name: "operator1".parse()?,
996                    role: UserRole::Operator,
997                    tags: vec!["tag1".to_string()]
998                },
999                UserState{
1000                    name: "admin".parse()?,
1001                    role: UserRole::Administrator,
1002                    tags: Vec::new(),
1003                },
1004            ],
1005            key_states: vec![
1006                KeyState{
1007                    name: "key1".parse()?,
1008                    namespace: None,
1009                    tags: vec!["tag1".to_string()],
1010                    key_type: KeyType::Curve25519,
1011                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1012                    key_cert_state: KeyCertificateState::KeyContext(
1013                        CryptographicKeyContext::OpenPgp {
1014                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
1015                            version: OpenPgpVersion::V4,
1016                        }
1017                    )
1018                },
1019            ],
1020        },
1021        SignstarConfigNetHsmState {
1022            user_states: vec![
1023                UserState{
1024                    name: "operator1".parse()?,
1025                    role: UserRole::Operator,
1026                    tags: vec!["tag1".to_string()]
1027                },
1028                UserState{
1029                    name: "admin".parse()?,
1030                    role: UserRole::Administrator,
1031                    tags: Vec::new(),
1032                },
1033            ],
1034            key_states: vec![
1035                KeyState{
1036                    name: "key1".parse()?,
1037                    namespace: None,
1038                    tags: vec!["tag1".to_string()],
1039                    key_type: KeyType::Curve25519,
1040                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1041                    key_cert_state: KeyCertificateState::KeyContext(
1042                        CryptographicKeyContext::OpenPgp {
1043                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
1044                            version: OpenPgpVersion::V4,
1045                        }
1046                    )
1047                },
1048            ],
1049        },
1050    )]
1051    #[case::nethsm_vs_nethsm_with_users_and_keys(
1052        NetHsmState {
1053            user_states: vec![
1054                UserState{
1055                    name: "operator1".parse()?,
1056                    role: UserRole::Operator,
1057                    tags: vec!["tag1".to_string()]
1058                },
1059                UserState{
1060                    name: "admin".parse()?,
1061                    role: UserRole::Administrator,
1062                    tags: Vec::new(),
1063                },
1064            ],
1065            key_states: vec![
1066                KeyState{
1067                    name: "key1".parse()?,
1068                    namespace: None,
1069                    tags: vec!["tag1".to_string()],
1070                    key_type: KeyType::Curve25519,
1071                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1072                    key_cert_state: KeyCertificateState::KeyContext(
1073                        CryptographicKeyContext::OpenPgp {
1074                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
1075                            version: OpenPgpVersion::V4,
1076                        }
1077                    )
1078                },
1079            ],
1080        },
1081        NetHsmState {
1082            user_states: vec![
1083                UserState{
1084                    name: "operator1".parse()?,
1085                    role: UserRole::Operator,
1086                    tags: vec!["tag1".to_string()]
1087                },
1088                UserState{
1089                    name: "admin".parse()?,
1090                    role: UserRole::Administrator,
1091                    tags: Vec::new(),
1092                },
1093            ],
1094            key_states: vec![
1095                KeyState{
1096                    name: "key1".parse()?,
1097                    namespace: None,
1098                    tags: vec!["tag1".to_string()],
1099                    key_type: KeyType::Curve25519,
1100                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1101                    key_cert_state: KeyCertificateState::KeyContext(
1102                        CryptographicKeyContext::OpenPgp {
1103                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
1104                            version: OpenPgpVersion::V4,
1105                        }
1106                    )
1107                },
1108            ],
1109        },
1110    )]
1111    fn state_compare_succeeds(
1112        #[case] state_a: impl StateHandling,
1113        #[case] state_b: impl StateHandling,
1114    ) -> TestResult {
1115        setup_logging(LevelFilter::Trace)?;
1116
1117        let comparison_report = state_a.compare(&state_b);
1118
1119        if !matches!(comparison_report, StateComparisonReport::Success) {
1120            panic!("Comparison should have succeeded but failed:\n{comparison_report:?}")
1121        }
1122
1123        Ok(())
1124    }
1125
1126    /// Ensures that [`StateHandling::compare`] fails on [`NetHsmState`] and
1127    /// [`SignstarConfigNetHsmState`] containing differing data.
1128    #[rstest]
1129    #[case::one_empty(
1130        SignstarConfigNetHsmState {
1131            user_states: vec![
1132                UserState{
1133                    name: "operator1".parse()?,
1134                    role: UserRole::Operator,
1135                    tags: vec!["tag1".to_string()]
1136                },
1137                UserState{
1138                    name: "admin".parse()?,
1139                    role: UserRole::Administrator,
1140                    tags: Vec::new(),
1141                },
1142            ],
1143            key_states: vec![
1144                KeyState{
1145                    name: "key1".parse()?,
1146                    namespace: None,
1147                    tags: vec!["tag1".to_string()],
1148                    key_type: KeyType::Curve25519,
1149                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1150                    key_cert_state: KeyCertificateState::KeyContext(
1151                        CryptographicKeyContext::OpenPgp {
1152                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
1153                            version: OpenPgpVersion::V4,
1154                        }
1155                    )
1156                },
1157            ],
1158        },
1159        NetHsmState {
1160            user_states: Vec::new(),
1161            key_states: Vec::new(),
1162        },
1163        r#"User state present in Signstar configuration (NetHSM), but not in NetHSM:
1164operator1 (role: Operator; tags: tag1)
1165
1166User state present in Signstar configuration (NetHSM), but not in NetHSM:
1167admin (role: Administrator)
1168
1169Key state present in Signstar configuration (NetHSM), but not in NetHSM:
1170key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "John Doe <john@example.org>"))
1171"#,
1172    )]
1173    #[case::differing_users_and_keys(
1174        SignstarConfigNetHsmState {
1175            user_states: vec![
1176                UserState{
1177                    name: "operator1".parse()?,
1178                    role: UserRole::Operator,
1179                    tags: vec!["tag1".to_string()]
1180                },
1181                UserState{
1182                    name: "admin".parse()?,
1183                    role: UserRole::Administrator,
1184                    tags: Vec::new(),
1185                },
1186            ],
1187            key_states: vec![
1188                KeyState{
1189                    name: "key1".parse()?,
1190                    namespace: None,
1191                    tags: vec!["tag1".to_string()],
1192                    key_type: KeyType::Curve25519,
1193                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1194                    key_cert_state: KeyCertificateState::KeyContext(
1195                        CryptographicKeyContext::OpenPgp {
1196                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
1197                            version: OpenPgpVersion::V4,
1198                        }
1199                    )
1200                },
1201            ],
1202        },
1203        NetHsmState {
1204            user_states: vec![
1205                UserState{
1206                    name: "operator2".parse()?,
1207                    role: UserRole::Operator,
1208                    tags: vec!["tag2".to_string(), "tag3".to_string()]
1209                },
1210                UserState{
1211                    name: "admin".parse()?,
1212                    role: UserRole::Administrator,
1213                    tags: Vec::new(),
1214                },
1215            ],
1216            key_states: vec![
1217                KeyState{
1218                    name: "key2".parse()?,
1219                    namespace: None,
1220                    tags: vec!["tag2".to_string()],
1221                    key_type: KeyType::Curve25519,
1222                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1223                    key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
1224                },
1225                KeyState{
1226                    name: "key3".parse()?,
1227                    namespace: None,
1228                    tags: vec!["tag3".to_string()],
1229                    key_type: KeyType::Curve25519,
1230                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1231                    key_cert_state: KeyCertificateState::KeyContext(
1232                        CryptographicKeyContext::OpenPgp {
1233                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
1234                            version: OpenPgpVersion::V4,
1235                        }
1236                    )
1237                },
1238            ],
1239        },
1240        r#"User state present in Signstar configuration (NetHSM), but not in NetHSM:
1241operator1 (role: Operator; tags: tag1)
1242
1243User state present in NetHSM, but not in Signstar configuration (NetHSM):
1244operator2 (role: Operator; tags: tag2, tag3)
1245
1246Key state present in Signstar configuration (NetHSM), but not in NetHSM:
1247key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "John Doe <john@example.org>"))
1248
1249Key state present in NetHSM, but not in Signstar configuration (NetHSM):
1250key2 (tags: tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Raw)
1251
1252Key state present in NetHSM, but not in Signstar configuration (NetHSM):
1253key3 (tags: tag3; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "John Doe <john@example.org>"))
1254"#,
1255    )]
1256    #[case::user_and_key_mismatch(
1257        SignstarConfigNetHsmState {
1258            user_states: vec![
1259                UserState{
1260                    name: "operator1".parse()?,
1261                    role: UserRole::Operator,
1262                    tags: vec!["tag1".to_string()]
1263                },
1264                UserState{
1265                    name: "admin".parse()?,
1266                    role: UserRole::Administrator,
1267                    tags: Vec::new(),
1268                },
1269            ],
1270            key_states: vec![
1271                KeyState{
1272                    name: "key1".parse()?,
1273                    namespace: None,
1274                    tags: vec!["tag1".to_string()],
1275                    key_type: KeyType::Curve25519,
1276                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1277                    key_cert_state: KeyCertificateState::KeyContext(
1278                        CryptographicKeyContext::OpenPgp {
1279                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
1280                            version: OpenPgpVersion::V4,
1281                        }
1282                    )
1283                },
1284            ],
1285        },
1286        NetHsmState {
1287            user_states: vec![
1288                UserState{
1289                    name: "operator1".parse()?,
1290                    role: UserRole::Metrics,
1291                    tags: Vec::new(),
1292                },
1293                UserState{
1294                    name: "admin".parse()?,
1295                    role: UserRole::Administrator,
1296                    tags: Vec::new(),
1297                },
1298            ],
1299            key_states: vec![
1300                KeyState{
1301                    name: "key1".parse()?,
1302                    namespace: None,
1303                    tags: vec!["tag1".to_string()],
1304                    key_type: KeyType::Curve25519,
1305                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1306                    key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
1307                },
1308            ],
1309        },
1310        r#"Differing user state between Signstar configuration (NetHSM) (A) and NetHSM (B):
1311A: operator1 (role: Operator; tags: tag1)
1312B: operator1 (role: Metrics)
1313
1314Differing key state between Signstar configuration (NetHSM) (A) and NetHSM (B):
1315A: key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "John Doe <john@example.org>"))
1316B: key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: Raw)
1317"#,
1318    )]
1319    fn state_compare_fails(
1320        #[case] state_a: impl StateHandling,
1321        #[case] state_b: impl StateHandling,
1322        #[case] expected: &str,
1323    ) -> TestResult {
1324        setup_logging(LevelFilter::Debug)?;
1325
1326        let comparison_report = state_a.compare(&state_b);
1327
1328        match comparison_report {
1329            StateComparisonReport::Success => panic!("Comparison should have failed but succeeded"),
1330            StateComparisonReport::Incompatible { .. } => {
1331                panic!("Comparison should have failed but was incompatible")
1332            }
1333            StateComparisonReport::Failure(failures) => assert_eq!(failures.join("\n"), expected),
1334        }
1335
1336        Ok(())
1337    }
1338
1339    /// A dummy YubiHSM2 backend.
1340    ///
1341    /// This backend is only used in tests as a [`StateHandling`] implementation.
1342    struct DummyYubiHsm2ConfigBackend;
1343
1344    impl DummyYubiHsm2ConfigBackend {
1345        pub fn new() -> Self {
1346            DummyYubiHsm2ConfigBackend
1347        }
1348    }
1349
1350    impl StateHandling for DummyYubiHsm2ConfigBackend {
1351        fn state_type(&self) -> StateType {
1352            StateType::SignstarConfigYubiHsm2
1353        }
1354
1355        fn as_any(&self) -> &dyn Any {
1356            self
1357        }
1358
1359        fn compare(&self, other: &dyn StateHandling) -> StateComparisonReport {
1360            StateComparisonReport::Incompatible {
1361                self_state: self.state_type(),
1362                other_state: other.state_type(),
1363            }
1364        }
1365    }
1366
1367    #[rstest]
1368    #[case::dummy_and_signstar_config_nethsm_state(
1369        DummyYubiHsm2ConfigBackend::new(),
1370        SignstarConfigNetHsmState {
1371            user_states: vec![
1372                UserState{
1373                    name: "operator1".parse()?,
1374                    role: UserRole::Operator,
1375                    tags: vec!["tag1".to_string()]
1376                },
1377                UserState{
1378                    name: "admin".parse()?,
1379                    role: UserRole::Administrator,
1380                    tags: Vec::new(),
1381                },
1382            ],
1383            key_states: vec![
1384                KeyState{
1385                    name: "key1".parse()?,
1386                    namespace: None,
1387                    tags: vec!["tag1".to_string()],
1388                    key_type: KeyType::Curve25519,
1389                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1390                    key_cert_state: KeyCertificateState::KeyContext(
1391                        CryptographicKeyContext::OpenPgp {
1392                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
1393                            version: OpenPgpVersion::V4,
1394                        }
1395                    )
1396                },
1397            ],
1398        },
1399    )]
1400    #[case::dummy_and_nethsm_state(
1401        DummyYubiHsm2ConfigBackend::new(),
1402        NetHsmState {
1403            user_states: vec![
1404                UserState{
1405                    name: "operator1".parse()?,
1406                    role: UserRole::Metrics,
1407                    tags: Vec::new(),
1408                },
1409                UserState{
1410                    name: "admin".parse()?,
1411                    role: UserRole::Administrator,
1412                    tags: Vec::new(),
1413                },
1414            ],
1415            key_states: vec![
1416                KeyState{
1417                    name: "key1".parse()?,
1418                    namespace: None,
1419                    tags: vec!["tag1".to_string()],
1420                    key_type: KeyType::Curve25519,
1421                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1422                    key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
1423                },
1424            ],
1425        },
1426    )]
1427    #[case::signstar_config_nethsm_state_and_dummy(
1428        SignstarConfigNetHsmState {
1429            user_states: vec![
1430                UserState{
1431                    name: "operator1".parse()?,
1432                    role: UserRole::Operator,
1433                    tags: vec!["tag1".to_string()]
1434                },
1435                UserState{
1436                    name: "admin".parse()?,
1437                    role: UserRole::Administrator,
1438                    tags: Vec::new(),
1439                },
1440            ],
1441            key_states: vec![
1442                KeyState{
1443                    name: "key1".parse()?,
1444                    namespace: None,
1445                    tags: vec!["tag1".to_string()],
1446                    key_type: KeyType::Curve25519,
1447                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1448                    key_cert_state: KeyCertificateState::KeyContext(
1449                        CryptographicKeyContext::OpenPgp {
1450                            user_ids: OpenPgpUserIdList::new(vec!["John Doe <john@example.org>".parse()?])?,
1451                            version: OpenPgpVersion::V4,
1452                        }
1453                    )
1454                },
1455            ],
1456        },
1457        DummyYubiHsm2ConfigBackend::new(),
1458    )]
1459    #[case::nethsm_state_and_dummy(
1460        NetHsmState {
1461            user_states: vec![
1462                UserState{
1463                    name: "operator1".parse()?,
1464                    role: UserRole::Metrics,
1465                    tags: Vec::new(),
1466                },
1467                UserState{
1468                    name: "admin".parse()?,
1469                    role: UserRole::Administrator,
1470                    tags: Vec::new(),
1471                },
1472            ],
1473            key_states: vec![
1474                KeyState{
1475                    name: "key1".parse()?,
1476                    namespace: None,
1477                    tags: vec!["tag1".to_string()],
1478                    key_type: KeyType::Curve25519,
1479                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1480                    key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
1481                },
1482            ],
1483        },
1484        DummyYubiHsm2ConfigBackend::new(),
1485    )]
1486    fn state_compare_incompatible(
1487        #[case] state_a: impl StateHandling,
1488        #[case] state_b: impl StateHandling,
1489    ) -> TestResult {
1490        setup_logging(LevelFilter::Debug)?;
1491
1492        let comparison_report = state_a.compare(&state_b);
1493
1494        match comparison_report {
1495            StateComparisonReport::Incompatible { .. } => {}
1496            StateComparisonReport::Success => panic!("Comparison should have failed but succeeded"),
1497            StateComparisonReport::Failure(failures) => panic!(
1498                "Comparison should have been incompatible but failed instead: {}",
1499                failures.join("\n")
1500            ),
1501        }
1502
1503        Ok(())
1504    }
1505}