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