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