1use std::{cell::RefCell, collections::HashSet, fmt::Debug};
13
14use log::{debug, error, info, warn};
15use pgp::types::Timestamp;
16use signstar_crypto::{
17 key::CryptographicKeyContext,
18 openpgp::OpenPgpKeyUsageFlags,
19 signer::openpgp::generate_certificate,
20 traits::UserWithPassphrase,
21};
22use signstar_yubihsm2::{
23 Credentials,
24 YubiHsm2SigningKey,
25 automation::{
26 AuthenticatedCommandChain,
27 Command,
28 CommandName,
29 CommandReturnValue,
30 ListObjectFilter,
31 ObjectType,
32 OpaqueData,
33 OpaqueDataAlgorithm,
34 OpaqueDataCapabilities,
35 Scenario,
36 ScenarioRunner,
37 },
38 object::{
39 AuthenticationKey,
40 Capabilities,
41 Domains,
42 KeyInfo,
43 ObjectId,
44 WrapKey,
45 WrapKeyFromPassphrase,
46 WrapKeyKind,
47 },
48 yubihsm::{Client, Connector, Id, Info},
49};
50
51use crate::{
52 admin_credentials::AdminCredentials,
53 config::Config,
54 yubihsm2::{
55 Error,
56 YubiHsm2Config,
57 YubiHsm2UserMapping,
58 admin_credentials::YubiHsm2AdminCredentials,
59 state::{YubiHsm2BackendUserData, YubiHsm2BackendUserKeyData},
60 },
61};
62
63fn are_admin_creds_usable(runner: &ScenarioRunner, credentials: &Credentials) -> bool {
68 info!(
69 "Checking whether the authentication key {} is usable...",
70 credentials.id()
71 );
72
73 let object_infos = match get_object_infos_for_object_ids(
74 runner,
75 credentials,
76 &[ObjectId::AuthenticationKey(credentials.id())],
77 ) {
78 Ok(object_infos) => object_infos,
79 Err(error) => {
80 warn!("{error}");
81 return false;
82 }
83 };
84
85 let Some(info) = object_infos.first() else {
86 warn!(
87 "{}",
88 crate::Error::YubiHsm2Backend(Error::ScenarioLogic {
89 context: format!(
90 "there is no object info for the authentication key ID {}",
91 credentials.id()
92 ),
93 })
94 );
95 return false;
96 };
97
98 let temp_admin_mapping = YubiHsm2UserMapping::Admin {
99 authentication_key_id: credentials.id(),
100 };
101
102 let admin_capabilities = temp_admin_mapping.capabilities();
104 let device_capabilities = Capabilities::from(info.capabilities);
105 if !admin_capabilities
106 .as_ref()
107 .is_subset(device_capabilities.as_ref())
108 {
109 error!(
110 "The admin credentials for authentication key ID {} are valid, but its capabilities ({device_capabilities}) do not include all of the necessary ones ({admin_capabilities})!",
111 credentials.id(),
112 );
113 return false;
114 }
115
116 let admin_domains = temp_admin_mapping.domains();
118 let device_domains = Domains::from(info.domains);
119 if !admin_domains.as_ref().is_subset(device_domains.as_ref()) {
120 error!(
121 "The admin credentials for authentication key ID {} are valid, but its domains ({device_domains}) do not include all of the necessary ones ({admin_domains})!",
122 credentials.id(),
123 );
124 return false;
125 }
126
127 true
128}
129
130fn get_key_ids(
143 runner: &ScenarioRunner,
144 credentials: &Credentials,
145 object_type: ObjectType,
146) -> Result<Vec<Id>, crate::Error> {
147 debug!(
148 "Retrieving list of {object_type} using the credentials {}",
149 credentials.id()
150 );
151
152 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(
153 credentials.clone(),
154 vec![Command::ListObjects(vec![ListObjectFilter::Type(
155 object_type,
156 )])],
157 )]);
158 let scenario_result = runner.run(&scenario)?;
159
160 let Some(command_return_values) = scenario_result.chains().first() else {
161 return Err(Error::ScenarioLogic {
162 context: format!("there are no command return values when retrieving the list of keys for the object type {object_type}"),
163 }
164 .into());
165 };
166 let Some(command_return_value) = command_return_values.first() else {
167 return Err(Error::ScenarioLogic {
168 context: format!("there is no command return value when retrieving the list of keys for the object type {object_type}"),
169 }
170 .into());
171 };
172 let CommandReturnValue::ListObjects(entries) = command_return_value else {
173 return Err(Error::ScenarioLogic {
174 context: format!("there are no entries when retrieving the list of keys for the object type {object_type}"),
175 }
176 .into());
177 };
178
179 Ok(entries
180 .iter()
181 .map(|entry| entry.object_id)
182 .collect::<Vec<_>>())
183}
184
185fn get_object_infos_for_object_ids(
194 runner: &ScenarioRunner,
195 credentials: &Credentials,
196 object_ids: &[ObjectId],
197) -> Result<Vec<Info>, crate::Error> {
198 debug!(
199 "Retrieving list of object infos for object IDs {} using the credentials {}",
200 object_ids
201 .iter()
202 .map(|id| format!("{id:?}"))
203 .collect::<Vec<_>>()
204 .join(", "),
205 credentials.id()
206 );
207
208 if object_ids.is_empty() {
209 return Ok(Vec::new());
210 }
211
212 let commands = object_ids
213 .iter()
214 .map(|id| Command::GetObjectInfo(*id))
215 .collect::<Vec<Command>>();
216 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(
217 credentials.clone(),
218 commands,
219 )]);
220 let scenario_return_value = runner.run(&scenario)?;
221
222 let command_return_values = {
223 let return_values: Vec<Vec<CommandReturnValue>> = scenario_return_value.into();
224 let mut return_values_iter = return_values.into_iter();
225
226 let Some(command_return_values) = return_values_iter.next() else {
227 return Err(Error::ScenarioLogic {
228 context: format!("there are no command return values when retrieving object infos for the objects {}",
229 object_ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")),
230 }
231 .into());
232 };
233 if return_values_iter.next().is_some() {
234 return Err(Error::ScenarioLogic {
235 context: format!("there are more command return values than there were chains of commands when retrieving object infos for the objects {}",
236 object_ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
237 ),
238 }
239 .into());
240 }
241 if command_return_values.len() != object_ids.len() {
242 return Err(Error::ScenarioLogic {
243 context: format!("the number of returned object infos ({}) does not match the number of requested ones ({}) when retrieving object infos for the objects {}",
244 command_return_values.len(),
245 object_ids.len(),
246 object_ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
247 ),
248 }
249 .into());
250 }
251
252 command_return_values
253 };
254
255 let infos = {
256 let mut infos = Vec::new();
257
258 for (command_return_value, object_id) in command_return_values.into_iter().zip(object_ids) {
259 let CommandReturnValue::GetObjectInfo(info) = command_return_value else {
260 return Err(Error::ScenarioLogic {
261 context: format!("something different from object information was returned when requesting object information for the objects {}",
262 object_ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
263 ),
264 }
265 .into());
266 };
267
268 if object_id.id() != info.object_id {
269 return Err(Error::ScenarioLogic {
270 context: format!(
271 "the retrieved object information with ID {} does not match the object {object_id} used for the request", info.object_id
272 ),
273 }
274 .into());
275 }
276
277 infos.push(info);
278 }
279 infos
280 };
281
282 Ok(infos)
283}
284
285fn get_object_infos_for_object_types(
294 runner: &ScenarioRunner,
295 credentials: &Credentials,
296 object_types: &[ObjectType],
297) -> Result<Vec<Info>, crate::Error> {
298 debug!(
299 "Retrieve object infos for objects of the types {}",
300 object_types
301 .iter()
302 .map(ToString::to_string)
303 .collect::<Vec<_>>()
304 .join(", ")
305 );
306
307 let output = {
308 let mut output = Vec::new();
309 for object_type in object_types {
310 let ids = get_key_ids(runner, credentials, *object_type)?;
311 let mut infos = get_object_infos_for_object_ids(
312 runner,
313 credentials,
314 &ids.iter()
315 .map(|id| ObjectId::from((*object_type, *id)))
316 .collect::<Vec<_>>(),
317 )?;
318 output.append(&mut infos);
319 }
320 output
321 };
322
323 Ok(output)
324}
325
326#[derive(Clone, Debug)]
336struct UserMappingsAndCredentials<'config, 'creds>(
337 Vec<(&'config YubiHsm2UserMapping, &'creds Credentials)>,
338);
339
340impl<'config, 'creds> UserMappingsAndCredentials<'config, 'creds> {
341 pub fn new_from_non_admin(
352 config: &'config YubiHsm2Config,
353 creds: &'creds [Credentials],
354 ) -> Result<Self, crate::Error> {
355 let mappings = config
356 .mappings()
357 .iter()
358 .filter(|mapping| !matches!(mapping, YubiHsm2UserMapping::Admin { .. }))
359 .collect::<HashSet<_>>();
360
361 Self::try_from((&mappings, creds))
362 }
363
364 pub fn new_from_admin(
375 config: &'config YubiHsm2Config,
376 creds: &'creds YubiHsm2AdminCredentials,
377 ) -> Result<Self, crate::Error> {
378 let mappings = config
379 .mappings()
380 .iter()
381 .filter(|mapping| matches!(mapping, YubiHsm2UserMapping::Admin { .. }))
382 .collect::<HashSet<_>>();
383 let creds = creds.administrators();
384
385 Self::try_from((&mappings, creds))
386 }
387}
388
389impl<'config, 'creds>
390 TryFrom<(
391 &HashSet<&'config YubiHsm2UserMapping>,
392 &'creds [Credentials],
393 )> for UserMappingsAndCredentials<'config, 'creds>
394{
395 type Error = crate::Error;
396
397 fn try_from(
407 value: (
408 &HashSet<&'config YubiHsm2UserMapping>,
409 &'creds [Credentials],
410 ),
411 ) -> Result<Self, Self::Error> {
412 let (mappings, creds) = value;
413
414 {
416 let mut dupes = HashSet::new();
417 for credentials in creds {
418 if creds
419 .iter()
420 .filter(|creds| creds.id() == credentials.id())
421 .count()
422 > 1
423 {
424 dupes.insert(credentials.id());
425 }
426 }
427 if !dupes.is_empty() {
428 let duplicates = {
429 let mut duplicates = Vec::from_iter(dupes);
430 duplicates.sort_unstable();
431 duplicates
432 };
433 return Err(Error::DuplicateCredentials {
434 context: "creating a validated set of user mappings and respective credentials",
435 duplicates,
436 }
437 .into());
438 }
439 }
440
441 {
443 let mut ids = Vec::new();
444 for credentials in creds {
445 if !mappings
446 .iter()
447 .any(|mapping| mapping.backend_user_id() == credentials.id())
448 {
449 ids.push(credentials.id());
450 }
451 }
452 if !ids.is_empty() {
453 return Err(Error::CredentialsWithoutUserMapping {
454 context: "creating a validated set of user mappings and respective credentials",
455 ids,
456 }
457 .into());
458 }
459 }
460
461 let output = {
463 let mut output = Vec::new();
464 for user_mapping in mappings {
465 let Some(matching_creds) = creds
466 .iter()
467 .find(|credentials| credentials.id() == user_mapping.backend_user_id())
468 else {
469 return Err(Error::UserMappingWithoutCredentials {
470 context: "creating a validated set of user mappings and respective credentials",
471 ids: vec![user_mapping.backend_user_id()],
472 }
473 .into());
474 };
475 output.push((*user_mapping, matching_creds));
476 }
477
478 output
479 };
480
481 Ok(Self(output))
482 }
483}
484
485impl<'config, 'creds> AsRef<[(&'config YubiHsm2UserMapping, &'creds Credentials)]>
486 for UserMappingsAndCredentials<'config, 'creds>
487{
488 fn as_ref(&self) -> &[(&'config YubiHsm2UserMapping, &'creds Credentials)] {
489 &self.0
490 }
491}
492
493#[derive(Debug)]
498pub struct YubiHsm2Backend<'admin_creds, 'config> {
499 connector: Connector,
500 runner: ScenarioRunner,
501 admin_credentials: &'admin_creds YubiHsm2AdminCredentials,
502 yubihsm2_config: &'config YubiHsm2Config,
503 admin_user_mappings_and_creds: UserMappingsAndCredentials<'config, 'admin_creds>,
504 default_credentials: RefCell<bool>,
506}
507
508impl<'admin_creds, 'config> YubiHsm2Backend<'admin_creds, 'config> {
509 pub fn new(
521 connector: Connector,
522 admin_credentials: &'admin_creds YubiHsm2AdminCredentials,
523 signstar_config: &'config Config,
524 ) -> Result<Option<Self>, crate::Error> {
525 debug!("Create a new YubiHSM2 backend for Signstar config");
526
527 let Some(yubihsm2_config) = signstar_config.yubihsm2() else {
528 return Ok(None);
529 };
530
531 if admin_credentials.iteration() != signstar_config.system().iteration() {
533 return Err(crate::Error::IterationMismatch {
534 admin_creds: admin_credentials.iteration(),
535 signstar_config: signstar_config.system().iteration(),
536 });
537 }
538
539 let admin_user_mappings_and_creds =
540 UserMappingsAndCredentials::new_from_admin(yubihsm2_config, admin_credentials)?;
541
542 let backend = Self {
543 connector: connector.clone(),
544 runner: ScenarioRunner::new(connector),
545 admin_credentials,
546 yubihsm2_config,
547 admin_user_mappings_and_creds,
548 default_credentials: RefCell::new(true),
549 };
550 backend.check_set_default_credentials();
551
552 Ok(Some(backend))
553 }
554
555 pub fn yubihsm2_config(&self) -> &YubiHsm2Config {
557 self.yubihsm2_config
558 }
559
560 pub fn default_credentials_in_use(&self) -> bool {
567 *self.default_credentials.borrow()
568 }
569
570 fn check_set_default_credentials(&self) -> bool {
577 info!("Checking whether the default credentials are still set...");
578
579 *self.default_credentials.borrow_mut() = are_admin_creds_usable(
580 &self.runner,
581 &YubiHsm2AdminCredentials::default_credentials(),
582 );
583
584 *self.default_credentials.borrow()
585 }
586
587 pub fn sync(&self, user_credentials: &[Credentials]) -> Result<(), crate::Error> {
590 info!("Syncing the YubiHSM2 backend with the Signstar configuration...");
591
592 let non_admin_users_and_creds =
593 UserMappingsAndCredentials::new_from_non_admin(self.yubihsm2_config, user_credentials)?;
594 debug!(
595 "valid mappings and their creds: {}",
596 non_admin_users_and_creds
597 .as_ref()
598 .iter()
599 .map(|(mapping, creds)| format!("{mapping:?}: {creds:?}"))
600 .collect::<Vec<_>>()
601 .join(", ")
602 );
603
604 self.add_admin_users()?;
605 self.add_wrap_key()?;
606 self.add_non_admin_users(&non_admin_users_and_creds)?;
607 self.add_signing_keys()?;
608 self.add_openpgp_certificates(&non_admin_users_and_creds)?;
609
610 Ok(())
611 }
612
613 fn add_openpgp_certificates(
629 &self,
630 non_admin_user_mappings_and_creds: &UserMappingsAndCredentials,
631 ) -> Result<(), crate::Error> {
632 info!("Adding OpenPGP certificates for signing keys...");
633
634 let credentials = self.usable_admin_creds()?;
635
636 let (commands, ids) = {
637 let opaque_data_infos = get_object_infos_for_object_types(
638 &self.runner,
639 &credentials,
640 &[ObjectType::Opaque],
641 )?;
642 let mut commands = Vec::new();
643 let mut ids = Vec::new();
644
645 for (mapping, credentials) in non_admin_user_mappings_and_creds.as_ref() {
646 if let YubiHsm2UserMapping::Signing {
649 domain,
650 signing_key_id,
651 key_setup,
652 ..
653 } = mapping
654 {
655 let CryptographicKeyContext::OpenPgp {
656 user_ids, version, ..
657 } = key_setup.key_context()
658 else {
659 debug!(
660 "Skipping the generation of an OpenPGP certificate for signing key {signing_key_id}, because it is not setup for use with OpenPGP..."
661 );
662 continue;
663 };
664
665 if opaque_data_infos
666 .iter()
667 .any(|info| info.object_id == *signing_key_id)
668 {
669 warn!(
670 "Skipping the generation of an OpenPGP certificate for signing key {signing_key_id}, because data exists already..."
671 );
672 continue;
673 }
674
675 let client = Client::create(self.connector.clone(), (*credentials).into())
676 .map_err(|source| signstar_yubihsm2::Error::Client { context: "creating a client for adding OpenPGP certificates for a signing key", source })?;
677 let signer = YubiHsm2SigningKey::new(client, *signing_key_id);
678 let flags = {
679 let mut flags = OpenPgpKeyUsageFlags::default();
680 flags.set_sign();
681 flags
682 };
683 let certificate = generate_certificate(
684 &signer,
685 flags,
686 user_ids.as_ref(),
687 Default::default(),
688 Timestamp::now(),
689 *version,
690 )?;
691 signer.close_session()?;
692
693 commands.push(Command::PutOpaque {
694 id: *signing_key_id,
695 label: YubiHsm2Config::openpgp_certificate_label(),
696 domains: Domains::from(*domain),
697 capabilities: OpaqueDataCapabilities::ExportableUnderWrap,
698 algorithm: OpaqueDataAlgorithm::OpaqueData,
699 data: OpaqueData::new(certificate)?,
700 });
701 ids.push(*signing_key_id);
702 }
703 }
704
705 (commands, ids)
706 };
707
708 if commands.is_empty() {
710 return Ok(());
711 }
712
713 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
714 let scenario_result = self.runner.run(&scenario)?;
715
716 let Some(command_return_values) = scenario_result.chains().first() else {
718 return Err(Error::ScenarioLogic {
719 context: format!(
720 "there are no command return values when adding OpenPGP certificates for signing keys {}",
721 ids.iter()
722 .map(ToString::to_string)
723 .collect::<Vec<_>>()
724 .join(", ")
725 ),
726 }
727 .into());
728 };
729 if command_return_values.len() != ids.len() {
730 return Err(Error::ScenarioLogic {
731 context: format!("the number of command return values ({}) does not match the number of requested IDs ({}) when adding OpenPGP certificates for signing keys {}",
732 command_return_values.len(),
733 ids.len(),
734 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
735 ),
736 }
737 .into());
738 };
739
740 for (command_return_value, requested_id) in command_return_values.iter().zip(ids.iter()) {
741 let returned_id = match command_return_value {
742 CommandReturnValue::PutOpaque(returned_id) => *returned_id,
743 command => {
744 let command_name = CommandName::from(command);
745 return Err(Error::ScenarioLogic {
746 context: format!("instead of the command return value for adding an OpenPGP certificate for signing key {requested_id} the return value for {command_name} was returned"),
747 }
748 .into());
749 }
750 };
751
752 if *requested_id != returned_id {
753 return Err(Error::ScenarioLogic {
754 context: format!("the returned signing key ID ({returned_id}) does not match the requested one ({requested_id}) when adding OpenPGP certificates for the signing keys {}",
755 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
756 ),
757 }
758 .into());
759 }
760 }
761
762 Ok(())
763 }
764
765 fn add_signing_keys(&self) -> Result<(), crate::Error> {
780 info!("Adding asymmetric signing keys...");
781
782 let credentials = self.usable_admin_creds()?;
783 let (commands, ids) = {
784 let signing_key_infos = get_object_infos_for_object_types(
785 &self.runner,
786 &credentials,
787 &[ObjectType::AsymmetricKey],
788 )?;
789 let mut commands = Vec::new();
790 let mut ids = Vec::new();
791
792 for mapping in self.yubihsm2_config.mappings() {
793 if let YubiHsm2UserMapping::Signing {
796 domain,
797 signing_key_id,
798 ..
799 } = mapping
800 {
801 if signing_key_infos
803 .iter()
804 .any(|info| info.object_id == *signing_key_id)
805 {
806 warn!("Not adding signing key {signing_key_id}, as it exists already...");
807 continue;
808 }
809
810 commands.push(Command::GenerateAsymmetricKey {
811 info: KeyInfo {
812 key_id: *signing_key_id,
813 domains: Domains::from(*domain),
814 caps: mapping.capabilities(),
815 label: mapping.label(),
816 },
817 });
818 ids.push(*signing_key_id);
819 }
820 }
821
822 (commands, ids)
823 };
824
825 if commands.is_empty() {
827 return Ok(());
828 }
829
830 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
831 let scenario_result = self.runner.run(&scenario)?;
832
833 let Some(command_return_values) = scenario_result.chains().first() else {
834 return Err(Error::ScenarioLogic {
835 context: format!(
836 "there are no command return values when adding the signing keys {}",
837 ids.iter()
838 .map(ToString::to_string)
839 .collect::<Vec<_>>()
840 .join(", ")
841 ),
842 }
843 .into());
844 };
845 if command_return_values.len() != ids.len() {
846 return Err(Error::ScenarioLogic {
847 context: format!("the number of command return values ({}) does not match the number of requested IDs ({}) when adding the signing keys {}",
848 command_return_values.len(),
849 ids.len(),
850 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
851 ),
852 }
853 .into());
854 };
855
856 for (number, command_return_value) in command_return_values.iter().enumerate() {
857 let Some(id) = ids.get(number) else {
858 return Err(Error::ScenarioLogic {
859 context: format!("the signing key ID number {number} does not exist in the list of signing keys {}",
860 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
861 ),
862 }
863 .into());
864 };
865 let CommandReturnValue::GenerateAsymmetricKey(returned_id) = command_return_value
866 else {
867 return Err(Error::ScenarioLogic {
868 context: format!("the command return value when adding the signing key {id} is that of a different action"),
869 }
870 .into());
871 };
872 if id != returned_id {
873 return Err(Error::ScenarioLogic {
874 context: format!("the returned signing key ID ({returned_id}) does not match the requested one ({id}) when adding the signing keys {}",
875 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
876 ),
877 }
878 .into());
879 }
880 }
881
882 Ok(())
883 }
884
885 fn add_non_admin_users(
901 &self,
902 non_admin_user_mappings_and_creds: &UserMappingsAndCredentials,
903 ) -> Result<(), crate::Error> {
904 info!("Setting up non-administrative users...");
905
906 let credentials = self.usable_admin_creds()?;
907
908 let (commands, ids) = {
909 let authentication_key_infos = get_object_infos_for_object_types(
910 &self.runner,
911 &credentials,
912 &[ObjectType::AuthenticationKey],
913 )?;
914 let mut commands = Vec::new();
915 let mut ids = Vec::new();
916
917 for (mapping, creds) in non_admin_user_mappings_and_creds.as_ref() {
918 let id = mapping.backend_user_id();
919
920 if authentication_key_infos
921 .iter()
922 .any(|info| info.object_id == id)
923 {
924 warn!("The existing authentication key {id} will be replaced...");
925 commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(id)));
926 }
927
928 commands.push(Command::PutAuthenticationKey {
929 info: mapping.authentication_key_info(),
930 delegated_caps: Capabilities::from(vec![].as_slice()),
931 authentication_key: AuthenticationKey::try_from(creds.passphrase())
932 .map_err(crate::Error::SignstarYubiHsm2)?,
933 });
934 ids.push(id);
935 }
936
937 (commands, ids)
938 };
939
940 if commands.is_empty() {
942 return Ok(());
943 }
944
945 let number_of_commands = commands.len();
946 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
947 let scenario_result = self.runner.run(&scenario)?;
948
949 let Some(command_return_values) = scenario_result.chains().first() else {
951 return Err(Error::ScenarioLogic {
952 context: format!("there are no command return values when adding the non-administrative users {}",
953 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
954 ),
955 }
956 .into());
957 };
958 if command_return_values.len() != number_of_commands {
959 return Err(Error::ScenarioLogic {
960 context: format!("the number of command return values ({}) does not match the number of requested IDs ({number_of_commands}) when adding the non-administrative users {}",
961 command_return_values.len(),
962 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
963 ),
964 }
965 .into());
966 };
967
968 for (number, command_return_value) in command_return_values
970 .iter()
971 .filter(|command_return_value| {
972 matches!(
973 command_return_value,
974 CommandReturnValue::PutAuthenticationKey(_)
975 )
976 })
977 .enumerate()
978 {
979 let Some(id) = ids.get(number) else {
980 return Err(Error::ScenarioLogic {
981 context: format!("the authentication key ID number {number} does not exist in the list of non-administrative users {}",
982 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
983 ),
984 }
985 .into());
986 };
987 let CommandReturnValue::PutAuthenticationKey(returned_id) = command_return_value else {
988 return Err(Error::ScenarioLogic {
989 context: format!("the command return value when adding the non-administrative user {id} is that of a different action"),
990 }
991 .into());
992 };
993 if id != returned_id {
994 return Err(Error::ScenarioLogic {
995 context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({id}) when adding the non-administrative users {}",
996 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
997 ),
998 }
999 .into());
1000 }
1001 }
1002
1003 Ok(())
1004 }
1005
1006 fn add_wrap_key(&self) -> Result<(), crate::Error> {
1022 info!("Setting up wrapping key...");
1023
1024 let credentials = self.usable_admin_creds()?;
1025
1026 let commands = {
1027 let wrap_key_infos = get_object_infos_for_object_types(
1028 &self.runner,
1029 &credentials,
1030 &[ObjectType::WrapKey],
1031 )?;
1032 let mut commands = Vec::new();
1033
1034 if wrap_key_infos
1036 .iter()
1037 .any(|info| info.object_id == YubiHsm2Config::WRAP_KEY_ID)
1038 {
1039 warn!(
1040 "The existing wrapping key ({}) will be replaced...",
1041 YubiHsm2Config::WRAP_KEY_ID
1042 );
1043 commands.push(Command::DeleteObject(ObjectId::WrappingKey(
1044 YubiHsm2Config::WRAP_KEY_ID,
1045 )))
1046 }
1047
1048 let passphrase = self.admin_credentials.backup_passphrase();
1049 let wrapping_key: WrapKey =
1050 WrapKeyFromPassphrase::new(passphrase, WrapKeyKind::Aes256)?.try_into()?;
1051 commands.push(Command::PutWrapKey {
1052 info: KeyInfo {
1053 key_id: YubiHsm2Config::WRAP_KEY_ID,
1054 domains: Domains::all(),
1055 caps: Capabilities::from(YubiHsm2UserMapping::CAP_BACKUP),
1056 label: YubiHsm2Config::wrap_key_label(),
1057 },
1058 delegated_caps: Capabilities::from(YubiHsm2UserMapping::CAP_BACKUP),
1059 wrapping_key,
1060 });
1061 commands
1062 };
1063 let number_of_commands = commands.len();
1064
1065 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(
1066 credentials.clone(),
1067 commands,
1068 )]);
1069 let scenario_return_value = self.runner.run(&scenario)?;
1070
1071 let command_return_values = {
1073 let return_values: Vec<Vec<CommandReturnValue>> = scenario_return_value.into();
1074 let return_values_len = return_values.len();
1075 let mut return_values_iter = return_values.into_iter();
1076
1077 let Some(command_return_values) = return_values_iter.next() else {
1078 return Err(Error::ScenarioLogic {
1079 context: format!(
1080 "there are no command return values when adding the wrap key {}",
1081 YubiHsm2Config::WRAP_KEY_ID
1082 ),
1083 }
1084 .into());
1085 };
1086 if return_values_iter.next().is_some() {
1087 return Err(Error::ScenarioLogic {
1088 context: format!("there are {} return value lists when adding the wrap key {}, but we expected exactly 1",
1089 return_values_len,
1090 YubiHsm2Config::WRAP_KEY_ID
1091 ),
1092 }
1093 .into());
1094 }
1095 command_return_values
1096 };
1097 let returned_id = {
1098 match command_return_values.len() {
1099 2 => {
1100 let Some(CommandReturnValue::DeleteObject) = command_return_values.first()
1101 else {
1102 return Err(Error::ScenarioLogic {
1103 context: format!(
1104 "there is no return value for removing a previously existing wrap key {}",
1105 YubiHsm2Config::WRAP_KEY_ID
1106 ),
1107 }
1108 .into());
1109 };
1110 let Some(CommandReturnValue::PutWrapKey(id)) = command_return_values.get(1)
1111 else {
1112 return Err(Error::ScenarioLogic {
1113 context: format!(
1114 "there is no return value for adding the wrap key {}",
1115 YubiHsm2Config::WRAP_KEY_ID
1116 ),
1117 }
1118 .into());
1119 };
1120 *id
1121 }
1122 1 => {
1123 let Some(CommandReturnValue::PutWrapKey(id)) = command_return_values.first()
1124 else {
1125 return Err(Error::ScenarioLogic {
1126 context: format!(
1127 "there is no return value for adding the wrap key {}",
1128 YubiHsm2Config::WRAP_KEY_ID
1129 ),
1130 }
1131 .into());
1132 };
1133 *id
1134 }
1135 _ => {
1136 return Err(Error::ScenarioLogic {
1137 context: format!("there are {} return values when removing and/or adding the wrap key {}, but we expected exactly {number_of_commands}",
1138 command_return_values.len(),
1139 YubiHsm2Config::WRAP_KEY_ID
1140 ),
1141 }
1142 .into());
1143 }
1144 }
1145 };
1146 if returned_id != YubiHsm2Config::WRAP_KEY_ID {
1147 return Err(Error::ScenarioLogic {
1148 context: format!("the returned key ID ({returned_id}) does not match the requested one when adding the wrap key {}",
1149 YubiHsm2Config::WRAP_KEY_ID
1150 ),
1151 }
1152 .into());
1153 }
1154
1155 Ok(())
1156 }
1157
1158 fn add_admin_users(&self) -> Result<(), crate::Error> {
1176 info!("Setting up administrative users...");
1177
1178 let credentials = self.usable_admin_creds()?;
1179 let (commands, ids) = {
1180 let authentication_key_infos = get_object_infos_for_object_types(
1181 &self.runner,
1182 &credentials,
1183 &[ObjectType::AuthenticationKey],
1184 )?;
1185 let mut commands = Vec::new();
1186 let mut ids = Vec::new();
1187
1188 for (mapping, creds) in self
1190 .admin_user_mappings_and_creds
1191 .as_ref()
1192 .iter()
1193 .filter(|(mapping, ..)| mapping.backend_user_id() != credentials.id())
1194 {
1195 let id = mapping.backend_user_id();
1196
1197 if authentication_key_infos
1198 .iter()
1199 .any(|info| info.object_id == id)
1200 {
1201 warn!("Replacing the existing authentication key {id}...");
1202 commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(id)));
1203 ids.push(id);
1204 }
1205
1206 commands.push(Command::PutAuthenticationKey {
1207 info: KeyInfo {
1208 key_id: creds.id(),
1209 domains: mapping.domains(),
1210 caps: mapping.capabilities(),
1211 label: mapping.label(),
1212 },
1213 delegated_caps: mapping.capabilities(),
1214 authentication_key: AuthenticationKey::try_from(creds.passphrase())?,
1215 });
1216 ids.push(id);
1217 }
1218
1219 if authentication_key_infos
1223 .iter()
1224 .any(|info| info.object_id == YubiHsm2AdminCredentials::DEFAULT_ID)
1225 && !self
1226 .admin_user_mappings_and_creds
1227 .as_ref()
1228 .iter()
1229 .any(|(mapping, _)| {
1230 mapping.backend_user_id() == YubiHsm2AdminCredentials::DEFAULT_ID
1231 })
1232 {
1233 warn!(
1234 "Removing the default authentication key {}...",
1235 YubiHsm2AdminCredentials::DEFAULT_ID
1236 );
1237 commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(
1238 YubiHsm2AdminCredentials::DEFAULT_ID,
1239 )));
1240 ids.push(YubiHsm2AdminCredentials::DEFAULT_ID)
1241 }
1242
1243 if let Some((_, current_credentials)) = self
1246 .admin_user_mappings_and_creds
1247 .as_ref()
1248 .iter()
1249 .find(|(mapping, ..)| mapping.backend_user_id() == credentials.id())
1250 {
1251 warn!(
1252 "Changing the authentication key {}, which is currently used...",
1253 current_credentials.id()
1254 );
1255 commands.push(Command::ChangeAuthenticationKey {
1256 key_id: current_credentials.id(),
1257 authentication_key: AuthenticationKey::try_from(
1258 current_credentials.passphrase(),
1259 )?,
1260 });
1261 ids.push(current_credentials.id());
1262 }
1263
1264 (commands, ids)
1265 };
1266
1267 if commands.is_empty() {
1269 return Ok(());
1270 }
1271
1272 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
1273 let scenario_result = self.runner.run(&scenario)?;
1274
1275 let Some(command_return_values) = scenario_result.chains().first() else {
1277 return Err(Error::ScenarioLogic {
1278 context: format!(
1279 "there are no command return values when adding the administrative users {}",
1280 ids.iter()
1281 .map(ToString::to_string)
1282 .collect::<Vec<_>>()
1283 .join(", ")
1284 ),
1285 }
1286 .into());
1287 };
1288 if command_return_values.len() != ids.len() {
1289 return Err(Error::ScenarioLogic {
1290 context: format!("the number of command return values ({}) does not match the number of requested commands ({}) when removing and/or adding the administrative users {}",
1291 command_return_values.len(),
1292 ids.len(),
1293 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1294 ),
1295 }
1296 .into());
1297 };
1298
1299 for (command_return_value, requested_id) in command_return_values.iter().zip(ids.iter()) {
1301 match command_return_value {
1302 CommandReturnValue::PutAuthenticationKey(returned_id) => {
1303 if returned_id != requested_id {
1304 return Err(Error::ScenarioLogic {
1305 context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({requested_id}) when adding the administrative user"),
1306 }
1307 .into());
1308 }
1309 }
1310 CommandReturnValue::DeleteObject => {
1311 }
1313 CommandReturnValue::ChangeAuthenticationKey(returned_id) => {
1314 if returned_id != requested_id {
1315 return Err(Error::ScenarioLogic {
1316 context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({requested_id}) when changing the administrative user"),
1317 }
1318 .into());
1319 }
1320 }
1321 _ => return Err(Error::ScenarioLogic {
1322 context: format!("the command return value is not for adding, removing or changing the administrative user {requested_id}: {command_return_value:?}"),
1323 }
1324 .into()),
1325 }
1326 }
1327
1328 info!("Successfully added all admins");
1329
1330 if self.check_set_default_credentials() {
1333 return Err(Error::DefaultAdminStillUsable.into());
1334 }
1335
1336 Ok(())
1337 }
1338
1339 fn usable_admin_creds(&self) -> Result<Credentials, crate::Error> {
1345 info!("Finding usable administrative credentials...");
1346
1347 let credentials = if self.default_credentials_in_use() {
1348 YubiHsm2AdminCredentials::default_credentials()
1349 } else {
1350 let Some(credentials) = self
1351 .admin_credentials
1352 .administrators()
1353 .iter()
1354 .find(|credentials| are_admin_creds_usable(&self.runner, credentials))
1355 else {
1356 return Err(Error::NoUsableAdmin.into());
1357 };
1358
1359 credentials.clone()
1360 };
1361
1362 info!(
1363 "Using administrative credentials with authentication ID {}",
1364 credentials.id()
1365 );
1366
1367 Ok(credentials)
1368 }
1369
1370 pub(crate) fn user_states(&self) -> Result<Vec<YubiHsm2BackendUserData>, crate::Error> {
1378 let credentials = self.usable_admin_creds()?;
1379 let infos = get_object_infos_for_object_types(
1381 &self.runner,
1382 &credentials,
1383 &[ObjectType::AuthenticationKey],
1384 )?;
1385
1386 let user_states = infos
1387 .iter()
1388 .map(|info| YubiHsm2BackendUserData {
1389 id: info.object_id,
1390 capabilities: info.capabilities.into(),
1391 domains: info.domains.into(),
1392 })
1393 .collect::<Vec<_>>();
1394
1395 Ok(user_states)
1396 }
1397
1398 pub(crate) fn key_states(&self) -> Result<Vec<YubiHsm2BackendUserKeyData>, crate::Error> {
1407 let credentials = self.usable_admin_creds()?;
1408 let object_types = [
1410 ObjectType::AsymmetricKey,
1411 ObjectType::HmacKey,
1412 ObjectType::Opaque,
1413 ObjectType::OtpAeakey,
1414 ObjectType::Template,
1415 ObjectType::WrapKey,
1416 ];
1417 let infos = get_object_infos_for_object_types(&self.runner, &credentials, &object_types)?;
1418
1419 let key_states = infos
1420 .into_iter()
1421 .filter_map(|info| {
1422 if info.object_id == 0 || info.object_id == Id::MAX {
1425 None
1426 } else {
1427 Some(YubiHsm2BackendUserKeyData {
1428 id: info.object_id,
1429 object_type: info.object_type.into(),
1430 capabilities: info.capabilities.into(),
1431 domains: info.domains.into(),
1432 algorithm: info.algorithm.into(),
1433 label: info.label.into(),
1434 length: info.length,
1435 })
1436 }
1437 })
1438 .collect::<Vec<_>>();
1439
1440 Ok(key_states)
1441 }
1442}
1443
1444#[cfg(all(test, feature = "_yubihsm2-mockhsm"))]
1445mod tests {
1446 use std::collections::BTreeSet;
1447
1448 use log::LevelFilter;
1449 use rstest::{fixture, rstest};
1450 use signstar_common::logging::setup_logging;
1451 use signstar_crypto::{
1452 AdministrativeSecretHandling,
1453 NonAdministrativeSecretHandling,
1454 key::{
1455 CryptographicKeyContext,
1456
1457 SigningKeySetup,
1458 base::{KeyMechanism, KeyType, SignatureType},
1459 },
1460 openpgp::OpenPgpUserIdList,
1461 passphrase::Passphrase,
1462 };
1463 use signstar_yubihsm2::{Connection, object::Domain};
1464 use testresult::TestResult;
1465
1466 use super::*;
1467 use crate::{
1468 config::{ConfigBuilder, SystemConfig},
1469 yubihsm2::{YubiHsm2Config, YubiHsm2UserMapping, state::YubiHsm2BackendState},
1470 };
1471
1472 #[fixture]
1474 fn connector() -> Connector {
1475 Connector::mockhsm()
1476 }
1477
1478 #[fixture]
1480 fn yubihsm2_config() -> TestResult<YubiHsm2Config> {
1481 Ok(YubiHsm2Config::new(
1482 BTreeSet::from_iter([
1483 Connection::Mock
1484 ]),
1485 BTreeSet::from_iter([
1486 YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1487 YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1488 YubiHsm2UserMapping::AuditLog {
1489 authentication_key_id: 3,
1490 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1491 system_user: "yubihsm2-metrics-user".parse()?,
1492 },
1493 YubiHsm2UserMapping::Backup{
1494 authentication_key_id: 2,
1495 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1496 system_user: "yubihsm2-backup-user".parse()?,
1497 wrapping_key_id: 1,
1498 },
1499 YubiHsm2UserMapping::HermeticAuditLog {
1500 authentication_key_id: 4,
1501 system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1502 },
1503 YubiHsm2UserMapping::Signing {
1504 authentication_key_id: 5,
1505 signing_key_id: 1,
1506 key_setup: SigningKeySetup::new(
1507 KeyType::Curve25519,
1508 vec![KeyMechanism::EdDsaSignature],
1509 None,
1510 SignatureType::EdDsa,
1511 CryptographicKeyContext::OpenPgp {
1512 notations: Default::default(),
1513 user_ids: OpenPgpUserIdList::new(vec![
1514 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1515 ])?,
1516 version: "v4".parse()?,
1517 },
1518 )?,
1519 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1520 system_user: "yubihsm2-signing-user".parse()?,
1521 domain: Domain::One,
1522 }
1523 ]),
1524 )?)
1525 }
1526
1527 #[fixture]
1529 fn signstar_config(yubihsm2_config: TestResult<YubiHsm2Config>) -> TestResult<Config> {
1530 let yubihsm2_config = yubihsm2_config?;
1531 let config = ConfigBuilder::new(SystemConfig::new(
1532 1,
1533 signstar_crypto::AdministrativeSecretHandling::Plaintext,
1534 signstar_crypto::NonAdministrativeSecretHandling::Plaintext,
1535 BTreeSet::from_iter([]),
1536 )?)
1537 .set_yubihsm2_config(yubihsm2_config)
1538 .finish()?;
1539
1540 Ok(config)
1541 }
1542
1543 #[fixture]
1544 fn admin_credentials() -> TestResult<YubiHsm2AdminCredentials> {
1545 let admin_creds = YubiHsm2AdminCredentials::new(
1546 1,
1547 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1548 vec![
1549 Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1550 Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1551 ]
1552 )?;
1553 Ok(admin_creds)
1554 }
1555
1556 fn create_yubihsm2_backend<'admin_creds, 'config>(
1566 connector: Connector,
1567 admin_credentials: &'admin_creds YubiHsm2AdminCredentials,
1568 signstar_config: &'config Config,
1569 ) -> TestResult<YubiHsm2Backend<'admin_creds, 'config>> {
1570 setup_logging(LevelFilter::Debug)?;
1571 let Some(backend) = YubiHsm2Backend::new(connector, admin_credentials, signstar_config)?
1572 else {
1573 panic!("The Config did not contain a YubiHsm2Config.");
1574 };
1575
1576 Ok(backend)
1577 }
1578
1579 #[rstest]
1582 #[case::single_admin_uses_default_id(
1583 YubiHsm2AdminCredentials::new(
1584 1,
1585 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1586 vec![
1587 Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1588 ]
1589 )?,
1590 YubiHsm2Config::new(
1591 BTreeSet::from_iter([
1592 Connection::Mock
1593 ]),
1594 BTreeSet::from_iter([
1595 YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1596 YubiHsm2UserMapping::AuditLog {
1597 authentication_key_id: 3,
1598 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1599 system_user: "yubihsm2-metrics-user".parse()?,
1600 },
1601 YubiHsm2UserMapping::Backup{
1602 authentication_key_id: 2,
1603 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1604 system_user: "yubihsm2-backup-user".parse()?,
1605 wrapping_key_id: 1,
1606 },
1607 YubiHsm2UserMapping::HermeticAuditLog {
1608 authentication_key_id: 4,
1609 system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1610 },
1611 YubiHsm2UserMapping::Signing {
1612 authentication_key_id: 5,
1613 signing_key_id: 1,
1614 key_setup: SigningKeySetup::new(
1615 KeyType::Curve25519,
1616 vec![KeyMechanism::EdDsaSignature],
1617 None,
1618 SignatureType::EdDsa,
1619 CryptographicKeyContext::OpenPgp {
1620 notations: Default::default(),
1621 user_ids: OpenPgpUserIdList::new(vec![
1622 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1623 ])?,
1624 version: "v4".parse()?,
1625 },
1626 )?,
1627 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1628 system_user: "yubihsm2-signing-user".parse()?,
1629 domain: Domain::One,
1630 }
1631 ]),
1632 )?
1633 )]
1634 #[case::single_admin_does_not_use_default_id(
1635 YubiHsm2AdminCredentials::new(
1636 1,
1637 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1638 vec![
1639 Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1640 ]
1641 )?,
1642 YubiHsm2Config::new(
1643 BTreeSet::from_iter([
1644 Connection::Mock
1645 ]),
1646 BTreeSet::from_iter([
1647 YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1648 YubiHsm2UserMapping::AuditLog {
1649 authentication_key_id: 3,
1650 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1651 system_user: "yubihsm2-metrics-user".parse()?,
1652 },
1653 YubiHsm2UserMapping::Backup{
1654 authentication_key_id: 2,
1655 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1656 system_user: "yubihsm2-backup-user".parse()?,
1657 wrapping_key_id: 1,
1658 },
1659 YubiHsm2UserMapping::HermeticAuditLog {
1660 authentication_key_id: 4,
1661 system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1662 },
1663 YubiHsm2UserMapping::Signing {
1664 authentication_key_id: 5,
1665 signing_key_id: 1,
1666 key_setup: SigningKeySetup::new(
1667 KeyType::Curve25519,
1668 vec![KeyMechanism::EdDsaSignature],
1669 None,
1670 SignatureType::EdDsa,
1671 CryptographicKeyContext::OpenPgp {
1672 notations: Default::default(),
1673 user_ids: OpenPgpUserIdList::new(vec![
1674 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1675 ])?,
1676 version: "v4".parse()?,
1677 },
1678 )?,
1679 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1680 system_user: "yubihsm2-signing-user".parse()?,
1681 domain: Domain::One,
1682 }
1683 ]),
1684 )?
1685 )]
1686 #[case::multiple_admins_do_not_use_default_id(
1687 YubiHsm2AdminCredentials::new(
1688 1,
1689 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1690 vec![
1691 Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1692 Credentials::new(7, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1693 ]
1694 )?,
1695 YubiHsm2Config::new(
1696 BTreeSet::from_iter([
1697 Connection::Mock
1698 ]),
1699 BTreeSet::from_iter([
1700 YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1701 YubiHsm2UserMapping::Admin { authentication_key_id: 7 },
1702 YubiHsm2UserMapping::AuditLog {
1703 authentication_key_id: 3,
1704 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1705 system_user: "yubihsm2-metrics-user".parse()?,
1706 },
1707 YubiHsm2UserMapping::Backup{
1708 authentication_key_id: 2,
1709 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1710 system_user: "yubihsm2-backup-user".parse()?,
1711 wrapping_key_id: 1,
1712 },
1713 YubiHsm2UserMapping::HermeticAuditLog {
1714 authentication_key_id: 4,
1715 system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1716 },
1717 YubiHsm2UserMapping::Signing {
1718 authentication_key_id: 5,
1719 signing_key_id: 1,
1720 key_setup: SigningKeySetup::new(
1721 KeyType::Curve25519,
1722 vec![KeyMechanism::EdDsaSignature],
1723 None,
1724 SignatureType::EdDsa,
1725 CryptographicKeyContext::OpenPgp {
1726 notations: Default::default(),
1727 user_ids: OpenPgpUserIdList::new(vec![
1728 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1729 ])?,
1730 version: "v4".parse()?,
1731 },
1732 )?,
1733 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1734 system_user: "yubihsm2-signing-user".parse()?,
1735 domain: Domain::One,
1736 }
1737 ]),
1738 )?
1739 )]
1740 #[case::multiple_admins_one_uses_default_id(
1741 YubiHsm2AdminCredentials::new(
1742 1,
1743 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1744 vec![
1745 Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1746 Credentials::new(7, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1747 ]
1748 )?,
1749 YubiHsm2Config::new(
1750 BTreeSet::from_iter([
1751 Connection::Mock
1752 ]),
1753 BTreeSet::from_iter([
1754 YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1755 YubiHsm2UserMapping::Admin { authentication_key_id: 7 },
1756 YubiHsm2UserMapping::AuditLog {
1757 authentication_key_id: 3,
1758 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1759 system_user: "yubihsm2-metrics-user".parse()?,
1760 },
1761 YubiHsm2UserMapping::Backup{
1762 authentication_key_id: 2,
1763 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1764 system_user: "yubihsm2-backup-user".parse()?,
1765 wrapping_key_id: 1,
1766 },
1767 YubiHsm2UserMapping::HermeticAuditLog {
1768 authentication_key_id: 4,
1769 system_user: "yubihsm2-hermetic-metrics-user".parse()?,
1770 },
1771 YubiHsm2UserMapping::Signing {
1772 authentication_key_id: 5,
1773 signing_key_id: 1,
1774 key_setup: SigningKeySetup::new(
1775 KeyType::Curve25519,
1776 vec![KeyMechanism::EdDsaSignature],
1777 None,
1778 SignatureType::EdDsa,
1779 CryptographicKeyContext::OpenPgp {
1780 notations: Default::default(),
1781 user_ids: OpenPgpUserIdList::new(vec![
1782 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1783 ])?,
1784 version: "v4".parse()?,
1785 },
1786 )?,
1787 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1788 system_user: "yubihsm2-signing-user".parse()?,
1789 domain: Domain::One,
1790 }
1791 ]),
1792 )?
1793 )]
1794 fn yubihsm2_backend_sync_succeeds(
1795 connector: Connector,
1796 #[case] admin_credentials: YubiHsm2AdminCredentials,
1797 #[case] yubihsm2_config: YubiHsm2Config,
1798 ) -> TestResult {
1799 setup_logging(LevelFilter::Debug)?;
1800
1801 let config = ConfigBuilder::new(SystemConfig::new(
1802 1,
1803 AdministrativeSecretHandling::Plaintext,
1804 NonAdministrativeSecretHandling::Plaintext,
1805 BTreeSet::from_iter([]),
1806 )?)
1807 .set_yubihsm2_config(yubihsm2_config)
1808 .finish()?;
1809 let Some(backend) = YubiHsm2Backend::new(connector, &admin_credentials, &config)? else {
1810 panic!("No YubiHsm2Config in the provided Signstar config");
1811 };
1812 let credentials = vec![
1813 Credentials::new("2".parse()?, Passphrase::generate(Some(50))),
1814 Credentials::new("3".parse()?, Passphrase::generate(Some(50))),
1815 Credentials::new("4".parse()?, Passphrase::generate(Some(50))),
1816 Credentials::new("5".parse()?, Passphrase::generate(Some(50))),
1817 ];
1818
1819 backend.sync(&credentials)?;
1820
1821 backend.sync(&credentials)?;
1823
1824 Ok(())
1825 }
1826
1827 #[test]
1829 fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_duplicate_creds()
1830 -> TestResult {
1831 let user_mappings = HashSet::new();
1832 let creds = vec![
1833 Credentials::new(1, Passphrase::generate(Some(50))),
1834 Credentials::new(1, Passphrase::generate(Some(50))),
1835 ];
1836 match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1837 Err(crate::Error::YubiHsm2Backend(crate::yubihsm2::Error::DuplicateCredentials {
1838 ..
1839 })) => {}
1840 Err(error) => panic!(
1841 "Expected to fail with Error::DuplicateCredentials but failed with a different error: {error}"
1842 ),
1843 Ok(_) => {
1844 panic!("Expected to fail with Error::DuplicateCredentials but succeeded instead!")
1845 }
1846 }
1847
1848 Ok(())
1849 }
1850
1851 #[test]
1854 fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_missing_creds_for_mappings()
1855 -> TestResult {
1856 let user_mappings_list = [
1857 YubiHsm2UserMapping::Admin {
1858 authentication_key_id: 1,
1859 },
1860 YubiHsm2UserMapping::Admin {
1861 authentication_key_id: 2,
1862 },
1863 ];
1864 let user_mappings = HashSet::from_iter(user_mappings_list.iter());
1865 let creds = vec![Credentials::new(1, Passphrase::generate(Some(50)))];
1866 match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1867 Err(crate::Error::YubiHsm2Backend(
1868 crate::yubihsm2::Error::UserMappingWithoutCredentials { .. },
1869 )) => {}
1870 Err(error) => panic!(
1871 "Expected to fail with Error::UserMappingWithoutCredentials but failed with a different error: {error}"
1872 ),
1873 Ok(_) => {
1874 panic!(
1875 "Expected to fail with Error::UserMappingWithoutCredentials but succeeded instead!"
1876 )
1877 }
1878 }
1879
1880 Ok(())
1881 }
1882
1883 #[test]
1886 fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_missing_mappings_for_creds()
1887 -> TestResult {
1888 let user_mappings_list = [YubiHsm2UserMapping::Admin {
1889 authentication_key_id: 1,
1890 }];
1891 let user_mappings = HashSet::from_iter(user_mappings_list.iter());
1892 let creds = vec![
1893 Credentials::new(1, Passphrase::generate(Some(50))),
1894 Credentials::new(2, Passphrase::generate(Some(50))),
1895 ];
1896 match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1897 Err(crate::Error::YubiHsm2Backend(
1898 crate::yubihsm2::Error::CredentialsWithoutUserMapping { .. },
1899 )) => {}
1900 Err(error) => panic!(
1901 "Expected to fail with Error::CredentialsWithoutUserMapping but failed with a different error: {error}"
1902 ),
1903 Ok(_) => {
1904 panic!(
1905 "Expected to fail with Error::CredentialsWithoutUserMapping but succeeded instead!"
1906 )
1907 }
1908 }
1909
1910 Ok(())
1911 }
1912
1913 #[rstest]
1916 fn yubihsm2_backend_state_try_from_yubihsm2_backend_succeeds(
1917 connector: Connector,
1918 admin_credentials: TestResult<YubiHsm2AdminCredentials>,
1919 signstar_config: TestResult<Config>,
1920 ) -> TestResult {
1921 let signstar_config = signstar_config?;
1922 let admin_credentials = admin_credentials?;
1923 let backend = create_yubihsm2_backend(connector, &admin_credentials, &signstar_config)?;
1924
1925 let _state = YubiHsm2BackendState::try_from(&backend)?;
1926
1927 Ok(())
1928 }
1929
1930 #[rstest]
1933 fn yubihsm2_backend_new_fails_on_mismatching_iterations(
1934 connector: Connector,
1935 signstar_config: TestResult<Config>,
1936 ) -> TestResult {
1937 let signstar_config = signstar_config?;
1938 let admin_credentials = YubiHsm2AdminCredentials::new(
1939 2,
1940 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1941 vec![
1942 Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1943 Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1944 ]
1945 )?;
1946 match YubiHsm2Backend::new(connector, &admin_credentials, &signstar_config) {
1947 Err(crate::Error::IterationMismatch { .. }) => {}
1948 Err(error) => panic!(
1949 "Expected to fail with Error::IterationMismatch, but failed differently instead: {error}"
1950 ),
1951 Ok(_) => {
1952 panic!("Expected to fail with Error::IterationMismatch, but succeeded instead")
1953 }
1954 }
1955
1956 Ok(())
1957 }
1958
1959 #[rstest]
1962 fn yubihsm2_backend_new_fails_on_missing_yubihsm2_config(
1963 connector: Connector,
1964 admin_credentials: TestResult<YubiHsm2AdminCredentials>,
1965 ) -> TestResult {
1966 let admin_credentials = admin_credentials?;
1967 let signstar_config = ConfigBuilder::new(SystemConfig::new(
1968 1,
1969 signstar_crypto::AdministrativeSecretHandling::Plaintext,
1970 signstar_crypto::NonAdministrativeSecretHandling::Plaintext,
1971 BTreeSet::from_iter([]),
1972 )?)
1973 .finish()?;
1974
1975 match YubiHsm2Backend::new(connector, &admin_credentials, &signstar_config) {
1976 Ok(None) => {}
1977 Ok(Some(_)) => {
1978 panic!("Expected to succeed with None, but succeeded with Some instead")
1979 }
1980 Err(error) => panic!("Expected to succeed with None, but failed instead: {error}"),
1981 }
1982
1983 Ok(())
1984 }
1985}