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