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 command::Code as CommandCode,
20 device::Info as DeviceInfo,
21 ed25519::Signature,
22 object::{Handle, Id as YubiHsmObjectId, Info as ObjectInfo},
23 response::Code as ResponseCode,
24 wrap::{Algorithm as WrapAlgorithm, Message},
25};
26
27#[cfg(feature = "cli")]
28use crate::automation::{
29 Error as AutomationError,
30 FileBackedCommand,
31 FileBackedScenario,
32 error::FileBackedScenarioReturnValueMismatch,
33};
34use crate::{
35 Error,
36 automation::{Command, Scenario},
37 object::KeyInfo,
38};
39
40#[derive(Debug)]
46#[cfg_attr(feature = "serde", derive(Serialize))]
47#[cfg_attr(
48 any(
49 all(not(feature = "serde"), feature = "_yubihsm2-mockhsm"),
50 all(
51 not(feature = "serde"),
52 not(feature = "_yubihsm2-mockhsm"),
53 not(feature = "cli")
54 )
55 ),
56 allow(unused)
57)]
58pub struct Ed25519Signature {
59 r: Vec<u8>,
61 s: Vec<u8>,
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#[derive(Debug)]
81#[cfg_attr(feature = "serde", derive(Serialize))]
82pub struct LogEntries {
83 pub unlogged_boot_events: u16,
85
86 pub unlogged_auth_events: u16,
88
89 pub num_entries: u8,
91
92 pub entries: Vec<LogEntry>,
94}
95
96#[derive(Debug, Eq, PartialEq)]
103#[cfg_attr(feature = "serde", derive(Serialize))]
104pub struct LogEntry {
105 pub item: u16,
107
108 pub cmd: CommandCode,
110
111 pub length: u16,
113
114 pub session_key: YubiHsmObjectId,
116
117 pub target_key: YubiHsmObjectId,
119
120 pub second_key: YubiHsmObjectId,
122
123 pub result: ResponseCode,
125
126 pub tick: u32,
128
129 pub digest: LogDigest,
131}
132
133pub const LOG_DIGEST_SIZE: usize = 16;
140
141#[derive(Eq, PartialEq)]
148#[cfg_attr(feature = "serde", derive(Serialize))]
149pub struct LogDigest(pub [u8; LOG_DIGEST_SIZE]);
150
151impl AsRef<[u8]> for LogDigest {
152 fn as_ref(&self) -> &[u8] {
153 &self.0[..]
154 }
155}
156
157impl Debug for LogDigest {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 write!(f, "LogDigest(")?;
160 for (i, byte) in self.0.iter().enumerate() {
161 write!(f, "{byte:02x}")?;
162 write!(f, "{}", if i == LOG_DIGEST_SIZE - 1 { ")" } else { ":" })?;
163 }
164 Ok(())
165 }
166}
167
168#[cfg(feature = "serde")]
176fn serialize_with_newline(mut writer: &mut dyn Write, object: impl Serialize) -> Result<(), Error> {
177 serde_json::to_writer(&mut writer, &object).map_err(|source| Error::Json {
178 context: "serializing response",
179 source,
180 })?;
181 writer.write_all(b"\n").map_err(|source| Error::Io {
182 context: "writing record delimiter",
183 source,
184 })?;
185 Ok(())
186}
187
188#[derive(Debug)]
190#[cfg_attr(feature = "serde", derive(Serialize))]
191#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
192pub enum CommandReturnValue {
193 DeviceInfo(DeviceInfo),
195
196 ResetDeviceAndReconnect,
198
199 PutAuthenticationKey(YubiHsmObjectId),
201
202 GenerateAsymmetricKey(YubiHsmObjectId),
204
205 SignEd25519(Ed25519Signature),
207
208 PutWrapKey(YubiHsmObjectId),
210
211 ExportWrapped(Message),
213
214 ImportWrapped(Handle),
216
217 DeleteObject,
219
220 GetObjectInfo(ObjectInfo),
222
223 SetForceAuditOption,
225
226 SetCommandAuditOption,
228
229 GetLogEntries(LogEntries),
231}
232
233impl PartialEq<Command> for &CommandReturnValue {
234 fn eq(&self, other: &Command) -> bool {
241 match (self, other) {
242 (CommandReturnValue::DeviceInfo(_), Command::DeviceInfo)
243 | (CommandReturnValue::ResetDeviceAndReconnect, Command::ResetDeviceAndReconnect)
244 | (CommandReturnValue::PutAuthenticationKey(_), Command::PutAuthenticationKey { .. })
245 | (
246 CommandReturnValue::GenerateAsymmetricKey(_),
247 Command::GenerateAsymmetricKey { .. },
248 )
249 | (CommandReturnValue::SignEd25519(_), Command::SignEd25519 { .. })
250 | (CommandReturnValue::PutWrapKey(_), Command::PutWrapKey { .. })
251 | (CommandReturnValue::ExportWrapped(_), Command::ExportWrapped { .. })
252 | (CommandReturnValue::ImportWrapped(_), Command::ImportWrapped { .. })
253 | (CommandReturnValue::DeleteObject, Command::DeleteObject(_))
254 | (CommandReturnValue::GetObjectInfo(_), Command::GetObjectInfo(_))
255 | (CommandReturnValue::SetForceAuditOption, Command::SetForceAuditOption(_))
256 | (CommandReturnValue::SetCommandAuditOption, Command::SetCommandAuditOption { .. })
257 | (CommandReturnValue::GetLogEntries(_), Command::GetLogEntries) => true,
258 (CommandReturnValue::DeviceInfo(_), _)
259 | (CommandReturnValue::ResetDeviceAndReconnect, _)
260 | (CommandReturnValue::PutAuthenticationKey(_), _)
261 | (CommandReturnValue::GenerateAsymmetricKey(_), _)
262 | (CommandReturnValue::SignEd25519(_), _)
263 | (CommandReturnValue::PutWrapKey(_), _)
264 | (CommandReturnValue::ExportWrapped(_), _)
265 | (CommandReturnValue::ImportWrapped(_), _)
266 | (CommandReturnValue::DeleteObject, _)
267 | (CommandReturnValue::GetObjectInfo(_), _)
268 | (CommandReturnValue::SetForceAuditOption, _)
269 | (CommandReturnValue::SetCommandAuditOption, _)
270 | (CommandReturnValue::GetLogEntries(_), _) => false,
271 }
272 }
273}
274
275#[cfg(feature = "cli")]
276impl PartialEq<FileBackedCommand> for &CommandReturnValue {
277 fn eq(&self, other: &FileBackedCommand) -> bool {
284 match (self, other) {
285 (CommandReturnValue::DeviceInfo(_), FileBackedCommand::DeviceInfo)
286 | (
287 CommandReturnValue::ResetDeviceAndReconnect,
288 FileBackedCommand::ResetDeviceAndReconnect,
289 )
290 | (
291 CommandReturnValue::PutAuthenticationKey(_),
292 FileBackedCommand::PutAuthenticationKey { .. },
293 )
294 | (
295 CommandReturnValue::GenerateAsymmetricKey(_),
296 FileBackedCommand::GenerateAsymmetricKey { .. },
297 )
298 | (CommandReturnValue::SignEd25519(_), FileBackedCommand::SignEd25519 { .. })
299 | (CommandReturnValue::PutWrapKey(_), FileBackedCommand::PutWrapKey { .. })
300 | (CommandReturnValue::ExportWrapped(_), FileBackedCommand::ExportWrapped { .. })
301 | (CommandReturnValue::ImportWrapped(_), FileBackedCommand::ImportWrapped { .. })
302 | (CommandReturnValue::DeleteObject, FileBackedCommand::DeleteObject(_))
303 | (CommandReturnValue::GetObjectInfo(_), FileBackedCommand::GetObjectInfo(_))
304 | (
305 CommandReturnValue::SetForceAuditOption,
306 FileBackedCommand::SetForceAuditOption(_),
307 )
308 | (
309 CommandReturnValue::SetCommandAuditOption,
310 FileBackedCommand::SetCommandAuditOption { .. },
311 )
312 | (CommandReturnValue::GetLogEntries(_), FileBackedCommand::GetLogEntries) => true,
313 (CommandReturnValue::DeviceInfo(_), _)
314 | (CommandReturnValue::ResetDeviceAndReconnect, _)
315 | (CommandReturnValue::PutAuthenticationKey(_), _)
316 | (CommandReturnValue::GenerateAsymmetricKey(_), _)
317 | (CommandReturnValue::SignEd25519(_), _)
318 | (CommandReturnValue::PutWrapKey(_), _)
319 | (CommandReturnValue::ExportWrapped(_), _)
320 | (CommandReturnValue::ImportWrapped(_), _)
321 | (CommandReturnValue::DeleteObject, _)
322 | (CommandReturnValue::GetObjectInfo(_), _)
323 | (CommandReturnValue::SetForceAuditOption, _)
324 | (CommandReturnValue::SetCommandAuditOption, _)
325 | (CommandReturnValue::GetLogEntries(_), _) => false,
326 }
327 }
328}
329
330#[derive(Debug)]
334pub struct ScenarioReturnValue {
335 authenticated_command_chains: Vec<Vec<CommandReturnValue>>,
336}
337
338impl ScenarioReturnValue {
339 pub fn chains(&self) -> &[Vec<CommandReturnValue>] {
341 self.authenticated_command_chains.as_slice()
342 }
343
344 #[cfg(feature = "cli")]
356 fn compare_with_file_backed_scenario(
357 &self,
358 file_backed_scenario: &FileBackedScenario,
359 ) -> Result<(), Error> {
360 debug!(
361 "Comparing the return values of the scenario with the requested commands of the file backed scenario"
362 );
363
364 let mut mismatches = Vec::new();
365
366 if file_backed_scenario.as_ref().len() != self.authenticated_command_chains.len() {
367 return Err(
368 AutomationError::MismatchingNumberOfAuthenticatedCommandChains {
369 scenario: file_backed_scenario.as_ref().len(),
370 scenario_return_value: self.authenticated_command_chains.len(),
371 }
372 .into(),
373 );
374 }
375
376 for (file_backed_authenticated_command_chain, command_return_values) in file_backed_scenario
377 .as_ref()
378 .iter()
379 .zip(self.authenticated_command_chains.iter())
380 {
381 if file_backed_authenticated_command_chain.commands.len() != command_return_values.len()
382 {
383 return Err(AutomationError::MismatchingNumberOfCommands {
384 authenticated_command_chain: file_backed_authenticated_command_chain
385 .commands
386 .len(),
387 command_return_values: command_return_values.len(),
388 }
389 .into());
390 }
391
392 for (file_backed_command, command_return_value) in
393 file_backed_authenticated_command_chain
394 .commands
395 .iter()
396 .zip(command_return_values.iter())
397 {
398 if command_return_value.ne(file_backed_command) {
399 mismatches.push(FileBackedScenarioReturnValueMismatch {
400 file_backed_scenario_command: file_backed_command.into(),
401 command_return_value: command_return_value.into(),
402 });
403 }
404 }
405 }
406
407 if !mismatches.is_empty() {
408 return Err(
409 AutomationError::MismatchingReturnValueForFileBackedScenario { mismatches }.into(),
410 );
411 }
412
413 Ok(())
414 }
415
416 #[cfg(feature = "cli")]
425 pub fn persist_file_backed_scenario(
426 &self,
427 file_backed_scenario: &FileBackedScenario,
428 ) -> Result<(), Error> {
429 self.compare_with_file_backed_scenario(file_backed_scenario)?;
430
431 for (file_backed_authenticated_command_chain, command_return_values) in file_backed_scenario
432 .as_ref()
433 .iter()
434 .zip(self.authenticated_command_chains.iter())
435 {
436 for (file_backed_command, command_return_value) in
437 file_backed_authenticated_command_chain
438 .commands
439 .iter()
440 .zip(command_return_values.iter())
441 {
442 if let (
443 FileBackedCommand::ExportWrapped { wrapped_file, .. },
444 CommandReturnValue::ExportWrapped(message),
445 ) = (file_backed_command, command_return_value)
446 {
447 write(wrapped_file.as_path(), message.clone().into_vec()).map_err(|source| {
448 Error::IoPath {
449 path: wrapped_file.clone(),
450 context: "writing an encrypted message to the file",
451 source,
452 }
453 })?
454 }
455 }
456 }
457
458 Ok(())
459 }
460}
461
462pub struct ScenarioRunner {
464 #[cfg_attr(
465 any(
466 all(not(feature = "serde"), feature = "_yubihsm2-mockhsm"),
467 all(
468 not(feature = "serde"),
469 not(feature = "_yubihsm2-mockhsm"),
470 not(feature = "cli")
471 )
472 ),
473 allow(unused)
474 )]
475 connector: Connector,
476}
477
478impl Debug for ScenarioRunner {
479 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480 f.debug_struct("ScenarioRunner").finish()
482 }
483}
484
485impl ScenarioRunner {
486 pub fn new(connector: Connector) -> Self {
488 Self { connector }
489 }
490
491 pub fn run(&self, scenario: &Scenario) -> Result<ScenarioReturnValue, Error> {
500 let mut authenticated_command_chains = Vec::new();
501
502 for authenticated_commands in scenario.as_ref().iter() {
503 let mut client = Client::open(
504 self.connector.clone(),
505 Credentials::from(authenticated_commands.auth()),
506 true,
507 )
508 .map_err(|source| Error::Client {
509 context: "opening new client",
510 source,
511 })?;
512 let mut command_return_values = Vec::new();
513
514 for command in authenticated_commands.commands().iter() {
515 info!("Executing {command:?}");
516 match self.run_command(&mut client, command) {
517 Ok(return_value) => command_return_values.push(return_value),
518 Err(error) => {
519 error!(
521 "{}",
522 authenticated_command_chains
523 .iter()
524 .flatten()
525 .map(|return_value| format!("{return_value:?}"))
526 .chain(
527 command_return_values
528 .iter()
529 .map(|return_value| format!("{return_value:?}"))
530 )
531 .collect::<Vec<_>>()
532 .join("\n")
533 );
534 return Err(error);
535 }
536 }
537 }
538
539 authenticated_command_chains.push(command_return_values);
540 }
541
542 Ok(ScenarioReturnValue {
543 authenticated_command_chains,
544 })
545 }
546
547 #[cfg(feature = "serde")]
560 pub fn run_with_writer(
561 &self,
562 scenario: &Scenario,
563 writer: &mut dyn Write,
564 ) -> Result<ScenarioReturnValue, Error> {
565 let scenario_return_value = self.run(scenario)?;
566 for return_value in scenario_return_value
567 .authenticated_command_chains
568 .iter()
569 .flatten()
570 {
571 serialize_with_newline(writer, return_value)?;
572 }
573
574 Ok(scenario_return_value)
575 }
576
577 fn run_command(
585 &self,
586 client: &mut Client,
587 command: &Command,
588 ) -> Result<CommandReturnValue, Error> {
589 Ok(match command {
590 Command::DeviceInfo => {
591 CommandReturnValue::DeviceInfo(client.device_info().map_err(|source| {
592 Error::Client {
593 context: "executing device info command",
594 source,
595 }
596 })?)
597 }
598 Command::ResetDeviceAndReconnect => {
599 client
600 .reset_device_and_reconnect(Duration::from_secs(2))
601 .map_err(|source| Error::Client {
602 context: "executing device info command",
603 source,
604 })?;
605 CommandReturnValue::ResetDeviceAndReconnect
606 }
607 Command::PutAuthenticationKey {
608 info:
609 KeyInfo {
610 key_id,
611 domains,
612 caps,
613 },
614 delegated_caps,
615 authentication_key,
616 } => CommandReturnValue::PutAuthenticationKey(
617 client
618 .put_authentication_key(
619 key_id.into(),
620 Default::default(),
621 domains.into(),
622 caps.into(),
623 delegated_caps.into(),
624 Default::default(),
625 authentication_key,
626 )
627 .map_err(|source| Error::Client {
628 context: "putting authentication key",
629 source,
630 })?,
631 ),
632 Command::GenerateAsymmetricKey {
633 info:
634 KeyInfo {
635 key_id,
636 domains,
637 caps,
638 },
639 } => CommandReturnValue::GenerateAsymmetricKey(
640 client
641 .generate_asymmetric_key(
642 key_id.into(),
643 Default::default(),
644 domains.into(),
645 caps.into(),
646 AsymmetricAlgorithm::Ed25519,
647 )
648 .map_err(|source| Error::Client {
649 context: "generating asymmetric key",
650 source,
651 })?,
652 ),
653 Command::SignEd25519 { key_id, data } => CommandReturnValue::SignEd25519(
654 client
655 .sign_ed25519(key_id.into(), &data[..])
656 .map_err(|source| Error::Client {
657 context: "signing with ed25519 key",
658 source,
659 })?
660 .into(),
661 ),
662 Command::PutWrapKey {
663 info:
664 KeyInfo {
665 key_id,
666 domains,
667 caps,
668 },
669 delegated_caps,
670 wrapping_key,
671 } => CommandReturnValue::PutWrapKey(
672 client
673 .put_wrap_key(
674 key_id.into(),
675 Default::default(),
676 domains.into(),
677 caps.into(),
678 delegated_caps.into(),
679 WrapAlgorithm::Aes256Ccm,
680 wrapping_key,
681 )
682 .map_err(|source| Error::Client {
683 context: "putting wrap key",
684 source,
685 })?,
686 ),
687 Command::ExportWrapped {
688 wrap_key_id,
689 object,
690 } => CommandReturnValue::ExportWrapped(
691 client
692 .export_wrapped(wrap_key_id.into(), object.object_type(), object.id().into())
693 .map_err(|source| Error::Client {
694 context: "exporting wrapped key",
695 source,
696 })?,
697 ),
698 Command::ImportWrapped {
699 wrap_key_id,
700 message,
701 } => CommandReturnValue::ImportWrapped(
702 client
703 .import_wrapped(wrap_key_id.into(), message.clone())
704 .map_err(|source| Error::Client {
705 context: "importing wrapped key",
706 source,
707 })?,
708 ),
709 Command::DeleteObject(object) => {
710 client
711 .delete_object(object.id().into(), object.object_type())
712 .map_err(|source| Error::Client {
713 context: "deleting object",
714 source,
715 })?;
716 CommandReturnValue::DeleteObject
717 }
718 Command::GetObjectInfo(object) => CommandReturnValue::GetObjectInfo(
719 client
720 .get_object_info(object.id().into(), object.object_type())
721 .map_err(|source| Error::Client {
722 context: "getting object info",
723 source,
724 })?,
725 ),
726 Command::SetForceAuditOption(setting) => {
727 client
728 .set_force_audit_option((*setting).into())
729 .map_err(|source| Error::Client {
730 context: "setting force audit option",
731 source,
732 })?;
733 CommandReturnValue::SetForceAuditOption
734 }
735 Command::SetCommandAuditOption { command, setting } => {
736 client
737 .set_command_audit_option(*command, (*setting).into())
738 .map_err(|source| Error::Client {
739 context: "setting command audit option",
740 source,
741 })?;
742 CommandReturnValue::SetCommandAuditOption
743 }
744 Command::GetLogEntries => {
745 let log_entries = client.get_log_entries().map_err(|source| Error::Client {
746 context: "getting log entries",
747 source,
748 })?;
749
750 CommandReturnValue::GetLogEntries(LogEntries {
751 unlogged_boot_events: log_entries.unlogged_boot_events,
752 unlogged_auth_events: log_entries.unlogged_auth_events,
753 num_entries: log_entries.num_entries,
754 entries: log_entries
755 .entries
756 .into_iter()
757 .map(|entry| LogEntry {
758 item: entry.item,
759 cmd: entry.cmd,
760 length: entry.length,
761 session_key: entry.session_key,
762 target_key: entry.target_key,
763 second_key: entry.second_key,
764 result: entry.result,
765 tick: entry.tick,
766 digest: LogDigest(entry.digest.0),
767 })
768 .collect::<Vec<_>>(),
769 })
770 }
771 })
772 }
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778
779 #[test]
780 fn ed25519_signature() {
781 let signature = Ed25519Signature {
782 r: vec![],
783 s: vec![],
784 };
785
786 println!("r: {:?}, s: {:?}", signature.r, signature.s);
787 }
788
789 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde", feature = "cli"))]
790 mod scenario {
791 use std::{
792 fs::File,
793 io::stdout,
794 path::{Path, PathBuf},
795 };
796
797 use rstest::rstest;
798 use testresult::TestResult;
799
800 use super::*;
801 use crate::automation::{FileBackedScenario, Scenario};
802
803 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
804 fn run_scenario(scenario_file: impl AsRef<Path>) -> TestResult {
805 let scenario_file = scenario_file.as_ref();
806 eprintln!(
807 "Running scenario file {scenario_file}",
808 scenario_file = scenario_file.display()
809 );
810 let file_backed_scenario: FileBackedScenario =
811 serde_json::from_reader(File::open(scenario_file)?)?;
812 let runner = ScenarioRunner::new(Connector::mockhsm());
813 let return_value = runner
814 .run_with_writer(&Scenario::try_from(&file_backed_scenario)?, &mut stdout())?;
815 return_value.persist_file_backed_scenario(&file_backed_scenario)?;
816
817 Ok(())
818 }
819
820 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
821 #[rstest]
822 fn scenario_test(#[files("tests/scenarios/*.json")] scenario_file: PathBuf) -> TestResult {
823 run_scenario(scenario_file)?;
824 Ok(())
825 }
826
827 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
828 #[test]
829 fn wrapping_test() -> TestResult {
830 run_scenario("tests/scenarios/wrapping/export-wrapped.json")?;
832 run_scenario("tests/scenarios/wrapping/import-wrapped.json")?;
833 Ok(())
834 }
835 }
836}