1use std::fmt::Debug;
4#[cfg(feature = "cli")]
5use std::fs::write;
6#[cfg(feature = "serde")]
7use std::io::Write;
8
9#[cfg(feature = "cli")]
10use log::debug;
11use log::{error, info};
12#[cfg(feature = "serde")]
13use serde::Serialize;
14use yubihsm::{
15 Client,
16 Connector,
17 Credentials,
18 asymmetric::Algorithm as AsymmetricAlgorithm,
19 audit::LogEntries,
20 device::Info as DeviceInfo,
21 ed25519::Signature,
22 object::{Entry, Filter, Handle, Id as YubiHsmObjectId, Info as ObjectInfo},
23 wrap::{Algorithm as WrapAlgorithm, Message},
24};
25
26#[cfg(feature = "cli")]
27use crate::automation::{
28 Error as AutomationError,
29 FileBackedCommand,
30 FileBackedScenario,
31 error::FileBackedScenarioReturnValueMismatch,
32};
33use crate::{
34 Error,
35 automation::{Command, Scenario},
36 object::KeyInfo,
37};
38
39#[derive(Debug)]
45#[cfg_attr(feature = "serde", derive(Serialize))]
46pub struct Ed25519Signature {
47 r: Vec<u8>,
49 s: Vec<u8>,
51}
52
53impl Ed25519Signature {
54 pub fn r(&self) -> &[u8] {
56 &self.r
57 }
58
59 pub fn s(&self) -> &[u8] {
61 &self.s
62 }
63}
64
65impl From<Signature> for Ed25519Signature {
66 fn from(value: Signature) -> Self {
67 Self {
68 r: value.r_bytes().to_vec(),
69 s: value.s_bytes().to_vec(),
70 }
71 }
72}
73
74#[cfg(feature = "serde")]
82fn serialize_with_newline(mut writer: &mut dyn Write, object: impl Serialize) -> Result<(), Error> {
83 serde_json::to_writer(&mut writer, &object).map_err(|source| Error::Json {
84 context: "serializing response",
85 source,
86 })?;
87 writer.write_all(b"\n").map_err(|source| Error::Io {
88 context: "writing record delimiter",
89 source,
90 })?;
91 Ok(())
92}
93
94#[derive(Debug)]
96#[cfg_attr(feature = "serde", derive(Serialize))]
97#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
98pub enum CommandReturnValue {
99 DeviceInfo(DeviceInfo),
101
102 ResetDeviceAndReconnect,
104
105 PutAuthenticationKey(YubiHsmObjectId),
107
108 ChangeAuthenticationKey(YubiHsmObjectId),
110
111 GenerateAsymmetricKey(YubiHsmObjectId),
113
114 SignEd25519(Ed25519Signature),
116
117 PutOpaque(YubiHsmObjectId),
119
120 GetOpaque(Vec<u8>),
122
123 PutWrapKey(YubiHsmObjectId),
125
126 ExportWrapped(Message),
128
129 ImportWrapped(Handle),
131
132 DeleteObject,
134
135 GetObjectInfo(ObjectInfo),
137
138 SetForceAuditOption,
140
141 SetCommandAuditOption,
143
144 GetLogEntries(LogEntries),
146
147 ListObjects(Vec<Entry>),
149}
150
151impl PartialEq<Command> for &CommandReturnValue {
152 fn eq(&self, other: &Command) -> bool {
159 match (self, other) {
160 (CommandReturnValue::DeviceInfo(_), Command::DeviceInfo)
161 | (
162 CommandReturnValue::ResetDeviceAndReconnect,
163 Command::ResetDeviceAndReconnect { .. },
164 )
165 | (CommandReturnValue::PutAuthenticationKey(_), Command::PutAuthenticationKey { .. })
166 | (
167 CommandReturnValue::ChangeAuthenticationKey(_),
168 Command::ChangeAuthenticationKey { .. },
169 )
170 | (
171 CommandReturnValue::GenerateAsymmetricKey(_),
172 Command::GenerateAsymmetricKey { .. },
173 )
174 | (CommandReturnValue::SignEd25519(_), Command::SignEd25519 { .. })
175 | (CommandReturnValue::PutOpaque(_), Command::PutOpaque { .. })
176 | (CommandReturnValue::GetOpaque(_), Command::GetOpaque { .. })
177 | (CommandReturnValue::PutWrapKey(_), Command::PutWrapKey { .. })
178 | (CommandReturnValue::ExportWrapped(_), Command::ExportWrapped { .. })
179 | (CommandReturnValue::ImportWrapped(_), Command::ImportWrapped { .. })
180 | (CommandReturnValue::DeleteObject, Command::DeleteObject(_))
181 | (CommandReturnValue::GetObjectInfo(_), Command::GetObjectInfo(_))
182 | (CommandReturnValue::SetForceAuditOption, Command::SetForceAuditOption(_))
183 | (CommandReturnValue::SetCommandAuditOption, Command::SetCommandAuditOption { .. })
184 | (CommandReturnValue::GetLogEntries(_), Command::GetLogEntries)
185 | (CommandReturnValue::ListObjects(_), Command::ListObjects(_)) => true,
186 (CommandReturnValue::DeviceInfo(_), _)
187 | (CommandReturnValue::ResetDeviceAndReconnect, _)
188 | (CommandReturnValue::PutAuthenticationKey(_), _)
189 | (CommandReturnValue::ChangeAuthenticationKey(_), _)
190 | (CommandReturnValue::GenerateAsymmetricKey(_), _)
191 | (CommandReturnValue::SignEd25519(_), _)
192 | (CommandReturnValue::PutOpaque(_), _)
193 | (CommandReturnValue::GetOpaque(_), _)
194 | (CommandReturnValue::PutWrapKey(_), _)
195 | (CommandReturnValue::ExportWrapped(_), _)
196 | (CommandReturnValue::ImportWrapped(_), _)
197 | (CommandReturnValue::DeleteObject, _)
198 | (CommandReturnValue::GetObjectInfo(_), _)
199 | (CommandReturnValue::SetForceAuditOption, _)
200 | (CommandReturnValue::SetCommandAuditOption, _)
201 | (CommandReturnValue::GetLogEntries(_), _)
202 | (CommandReturnValue::ListObjects(_), _) => false,
203 }
204 }
205}
206
207#[cfg(feature = "cli")]
208impl PartialEq<FileBackedCommand> for &CommandReturnValue {
209 fn eq(&self, other: &FileBackedCommand) -> bool {
216 match (self, other) {
217 (CommandReturnValue::DeviceInfo(_), FileBackedCommand::DeviceInfo)
218 | (
219 CommandReturnValue::ResetDeviceAndReconnect,
220 FileBackedCommand::ResetDeviceAndReconnect { .. },
221 )
222 | (
223 CommandReturnValue::PutAuthenticationKey(_),
224 FileBackedCommand::PutAuthenticationKey { .. },
225 )
226 | (
227 CommandReturnValue::ChangeAuthenticationKey(_),
228 FileBackedCommand::ChangeAuthenticationKey { .. },
229 )
230 | (
231 CommandReturnValue::GenerateAsymmetricKey(_),
232 FileBackedCommand::GenerateAsymmetricKey { .. },
233 )
234 | (CommandReturnValue::SignEd25519(_), FileBackedCommand::SignEd25519 { .. })
235 | (CommandReturnValue::PutOpaque(_), FileBackedCommand::PutOpaque { .. })
236 | (CommandReturnValue::GetOpaque(_), FileBackedCommand::GetOpaque { .. })
237 | (CommandReturnValue::PutWrapKey(_), FileBackedCommand::PutWrapKey { .. })
238 | (CommandReturnValue::ExportWrapped(_), FileBackedCommand::ExportWrapped { .. })
239 | (CommandReturnValue::ImportWrapped(_), FileBackedCommand::ImportWrapped { .. })
240 | (CommandReturnValue::DeleteObject, FileBackedCommand::DeleteObject(_))
241 | (CommandReturnValue::GetObjectInfo(_), FileBackedCommand::GetObjectInfo(_))
242 | (
243 CommandReturnValue::SetForceAuditOption,
244 FileBackedCommand::SetForceAuditOption(_),
245 )
246 | (
247 CommandReturnValue::SetCommandAuditOption,
248 FileBackedCommand::SetCommandAuditOption { .. },
249 )
250 | (CommandReturnValue::GetLogEntries(_), FileBackedCommand::GetLogEntries)
251 | (CommandReturnValue::ListObjects(_), FileBackedCommand::ListObjects(_)) => true,
252 (CommandReturnValue::DeviceInfo(_), _)
253 | (CommandReturnValue::ResetDeviceAndReconnect, _)
254 | (CommandReturnValue::PutAuthenticationKey(_), _)
255 | (CommandReturnValue::ChangeAuthenticationKey(_), _)
256 | (CommandReturnValue::GenerateAsymmetricKey(_), _)
257 | (CommandReturnValue::SignEd25519(_), _)
258 | (CommandReturnValue::PutOpaque(_), _)
259 | (CommandReturnValue::GetOpaque(_), _)
260 | (CommandReturnValue::PutWrapKey(_), _)
261 | (CommandReturnValue::ExportWrapped(_), _)
262 | (CommandReturnValue::ImportWrapped(_), _)
263 | (CommandReturnValue::DeleteObject, _)
264 | (CommandReturnValue::GetObjectInfo(_), _)
265 | (CommandReturnValue::SetForceAuditOption, _)
266 | (CommandReturnValue::SetCommandAuditOption, _)
267 | (CommandReturnValue::GetLogEntries(_), _)
268 | (CommandReturnValue::ListObjects(_), _) => false,
269 }
270 }
271}
272
273#[derive(Debug)]
277pub struct ScenarioReturnValue {
278 authenticated_command_chains: Vec<Vec<CommandReturnValue>>,
279}
280
281impl ScenarioReturnValue {
282 pub fn chains(&self) -> &[Vec<CommandReturnValue>] {
284 self.authenticated_command_chains.as_slice()
285 }
286
287 #[cfg(feature = "cli")]
299 fn compare_with_file_backed_scenario(
300 &self,
301 file_backed_scenario: &FileBackedScenario,
302 ) -> Result<(), Error> {
303 debug!(
304 "Comparing the return values of the scenario with the requested commands of the file backed scenario"
305 );
306
307 let mut mismatches = Vec::new();
308
309 if file_backed_scenario.as_ref().len() != self.authenticated_command_chains.len() {
310 return Err(
311 AutomationError::MismatchingNumberOfAuthenticatedCommandChains {
312 scenario: file_backed_scenario.as_ref().len(),
313 scenario_return_value: self.authenticated_command_chains.len(),
314 }
315 .into(),
316 );
317 }
318
319 for (file_backed_authenticated_command_chain, command_return_values) in file_backed_scenario
320 .as_ref()
321 .iter()
322 .zip(self.authenticated_command_chains.iter())
323 {
324 if file_backed_authenticated_command_chain.commands.len() != command_return_values.len()
325 {
326 return Err(AutomationError::MismatchingNumberOfCommands {
327 authenticated_command_chain: file_backed_authenticated_command_chain
328 .commands
329 .len(),
330 command_return_values: command_return_values.len(),
331 }
332 .into());
333 }
334
335 for (file_backed_command, command_return_value) in
336 file_backed_authenticated_command_chain
337 .commands
338 .iter()
339 .zip(command_return_values.iter())
340 {
341 if command_return_value.ne(file_backed_command) {
342 mismatches.push(FileBackedScenarioReturnValueMismatch {
343 file_backed_scenario_command: file_backed_command.into(),
344 command_return_value: command_return_value.into(),
345 });
346 }
347 }
348 }
349
350 if !mismatches.is_empty() {
351 return Err(
352 AutomationError::MismatchingReturnValueForFileBackedScenario { mismatches }.into(),
353 );
354 }
355
356 Ok(())
357 }
358
359 #[cfg(feature = "cli")]
368 pub fn persist_file_backed_scenario(
369 &self,
370 file_backed_scenario: &FileBackedScenario,
371 ) -> Result<(), Error> {
372 self.compare_with_file_backed_scenario(file_backed_scenario)?;
373
374 for (file_backed_authenticated_command_chain, command_return_values) in file_backed_scenario
375 .as_ref()
376 .iter()
377 .zip(self.authenticated_command_chains.iter())
378 {
379 for (file_backed_command, command_return_value) in
380 file_backed_authenticated_command_chain
381 .commands
382 .iter()
383 .zip(command_return_values.iter())
384 {
385 match (file_backed_command, command_return_value) {
386 (
387 FileBackedCommand::ExportWrapped { wrapped_file, .. },
388 CommandReturnValue::ExportWrapped(message),
389 ) => write(wrapped_file.as_path(), message.clone().into_vec()).map_err(
390 |source| Error::IoPath {
391 path: wrapped_file.clone(),
392 context: "writing an encrypted message to the file",
393 source,
394 },
395 )?,
396 (
397 FileBackedCommand::GetOpaque { data_file, .. },
398 CommandReturnValue::GetOpaque(data),
399 ) => write(data_file.as_path(), data).map_err(|source| Error::IoPath {
400 path: data_file.clone(),
401 context: "writing an encrypted message to the file",
402 source,
403 })?,
404 _ => {}
405 }
406 }
407 }
408
409 Ok(())
410 }
411}
412
413impl From<ScenarioReturnValue> for Vec<Vec<CommandReturnValue>> {
414 fn from(value: ScenarioReturnValue) -> Self {
415 value.authenticated_command_chains
416 }
417}
418
419pub struct ScenarioRunner {
421 connector: Connector,
422}
423
424impl Debug for ScenarioRunner {
425 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 f.debug_struct("ScenarioRunner").finish()
428 }
429}
430
431impl ScenarioRunner {
432 pub fn new(connector: Connector) -> Self {
434 Self { connector }
435 }
436
437 pub fn run(&self, scenario: &Scenario) -> Result<ScenarioReturnValue, Error> {
446 let mut authenticated_command_chains = Vec::new();
447
448 for authenticated_commands in scenario.as_ref().iter() {
449 let mut client = Client::open(
450 self.connector.clone(),
451 Credentials::from(authenticated_commands.auth()),
452 true,
453 )
454 .map_err(|source| Error::Client {
455 context: "opening new client",
456 source,
457 })?;
458 let mut command_return_values = Vec::new();
459
460 for command in authenticated_commands.commands().iter() {
461 info!("Executing {command:?}");
462 match self.run_command(&mut client, command) {
463 Ok(return_value) => command_return_values.push(return_value),
464 Err(error) => {
465 error!(
467 "{}",
468 authenticated_command_chains
469 .iter()
470 .flatten()
471 .map(|return_value| format!("{return_value:?}"))
472 .chain(
473 command_return_values
474 .iter()
475 .map(|return_value| format!("{return_value:?}"))
476 )
477 .collect::<Vec<_>>()
478 .join("\n")
479 );
480 return Err(error);
481 }
482 }
483 }
484
485 authenticated_command_chains.push(command_return_values);
486 }
487
488 Ok(ScenarioReturnValue {
489 authenticated_command_chains,
490 })
491 }
492
493 #[cfg(feature = "serde")]
506 pub fn run_with_writer(
507 &self,
508 scenario: &Scenario,
509 writer: &mut dyn Write,
510 ) -> Result<ScenarioReturnValue, Error> {
511 let scenario_return_value = self.run(scenario)?;
512 for return_value in scenario_return_value
513 .authenticated_command_chains
514 .iter()
515 .flatten()
516 {
517 serialize_with_newline(writer, return_value)?;
518 }
519
520 Ok(scenario_return_value)
521 }
522
523 fn run_command(
531 &self,
532 client: &mut Client,
533 command: &Command,
534 ) -> Result<CommandReturnValue, Error> {
535 Ok(match command {
536 Command::DeviceInfo => {
537 CommandReturnValue::DeviceInfo(client.device_info().map_err(|source| {
538 Error::Client {
539 context: "executing device info command",
540 source,
541 }
542 })?)
543 }
544 Command::ResetDeviceAndReconnect { timeout } => {
545 client
546 .reset_device_and_reconnect(*timeout)
547 .map_err(|source| Error::Client {
548 context: "executing device info command",
549 source,
550 })?;
551 CommandReturnValue::ResetDeviceAndReconnect
552 }
553 Command::PutAuthenticationKey {
554 info:
555 KeyInfo {
556 key_id,
557 domains,
558 caps,
559 label,
560 },
561 delegated_caps,
562 authentication_key,
563 } => CommandReturnValue::PutAuthenticationKey(
564 client
565 .put_authentication_key(
566 *key_id,
567 label.into(),
568 domains.into(),
569 caps.into(),
570 delegated_caps.into(),
571 Default::default(),
572 authentication_key,
573 )
574 .map_err(|source| Error::Client {
575 context: "putting authentication key",
576 source,
577 })?,
578 ),
579 Command::ChangeAuthenticationKey {
580 key_id,
581 authentication_key,
582 } => CommandReturnValue::ChangeAuthenticationKey(
583 client
584 .change_authentication_key(*key_id, Default::default(), authentication_key)
585 .map_err(|source| Error::Client {
586 context: "changing authentication key",
587 source,
588 })?,
589 ),
590 Command::GenerateAsymmetricKey {
591 info:
592 KeyInfo {
593 key_id,
594 domains,
595 caps,
596 label,
597 },
598 } => CommandReturnValue::GenerateAsymmetricKey(
599 client
600 .generate_asymmetric_key(
601 *key_id,
602 label.into(),
603 domains.into(),
604 caps.into(),
605 AsymmetricAlgorithm::Ed25519,
606 )
607 .map_err(|source| Error::Client {
608 context: "generating asymmetric key",
609 source,
610 })?,
611 ),
612 Command::SignEd25519 { key_id, data } => CommandReturnValue::SignEd25519(
613 client
614 .sign_ed25519(*key_id, &data[..])
615 .map_err(|source| Error::Client {
616 context: "signing with ed25519 key",
617 source,
618 })?
619 .into(),
620 ),
621 Command::PutOpaque {
622 id,
623 label,
624 domains,
625 capabilities,
626 algorithm,
627 data,
628 } => CommandReturnValue::PutOpaque(
629 client
630 .put_opaque(
631 *id,
632 label.into(),
633 domains.into(),
634 capabilities.into(),
635 algorithm.into(),
636 data,
637 )
638 .map_err(|source| Error::Client {
639 context: "putting opaque data",
640 source,
641 })?,
642 ),
643 Command::PutWrapKey {
644 info:
645 KeyInfo {
646 key_id,
647 domains,
648 caps,
649 label,
650 },
651 delegated_caps,
652 wrapping_key,
653 } => CommandReturnValue::PutWrapKey(
654 client
655 .put_wrap_key(
656 *key_id,
657 label.into(),
658 domains.into(),
659 caps.into(),
660 delegated_caps.into(),
661 WrapAlgorithm::Aes256Ccm,
662 wrapping_key,
663 )
664 .map_err(|source| Error::Client {
665 context: "putting wrap key",
666 source,
667 })?,
668 ),
669 Command::GetOpaque { id } => {
670 CommandReturnValue::GetOpaque(client.get_opaque(*id).map_err(|source| {
671 Error::Client {
672 context: "retrieving opaque data",
673 source,
674 }
675 })?)
676 }
677 Command::ExportWrapped {
678 wrap_key_id,
679 object,
680 } => CommandReturnValue::ExportWrapped(
681 client
682 .export_wrapped(*wrap_key_id, object.object_type(), object.id())
683 .map_err(|source| Error::Client {
684 context: "exporting wrapped key",
685 source,
686 })?,
687 ),
688 Command::ImportWrapped {
689 wrap_key_id,
690 message,
691 } => CommandReturnValue::ImportWrapped(
692 client
693 .import_wrapped(*wrap_key_id, message.clone())
694 .map_err(|source| Error::Client {
695 context: "importing wrapped key",
696 source,
697 })?,
698 ),
699 Command::DeleteObject(object) => {
700 client
701 .delete_object(object.id(), object.object_type())
702 .map_err(|source| Error::Client {
703 context: "deleting object",
704 source,
705 })?;
706 CommandReturnValue::DeleteObject
707 }
708 Command::GetObjectInfo(object) => CommandReturnValue::GetObjectInfo(
709 client
710 .get_object_info(object.id(), object.object_type())
711 .map_err(|source| Error::Client {
712 context: "getting object info",
713 source,
714 })?,
715 ),
716 Command::SetForceAuditOption(setting) => {
717 client
718 .set_force_audit_option((*setting).into())
719 .map_err(|source| Error::Client {
720 context: "setting force audit option",
721 source,
722 })?;
723 CommandReturnValue::SetForceAuditOption
724 }
725 Command::SetCommandAuditOption { command, setting } => {
726 client
727 .set_command_audit_option(*command, (*setting).into())
728 .map_err(|source| Error::Client {
729 context: "setting command audit option",
730 source,
731 })?;
732 CommandReturnValue::SetCommandAuditOption
733 }
734 Command::GetLogEntries => {
735 let log_entries = client.get_log_entries().map_err(|source| Error::Client {
736 context: "getting log entries",
737 source,
738 })?;
739
740 CommandReturnValue::GetLogEntries(log_entries)
741 }
742 Command::ListObjects(filters) => {
743 let entries = client
744 .list_objects(
745 filters
746 .iter()
747 .map(|filter| filter.into())
748 .collect::<Vec<Filter>>()
749 .as_slice(),
750 )
751 .map_err(|source| Error::Client {
752 context: "retrieving information on objects based on a set of filters",
753 source,
754 })?;
755 CommandReturnValue::ListObjects(entries)
756 }
757 })
758 }
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 #[test]
766 fn ed25519_signature() {
767 let signature = Ed25519Signature {
768 r: vec![],
769 s: vec![],
770 };
771
772 println!("r: {:?}, s: {:?}", signature.r, signature.s);
773 }
774
775 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde", feature = "cli"))]
776 mod scenario {
777 use std::{
778 fs::File,
779 io::stdout,
780 path::{Path, PathBuf},
781 };
782
783 use rstest::rstest;
784 use testresult::TestResult;
785
786 use super::*;
787 use crate::automation::{FileBackedScenario, Scenario};
788
789 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
790 fn run_scenario(scenario_file: impl AsRef<Path>) -> TestResult {
791 let scenario_file = scenario_file.as_ref();
792 eprintln!(
793 "Running scenario file {scenario_file}",
794 scenario_file = scenario_file.display()
795 );
796 let file_backed_scenario: FileBackedScenario =
797 serde_json::from_reader(File::open(scenario_file)?)?;
798 let runner = ScenarioRunner::new(Connector::mockhsm());
799 let return_value = runner
800 .run_with_writer(&Scenario::try_from(&file_backed_scenario)?, &mut stdout())?;
801 return_value.persist_file_backed_scenario(&file_backed_scenario)?;
802
803 Ok(())
804 }
805
806 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
807 #[rstest]
808 fn scenario_test(#[files("tests/scenarios/*.json")] scenario_file: PathBuf) -> TestResult {
809 run_scenario(scenario_file)?;
810 Ok(())
811 }
812
813 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
814 #[test]
815 fn wrapping_test() -> TestResult {
816 run_scenario("tests/scenarios/wrapping/export-wrapped.json")?;
818 run_scenario("tests/scenarios/wrapping/import-wrapped.json")?;
819 Ok(())
820 }
821 }
822}