1use std::{collections::HashSet, fmt::Display};
4
5use log::{debug, info, warn};
6use signstar_crypto::key::{
7 SigningKeySetup,
8 base::{CryptographicKeyContext, KeyType},
9};
10use signstar_yubihsm2::{
11 automation::ObjectType,
12 backup::Label,
13 object::{
14 AsymmetricAlgorithm,
15 Capabilities,
16 Capability,
17 Domain,
18 Domains,
19 ObjectAlgorithm,
20 WrapKeyKind,
21 },
22 yubihsm::Id,
23};
24
25use crate::{
26 state::{
27 StateDiff,
28 StateDiffFailure,
29 StateDiffFailureTarget,
30 StateDiffReport,
31 StateOrigin,
32 StateOriginInfo,
33 },
34 yubihsm2::{YubiHsm2Backend, YubiHsm2Config, YubiHsm2UserMapping, config::AuthType},
35};
36
37fn implicit_wrap_key_state() -> YubiHsm2BackendUserKeyData {
39 YubiHsm2BackendUserKeyData {
40 id: YubiHsm2Config::WRAP_KEY_ID,
41 object_type: ObjectType::WrapKey,
42 capabilities: Capabilities::from(YubiHsm2UserMapping::CAP_BACKUP),
43 domains: Domains::all(),
44 algorithm: ObjectAlgorithm::Wrap(WrapKeyKind::Aes256),
45 label: YubiHsm2Config::wrap_key_label(),
46 length: 32,
47 }
48}
49
50#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub(crate) struct YubiHsm2BackendUserData {
53 pub id: Id,
55
56 pub capabilities: Capabilities,
58
59 pub domains: Domains,
61}
62
63impl Display for YubiHsm2BackendUserData {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 write!(
66 f,
67 "{} (capabilities: {}; domains: {})",
68 self.id, self.capabilities, self.domains
69 )
70 }
71}
72
73impl PartialEq<YubiHsm2ConfigUserData> for YubiHsm2BackendUserData {
74 fn eq(&self, other: &YubiHsm2ConfigUserData) -> bool {
75 self.id == other.authentication_key_id
76 && self.capabilities == other.capabilities
77 && self.domains == other.domains
78 }
79}
80
81#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
83pub(crate) struct YubiHsm2BackendUserKeyData {
84 pub id: Id,
86
87 pub object_type: ObjectType,
89
90 pub capabilities: Capabilities,
92
93 pub domains: Domains,
95
96 pub algorithm: ObjectAlgorithm,
98
99 pub label: Label,
101
102 pub length: u16,
104}
105
106impl Display for YubiHsm2BackendUserKeyData {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(
109 f,
110 "{} (type: {}; algorithm: {}; capabilities: {}; domains: {}; label: {}; length: {})",
111 self.id,
112 self.object_type,
113 self.algorithm,
114 self.capabilities,
115 self.domains,
116 self.label,
117 self.length
118 )
119 }
120}
121
122impl<'config> PartialEq<YubiHsm2ConfigUserKeyData<'config>> for YubiHsm2BackendUserKeyData {
123 fn eq(&self, other: &YubiHsm2ConfigUserKeyData<'config>) -> bool {
124 self.id == *other.signing_key_id
125 && self.object_type == ObjectType::AsymmetricKey
126 && other.key_setup.key_type() == KeyType::Curve25519
127 && self.algorithm == ObjectAlgorithm::Asymmetric(AsymmetricAlgorithm::Ed25519)
128 && self.capabilities == other.capabilities
129 && self.domains == Domains::from(*other.domain)
130 }
131}
132
133#[derive(Debug, Eq, PartialEq)]
140pub struct YubiHsm2BackendState {
141 pub(crate) user_data: Vec<YubiHsm2BackendUserData>,
143
144 pub(crate) key_data: Vec<YubiHsm2BackendUserKeyData>,
146}
147
148impl YubiHsm2BackendState {
149 pub const STATE_NAME: &'static str = "YubiHSM2 backend";
151}
152
153impl<'admin_creds, 'config> TryFrom<&YubiHsm2Backend<'admin_creds, 'config>>
154 for YubiHsm2BackendState
155{
156 type Error = crate::Error;
157
158 fn try_from(value: &YubiHsm2Backend<'admin_creds, 'config>) -> Result<Self, Self::Error> {
164 debug!(
165 "Retrieve state of the YubiHSM2 backend at {}",
166 value
167 .yubihsm2_config()
168 .connections()
169 .iter()
170 .map(|connection| format!("{connection:?}"))
171 .collect::<Vec<_>>()
172 .join(", ")
173 );
174
175 Ok(Self {
176 user_data: value.user_states()?,
177 key_data: value.key_states()?,
178 })
179 }
180}
181
182impl<'config> PartialEq<YubiHsm2ConfigState<'config>> for YubiHsm2BackendState {
183 fn eq(&self, other: &YubiHsm2ConfigState) -> bool {
184 debug!(
185 "Compare backend state ({} users, {} keys) and config state ({} users, {} keys)",
186 self.user_data.len(),
187 self.key_data.len(),
188 other.user_data.len(),
189 other.key_data.len()
190 );
191
192 let (found_self_user_data, found_other_user_data) = {
193 let mut found_self_user_data: HashSet<&YubiHsm2BackendUserData> = HashSet::new();
194 let mut found_other_user_data: HashSet<&YubiHsm2ConfigUserData> = HashSet::new();
195
196 'outer: for other_user_data in other.user_data.iter() {
197 for self_user_data in self.user_data.iter() {
198 if self_user_data == other_user_data
199 || (other_user_data.auth_type == AuthType::Admin
204 && other_user_data.authentication_key_id == self_user_data.id
205 && other_user_data
206 .capabilities
207 .as_ref()
208 .is_subset(self_user_data.capabilities.as_ref())
209 && other_user_data.domains == self_user_data.domains)
210 {
211 found_self_user_data.insert(self_user_data);
212 found_other_user_data.insert(other_user_data);
213 debug!(
214 "Found matching config item for backend user {}",
215 self_user_data.id
216 );
217 continue 'outer;
218 }
219 }
220
221 debug!(
222 "Unable to find a matching backend item for config user {}",
223 other_user_data.authentication_key_id
224 );
225 return false;
226 }
227
228 (found_self_user_data, found_other_user_data)
229 };
230
231 if !self.key_data.iter().any(|key_data| {
233 key_data.id == YubiHsm2Config::WRAP_KEY_ID
234 && key_data.object_type == ObjectType::WrapKey
235 }) {
236 debug!("The implicitly defined wrap key is not present");
237 return false;
238 }
239
240 let self_key_data_without_wrap_key = self.key_data.iter().filter(|key_data| {
241 !(key_data.id == YubiHsm2Config::WRAP_KEY_ID
242 && key_data.object_type == ObjectType::WrapKey)
243 });
244
245 let (found_self_user_key_data, found_other_user_key_data) = {
246 let mut found_self_user_key_data: HashSet<(Id, ObjectType)> = HashSet::new();
247 let mut found_other_user_key_data: HashSet<&YubiHsm2ConfigUserKeyData> = HashSet::new();
248
249 'outer: for other_user_key_data in other.key_data.iter() {
250 if matches!(
253 other_user_key_data.key_setup.key_context(),
254 CryptographicKeyContext::OpenPgp { .. }
255 ) {
256 let data = self_key_data_without_wrap_key
257 .clone()
258 .filter_map(|key_data| {
259 if key_data.id == *other_user_key_data.signing_key_id
260 && (key_data.object_type == ObjectType::AsymmetricKey
261 || key_data.object_type == ObjectType::Opaque)
262 {
263 Some((key_data.id, key_data.object_type))
264 } else {
265 None
266 }
267 })
268 .collect::<HashSet<_>>();
269
270 if !(data.contains(&(*other_user_key_data.signing_key_id, ObjectType::Opaque))
271 && data.contains(&(
272 *other_user_key_data.signing_key_id,
273 ObjectType::AsymmetricKey,
274 )))
275 {
276 debug!(
277 "Unable to find a matching backend asymmetric key and certificate for OpenPGP key in config: {}",
278 other_user_key_data.authentication_key_id
279 );
280 return false;
281 }
282
283 debug!(
284 "Found matching backend item for OpenPGP key {} in config",
285 other_user_key_data.signing_key_id
286 );
287 found_self_user_key_data.extend(data);
288 found_other_user_key_data.insert(other_user_key_data);
289 continue 'outer;
290 }
291
292 for self_user_key_data in self.key_data.iter().filter(|key_data| {
295 !(key_data.id == YubiHsm2Config::WRAP_KEY_ID
296 && key_data.object_type == ObjectType::WrapKey)
297 }) {
298 if self_user_key_data == other_user_key_data {
299 found_self_user_key_data
300 .insert((self_user_key_data.id, self_user_key_data.object_type));
301 found_other_user_key_data.insert(other_user_key_data);
302 debug!(
303 "Found matching config item for backend key {}",
304 other_user_key_data.signing_key_id
305 );
306 continue 'outer;
307 }
308 }
309
310 debug!(
311 "Unable to find a matching backend item for key {} in config",
312 other_user_key_data.authentication_key_id
313 );
314 return false;
315 }
316
317 (found_self_user_key_data, found_other_user_key_data)
318 };
319
320 let mut return_value = true;
321 if found_self_user_data.len() != self.user_data.len() {
322 debug!(
323 "The following backend users have no matching item in the config: {}",
324 HashSet::from_iter(self.user_data.iter())
325 .difference(&found_self_user_data)
326 .map(ToString::to_string)
327 .collect::<Vec<_>>()
328 .join(", ")
329 );
330 return_value = false;
331 }
332 if found_other_user_data.len() != other.user_data.len() {
333 debug!(
334 "The following config users have no matching item in the backend: {}",
335 HashSet::from_iter(other.user_data.iter())
336 .difference(&found_other_user_data)
337 .map(ToString::to_string)
338 .collect::<Vec<_>>()
339 .join(", ")
340 );
341 return_value = false;
342 }
343
344 if found_self_user_key_data.len() != self_key_data_without_wrap_key.clone().count() {
345 debug!(
346 "The following backend keys have no matching item in the config: {}",
347 HashSet::from_iter(
348 self_key_data_without_wrap_key
349 .clone()
350 .map(|key_data| { (key_data.id, key_data.object_type) })
351 )
352 .difference(&found_self_user_key_data)
353 .map(|data| format!("{} ({})", data.0, data.1))
354 .collect::<Vec<_>>()
355 .join(", ")
356 );
357 return_value = false;
358 }
359 if found_other_user_key_data.len() != other.key_data.len() {
360 debug!(
361 "The following config keys have no matching item in the backend: {}",
362 HashSet::from_iter(other.key_data.iter())
363 .difference(&found_other_user_key_data)
364 .map(ToString::to_string)
365 .collect::<Vec<_>>()
366 .join(", ")
367 );
368 return_value = false;
369 }
370
371 if return_value {
372 debug!("The backend state and config state are considered equal.");
373 }
374
375 return_value
376 }
377}
378
379impl StateOriginInfo for YubiHsm2BackendState {
380 fn state_name(&self) -> &str {
381 Self::STATE_NAME
382 }
383
384 fn state_origin(&self) -> StateOrigin {
385 StateOrigin::Backend
386 }
387}
388
389#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
391pub(crate) struct YubiHsm2ConfigUserData {
392 pub authentication_key_id: Id,
394
395 pub auth_type: AuthType,
397
398 pub capabilities: Capabilities,
400
401 pub domains: Domains,
403}
404
405impl Display for YubiHsm2ConfigUserData {
406 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407 write!(
408 f,
409 "{} (auth type: {}; capabilities: {}; domains: {})",
410 self.authentication_key_id, self.auth_type, self.capabilities, self.domains
411 )?;
412
413 Ok(())
414 }
415}
416
417#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
419pub(crate) struct YubiHsm2ConfigUserKeyData<'config> {
420 pub signing_key_id: &'config Id,
422
423 pub authentication_key_id: &'config Id,
425
426 pub capabilities: Capabilities,
428
429 pub domain: &'config Domain,
431
432 pub key_setup: &'config SigningKeySetup,
434}
435
436impl<'config> Display for YubiHsm2ConfigUserKeyData<'config> {
437 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438 write!(
439 f,
440 "{} (authentication: {}; capabilities: {}; domain: {}; ",
441 self.signing_key_id, self.authentication_key_id, self.capabilities, self.domain,
442 )?;
443 write!(f, "type: {}; ", self.key_setup.key_type())?;
444 write!(
445 f,
446 "mechanisms: {}; ",
447 self.key_setup
448 .key_mechanisms()
449 .iter()
450 .map(|mechanism| mechanism.to_string())
451 .collect::<Vec<String>>()
452 .join(", ")
453 )?;
454 write!(f, "context: {}", self.key_setup.key_context())?;
455 write!(f, ")")?;
456
457 Ok(())
458 }
459}
460
461#[derive(Debug)]
466pub struct YubiHsm2ConfigState<'config> {
467 pub(crate) user_data: Vec<YubiHsm2ConfigUserData>,
469
470 pub(crate) key_data: Vec<YubiHsm2ConfigUserKeyData<'config>>,
472}
473
474impl<'config> YubiHsm2ConfigState<'config> {
475 pub const STATE_NAME: &'static str = "YubiHSM2 config";
477}
478
479impl<'config> From<&'config YubiHsm2Config> for YubiHsm2ConfigState<'config> {
480 fn from(value: &'config YubiHsm2Config) -> Self {
482 let mut user_data = Vec::new();
483 let mut key_data = Vec::new();
484
485 for mapping in value.mappings() {
486 if let YubiHsm2UserMapping::Signing {
487 authentication_key_id,
488 key_setup,
489 domain,
490 signing_key_id,
491 ..
492 } = mapping
493 {
494 key_data.push(YubiHsm2ConfigUserKeyData {
495 signing_key_id,
496 authentication_key_id,
497 capabilities: mapping.capabilities(),
498 domain,
499 key_setup,
500 })
501 }
502
503 user_data.push(YubiHsm2ConfigUserData {
504 authentication_key_id: mapping.backend_user_id(),
505 auth_type: mapping.into(),
506 capabilities: mapping.capabilities(),
507 domains: mapping.domains(),
508 })
509 }
510
511 Self {
512 user_data,
513 key_data,
514 }
515 }
516}
517
518impl<'config> StateOriginInfo for YubiHsm2ConfigState<'config> {
519 fn state_name(&self) -> &str {
520 Self::STATE_NAME
521 }
522
523 fn state_origin(&self) -> StateOrigin {
524 StateOrigin::Config
525 }
526}
527
528#[derive(Debug)]
530pub struct YubiHsm2Diff<'config_state, 'backend_state, 'config_items> {
531 pub(crate) config: &'config_state YubiHsm2ConfigState<'config_items>,
533
534 pub(crate) backend: &'backend_state YubiHsm2BackendState,
536}
537
538impl<'config_state, 'backend_state, 'config_items> StateDiff<'config_state, 'backend_state>
539 for YubiHsm2Diff<'config_state, 'backend_state, 'config_items>
540{
541 fn diff(&self) -> StateDiffReport<'config_state, 'backend_state> {
542 info!(
543 "Creating state diff report between {} and {}",
544 self.config.state_name(),
545 self.backend.state_name()
546 );
547
548 if self.backend == self.config {
549 debug!(
550 "The states of {} and {} are considered equal.",
551 self.config.state_name(),
552 self.backend.state_name()
553 );
554 return StateDiffReport::Success;
555 }
556
557 warn!(
558 "The states of {} and {} are not considered equal. Collecting discrepancies...",
559 self.config.state_name(),
560 self.backend.state_name()
561 );
562 let mut messages = Vec::new();
563
564 {
565 let mut matched_config_states = Vec::new();
566
567 'outer: for backend_user_data in self.backend.user_data.iter() {
568 for config_user_data in self.config.user_data.iter() {
569 if (backend_user_data == config_user_data)
576 || (config_user_data.auth_type == AuthType::Admin
577 && backend_user_data.id == config_user_data.authentication_key_id
578 && config_user_data
579 .capabilities
580 .as_ref()
581 .is_subset(backend_user_data.capabilities.as_ref())
582 && backend_user_data.domains == config_user_data.domains)
583 {
584 matched_config_states.push(config_user_data);
585 debug!(
586 "Found matching config item for backend user {}.",
587 backend_user_data.id
588 );
589 continue 'outer;
590 }
591
592 if backend_user_data.id == config_user_data.authentication_key_id {
594 matched_config_states.push(config_user_data);
595 messages.push(StateDiffFailure::Mismatch {
596 one: Box::new(self.config),
597 other: Box::new(self.backend),
598 one_state: config_user_data.to_string(),
599 other_state: backend_user_data.to_string(),
600 });
601 debug!(
602 "Found mismatching data in config and backend for authentication key {}.",
603 backend_user_data.id
604 );
605 continue 'outer;
606 }
607 }
608
609 debug!(
611 "Unable to find a matching config item for backend user {}.",
612 backend_user_data.id
613 );
614 messages.push(StateDiffFailure::DoesNotExist {
615 one: Box::new(self.config),
616 other: Box::new(self.backend),
617 target: StateDiffFailureTarget::One,
618 state: backend_user_data.to_string(),
619 });
620 }
621
622 self.config
624 .user_data
625 .iter()
626 .filter(|state| !matched_config_states.contains(state))
627 .for_each(|config_user_data| {
628 debug!(
629 "Unable to find a matching backend item for config user {}.",
630 config_user_data.authentication_key_id
631 );
632 messages.push(StateDiffFailure::DoesNotExist {
633 one: Box::new(self.config),
634 other: Box::new(self.backend),
635 target: StateDiffFailureTarget::Other,
636 state: config_user_data.to_string(),
637 })
638 });
639 }
640
641 {
642 let implicit_wrap_key_state = implicit_wrap_key_state();
643 let mut matched_config_states = Vec::new();
644
645 if !self
647 .backend
648 .key_data
649 .iter()
650 .any(|key_state| key_state.object_type == ObjectType::WrapKey)
651 {
652 debug!("Unable to find any wrap key in backend.");
653 messages.push(StateDiffFailure::DoesNotExist {
654 one: Box::new(self.config),
655 other: Box::new(self.backend),
656 target: StateDiffFailureTarget::Other,
657 state: implicit_wrap_key_state.to_string(),
658 });
659 }
660
661 'outer: for backend_user_key_data in self.backend.key_data.iter() {
662 match backend_user_key_data.object_type {
663 ObjectType::WrapKey => {
664 if backend_user_key_data != &implicit_wrap_key_state {
670 debug!("The implicit wrap key in the backend is not correct.");
671 messages.push(StateDiffFailure::Mismatch {
672 one: Box::new(self.config),
673 other: Box::new(self.backend),
674 one_state: implicit_wrap_key_state.to_string(),
675 other_state: backend_user_key_data.to_string(),
676 });
677 }
678 }
679 ObjectType::AsymmetricKey => {
680 for config_user_key_data in self.config.key_data.iter() {
681 if backend_user_key_data == config_user_key_data {
683 matched_config_states.push(config_user_key_data);
684 debug!(
685 "Found matching config item for asymmetric key {} in backend.",
686 backend_user_key_data.id
687 );
688 continue 'outer;
689 }
690
691 if &backend_user_key_data.id == config_user_key_data.signing_key_id {
693 matched_config_states.push(config_user_key_data);
694 messages.push(StateDiffFailure::Mismatch {
695 one: Box::new(self.config),
696 other: Box::new(self.backend),
697 one_state: config_user_key_data.to_string(),
698 other_state: backend_user_key_data.to_string(),
699 });
700 debug!(
701 "Found mismatching data in config and backend for asymmetric key {}.",
702 backend_user_key_data.id
703 );
704 continue 'outer;
705 }
706 }
707
708 debug!(
710 "Unable to find a matching config item for asymmetric key {} in backend.",
711 backend_user_key_data.id
712 );
713 messages.push(StateDiffFailure::DoesNotExist {
714 one: Box::new(self.config),
715 other: Box::new(self.backend),
716 target: StateDiffFailureTarget::One,
717 state: backend_user_key_data.to_string(),
718 });
719 }
720 ObjectType::Opaque => {
723 for config_user_key_data in self.config.key_data.iter() {
724 if (backend_user_key_data == config_user_key_data)
726 || (backend_user_key_data.id
727 == *config_user_key_data.signing_key_id
728 && backend_user_key_data.capabilities
729 == Capabilities::from(
730 vec![Capability::ExportableUnderWrap].as_slice(),
731 )
732 && backend_user_key_data.domains
733 == Domains::from(*config_user_key_data.domain)
734 && backend_user_key_data.label
735 == YubiHsm2Config::openpgp_certificate_label())
736 {
737 matched_config_states.push(config_user_key_data);
738 debug!(
739 "Found matching config item for opaque object {} in backend.",
740 backend_user_key_data.id
741 );
742 continue 'outer;
743 }
744
745 if &backend_user_key_data.id == config_user_key_data.signing_key_id {
747 matched_config_states.push(config_user_key_data);
748 messages.push(StateDiffFailure::Mismatch {
749 one: Box::new(self.config),
750 other: Box::new(self.backend),
751 one_state: config_user_key_data.to_string(),
752 other_state: backend_user_key_data.to_string(),
753 });
754 debug!(
755 "Found mismatching data in config and backend for opaque object {}.",
756 backend_user_key_data.id
757 );
758 continue 'outer;
759 }
760 }
761
762 debug!(
764 "Unable to find a matching config item for opaque object {} in backend.",
765 backend_user_key_data.id
766 );
767 messages.push(StateDiffFailure::DoesNotExist {
768 one: Box::new(self.config),
769 other: Box::new(self.backend),
770 target: StateDiffFailureTarget::One,
771 state: backend_user_key_data.to_string(),
772 });
773 }
774 ObjectType::AuthenticationKey
775 | ObjectType::HmacKey
776 | ObjectType::SymmetricKey
777 | ObjectType::Template
778 | ObjectType::OtpAeakey => {
779 warn!(
781 "Found unsupported key type ({}) for backend key {}",
782 backend_user_key_data.object_type, backend_user_key_data.id
783 );
784 messages.push(StateDiffFailure::DoesNotExist {
785 one: Box::new(self.config),
786 other: Box::new(self.backend),
787 target: StateDiffFailureTarget::One,
788 state: backend_user_key_data.to_string(),
789 });
790 }
791 }
792 }
793
794 self.config
796 .key_data
797 .iter()
798 .filter(|state| !matched_config_states.contains(state))
799 .for_each(|key_data| {
800 debug!(
801 "Unable to find a matching backend item for config key {}",
802 key_data.signing_key_id
803 );
804 messages.push(StateDiffFailure::DoesNotExist {
805 one: Box::new(self.config),
806 other: Box::new(self.backend),
807 target: StateDiffFailureTarget::Other,
808 state: key_data.to_string(),
809 })
810 });
811 }
812
813 StateDiffReport::Failure { messages }
814 }
815}
816
817#[cfg(all(test, feature = "_yubihsm2-mockhsm"))]
818mod tests {
819 use std::{collections::BTreeSet, thread::current};
820
821 use insta::{assert_snapshot, with_settings};
822 use log::{LevelFilter, info};
823 use rstest::{fixture, rstest};
824 use signstar_common::logging::setup_logging;
825 use signstar_crypto::{
826 AdministrativeSecretHandling,
827 NonAdministrativeSecretHandling,
828 key::{
829 CryptographicKeyContext,
830
831 SigningKeySetup,
832 base::{KeyMechanism, KeyType, SignatureType},
833 },
834 openpgp::OpenPgpUserIdList,
835 passphrase::Passphrase,
836 };
837 use signstar_yubihsm2::{
838 Connection,
839 Credentials,
840 object::{Domain, WrapKey, WrapKeyFromPassphrase},
841 yubihsm::{Client, Connector},
842 };
843 use testresult::TestResult;
844
845 use super::*;
846 use crate::{
847 admin_credentials::AdminCredentials,
848 config::{Config, ConfigBuilder, SystemConfig},
849 state::{StateDiff, StateDiffReport},
850 yubihsm2::{
851 YubiHsm2Config,
852 YubiHsm2UserMapping,
853 admin_credentials::YubiHsm2AdminCredentials,
854 },
855 };
856
857 const SNAPSHOT_PATH: &str = "fixtures/state/";
858
859 #[fixture]
861 fn connector() -> Connector {
862 Connector::mockhsm()
863 }
864
865 #[fixture]
867 fn yubihsm2_admin_credentials() -> TestResult<YubiHsm2AdminCredentials> {
868 Ok(YubiHsm2AdminCredentials::new(
869 1,
870 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
871 vec![
872 Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
873 ]
874 )?)
875 }
876
877 #[fixture]
879 fn yubihsm2_config() -> TestResult<YubiHsm2Config> {
880 Ok(YubiHsm2Config::new(
881 BTreeSet::from_iter([
882 Connection::Mock
883 ]),
884 BTreeSet::from_iter([
885 YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
886 YubiHsm2UserMapping::AuditLog {
887 authentication_key_id: 3,
888 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
889 system_user: "yubihsm2-metrics-user".parse()?,
890 },
891 YubiHsm2UserMapping::Backup{
892 authentication_key_id: 2,
893 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
894 system_user: "yubihsm2-backup-user".parse()?,
895 wrapping_key_id: 1,
896 },
897 YubiHsm2UserMapping::HermeticAuditLog {
898 authentication_key_id: 4,
899 system_user: "yubihsm2-hermetic-metrics-user".parse()?,
900 },
901 YubiHsm2UserMapping::Signing {
902 authentication_key_id: 5,
903 signing_key_id: 1,
904 key_setup: SigningKeySetup::new(
905 KeyType::Curve25519,
906 vec![KeyMechanism::EdDsaSignature],
907 None,
908 SignatureType::EdDsa,
909 CryptographicKeyContext::OpenPgp {
910 notations: Default::default(),
911 user_ids: OpenPgpUserIdList::new(vec![
912 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
913 ])?,
914 version: "v4".parse()?,
915 },
916 )?,
917 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
918 system_user: "yubihsm2-signing-user".parse()?,
919 domain: Domain::One,
920 }
921 ]),
922 )?)
923 }
924
925 #[fixture]
927 fn config(yubihsm2_config: TestResult<YubiHsm2Config>) -> TestResult<Config> {
928 Ok(ConfigBuilder::new(SystemConfig::new(
929 1,
930 AdministrativeSecretHandling::Plaintext,
931 NonAdministrativeSecretHandling::Plaintext,
932 BTreeSet::from_iter([]),
933 )?)
934 .set_yubihsm2_config(yubihsm2_config?)
935 .finish()?)
936 }
937
938 #[fixture]
940 fn yubihsm2_config_no_non_admin_users() -> TestResult<YubiHsm2Config> {
941 Ok(YubiHsm2Config::new(
942 BTreeSet::from_iter([Connection::Mock]),
943 BTreeSet::from_iter([YubiHsm2UserMapping::Admin {
944 authentication_key_id: 1,
945 }]),
946 )?)
947 }
948
949 #[fixture]
951 fn yubihsm2_config_fully_mismatching_ids() -> TestResult<YubiHsm2Config> {
952 Ok(YubiHsm2Config::new(
953 BTreeSet::from_iter([
954 Connection::Mock
955 ]),
956 BTreeSet::from_iter([
957 YubiHsm2UserMapping::Admin { authentication_key_id: 7 },
958 YubiHsm2UserMapping::AuditLog {
959 authentication_key_id: 9,
960 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
961 system_user: "yubihsm2-metrics-user".parse()?,
962 },
963 YubiHsm2UserMapping::Backup{
964 authentication_key_id: 8,
965 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
966 system_user: "yubihsm2-backup-user".parse()?,
967 wrapping_key_id: 1,
968 },
969 YubiHsm2UserMapping::HermeticAuditLog {
970 authentication_key_id: 10,
971 system_user: "yubihsm2-hermetic-metrics-user".parse()?,
972 },
973 YubiHsm2UserMapping::Signing {
974 authentication_key_id: 11,
975 signing_key_id: 2,
976 key_setup: SigningKeySetup::new(
977 KeyType::Curve25519,
978 vec![KeyMechanism::EdDsaSignature],
979 None,
980 SignatureType::EdDsa,
981 CryptographicKeyContext::OpenPgp {
982 notations: Default::default(),
983 user_ids: OpenPgpUserIdList::new(vec![
984 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
985 ])?,
986 version: "v4".parse()?,
987 },
988 )?,
989 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
990 system_user: "yubihsm2-signing-user".parse()?,
991 domain: Domain::One,
992 }
993 ]),
994 )?)
995 }
996
997 #[fixture]
999 fn yubihsm2_config_mismatching_roles() -> TestResult<YubiHsm2Config> {
1000 Ok(YubiHsm2Config::new(
1001 BTreeSet::from_iter([
1002 Connection::Mock
1003 ]),
1004 BTreeSet::from_iter([
1005 YubiHsm2UserMapping::Admin { authentication_key_id: 5 },
1006 YubiHsm2UserMapping::AuditLog {
1007 authentication_key_id: 2,
1008 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1009 system_user: "yubihsm2-metrics-user".parse()?,
1010 },
1011 YubiHsm2UserMapping::Backup{
1012 authentication_key_id: 3,
1013 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1014 system_user: "yubihsm2-backup-user".parse()?,
1015 wrapping_key_id: 1,
1016 },
1017 YubiHsm2UserMapping::HermeticAuditLog {
1018 authentication_key_id: 4,
1019 system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1020 },
1021 YubiHsm2UserMapping::Signing {
1022 authentication_key_id: 1,
1023 signing_key_id: 1,
1024 key_setup: SigningKeySetup::new(
1025 KeyType::Curve25519,
1026 vec![KeyMechanism::EdDsaSignature],
1027 None,
1028 SignatureType::EdDsa,
1029 CryptographicKeyContext::OpenPgp {
1030 notations: Default::default(),
1031 user_ids: OpenPgpUserIdList::new(vec![
1032 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1033 ])?,
1034 version: "v4".parse()?,
1035 },
1036 )?,
1037 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1038 system_user: "yubihsm2-signing-user".parse()?,
1039 domain: Domain::One,
1040 }
1041 ]),
1042 )?)
1043 }
1044
1045 #[fixture]
1047 fn yubihsm2_config_mismatching_signing_key_id() -> TestResult<YubiHsm2Config> {
1048 Ok(YubiHsm2Config::new(
1049 BTreeSet::from_iter([
1050 Connection::Mock
1051 ]),
1052 BTreeSet::from_iter([
1053 YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1054 YubiHsm2UserMapping::AuditLog {
1055 authentication_key_id: 3,
1056 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1057 system_user: "yubihsm2-metrics-user".parse()?,
1058 },
1059 YubiHsm2UserMapping::Backup{
1060 authentication_key_id: 2,
1061 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1062 system_user: "yubihsm2-backup-user".parse()?,
1063 wrapping_key_id: 1,
1064 },
1065 YubiHsm2UserMapping::HermeticAuditLog {
1066 authentication_key_id: 4,
1067 system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1068 },
1069 YubiHsm2UserMapping::Signing {
1070 authentication_key_id: 5,
1071 signing_key_id: 2,
1072 key_setup: SigningKeySetup::new(
1073 KeyType::Curve25519,
1074 vec![KeyMechanism::EdDsaSignature],
1075 None,
1076 SignatureType::EdDsa,
1077 CryptographicKeyContext::OpenPgp {
1078 notations: Default::default(),
1079 user_ids: OpenPgpUserIdList::new(vec![
1080 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1081 ])?,
1082 version: "v4".parse()?,
1083 },
1084 )?,
1085 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1086 system_user: "yubihsm2-signing-user".parse()?,
1087 domain: Domain::One,
1088 }
1089 ]),
1090 )?)
1091 }
1092
1093 #[fixture]
1095 fn yubihsm2_config_additional_signing_key() -> TestResult<YubiHsm2Config> {
1096 Ok(YubiHsm2Config::new(
1097 BTreeSet::from_iter([
1098 Connection::Mock
1099 ]),
1100 BTreeSet::from_iter([
1101 YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1102 YubiHsm2UserMapping::AuditLog {
1103 authentication_key_id: 3,
1104 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1105 system_user: "yubihsm2-metrics-user".parse()?,
1106 },
1107 YubiHsm2UserMapping::Backup{
1108 authentication_key_id: 2,
1109 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1110 system_user: "yubihsm2-backup-user".parse()?,
1111 wrapping_key_id: 1,
1112 },
1113 YubiHsm2UserMapping::HermeticAuditLog {
1114 authentication_key_id: 4,
1115 system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1116 },
1117 YubiHsm2UserMapping::Signing {
1118 authentication_key_id: 5,
1119 signing_key_id: 1,
1120 key_setup: SigningKeySetup::new(
1121 KeyType::Curve25519,
1122 vec![KeyMechanism::EdDsaSignature],
1123 None,
1124 SignatureType::EdDsa,
1125 CryptographicKeyContext::OpenPgp {
1126 notations: Default::default(),
1127 user_ids: OpenPgpUserIdList::new(vec![
1128 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1129 ])?,
1130 version: "v4".parse()?,
1131 },
1132 )?,
1133 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1134 system_user: "yubihsm2-signing-user".parse()?,
1135 domain: Domain::One,
1136 },
1137 YubiHsm2UserMapping::Signing {
1138 authentication_key_id: 6,
1139 signing_key_id: 2,
1140 key_setup: SigningKeySetup::new(
1141 KeyType::Curve25519,
1142 vec![KeyMechanism::EdDsaSignature],
1143 None,
1144 SignatureType::EdDsa,
1145 CryptographicKeyContext::OpenPgp {
1146 notations: Default::default(),
1147 user_ids: OpenPgpUserIdList::new(vec![
1148 "Foobar McBehface <foobar@mcbehface.org>".parse()?,
1149 ])?,
1150 version: "v4".parse()?,
1151 },
1152 )?,
1153 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGoUTEsi84KZGtD3jqSDbQxkLQPqSJsdc0mxQjODm/Oy user@host".parse()?,
1154 system_user: "yubihsm2-signing-user-2".parse()?,
1155 domain: Domain::Two,
1156 },
1157 ]),
1158 )?)
1159 }
1160
1161 #[fixture]
1163 fn yubihsm2_config_mismatching_signing_key_domain() -> TestResult<YubiHsm2Config> {
1164 Ok(YubiHsm2Config::new(
1165 BTreeSet::from_iter([
1166 Connection::Mock
1167 ]),
1168 BTreeSet::from_iter([
1169 YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1170 YubiHsm2UserMapping::AuditLog {
1171 authentication_key_id: 3,
1172 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1173 system_user: "yubihsm2-metrics-user".parse()?,
1174 },
1175 YubiHsm2UserMapping::Backup{
1176 authentication_key_id: 2,
1177 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1178 system_user: "yubihsm2-backup-user".parse()?,
1179 wrapping_key_id: 1,
1180 },
1181 YubiHsm2UserMapping::HermeticAuditLog {
1182 authentication_key_id: 4,
1183 system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1184 },
1185 YubiHsm2UserMapping::Signing {
1186 authentication_key_id: 5,
1187 signing_key_id: 1,
1188 key_setup: SigningKeySetup::new(
1189 KeyType::Curve25519,
1190 vec![KeyMechanism::EdDsaSignature],
1191 None,
1192 SignatureType::EdDsa,
1193 CryptographicKeyContext::OpenPgp {
1194 notations: Default::default(),
1195 user_ids: OpenPgpUserIdList::new(vec![
1196 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1197 ])?,
1198 version: "v4".parse()?,
1199 },
1200 )?,
1201 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1202 system_user: "yubihsm2-signing-user".parse()?,
1203 domain: Domain::Two,
1204 }
1205 ]),
1206 )?)
1207 }
1208
1209 #[fixture]
1210 fn yubihsm2_mappings() -> TestResult<[YubiHsm2UserMapping; 5]> {
1211 Ok([
1212 YubiHsm2UserMapping::Admin { authentication_key_id: "1".parse()? },
1213 YubiHsm2UserMapping::Backup{
1214 authentication_key_id: "2".parse()?,
1215 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host".parse()?,
1216 system_user: "backup-user".parse()?,
1217 wrapping_key_id: "1".parse()?,
1218 },
1219 YubiHsm2UserMapping::AuditLog {
1220 authentication_key_id: "3".parse()?,
1221 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1222 system_user: "metrics-user".parse()?,
1223 },
1224 YubiHsm2UserMapping::HermeticAuditLog {
1225 authentication_key_id: "4".parse()?,
1226 system_user: "hermetic-metrics".parse()?,
1227 },
1228 YubiHsm2UserMapping::Signing {
1229 authentication_key_id: "5".parse()?,
1230 signing_key_id: "1".parse()?,
1231 key_setup: SigningKeySetup::new(
1232 KeyType::Curve25519,
1233 vec![KeyMechanism::EdDsaSignature],
1234 None,
1235 SignatureType::EdDsa,
1236 CryptographicKeyContext::OpenPgp {
1237 notations: Default::default(),
1238 user_ids: OpenPgpUserIdList::new(vec![
1239 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1240 ])?,
1241 version: "v4".parse()?,
1242 },
1243 )?,
1244 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1245 system_user: "signing-user".parse()?,
1246 domain: Domain::One,
1247 }
1248 ])
1249 }
1250
1251 #[fixture]
1254 fn yubihsm2_non_admin_credentials() -> TestResult<Vec<Credentials>> {
1255 Ok(vec![
1256 Credentials::new("2".parse()?, Passphrase::generate(Some(50))),
1257 Credentials::new("3".parse()?, Passphrase::generate(Some(50))),
1258 Credentials::new("4".parse()?, Passphrase::generate(Some(50))),
1259 Credentials::new("5".parse()?, Passphrase::generate(Some(50))),
1260 ])
1261 }
1262
1263 struct ConfigConnectorCreds {
1265 pub config: Config,
1266 pub connector: Connector,
1267 pub admin_creds: YubiHsm2AdminCredentials,
1268 }
1269
1270 #[fixture]
1275 fn config_connector_creds(
1276 connector: Connector,
1277 config: TestResult<Config>,
1278 yubihsm2_admin_credentials: TestResult<YubiHsm2AdminCredentials>,
1279 yubihsm2_non_admin_credentials: TestResult<Vec<Credentials>>,
1280 ) -> TestResult<ConfigConnectorCreds> {
1281 let config = config?;
1282 let admin_creds = yubihsm2_admin_credentials?;
1283 let non_admin_creds = yubihsm2_non_admin_credentials?;
1284
1285 let Some(backend) = YubiHsm2Backend::new(connector.clone(), &admin_creds, &config)? else {
1286 panic!("No YubiHsm2Config in the provided Signstar config");
1287 };
1288 backend.sync(&non_admin_creds)?;
1289
1290 Ok(ConfigConnectorCreds {
1291 config,
1292 connector,
1293 admin_creds,
1294 })
1295 }
1296
1297 #[rstest]
1299 #[case::single_cap_single_domain(
1300 AuthType::Signing,
1301 Capabilities::from(vec![Capability::SignEddsa].as_slice()),
1302 Domains::from(vec![Domain::One].as_slice()),
1303 "1 (auth type: signing; capabilities: sign-eddsa; domains: 1)"
1304 )]
1305 #[case::multi_cap_multi_domain(
1306 AuthType::Signing,
1307 Capabilities::from(vec![Capability::SignEddsa, Capability::SignEcdsa].as_slice()),
1308 Domains::from(vec![Domain::One, Domain::Two].as_slice()),
1309 "1 (auth type: signing; capabilities: sign-ecdsa, sign-eddsa; domains: 1, 2)"
1310 )]
1311 #[case::multi_cap_single_domain(
1312 AuthType::Signing,
1313 Capabilities::from(vec![Capability::SignEddsa, Capability::SignEcdsa].as_slice()),
1314 Domains::from(vec![Domain::One].as_slice()),
1315 "1 (auth type: signing; capabilities: sign-ecdsa, sign-eddsa; domains: 1)"
1316 )]
1317 fn yubihsm2_config_user_data_display(
1318 #[case] auth_type: AuthType,
1319 #[case] capabilities: Capabilities,
1320 #[case] domains: Domains,
1321 #[case] display: &str,
1322 ) -> TestResult {
1323 let data = YubiHsm2ConfigUserData {
1324 authentication_key_id: "1".parse()?,
1325 auth_type,
1326 capabilities,
1327 domains,
1328 };
1329
1330 assert_eq!(format!("{data}"), display);
1331
1332 Ok(())
1333 }
1334
1335 #[test]
1337 fn yubihsm2_config_user_key_data_display() -> TestResult {
1338 let capabilities = Capabilities::from(vec![Capability::SignEddsa].as_slice());
1339 let domain = Domain::One;
1340 let key_setup = SigningKeySetup::new(
1341 KeyType::Curve25519,
1342 vec![KeyMechanism::EdDsaSignature],
1343 None,
1344 SignatureType::EdDsa,
1345 CryptographicKeyContext::OpenPgp {
1346 notations: Default::default(),
1347 user_ids: vec!["John Doe <john.doe@example.org>".to_string()].try_into()?,
1348 version: "4".parse()?,
1349 },
1350 )?;
1351 let display = "1 (authentication: 1; capabilities: sign-eddsa; domain: 1; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: \"John Doe <john.doe@example.org>\"))";
1352 let data = YubiHsm2ConfigUserKeyData {
1353 authentication_key_id: &"1".parse()?,
1354 signing_key_id: &"1".parse()?,
1355 capabilities,
1356 domain: &domain,
1357 key_setup: &key_setup,
1358 };
1359
1360 assert_eq!(data.to_string(), display);
1361
1362 Ok(())
1363 }
1364
1365 #[rstest]
1367 fn yubihsm2_config_state_from_yubihsm_config(
1368 yubihsm2_config: TestResult<YubiHsm2Config>,
1369 yubihsm2_mappings: TestResult<[YubiHsm2UserMapping; 5]>,
1370 ) -> TestResult {
1371 setup_logging(LevelFilter::Debug)?;
1372 let yubihsm2_config = yubihsm2_config?;
1373 let yubihsm2_mappings = yubihsm2_mappings?;
1374 let state = YubiHsm2ConfigState::from(&yubihsm2_config);
1375
1376 for authentication_key_id in yubihsm2_mappings
1377 .iter()
1378 .map(|mapping| mapping.backend_user_id())
1379 {
1380 debug!(
1381 "Ensuring that the YubiHSM2 authentication key ID {authentication_key_id} can be found in the YubiHSM2 config state."
1382 );
1383 assert!(
1384 state
1385 .user_data
1386 .iter()
1387 .any(|user_data| user_data.authentication_key_id == authentication_key_id)
1388 );
1389 }
1390
1391 for (authentication_key_id, signing_key_id) in
1392 yubihsm2_mappings.iter().filter_map(|mapping| {
1393 if let YubiHsm2UserMapping::Signing {
1394 authentication_key_id,
1395 signing_key_id,
1396 ..
1397 } = mapping
1398 {
1399 Some((authentication_key_id, signing_key_id))
1400 } else {
1401 None
1402 }
1403 })
1404 {
1405 debug!(
1406 "Ensuring that the YubiHSM2 authentication key ID {authentication_key_id} and signing key ID {signing_key_id} can be found in the YubiHSM2 config state."
1407 );
1408 assert!(
1409 state
1410 .key_data
1411 .iter()
1412 .any(|data| data.authentication_key_id == authentication_key_id
1413 && data.signing_key_id == signing_key_id)
1414 );
1415 }
1416
1417 Ok(())
1418 }
1419
1420 #[rstest]
1422 fn yubihsm_config_state_state_name(yubihsm2_config: TestResult<YubiHsm2Config>) -> TestResult {
1423 setup_logging(LevelFilter::Debug)?;
1424 let yubihsm2_config = yubihsm2_config?;
1425 let state = YubiHsm2ConfigState::from(&yubihsm2_config);
1426
1427 assert_eq!(state.state_name(), YubiHsm2ConfigState::STATE_NAME);
1428
1429 Ok(())
1430 }
1431
1432 #[rstest]
1434 fn yubihsm_config_state_state_origin(
1435 yubihsm2_config: TestResult<YubiHsm2Config>,
1436 ) -> TestResult {
1437 setup_logging(LevelFilter::Debug)?;
1438 let yubihsm2_config = yubihsm2_config?;
1439 let state = YubiHsm2ConfigState::from(&yubihsm2_config);
1440
1441 assert_eq!(state.state_origin(), StateOrigin::Config);
1442
1443 Ok(())
1444 }
1445
1446 #[rstest]
1448 #[case::no_non_admin_users(
1449 yubihsm2_config_no_non_admin_users()?,
1450 "State diff of pre-populated YubiHSM2 backend and mismatching YubiHSM2 config: No non-admin users.",
1451 )]
1452 #[case::fully_mismatching_user_ids(
1453 yubihsm2_config_fully_mismatching_ids()?,
1454 "State diff of pre-populated YubiHSM2 backend and mismatching YubiHSM2 config: Fully mismatching user IDs.",
1455 )]
1456 #[case::fully_mismatching_roles(
1457 yubihsm2_config_mismatching_roles()?,
1458 "State diff of pre-populated YubiHSM2 backend and mismatching YubiHSM2 config: Mismatching user roles.",
1459 )]
1460 #[case::mismatching_signing_key_id(
1461 yubihsm2_config_mismatching_signing_key_id()?,
1462 "State diff of pre-populated YubiHSM2 backend and mismatching YubiHSM2 config: Mismatching signing key ID.",
1463 )]
1464 #[case::mismatching_additional_signing_key(
1465 yubihsm2_config_additional_signing_key()?,
1466 "State diff of pre-populated YubiHSM2 backend and mismatching YubiHSM2 config: Additional signing key.",
1467 )]
1468 #[case::mismatching_mismatching_signing_key_domain(
1469 yubihsm2_config_mismatching_signing_key_domain()?,
1470 "State diff of pre-populated YubiHSM2 backend and mismatching YubiHSM2 config: Mismatching signing key domain.",
1471 )]
1472 fn yubihsm2_diff_diff_fails_on_mismatching_data(
1473 config_connector_creds: TestResult<ConfigConnectorCreds>,
1474 #[case] yubihsm2_config: YubiHsm2Config,
1475 #[case] description: &str,
1476 ) -> TestResult {
1477 let config_connector_creds = config_connector_creds?;
1478
1479 let Some(backend) = YubiHsm2Backend::new(
1480 config_connector_creds.connector.clone(),
1481 &config_connector_creds.admin_creds,
1482 &config_connector_creds.config,
1483 )?
1484 else {
1485 panic!("No YubiHsm2Config in the provided Signstar config");
1486 };
1487 let backend_state = YubiHsm2BackendState::try_from(&backend)?;
1488 let yubihsm2_config_state = YubiHsm2ConfigState::from(&yubihsm2_config);
1489
1490 let yubihsm2_diff = YubiHsm2Diff {
1491 config: &yubihsm2_config_state,
1492 backend: &backend_state,
1493 };
1494 let diff = yubihsm2_diff.diff();
1495 with_settings!({
1496 description => description,
1497 snapshot_path => SNAPSHOT_PATH,
1498 prepend_module_to_snapshot => false,
1499 }, {
1500 assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), diff);
1501 });
1502 info!("StateDiffReport:\n{diff}");
1503 assert!(matches!(diff, StateDiffReport::Failure { .. }));
1504
1505 Ok(())
1506 }
1507
1508 #[rstest]
1510 fn yubihsm2_diff_diff_fails_on_unprovisioned_backend(
1511 connector: Connector,
1512 yubihsm2_config: TestResult<YubiHsm2Config>,
1513 yubihsm2_admin_credentials: TestResult<YubiHsm2AdminCredentials>,
1514 ) -> TestResult {
1515 let yubihsm2_admin_credentials = yubihsm2_admin_credentials?;
1516 let yubihsm2_config = yubihsm2_config?;
1517 let config = ConfigBuilder::new(SystemConfig::new(
1518 1,
1519 AdministrativeSecretHandling::Plaintext,
1520 NonAdministrativeSecretHandling::Plaintext,
1521 BTreeSet::from_iter([]),
1522 )?)
1523 .set_yubihsm2_config(yubihsm2_config.clone())
1524 .finish()?;
1525 let Some(backend) =
1526 YubiHsm2Backend::new(connector.clone(), &yubihsm2_admin_credentials, &config)?
1527 else {
1528 panic!("No YubiHsm2Config in the provided Signstar config");
1529 };
1530 let backend_state = YubiHsm2BackendState::try_from(&backend)?;
1531 let yubihsm2_config_state = YubiHsm2ConfigState::from(&yubihsm2_config);
1532
1533 let yubihsm2_diff = YubiHsm2Diff {
1534 config: &yubihsm2_config_state,
1535 backend: &backend_state,
1536 };
1537 let diff = yubihsm2_diff.diff();
1538 info!("StateDiffReport:\n{diff}");
1539 assert!(matches!(diff, StateDiffReport::Failure { .. }));
1540
1541 with_settings!({
1542 description => "State diff of unprovisioned YubiHSM2 backend and YubiHSM2 config.",
1543 snapshot_path => SNAPSHOT_PATH,
1544 prepend_module_to_snapshot => false,
1545 }, {
1546 assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), diff);
1547 });
1548
1549 Ok(())
1550 }
1551
1552 #[rstest]
1555 fn yubihsm2_diff_diff_fails_on_mismatching_wrap_key_id(
1556 config_connector_creds: TestResult<ConfigConnectorCreds>,
1557 ) -> TestResult {
1558 let config_connector_creds = config_connector_creds?;
1559 let config = config_connector_creds.config;
1560
1561 let client = Client::open(
1563 config_connector_creds.connector.clone(),
1564 config_connector_creds
1565 .admin_creds
1566 .administrators()
1567 .first()
1568 .expect("there to be at least one administrator credentials")
1569 .into(),
1570 false,
1571 )?;
1572 client.delete_object(YubiHsm2Config::WRAP_KEY_ID, (&ObjectType::WrapKey).into())?;
1573
1574 let passphrase = config_connector_creds.admin_creds.backup_passphrase();
1576 let wrapping_key: WrapKey =
1577 WrapKeyFromPassphrase::new(passphrase, WrapKeyKind::Aes256)?.try_into()?;
1578 client.put_wrap_key(
1579 2,
1580 (&Label::from_truncated_str("foo")).into(),
1581 (&Domains::all()).into(),
1582 (&Capabilities::from(YubiHsm2UserMapping::CAP_BACKUP)).into(),
1583 (&Capabilities::from(YubiHsm2UserMapping::CAP_BACKUP)).into(),
1584 (&WrapKeyKind::default()).into(),
1585 &wrapping_key,
1586 )?;
1587
1588 let Some(backend) = YubiHsm2Backend::new(
1590 config_connector_creds.connector.clone(),
1591 &config_connector_creds.admin_creds,
1592 &config,
1593 )?
1594 else {
1595 panic!("No YubiHsm2Config in the provided Signstar config");
1596 };
1597 let backend_state = YubiHsm2BackendState::try_from(&backend)?;
1598
1599 let Some(yubihsm2_config) = config.yubihsm2() else {
1600 panic!("No YubiHsm2Config in the provided Signstar config");
1601 };
1602 let yubihsm2_config_state = YubiHsm2ConfigState::from(yubihsm2_config);
1603
1604 let yubihsm2_diff = YubiHsm2Diff {
1605 config: &yubihsm2_config_state,
1606 backend: &backend_state,
1607 };
1608 let diff = yubihsm2_diff.diff();
1609 info!("StateDiffReport:\n{diff}");
1610 assert!(matches!(diff, StateDiffReport::Failure { .. }));
1611
1612 with_settings!({
1613 description => "State diff of provisioned YubiHSM2 backend and YubiHSM2 config: Mismatching wrap key ID.",
1614 snapshot_path => SNAPSHOT_PATH,
1615 prepend_module_to_snapshot => false,
1616 }, {
1617 assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), diff);
1618 });
1619
1620 Ok(())
1621 }
1622
1623 #[rstest]
1625 fn yubihsm2_diff_diff_succeeds(
1626 connector: Connector,
1627 yubihsm2_admin_credentials: TestResult<YubiHsm2AdminCredentials>,
1628 yubihsm2_non_admin_credentials: TestResult<Vec<Credentials>>,
1629 config: TestResult<Config>,
1630 ) -> TestResult {
1631 setup_logging(LevelFilter::Debug)?;
1632 let yubihsm2_admin_credentials = yubihsm2_admin_credentials?;
1633 let yubihsm2_non_admin_credentials = yubihsm2_non_admin_credentials?;
1634 let config = config?;
1635
1636 let Some(yubihsm2_config) = config.yubihsm2() else {
1637 panic!("There should be a YubiHSM2 config");
1638 };
1639 let yubihsm2_config_state = YubiHsm2ConfigState::from(yubihsm2_config);
1640 let Some(backend) = YubiHsm2Backend::new(connector, &yubihsm2_admin_credentials, &config)?
1641 else {
1642 panic!("No YubiHsm2Config in the provided Signstar config");
1643 };
1644
1645 let backend_state = YubiHsm2BackendState::try_from(&backend)?;
1646 let yubihsm2_diff = YubiHsm2Diff {
1647 config: &yubihsm2_config_state,
1648 backend: &backend_state,
1649 };
1650 let diff = yubihsm2_diff.diff();
1651 info!("StateDiffReport:\n{diff}");
1652 assert!(matches!(diff, StateDiffReport::Failure { .. }));
1653
1654 backend.sync(&yubihsm2_non_admin_credentials)?;
1655
1656 let backend_state = YubiHsm2BackendState::try_from(&backend)?;
1657 let yubihsm2_diff = YubiHsm2Diff {
1658 config: &yubihsm2_config_state,
1659 backend: &backend_state,
1660 };
1661 let diff = yubihsm2_diff.diff();
1662 info!("StateDiffReport:\n{diff}");
1663 assert!(matches!(diff, StateDiffReport::Success));
1664
1665 backend.sync(&yubihsm2_non_admin_credentials)?;
1667
1668 let backend_state = YubiHsm2BackendState::try_from(&backend)?;
1669 let yubihsm2_diff = YubiHsm2Diff {
1670 config: &yubihsm2_config_state,
1671 backend: &backend_state,
1672 };
1673 let diff = yubihsm2_diff.diff();
1674 info!("StateDiffReport:\n{diff}");
1675 assert!(matches!(diff, StateDiffReport::Success));
1676
1677 Ok(())
1678 }
1679}