1use std::{cell::RefCell, collections::HashSet, fmt::Debug, time::Duration};
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 reset(&self) -> Result<(), crate::Error> {
597 info!("Reset the backend {:?} to its defaults...", self.connector);
598
599 let credentials = self.usable_admin_creds()?;
600
601 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(
602 credentials,
603 vec![Command::ResetDeviceAndReconnect {
604 timeout: Duration::from_secs(5),
606 }],
607 )]);
608 let scenario_result = self.runner.run(&scenario)?;
609
610 if scenario_result.chains().len() > 1 {
612 return Err(Error::ScenarioLogic {
613 context: format!("there are more return values ({}) than requested actions (1), when resetting the backend device {:?}",
614 scenario_result.chains().len(),
615 self.connector
616 ),
617 }
618 .into());
619 }
620 let Some(command_return_values) = scenario_result.chains().first() else {
621 return Err(Error::ScenarioLogic {
622 context: format!(
623 "there are no command return values when resetting the backend device {:?}",
624 self.connector
625 ),
626 }
627 .into());
628 };
629 if command_return_values.len() != 1 {
630 return Err(Error::ScenarioLogic {
631 context: format!("the number of command return values ({}) does not match the number of requested actions (1) when resetting the backend device {:?}",
632 command_return_values.len(),
633 self.connector
634 ),
635 }
636 .into());
637 };
638 match command_return_values.first() {
639 Some(CommandReturnValue::ResetDeviceAndReconnect) => {}
640 Some(_) => {
641 return Err(Error::ScenarioLogic {
642 context: format!("a different return value from the one expected when resetting the backend device {:?} was returned", self.connector)
643 }
644 .into());
645 }
646 _ => {
647 return Err(Error::ScenarioLogic {
648 context: format!(
649 "no return value returned when resetting the backend device {:?}",
650 self.connector
651 ),
652 }
653 .into());
654 }
655 }
656
657 Ok(())
658 }
659
660 pub fn sync(&self, user_credentials: &[Credentials]) -> Result<(), crate::Error> {
663 info!("Syncing the YubiHSM2 backend with the Signstar configuration...");
664
665 let non_admin_users_and_creds =
666 UserMappingsAndCredentials::new_from_non_admin(self.yubihsm2_config, user_credentials)?;
667 debug!(
668 "valid mappings and their creds: {}",
669 non_admin_users_and_creds
670 .as_ref()
671 .iter()
672 .map(|(mapping, creds)| format!("{mapping:?}: {creds:?}"))
673 .collect::<Vec<_>>()
674 .join(", ")
675 );
676
677 if self.check_set_default_credentials() {
683 self.reset()?;
684 }
685
686 self.add_admin_users()?;
687 self.add_wrap_key()?;
688 self.add_non_admin_users(&non_admin_users_and_creds)?;
689 self.add_signing_keys()?;
690 self.add_openpgp_certificates(&non_admin_users_and_creds)?;
691
692 Ok(())
693 }
694
695 fn add_openpgp_certificates(
711 &self,
712 non_admin_user_mappings_and_creds: &UserMappingsAndCredentials,
713 ) -> Result<(), crate::Error> {
714 info!("Adding OpenPGP certificates for signing keys...");
715
716 let credentials = self.usable_admin_creds()?;
717
718 let (commands, ids) = {
719 let opaque_data_infos = get_object_infos_for_object_types(
720 &self.runner,
721 &credentials,
722 &[ObjectType::Opaque],
723 )?;
724 let mut commands = Vec::new();
725 let mut ids = Vec::new();
726
727 for (mapping, credentials) in non_admin_user_mappings_and_creds.as_ref() {
728 if let YubiHsm2UserMapping::Signing {
731 domain,
732 signing_key_id,
733 key_setup,
734 ..
735 } = mapping
736 {
737 let CryptographicKeyContext::OpenPgp {
738 user_ids, version, ..
739 } = key_setup.key_context()
740 else {
741 debug!(
742 "Skipping the generation of an OpenPGP certificate for signing key {signing_key_id}, because it is not setup for use with OpenPGP..."
743 );
744 continue;
745 };
746
747 if opaque_data_infos
748 .iter()
749 .any(|info| info.object_id == *signing_key_id)
750 {
751 warn!(
752 "Skipping the generation of an OpenPGP certificate for signing key {signing_key_id}, because data exists already..."
753 );
754 continue;
755 }
756
757 let client = Client::create(self.connector.clone(), (*credentials).into())
758 .map_err(|source| signstar_yubihsm2::Error::Client { context: "creating a client for adding OpenPGP certificates for a signing key", source })?;
759 let signer = YubiHsm2SigningKey::new(client, *signing_key_id);
760 let flags = {
761 let mut flags = OpenPgpKeyUsageFlags::default();
762 flags.set_sign();
763 flags
764 };
765 let certificate = generate_certificate(
766 &signer,
767 flags,
768 user_ids.as_ref(),
769 Default::default(),
770 Timestamp::now(),
771 *version,
772 )?;
773 signer.close_session()?;
774
775 commands.push(Command::PutOpaque {
776 id: *signing_key_id,
777 label: YubiHsm2Config::openpgp_certificate_label(),
778 domains: Domains::from(*domain),
779 capabilities: OpaqueDataCapabilities::ExportableUnderWrap,
780 algorithm: OpaqueDataAlgorithm::OpaqueData,
781 data: OpaqueData::new(certificate)?,
782 });
783 ids.push(*signing_key_id);
784 }
785 }
786
787 (commands, ids)
788 };
789
790 if commands.is_empty() {
792 return Ok(());
793 }
794
795 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
796 let scenario_result = self.runner.run(&scenario)?;
797
798 let Some(command_return_values) = scenario_result.chains().first() else {
800 return Err(Error::ScenarioLogic {
801 context: format!(
802 "there are no command return values when adding OpenPGP certificates for signing keys {}",
803 ids.iter()
804 .map(ToString::to_string)
805 .collect::<Vec<_>>()
806 .join(", ")
807 ),
808 }
809 .into());
810 };
811 if command_return_values.len() != ids.len() {
812 return Err(Error::ScenarioLogic {
813 context: format!("the number of command return values ({}) does not match the number of requested IDs ({}) when adding OpenPGP certificates for signing keys {}",
814 command_return_values.len(),
815 ids.len(),
816 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
817 ),
818 }
819 .into());
820 };
821
822 for (command_return_value, requested_id) in command_return_values.iter().zip(ids.iter()) {
823 let returned_id = match command_return_value {
824 CommandReturnValue::PutOpaque(returned_id) => *returned_id,
825 command => {
826 let command_name = CommandName::from(command);
827 return Err(Error::ScenarioLogic {
828 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"),
829 }
830 .into());
831 }
832 };
833
834 if *requested_id != returned_id {
835 return Err(Error::ScenarioLogic {
836 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 {}",
837 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
838 ),
839 }
840 .into());
841 }
842 }
843
844 Ok(())
845 }
846
847 fn add_signing_keys(&self) -> Result<(), crate::Error> {
862 info!("Adding asymmetric signing keys...");
863
864 let credentials = self.usable_admin_creds()?;
865 let (commands, ids) = {
866 let signing_key_infos = get_object_infos_for_object_types(
867 &self.runner,
868 &credentials,
869 &[ObjectType::AsymmetricKey],
870 )?;
871 let mut commands = Vec::new();
872 let mut ids = Vec::new();
873
874 for mapping in self.yubihsm2_config.mappings() {
875 if let YubiHsm2UserMapping::Signing {
878 domain,
879 signing_key_id,
880 ..
881 } = mapping
882 {
883 if signing_key_infos
885 .iter()
886 .any(|info| info.object_id == *signing_key_id)
887 {
888 warn!("Not adding signing key {signing_key_id}, as it exists already...");
889 continue;
890 }
891
892 commands.push(Command::GenerateAsymmetricKey {
893 info: KeyInfo {
894 key_id: *signing_key_id,
895 domains: Domains::from(*domain),
896 caps: mapping.capabilities(),
897 label: mapping.label(),
898 },
899 });
900 ids.push(*signing_key_id);
901 }
902 }
903
904 (commands, ids)
905 };
906
907 if commands.is_empty() {
909 return Ok(());
910 }
911
912 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
913 let scenario_result = self.runner.run(&scenario)?;
914
915 let Some(command_return_values) = scenario_result.chains().first() else {
916 return Err(Error::ScenarioLogic {
917 context: format!(
918 "there are no command return values when adding the signing keys {}",
919 ids.iter()
920 .map(ToString::to_string)
921 .collect::<Vec<_>>()
922 .join(", ")
923 ),
924 }
925 .into());
926 };
927 if command_return_values.len() != ids.len() {
928 return Err(Error::ScenarioLogic {
929 context: format!("the number of command return values ({}) does not match the number of requested IDs ({}) when adding the signing keys {}",
930 command_return_values.len(),
931 ids.len(),
932 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
933 ),
934 }
935 .into());
936 };
937
938 for (number, command_return_value) in command_return_values.iter().enumerate() {
939 let Some(id) = ids.get(number) else {
940 return Err(Error::ScenarioLogic {
941 context: format!("the signing key ID number {number} does not exist in the list of signing keys {}",
942 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
943 ),
944 }
945 .into());
946 };
947 let CommandReturnValue::GenerateAsymmetricKey(returned_id) = command_return_value
948 else {
949 return Err(Error::ScenarioLogic {
950 context: format!("the command return value when adding the signing key {id} is that of a different action"),
951 }
952 .into());
953 };
954 if id != returned_id {
955 return Err(Error::ScenarioLogic {
956 context: format!("the returned signing key ID ({returned_id}) does not match the requested one ({id}) when adding the signing keys {}",
957 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
958 ),
959 }
960 .into());
961 }
962 }
963
964 Ok(())
965 }
966
967 fn add_non_admin_users(
983 &self,
984 non_admin_user_mappings_and_creds: &UserMappingsAndCredentials,
985 ) -> Result<(), crate::Error> {
986 info!("Setting up non-administrative users...");
987
988 let credentials = self.usable_admin_creds()?;
989
990 let (commands, ids) = {
991 let authentication_key_infos = get_object_infos_for_object_types(
992 &self.runner,
993 &credentials,
994 &[ObjectType::AuthenticationKey],
995 )?;
996 let mut commands = Vec::new();
997 let mut ids = Vec::new();
998
999 for (mapping, creds) in non_admin_user_mappings_and_creds.as_ref() {
1000 let id = mapping.backend_user_id();
1001
1002 if authentication_key_infos
1003 .iter()
1004 .any(|info| info.object_id == id)
1005 {
1006 warn!("The existing authentication key {id} will be replaced...");
1007 commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(id)));
1008 }
1009
1010 commands.push(Command::PutAuthenticationKey {
1011 info: mapping.authentication_key_info(),
1012 delegated_caps: Capabilities::from(vec![].as_slice()),
1013 authentication_key: AuthenticationKey::try_from(creds.passphrase())
1014 .map_err(crate::Error::SignstarYubiHsm2)?,
1015 });
1016 ids.push(id);
1017 }
1018
1019 (commands, ids)
1020 };
1021
1022 if commands.is_empty() {
1024 return Ok(());
1025 }
1026
1027 let number_of_commands = commands.len();
1028 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
1029 let scenario_result = self.runner.run(&scenario)?;
1030
1031 let Some(command_return_values) = scenario_result.chains().first() else {
1033 return Err(Error::ScenarioLogic {
1034 context: format!("there are no command return values when adding the non-administrative users {}",
1035 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1036 ),
1037 }
1038 .into());
1039 };
1040 if command_return_values.len() != number_of_commands {
1041 return Err(Error::ScenarioLogic {
1042 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 {}",
1043 command_return_values.len(),
1044 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1045 ),
1046 }
1047 .into());
1048 };
1049
1050 for (number, command_return_value) in command_return_values
1052 .iter()
1053 .filter(|command_return_value| {
1054 matches!(
1055 command_return_value,
1056 CommandReturnValue::PutAuthenticationKey(_)
1057 )
1058 })
1059 .enumerate()
1060 {
1061 let Some(id) = ids.get(number) else {
1062 return Err(Error::ScenarioLogic {
1063 context: format!("the authentication key ID number {number} does not exist in the list of non-administrative users {}",
1064 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1065 ),
1066 }
1067 .into());
1068 };
1069 let CommandReturnValue::PutAuthenticationKey(returned_id) = command_return_value else {
1070 return Err(Error::ScenarioLogic {
1071 context: format!("the command return value when adding the non-administrative user {id} is that of a different action"),
1072 }
1073 .into());
1074 };
1075 if id != returned_id {
1076 return Err(Error::ScenarioLogic {
1077 context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({id}) when adding the non-administrative users {}",
1078 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1079 ),
1080 }
1081 .into());
1082 }
1083 }
1084
1085 Ok(())
1086 }
1087
1088 fn add_wrap_key(&self) -> Result<(), crate::Error> {
1104 info!("Setting up wrapping key...");
1105
1106 let credentials = self.usable_admin_creds()?;
1107
1108 let commands = {
1109 let wrap_key_infos = get_object_infos_for_object_types(
1110 &self.runner,
1111 &credentials,
1112 &[ObjectType::WrapKey],
1113 )?;
1114 let mut commands = Vec::new();
1115
1116 if wrap_key_infos
1118 .iter()
1119 .any(|info| info.object_id == YubiHsm2Config::WRAP_KEY_ID)
1120 {
1121 warn!(
1122 "The existing wrapping key ({}) will be replaced...",
1123 YubiHsm2Config::WRAP_KEY_ID
1124 );
1125 commands.push(Command::DeleteObject(ObjectId::WrappingKey(
1126 YubiHsm2Config::WRAP_KEY_ID,
1127 )))
1128 }
1129
1130 let passphrase = self.admin_credentials.backup_passphrase();
1131 let wrapping_key: WrapKey =
1132 WrapKeyFromPassphrase::new(passphrase, WrapKeyKind::Aes256)?.try_into()?;
1133 commands.push(Command::PutWrapKey {
1134 info: KeyInfo {
1135 key_id: YubiHsm2Config::WRAP_KEY_ID,
1136 domains: Domains::all(),
1137 caps: Capabilities::from(YubiHsm2UserMapping::CAP_BACKUP),
1138 label: YubiHsm2Config::wrap_key_label(),
1139 },
1140 delegated_caps: Capabilities::from(YubiHsm2UserMapping::CAP_BACKUP),
1141 wrapping_key,
1142 });
1143 commands
1144 };
1145 let number_of_commands = commands.len();
1146
1147 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(
1148 credentials.clone(),
1149 commands,
1150 )]);
1151 let scenario_return_value = self.runner.run(&scenario)?;
1152
1153 let command_return_values = {
1155 let return_values: Vec<Vec<CommandReturnValue>> = scenario_return_value.into();
1156 let return_values_len = return_values.len();
1157 let mut return_values_iter = return_values.into_iter();
1158
1159 let Some(command_return_values) = return_values_iter.next() else {
1160 return Err(Error::ScenarioLogic {
1161 context: format!(
1162 "there are no command return values when adding the wrap key {}",
1163 YubiHsm2Config::WRAP_KEY_ID
1164 ),
1165 }
1166 .into());
1167 };
1168 if return_values_iter.next().is_some() {
1169 return Err(Error::ScenarioLogic {
1170 context: format!("there are {} return value lists when adding the wrap key {}, but we expected exactly 1",
1171 return_values_len,
1172 YubiHsm2Config::WRAP_KEY_ID
1173 ),
1174 }
1175 .into());
1176 }
1177 command_return_values
1178 };
1179 let returned_id = {
1180 match command_return_values.len() {
1181 2 => {
1182 let Some(CommandReturnValue::DeleteObject) = command_return_values.first()
1183 else {
1184 return Err(Error::ScenarioLogic {
1185 context: format!(
1186 "there is no return value for removing a previously existing wrap key {}",
1187 YubiHsm2Config::WRAP_KEY_ID
1188 ),
1189 }
1190 .into());
1191 };
1192 let Some(CommandReturnValue::PutWrapKey(id)) = command_return_values.get(1)
1193 else {
1194 return Err(Error::ScenarioLogic {
1195 context: format!(
1196 "there is no return value for adding the wrap key {}",
1197 YubiHsm2Config::WRAP_KEY_ID
1198 ),
1199 }
1200 .into());
1201 };
1202 *id
1203 }
1204 1 => {
1205 let Some(CommandReturnValue::PutWrapKey(id)) = command_return_values.first()
1206 else {
1207 return Err(Error::ScenarioLogic {
1208 context: format!(
1209 "there is no return value for adding the wrap key {}",
1210 YubiHsm2Config::WRAP_KEY_ID
1211 ),
1212 }
1213 .into());
1214 };
1215 *id
1216 }
1217 _ => {
1218 return Err(Error::ScenarioLogic {
1219 context: format!("there are {} return values when removing and/or adding the wrap key {}, but we expected exactly {number_of_commands}",
1220 command_return_values.len(),
1221 YubiHsm2Config::WRAP_KEY_ID
1222 ),
1223 }
1224 .into());
1225 }
1226 }
1227 };
1228 if returned_id != YubiHsm2Config::WRAP_KEY_ID {
1229 return Err(Error::ScenarioLogic {
1230 context: format!("the returned key ID ({returned_id}) does not match the requested one when adding the wrap key {}",
1231 YubiHsm2Config::WRAP_KEY_ID
1232 ),
1233 }
1234 .into());
1235 }
1236
1237 Ok(())
1238 }
1239
1240 fn add_admin_users(&self) -> Result<(), crate::Error> {
1258 info!("Setting up administrative users...");
1259
1260 let credentials = self.usable_admin_creds()?;
1261 let (commands, ids) = {
1262 let authentication_key_infos = get_object_infos_for_object_types(
1263 &self.runner,
1264 &credentials,
1265 &[ObjectType::AuthenticationKey],
1266 )?;
1267 let mut commands = Vec::new();
1268 let mut ids = Vec::new();
1269
1270 for (mapping, creds) in self
1272 .admin_user_mappings_and_creds
1273 .as_ref()
1274 .iter()
1275 .filter(|(mapping, ..)| mapping.backend_user_id() != credentials.id())
1276 {
1277 let id = mapping.backend_user_id();
1278
1279 if authentication_key_infos
1280 .iter()
1281 .any(|info| info.object_id == id)
1282 {
1283 warn!("Replacing the existing authentication key {id}...");
1284 commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(id)));
1285 ids.push(id);
1286 }
1287
1288 commands.push(Command::PutAuthenticationKey {
1289 info: KeyInfo {
1290 key_id: creds.id(),
1291 domains: mapping.domains(),
1292 caps: mapping.capabilities(),
1293 label: mapping.label(),
1294 },
1295 delegated_caps: mapping.capabilities(),
1296 authentication_key: AuthenticationKey::try_from(creds.passphrase())?,
1297 });
1298 ids.push(id);
1299 }
1300
1301 if authentication_key_infos
1305 .iter()
1306 .any(|info| info.object_id == YubiHsm2AdminCredentials::DEFAULT_ID)
1307 && !self
1308 .admin_user_mappings_and_creds
1309 .as_ref()
1310 .iter()
1311 .any(|(mapping, _)| {
1312 mapping.backend_user_id() == YubiHsm2AdminCredentials::DEFAULT_ID
1313 })
1314 {
1315 warn!(
1316 "Removing the default authentication key {}...",
1317 YubiHsm2AdminCredentials::DEFAULT_ID
1318 );
1319 commands.push(Command::DeleteObject(ObjectId::AuthenticationKey(
1320 YubiHsm2AdminCredentials::DEFAULT_ID,
1321 )));
1322 ids.push(YubiHsm2AdminCredentials::DEFAULT_ID)
1323 }
1324
1325 if let Some((_, current_credentials)) = self
1328 .admin_user_mappings_and_creds
1329 .as_ref()
1330 .iter()
1331 .find(|(mapping, ..)| mapping.backend_user_id() == credentials.id())
1332 {
1333 warn!(
1334 "Changing the authentication key {}, which is currently used...",
1335 current_credentials.id()
1336 );
1337 commands.push(Command::ChangeAuthenticationKey {
1338 key_id: current_credentials.id(),
1339 authentication_key: AuthenticationKey::try_from(
1340 current_credentials.passphrase(),
1341 )?,
1342 });
1343 ids.push(current_credentials.id());
1344 }
1345
1346 (commands, ids)
1347 };
1348
1349 if commands.is_empty() {
1351 return Ok(());
1352 }
1353
1354 let scenario = Scenario::new(vec![AuthenticatedCommandChain::new(credentials, commands)]);
1355 let scenario_result = self.runner.run(&scenario)?;
1356
1357 let Some(command_return_values) = scenario_result.chains().first() else {
1359 return Err(Error::ScenarioLogic {
1360 context: format!(
1361 "there are no command return values when adding the administrative users {}",
1362 ids.iter()
1363 .map(ToString::to_string)
1364 .collect::<Vec<_>>()
1365 .join(", ")
1366 ),
1367 }
1368 .into());
1369 };
1370 if command_return_values.len() != ids.len() {
1371 return Err(Error::ScenarioLogic {
1372 context: format!("the number of command return values ({}) does not match the number of requested commands ({}) when removing and/or adding the administrative users {}",
1373 command_return_values.len(),
1374 ids.len(),
1375 ids.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
1376 ),
1377 }
1378 .into());
1379 };
1380
1381 for (command_return_value, requested_id) in command_return_values.iter().zip(ids.iter()) {
1383 match command_return_value {
1384 CommandReturnValue::PutAuthenticationKey(returned_id) => {
1385 if returned_id != requested_id {
1386 return Err(Error::ScenarioLogic {
1387 context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({requested_id}) when adding the administrative user"),
1388 }
1389 .into());
1390 }
1391 }
1392 CommandReturnValue::DeleteObject => {
1393 }
1395 CommandReturnValue::ChangeAuthenticationKey(returned_id) => {
1396 if returned_id != requested_id {
1397 return Err(Error::ScenarioLogic {
1398 context: format!("the returned authentication key ID ({returned_id}) does not match the requested one ({requested_id}) when changing the administrative user"),
1399 }
1400 .into());
1401 }
1402 }
1403 _ => return Err(Error::ScenarioLogic {
1404 context: format!("the command return value is not for adding, removing or changing the administrative user {requested_id}: {command_return_value:?}"),
1405 }
1406 .into()),
1407 }
1408 }
1409
1410 info!("Successfully added all admins");
1411
1412 if self.check_set_default_credentials() {
1415 return Err(Error::DefaultAdminStillUsable.into());
1416 }
1417
1418 Ok(())
1419 }
1420
1421 fn usable_admin_creds(&self) -> Result<Credentials, crate::Error> {
1427 info!("Finding usable administrative credentials...");
1428
1429 let credentials = if self.default_credentials_in_use() {
1430 YubiHsm2AdminCredentials::default_credentials()
1431 } else {
1432 let Some(credentials) = self
1433 .admin_credentials
1434 .administrators()
1435 .iter()
1436 .find(|credentials| are_admin_creds_usable(&self.runner, credentials))
1437 else {
1438 return Err(Error::NoUsableAdmin.into());
1439 };
1440
1441 credentials.clone()
1442 };
1443
1444 info!(
1445 "Using administrative credentials with authentication ID {}",
1446 credentials.id()
1447 );
1448
1449 Ok(credentials)
1450 }
1451
1452 pub(crate) fn user_states(&self) -> Result<Vec<YubiHsm2BackendUserData>, crate::Error> {
1460 let credentials = self.usable_admin_creds()?;
1461 let infos = get_object_infos_for_object_types(
1463 &self.runner,
1464 &credentials,
1465 &[ObjectType::AuthenticationKey],
1466 )?;
1467
1468 let user_states = infos
1469 .iter()
1470 .map(|info| YubiHsm2BackendUserData {
1471 id: info.object_id,
1472 capabilities: info.capabilities.into(),
1473 domains: info.domains.into(),
1474 })
1475 .collect::<Vec<_>>();
1476
1477 Ok(user_states)
1478 }
1479
1480 pub(crate) fn key_states(&self) -> Result<Vec<YubiHsm2BackendUserKeyData>, crate::Error> {
1489 let credentials = self.usable_admin_creds()?;
1490 let object_types = [
1492 ObjectType::AsymmetricKey,
1493 ObjectType::HmacKey,
1494 ObjectType::Opaque,
1495 ObjectType::OtpAeakey,
1496 ObjectType::Template,
1497 ObjectType::WrapKey,
1498 ];
1499 let infos = get_object_infos_for_object_types(&self.runner, &credentials, &object_types)?;
1500
1501 let key_states = infos
1502 .into_iter()
1503 .map(|info| YubiHsm2BackendUserKeyData {
1504 id: info.object_id,
1505 object_type: info.object_type.into(),
1506 capabilities: info.capabilities.into(),
1507 domains: info.domains.into(),
1508 algorithm: info.algorithm.into(),
1509 label: info.label.into(),
1510 length: info.length,
1511 })
1512 .collect::<Vec<_>>();
1513
1514 Ok(key_states)
1515 }
1516}
1517
1518#[cfg(all(test, feature = "_yubihsm2-mockhsm"))]
1519mod tests {
1520 use std::collections::BTreeSet;
1521
1522 use log::LevelFilter;
1523 use rstest::{fixture, rstest};
1524 use signstar_common::logging::setup_terminal_logging;
1525 use signstar_crypto::{
1526 AdministrativeSecretHandling,
1527 NonAdministrativeSecretHandling,
1528 key::{
1529 CryptographicKeyContext,
1530
1531 SigningKeySetup,
1532 base::{KeyMechanism, KeyType, SignatureType},
1533 },
1534 openpgp::OpenPgpUserIdList,
1535 passphrase::Passphrase,
1536 };
1537 use signstar_yubihsm2::{Connection, object::Domain};
1538 use testresult::TestResult;
1539
1540 use super::*;
1541 use crate::{
1542 config::{ConfigBuilder, SystemConfig},
1543 yubihsm2::{YubiHsm2Config, YubiHsm2UserMapping, state::YubiHsm2BackendState},
1544 };
1545
1546 #[fixture]
1548 fn connector() -> Connector {
1549 Connector::mockhsm()
1550 }
1551
1552 #[fixture]
1554 fn yubihsm2_config() -> TestResult<YubiHsm2Config> {
1555 Ok(YubiHsm2Config::new(
1556 BTreeSet::from_iter([
1557 Connection::Mock
1558 ]),
1559 BTreeSet::from_iter([
1560 YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1561 YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1562 YubiHsm2UserMapping::AuditLog {
1563 authentication_key_id: 3,
1564 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1565 system_user: "yubihsm2-audit-log".parse()?,
1566 },
1567 YubiHsm2UserMapping::Backup{
1568 authentication_key_id: 2,
1569 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1570 system_user: "yubihsm2-backup".parse()?,
1571 },
1572 YubiHsm2UserMapping::HermeticAuditLog {
1573 authentication_key_id: 4,
1574 system_user: "yubihsm2-hermetic-audit-log".parse()?,
1575 },
1576 YubiHsm2UserMapping::Signing {
1577 authentication_key_id: 5,
1578 signing_key_id: 1,
1579 key_setup: SigningKeySetup::new(
1580 KeyType::Curve25519,
1581 vec![KeyMechanism::EdDsaSignature],
1582 None,
1583 SignatureType::EdDsa,
1584 CryptographicKeyContext::OpenPgp {
1585 notations: Default::default(),
1586 user_ids: OpenPgpUserIdList::new(vec![
1587 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1588 ])?,
1589 version: "v4".parse()?,
1590 },
1591 )?,
1592 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1593 system_user: "yubihsm2-signing".parse()?,
1594 domain: Domain::One,
1595 }
1596 ]),
1597 )?)
1598 }
1599
1600 #[fixture]
1602 fn signstar_config(yubihsm2_config: TestResult<YubiHsm2Config>) -> TestResult<Config> {
1603 let yubihsm2_config = yubihsm2_config?;
1604 let config = ConfigBuilder::new(SystemConfig::new(
1605 1,
1606 signstar_crypto::AdministrativeSecretHandling::Plaintext,
1607 signstar_crypto::NonAdministrativeSecretHandling::Plaintext,
1608 BTreeSet::from_iter([]),
1609 )?)
1610 .set_yubihsm2_config(yubihsm2_config)
1611 .finish()?;
1612
1613 Ok(config)
1614 }
1615
1616 #[fixture]
1617 fn admin_credentials() -> TestResult<YubiHsm2AdminCredentials> {
1618 let admin_creds = YubiHsm2AdminCredentials::new(
1619 1,
1620 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1621 vec![
1622 Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1623 Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1624 ]
1625 )?;
1626 Ok(admin_creds)
1627 }
1628
1629 fn create_yubihsm2_backend<'admin_creds, 'config>(
1639 connector: Connector,
1640 admin_credentials: &'admin_creds YubiHsm2AdminCredentials,
1641 signstar_config: &'config Config,
1642 ) -> TestResult<YubiHsm2Backend<'admin_creds, 'config>> {
1643 setup_terminal_logging(LevelFilter::Debug)?;
1644 let Some(backend) = YubiHsm2Backend::new(connector, admin_credentials, signstar_config)?
1645 else {
1646 panic!("The Config did not contain a YubiHsm2Config.");
1647 };
1648
1649 Ok(backend)
1650 }
1651
1652 #[rstest]
1655 #[case::single_admin_uses_default_id(
1656 YubiHsm2AdminCredentials::new(
1657 1,
1658 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1659 vec![
1660 Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1661 ]
1662 )?,
1663 YubiHsm2Config::new(
1664 BTreeSet::from_iter([
1665 Connection::Mock
1666 ]),
1667 BTreeSet::from_iter([
1668 YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1669 YubiHsm2UserMapping::AuditLog {
1670 authentication_key_id: 3,
1671 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1672 system_user: "yubihsm2-audit-log".parse()?,
1673 },
1674 YubiHsm2UserMapping::Backup{
1675 authentication_key_id: 2,
1676 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1677 system_user: "yubihsm2-backup".parse()?,
1678 },
1679 YubiHsm2UserMapping::HermeticAuditLog {
1680 authentication_key_id: 4,
1681 system_user: "yubihsm2-hermetic-audit-log".parse()?,
1682 },
1683 YubiHsm2UserMapping::Signing {
1684 authentication_key_id: 5,
1685 signing_key_id: 1,
1686 key_setup: SigningKeySetup::new(
1687 KeyType::Curve25519,
1688 vec![KeyMechanism::EdDsaSignature],
1689 None,
1690 SignatureType::EdDsa,
1691 CryptographicKeyContext::OpenPgp {
1692 notations: Default::default(),
1693 user_ids: OpenPgpUserIdList::new(vec![
1694 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1695 ])?,
1696 version: "v4".parse()?,
1697 },
1698 )?,
1699 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1700 system_user: "yubihsm2-signing".parse()?,
1701 domain: Domain::One,
1702 }
1703 ]),
1704 )?
1705 )]
1706 #[case::single_admin_does_not_use_default_id(
1707 YubiHsm2AdminCredentials::new(
1708 1,
1709 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1710 vec![
1711 Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1712 ]
1713 )?,
1714 YubiHsm2Config::new(
1715 BTreeSet::from_iter([
1716 Connection::Mock
1717 ]),
1718 BTreeSet::from_iter([
1719 YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1720 YubiHsm2UserMapping::AuditLog {
1721 authentication_key_id: 3,
1722 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1723 system_user: "yubihsm2-audit-log".parse()?,
1724 },
1725 YubiHsm2UserMapping::Backup{
1726 authentication_key_id: 2,
1727 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1728 system_user: "yubihsm2-backup".parse()?,
1729 },
1730 YubiHsm2UserMapping::HermeticAuditLog {
1731 authentication_key_id: 4,
1732 system_user: "yubihsm2-hermetic-audit-log".parse()?,
1733 },
1734 YubiHsm2UserMapping::Signing {
1735 authentication_key_id: 5,
1736 signing_key_id: 1,
1737 key_setup: SigningKeySetup::new(
1738 KeyType::Curve25519,
1739 vec![KeyMechanism::EdDsaSignature],
1740 None,
1741 SignatureType::EdDsa,
1742 CryptographicKeyContext::OpenPgp {
1743 notations: Default::default(),
1744 user_ids: OpenPgpUserIdList::new(vec![
1745 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1746 ])?,
1747 version: "v4".parse()?,
1748 },
1749 )?,
1750 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1751 system_user: "yubihsm2-signing".parse()?,
1752 domain: Domain::One,
1753 }
1754 ]),
1755 )?
1756 )]
1757 #[case::multiple_admins_do_not_use_default_id(
1758 YubiHsm2AdminCredentials::new(
1759 1,
1760 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1761 vec![
1762 Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1763 Credentials::new(7, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1764 ]
1765 )?,
1766 YubiHsm2Config::new(
1767 BTreeSet::from_iter([
1768 Connection::Mock
1769 ]),
1770 BTreeSet::from_iter([
1771 YubiHsm2UserMapping::Admin { authentication_key_id: 6 },
1772 YubiHsm2UserMapping::Admin { authentication_key_id: 7 },
1773 YubiHsm2UserMapping::AuditLog {
1774 authentication_key_id: 3,
1775 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1776 system_user: "yubihsm2-metrics".parse()?,
1777 },
1778 YubiHsm2UserMapping::Backup{
1779 authentication_key_id: 2,
1780 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1781 system_user: "yubihsm2-backup".parse()?,
1782 },
1783 YubiHsm2UserMapping::HermeticAuditLog {
1784 authentication_key_id: 4,
1785 system_user: "yubihsm2-hermetic-audit-log".parse()?,
1786 },
1787 YubiHsm2UserMapping::Signing {
1788 authentication_key_id: 5,
1789 signing_key_id: 1,
1790 key_setup: SigningKeySetup::new(
1791 KeyType::Curve25519,
1792 vec![KeyMechanism::EdDsaSignature],
1793 None,
1794 SignatureType::EdDsa,
1795 CryptographicKeyContext::OpenPgp {
1796 notations: Default::default(),
1797 user_ids: OpenPgpUserIdList::new(vec![
1798 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1799 ])?,
1800 version: "v4".parse()?,
1801 },
1802 )?,
1803 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1804 system_user: "yubihsm2-signing".parse()?,
1805 domain: Domain::One,
1806 }
1807 ]),
1808 )?
1809 )]
1810 #[case::multiple_admins_one_uses_default_id(
1811 YubiHsm2AdminCredentials::new(
1812 1,
1813 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
1814 vec![
1815 Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1816 Credentials::new(7, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
1817 ]
1818 )?,
1819 YubiHsm2Config::new(
1820 BTreeSet::from_iter([
1821 Connection::Mock
1822 ]),
1823 BTreeSet::from_iter([
1824 YubiHsm2UserMapping::Admin { authentication_key_id: 1 },
1825 YubiHsm2UserMapping::Admin { authentication_key_id: 7 },
1826 YubiHsm2UserMapping::AuditLog {
1827 authentication_key_id: 3,
1828 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?,
1829 system_user: "yubihsm2-audit-log".parse()?,
1830 },
1831 YubiHsm2UserMapping::Backup{
1832 authentication_key_id: 2,
1833 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOCMo+ODRchqIiXm89TxF7avi+LXRtqWZdBAvJ1SG5g user@host".parse()?,
1834 system_user: "yubihsm2-backup".parse()?,
1835 },
1836 YubiHsm2UserMapping::HermeticAuditLog {
1837 authentication_key_id: 4,
1838 system_user: "yubihsm2-hermetic-audit-log".parse()?,
1839 },
1840 YubiHsm2UserMapping::Signing {
1841 authentication_key_id: 5,
1842 signing_key_id: 1,
1843 key_setup: SigningKeySetup::new(
1844 KeyType::Curve25519,
1845 vec![KeyMechanism::EdDsaSignature],
1846 None,
1847 SignatureType::EdDsa,
1848 CryptographicKeyContext::OpenPgp {
1849 notations: Default::default(),
1850 user_ids: OpenPgpUserIdList::new(vec![
1851 "Foobar McFooface <foobar@mcfooface.org>".parse()?,
1852 ])?,
1853 version: "v4".parse()?,
1854 },
1855 )?,
1856 ssh_authorized_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host".parse()?,
1857 system_user: "yubihsm2-signing".parse()?,
1858 domain: Domain::One,
1859 }
1860 ]),
1861 )?
1862 )]
1863 fn yubihsm2_backend_sync_succeeds(
1864 connector: Connector,
1865 #[case] admin_credentials: YubiHsm2AdminCredentials,
1866 #[case] yubihsm2_config: YubiHsm2Config,
1867 ) -> TestResult {
1868 setup_terminal_logging(LevelFilter::Debug)?;
1869
1870 let config = ConfigBuilder::new(SystemConfig::new(
1871 1,
1872 AdministrativeSecretHandling::Plaintext,
1873 NonAdministrativeSecretHandling::Plaintext,
1874 BTreeSet::from_iter([]),
1875 )?)
1876 .set_yubihsm2_config(yubihsm2_config)
1877 .finish()?;
1878 let Some(backend) = YubiHsm2Backend::new(connector, &admin_credentials, &config)? else {
1879 panic!("No YubiHsm2Config in the provided Signstar config");
1880 };
1881 let credentials = vec![
1882 Credentials::new("2".parse()?, Passphrase::generate(Some(50))),
1883 Credentials::new("3".parse()?, Passphrase::generate(Some(50))),
1884 Credentials::new("4".parse()?, Passphrase::generate(Some(50))),
1885 Credentials::new("5".parse()?, Passphrase::generate(Some(50))),
1886 ];
1887
1888 backend.sync(&credentials)?;
1889
1890 backend.sync(&credentials)?;
1892
1893 Ok(())
1894 }
1895
1896 #[test]
1898 fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_duplicate_creds()
1899 -> TestResult {
1900 let user_mappings = HashSet::new();
1901 let creds = vec![
1902 Credentials::new(1, Passphrase::generate(Some(50))),
1903 Credentials::new(1, Passphrase::generate(Some(50))),
1904 ];
1905 match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1906 Err(crate::Error::YubiHsm2Backend(crate::yubihsm2::Error::DuplicateCredentials {
1907 ..
1908 })) => {}
1909 Err(error) => panic!(
1910 "Expected to fail with Error::DuplicateCredentials but failed with a different error: {error}"
1911 ),
1912 Ok(_) => {
1913 panic!("Expected to fail with Error::DuplicateCredentials but succeeded instead!")
1914 }
1915 }
1916
1917 Ok(())
1918 }
1919
1920 #[test]
1923 fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_missing_creds_for_mappings()
1924 -> TestResult {
1925 let user_mappings_list = [
1926 YubiHsm2UserMapping::Admin {
1927 authentication_key_id: 1,
1928 },
1929 YubiHsm2UserMapping::Admin {
1930 authentication_key_id: 2,
1931 },
1932 ];
1933 let user_mappings = HashSet::from_iter(user_mappings_list.iter());
1934 let creds = vec![Credentials::new(1, Passphrase::generate(Some(50)))];
1935 match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1936 Err(crate::Error::YubiHsm2Backend(
1937 crate::yubihsm2::Error::UserMappingWithoutCredentials { .. },
1938 )) => {}
1939 Err(error) => panic!(
1940 "Expected to fail with Error::UserMappingWithoutCredentials but failed with a different error: {error}"
1941 ),
1942 Ok(_) => {
1943 panic!(
1944 "Expected to fail with Error::UserMappingWithoutCredentials but succeeded instead!"
1945 )
1946 }
1947 }
1948
1949 Ok(())
1950 }
1951
1952 #[test]
1955 fn user_mappings_and_credentials_try_from_mappings_and_credentials_fails_on_missing_mappings_for_creds()
1956 -> TestResult {
1957 let user_mappings_list = [YubiHsm2UserMapping::Admin {
1958 authentication_key_id: 1,
1959 }];
1960 let user_mappings = HashSet::from_iter(user_mappings_list.iter());
1961 let creds = vec![
1962 Credentials::new(1, Passphrase::generate(Some(50))),
1963 Credentials::new(2, Passphrase::generate(Some(50))),
1964 ];
1965 match UserMappingsAndCredentials::try_from((&user_mappings, creds.as_slice())) {
1966 Err(crate::Error::YubiHsm2Backend(
1967 crate::yubihsm2::Error::CredentialsWithoutUserMapping { .. },
1968 )) => {}
1969 Err(error) => panic!(
1970 "Expected to fail with Error::CredentialsWithoutUserMapping but failed with a different error: {error}"
1971 ),
1972 Ok(_) => {
1973 panic!(
1974 "Expected to fail with Error::CredentialsWithoutUserMapping but succeeded instead!"
1975 )
1976 }
1977 }
1978
1979 Ok(())
1980 }
1981
1982 #[rstest]
1985 fn yubihsm2_backend_state_try_from_yubihsm2_backend_succeeds(
1986 connector: Connector,
1987 admin_credentials: TestResult<YubiHsm2AdminCredentials>,
1988 signstar_config: TestResult<Config>,
1989 ) -> TestResult {
1990 let signstar_config = signstar_config?;
1991 let admin_credentials = admin_credentials?;
1992 let backend = create_yubihsm2_backend(connector, &admin_credentials, &signstar_config)?;
1993
1994 let _state = YubiHsm2BackendState::try_from(&backend)?;
1995
1996 Ok(())
1997 }
1998
1999 #[rstest]
2002 fn yubihsm2_backend_new_fails_on_mismatching_iterations(
2003 connector: Connector,
2004 signstar_config: TestResult<Config>,
2005 ) -> TestResult {
2006 let signstar_config = signstar_config?;
2007 let admin_credentials = YubiHsm2AdminCredentials::new(
2008 2,
2009 Passphrase::new("backup-passphrase-really-just-for-testing-i-promise-but-it-is-really-really-long-sufficiently-long-really".to_string()),
2010 vec![
2011 Credentials::new(1, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
2012 Credentials::new(6, Passphrase::new("admin-passphrase-really-just-for-testing-i-promise".to_string())),
2013 ]
2014 )?;
2015 match YubiHsm2Backend::new(connector, &admin_credentials, &signstar_config) {
2016 Err(crate::Error::IterationMismatch { .. }) => {}
2017 Err(error) => panic!(
2018 "Expected to fail with Error::IterationMismatch, but failed differently instead: {error}"
2019 ),
2020 Ok(_) => {
2021 panic!("Expected to fail with Error::IterationMismatch, but succeeded instead")
2022 }
2023 }
2024
2025 Ok(())
2026 }
2027
2028 #[rstest]
2031 fn yubihsm2_backend_new_fails_on_missing_yubihsm2_config(
2032 connector: Connector,
2033 admin_credentials: TestResult<YubiHsm2AdminCredentials>,
2034 ) -> TestResult {
2035 let admin_credentials = admin_credentials?;
2036 let signstar_config = ConfigBuilder::new(SystemConfig::new(
2037 1,
2038 signstar_crypto::AdministrativeSecretHandling::Plaintext,
2039 signstar_crypto::NonAdministrativeSecretHandling::Plaintext,
2040 BTreeSet::from_iter([]),
2041 )?)
2042 .finish()?;
2043
2044 match YubiHsm2Backend::new(connector, &admin_credentials, &signstar_config) {
2045 Ok(None) => {}
2046 Ok(Some(_)) => {
2047 panic!("Expected to succeed with None, but succeeded with Some instead")
2048 }
2049 Err(error) => panic!("Expected to succeed with None, but failed instead: {error}"),
2050 }
2051
2052 Ok(())
2053 }
2054}