1use 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#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct UserState {
25 pub name: UserId,
27 pub role: UserRole,
29 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#[derive(Debug)]
47pub enum UserStateComparisonFailure {
48 Unmatched {
50 state_type: StateType,
52
53 other_state_type: StateType,
55
56 user_state: UserState,
58 },
59
60 Mismatch {
62 user: UserState,
64
65 state_type: StateType,
67
68 other_user: UserState,
70
71 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#[derive(Debug)]
109pub struct UserStates<'a> {
110 pub state_type: StateType,
112 pub users: &'a [UserState],
114}
115
116impl<'a> UserStates<'a> {
117 pub fn compare(&self, other: &UserStates) -> Vec<UserStateComparisonFailure> {
119 let mut failures = Vec::new();
120
121 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 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 !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 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#[derive(Debug)]
200pub enum KeyStateComparisonFailure {
201 Unmatched {
203 state_type: StateType,
205
206 other_state_type: StateType,
208
209 key_state: KeyState,
211 },
212
213 Mismatch {
215 key: KeyState,
217
218 state_type: StateType,
220
221 other_key: KeyState,
223
224 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#[derive(Clone, Debug, Eq, PartialEq)]
264pub struct KeyState {
265 pub name: KeyId,
267 pub namespace: Option<NamespaceId>,
269 pub tags: Vec<String>,
271 pub key_type: KeyType,
273 pub mechanisms: Vec<KeyMechanism>,
275 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#[derive(Debug)]
307pub struct KeyStates<'a> {
308 pub state_type: StateType,
310 pub keys: &'a [KeyState],
312}
313
314impl<'a> KeyStates<'a> {
315 pub fn compare(&self, other: &KeyStates) -> Vec<KeyStateComparisonFailure> {
317 let mut failures = Vec::new();
318
319 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 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 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 !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 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#[derive(Debug)]
417pub struct SignstarConfigNetHsmState {
418 pub(crate) user_states: Vec<UserState>,
420 pub(crate) key_states: Vec<KeyState>,
422}
423
424impl SignstarConfigNetHsmState {
425 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 #[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 #[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 #[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 #[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 #[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 #[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 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}