1#[cfg(feature = "serde")]
4use std::io::Write;
5use std::{fmt::Debug, fs::write, time::Duration};
6
7use log::{debug, error, info};
8#[cfg(feature = "serde")]
9use serde::Serialize;
10use yubihsm::{
11 Client,
12 Connector,
13 Credentials,
14 asymmetric::Algorithm as AsymmetricAlgorithm,
15 command::Code as CommandCode,
16 device::Info as DeviceInfo,
17 ed25519::Signature,
18 object::{Handle, Id as YubiHsmObjectId, Info as ObjectInfo},
19 response::Code as ResponseCode,
20 wrap::{Algorithm as WrapAlgorithm, Message},
21};
22
23use crate::{
24 Error,
25 automation::{
26 Command,
27 Error as AutomationError,
28 FileBackedCommand,
29 FileBackedScenario,
30 Scenario,
31 error::FileBackedScenarioReturnValueMismatch,
32 },
33 object::KeyInfo,
34};
35
36#[derive(Debug)]
42#[cfg_attr(feature = "serde", derive(Serialize))]
43#[cfg_attr(
44 any(
45 all(not(feature = "serde"), feature = "_yubihsm2-mockhsm"),
46 all(
47 not(feature = "serde"),
48 not(feature = "_yubihsm2-mockhsm"),
49 not(feature = "cli")
50 )
51 ),
52 allow(unused)
53)]
54pub struct Ed25519Signature {
55 r: Vec<u8>,
57 s: Vec<u8>,
59}
60
61impl From<Signature> for Ed25519Signature {
62 fn from(value: Signature) -> Self {
63 Self {
64 r: value.r_bytes().to_vec(),
65 s: value.s_bytes().to_vec(),
66 }
67 }
68}
69
70#[derive(Debug)]
77#[cfg_attr(feature = "serde", derive(Serialize))]
78pub struct LogEntries {
79 pub unlogged_boot_events: u16,
81
82 pub unlogged_auth_events: u16,
84
85 pub num_entries: u8,
87
88 pub entries: Vec<LogEntry>,
90}
91
92#[derive(Debug, Eq, PartialEq)]
99#[cfg_attr(feature = "serde", derive(Serialize))]
100pub struct LogEntry {
101 pub item: u16,
103
104 pub cmd: CommandCode,
106
107 pub length: u16,
109
110 pub session_key: YubiHsmObjectId,
112
113 pub target_key: YubiHsmObjectId,
115
116 pub second_key: YubiHsmObjectId,
118
119 pub result: ResponseCode,
121
122 pub tick: u32,
124
125 pub digest: LogDigest,
127}
128
129pub const LOG_DIGEST_SIZE: usize = 16;
136
137#[derive(Eq, PartialEq)]
144#[cfg_attr(feature = "serde", derive(Serialize))]
145pub struct LogDigest(pub [u8; LOG_DIGEST_SIZE]);
146
147impl AsRef<[u8]> for LogDigest {
148 fn as_ref(&self) -> &[u8] {
149 &self.0[..]
150 }
151}
152
153impl Debug for LogDigest {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 write!(f, "LogDigest(")?;
156 for (i, byte) in self.0.iter().enumerate() {
157 write!(f, "{byte:02x}")?;
158 write!(f, "{}", if i == LOG_DIGEST_SIZE - 1 { ")" } else { ":" })?;
159 }
160 Ok(())
161 }
162}
163
164#[cfg(feature = "serde")]
172fn serialize_with_newline(mut writer: &mut dyn Write, object: impl Serialize) -> Result<(), Error> {
173 serde_json::to_writer(&mut writer, &object).map_err(|source| Error::Json {
174 context: "serializing response",
175 source,
176 })?;
177 writer.write_all(b"\n").map_err(|source| Error::Io {
178 context: "writing record delimiter",
179 source,
180 })?;
181 Ok(())
182}
183
184#[derive(Debug)]
186#[cfg_attr(feature = "serde", derive(Serialize))]
187#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
188pub enum CommandReturnValue {
189 DeviceInfo(DeviceInfo),
191
192 ResetDeviceAndReconnect,
194
195 PutAuthenticationKey(YubiHsmObjectId),
197
198 GenerateAsymmetricKey(YubiHsmObjectId),
200
201 SignEd25519(Ed25519Signature),
203
204 PutWrapKey(YubiHsmObjectId),
206
207 ExportWrapped(Message),
209
210 ImportWrapped(Handle),
212
213 DeleteObject,
215
216 GetObjectInfo(ObjectInfo),
218
219 SetForceAuditOption,
221
222 SetCommandAuditOption,
224
225 GetLogEntries(LogEntries),
227}
228
229impl PartialEq<Command> for &CommandReturnValue {
230 fn eq(&self, other: &Command) -> bool {
237 match (self, other) {
238 (CommandReturnValue::DeviceInfo(_), Command::DeviceInfo)
239 | (CommandReturnValue::ResetDeviceAndReconnect, Command::ResetDeviceAndReconnect)
240 | (CommandReturnValue::PutAuthenticationKey(_), Command::PutAuthenticationKey { .. })
241 | (
242 CommandReturnValue::GenerateAsymmetricKey(_),
243 Command::GenerateAsymmetricKey { .. },
244 )
245 | (CommandReturnValue::SignEd25519(_), Command::SignEd25519 { .. })
246 | (CommandReturnValue::PutWrapKey(_), Command::PutWrapKey { .. })
247 | (CommandReturnValue::ExportWrapped(_), Command::ExportWrapped { .. })
248 | (CommandReturnValue::ImportWrapped(_), Command::ImportWrapped { .. })
249 | (CommandReturnValue::DeleteObject, Command::DeleteObject(_))
250 | (CommandReturnValue::GetObjectInfo(_), Command::GetObjectInfo(_))
251 | (CommandReturnValue::SetForceAuditOption, Command::SetForceAuditOption(_))
252 | (CommandReturnValue::SetCommandAuditOption, Command::SetCommandAuditOption { .. })
253 | (CommandReturnValue::GetLogEntries(_), Command::GetLogEntries) => true,
254 (CommandReturnValue::DeviceInfo(_), _)
255 | (CommandReturnValue::ResetDeviceAndReconnect, _)
256 | (CommandReturnValue::PutAuthenticationKey(_), _)
257 | (CommandReturnValue::GenerateAsymmetricKey(_), _)
258 | (CommandReturnValue::SignEd25519(_), _)
259 | (CommandReturnValue::PutWrapKey(_), _)
260 | (CommandReturnValue::ExportWrapped(_), _)
261 | (CommandReturnValue::ImportWrapped(_), _)
262 | (CommandReturnValue::DeleteObject, _)
263 | (CommandReturnValue::GetObjectInfo(_), _)
264 | (CommandReturnValue::SetForceAuditOption, _)
265 | (CommandReturnValue::SetCommandAuditOption, _)
266 | (CommandReturnValue::GetLogEntries(_), _) => false,
267 }
268 }
269}
270
271impl PartialEq<FileBackedCommand> for &CommandReturnValue {
272 fn eq(&self, other: &FileBackedCommand) -> bool {
279 match (self, other) {
280 (CommandReturnValue::DeviceInfo(_), FileBackedCommand::DeviceInfo)
281 | (
282 CommandReturnValue::ResetDeviceAndReconnect,
283 FileBackedCommand::ResetDeviceAndReconnect,
284 )
285 | (
286 CommandReturnValue::PutAuthenticationKey(_),
287 FileBackedCommand::PutAuthenticationKey { .. },
288 )
289 | (
290 CommandReturnValue::GenerateAsymmetricKey(_),
291 FileBackedCommand::GenerateAsymmetricKey { .. },
292 )
293 | (CommandReturnValue::SignEd25519(_), FileBackedCommand::SignEd25519 { .. })
294 | (CommandReturnValue::PutWrapKey(_), FileBackedCommand::PutWrapKey { .. })
295 | (CommandReturnValue::ExportWrapped(_), FileBackedCommand::ExportWrapped { .. })
296 | (CommandReturnValue::ImportWrapped(_), FileBackedCommand::ImportWrapped { .. })
297 | (CommandReturnValue::DeleteObject, FileBackedCommand::DeleteObject(_))
298 | (CommandReturnValue::GetObjectInfo(_), FileBackedCommand::GetObjectInfo(_))
299 | (
300 CommandReturnValue::SetForceAuditOption,
301 FileBackedCommand::SetForceAuditOption(_),
302 )
303 | (
304 CommandReturnValue::SetCommandAuditOption,
305 FileBackedCommand::SetCommandAuditOption { .. },
306 )
307 | (CommandReturnValue::GetLogEntries(_), FileBackedCommand::GetLogEntries) => true,
308 (CommandReturnValue::DeviceInfo(_), _)
309 | (CommandReturnValue::ResetDeviceAndReconnect, _)
310 | (CommandReturnValue::PutAuthenticationKey(_), _)
311 | (CommandReturnValue::GenerateAsymmetricKey(_), _)
312 | (CommandReturnValue::SignEd25519(_), _)
313 | (CommandReturnValue::PutWrapKey(_), _)
314 | (CommandReturnValue::ExportWrapped(_), _)
315 | (CommandReturnValue::ImportWrapped(_), _)
316 | (CommandReturnValue::DeleteObject, _)
317 | (CommandReturnValue::GetObjectInfo(_), _)
318 | (CommandReturnValue::SetForceAuditOption, _)
319 | (CommandReturnValue::SetCommandAuditOption, _)
320 | (CommandReturnValue::GetLogEntries(_), _) => false,
321 }
322 }
323}
324
325#[derive(Debug)]
329pub struct ScenarioReturnValue {
330 authenticated_command_chains: Vec<Vec<CommandReturnValue>>,
331}
332
333impl ScenarioReturnValue {
334 fn compare_with_file_backed_scenario(
336 &self,
337 file_backed_scenario: &FileBackedScenario,
338 ) -> Result<(), Error> {
339 debug!(
340 "Comparing the return values of the scenario with the requested commands of the file backed scenario"
341 );
342
343 let mut mismatches = Vec::new();
344
345 if file_backed_scenario.as_ref().len() != self.authenticated_command_chains.len() {
346 return Err(
347 AutomationError::MismatchingNumberOfAuthenticatedCommandChains {
348 scenario: file_backed_scenario.as_ref().len(),
349 scenario_return_value: self.authenticated_command_chains.len(),
350 }
351 .into(),
352 );
353 }
354
355 for (file_backed_authenticated_command_chain, command_return_values) in file_backed_scenario
356 .as_ref()
357 .iter()
358 .zip(self.authenticated_command_chains.iter())
359 {
360 if file_backed_authenticated_command_chain.commands.len() != command_return_values.len()
361 {
362 return Err(AutomationError::MismatchingNumberOfCommands {
363 authenticated_command_chain: file_backed_authenticated_command_chain
364 .commands
365 .len(),
366 command_return_values: command_return_values.len(),
367 }
368 .into());
369 }
370
371 for (file_backed_command, command_return_value) in
372 file_backed_authenticated_command_chain
373 .commands
374 .iter()
375 .zip(command_return_values.iter())
376 {
377 if command_return_value.ne(file_backed_command) {
378 mismatches.push(FileBackedScenarioReturnValueMismatch {
379 file_backed_scenario_command: file_backed_command.into(),
380 command_return_value: command_return_value.into(),
381 });
382 }
383 }
384 }
385
386 if !mismatches.is_empty() {
387 return Err(
388 AutomationError::MismatchingReturnValueForFileBackedScenario { mismatches }.into(),
389 );
390 }
391
392 Ok(())
393 }
394
395 pub fn persist_file_backed_scenario(
397 &self,
398 file_backed_scenario: &FileBackedScenario,
399 ) -> Result<(), Error> {
400 self.compare_with_file_backed_scenario(file_backed_scenario)?;
401
402 for (file_backed_authenticated_command_chain, command_return_values) in file_backed_scenario
403 .as_ref()
404 .iter()
405 .zip(self.authenticated_command_chains.iter())
406 {
407 for (file_backed_command, command_return_value) in
408 file_backed_authenticated_command_chain
409 .commands
410 .iter()
411 .zip(command_return_values.iter())
412 {
413 if let (
414 FileBackedCommand::ExportWrapped { wrapped_file, .. },
415 CommandReturnValue::ExportWrapped(message),
416 ) = (file_backed_command, command_return_value)
417 {
418 write(wrapped_file.as_path(), message.clone().into_vec()).map_err(|source| {
419 Error::IoPath {
420 path: wrapped_file.clone(),
421 context: "writing an encrypted message to the file",
422 source,
423 }
424 })?
425 }
426 }
427 }
428
429 Ok(())
430 }
431}
432
433pub struct ScenarioRunner {
435 #[cfg_attr(
436 any(
437 all(not(feature = "serde"), feature = "_yubihsm2-mockhsm"),
438 all(
439 not(feature = "serde"),
440 not(feature = "_yubihsm2-mockhsm"),
441 not(feature = "cli")
442 )
443 ),
444 allow(unused)
445 )]
446 connector: Connector,
447}
448
449impl Debug for ScenarioRunner {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 f.debug_struct("ScenarioRunner").finish()
453 }
454}
455
456impl ScenarioRunner {
457 pub fn new(connector: Connector) -> Self {
459 Self { connector }
460 }
461
462 pub fn run(&self, scenario: &Scenario) -> Result<ScenarioReturnValue, Error> {
471 let mut authenticated_command_chains = Vec::new();
472
473 for authenticated_commands in scenario.as_ref().iter() {
474 let mut client = Client::open(
475 self.connector.clone(),
476 Credentials::from(authenticated_commands.auth()),
477 true,
478 )
479 .map_err(|source| Error::Client {
480 context: "opening new client",
481 source,
482 })?;
483 let mut command_return_values = Vec::new();
484
485 for command in authenticated_commands.commands().iter() {
486 info!("Executing {command:?}");
487 match self.run_command(&mut client, command) {
488 Ok(return_value) => command_return_values.push(return_value),
489 Err(error) => {
490 error!(
492 "{}",
493 authenticated_command_chains
494 .iter()
495 .flatten()
496 .map(|return_value| format!("{return_value:?}"))
497 .chain(
498 command_return_values
499 .iter()
500 .map(|return_value| format!("{return_value:?}"))
501 )
502 .collect::<Vec<_>>()
503 .join("\n")
504 );
505 return Err(error);
506 }
507 }
508 }
509
510 authenticated_command_chains.push(command_return_values);
511 }
512
513 Ok(ScenarioReturnValue {
514 authenticated_command_chains,
515 })
516 }
517
518 #[cfg(feature = "serde")]
531 pub fn run_with_writer(
532 &self,
533 scenario: &Scenario,
534 writer: &mut dyn Write,
535 ) -> Result<ScenarioReturnValue, Error> {
536 let scenario_return_value = self.run(scenario)?;
537 for return_value in scenario_return_value
538 .authenticated_command_chains
539 .iter()
540 .flatten()
541 {
542 serialize_with_newline(writer, return_value)?;
543 }
544
545 Ok(scenario_return_value)
546 }
547
548 fn run_command(
556 &self,
557 client: &mut Client,
558 command: &Command,
559 ) -> Result<CommandReturnValue, Error> {
560 Ok(match command {
561 Command::DeviceInfo => {
562 CommandReturnValue::DeviceInfo(client.device_info().map_err(|source| {
563 Error::Client {
564 context: "executing device info command",
565 source,
566 }
567 })?)
568 }
569 Command::ResetDeviceAndReconnect => {
570 client
571 .reset_device_and_reconnect(Duration::from_secs(2))
572 .map_err(|source| Error::Client {
573 context: "executing device info command",
574 source,
575 })?;
576 CommandReturnValue::ResetDeviceAndReconnect
577 }
578 Command::PutAuthenticationKey {
579 info:
580 KeyInfo {
581 key_id,
582 domains,
583 caps,
584 },
585 delegated_caps,
586 authentication_key,
587 } => CommandReturnValue::PutAuthenticationKey(
588 client
589 .put_authentication_key(
590 key_id.into(),
591 Default::default(),
592 domains.into(),
593 caps.into(),
594 delegated_caps.into(),
595 Default::default(),
596 authentication_key,
597 )
598 .map_err(|source| Error::Client {
599 context: "putting authentication key",
600 source,
601 })?,
602 ),
603 Command::GenerateAsymmetricKey {
604 info:
605 KeyInfo {
606 key_id,
607 domains,
608 caps,
609 },
610 } => CommandReturnValue::GenerateAsymmetricKey(
611 client
612 .generate_asymmetric_key(
613 key_id.into(),
614 Default::default(),
615 domains.into(),
616 caps.into(),
617 AsymmetricAlgorithm::Ed25519,
618 )
619 .map_err(|source| Error::Client {
620 context: "generating asymmetric key",
621 source,
622 })?,
623 ),
624 Command::SignEd25519 { key_id, data } => CommandReturnValue::SignEd25519(
625 client
626 .sign_ed25519(key_id.into(), &data[..])
627 .map_err(|source| Error::Client {
628 context: "signing with ed25519 key",
629 source,
630 })?
631 .into(),
632 ),
633 Command::PutWrapKey {
634 info:
635 KeyInfo {
636 key_id,
637 domains,
638 caps,
639 },
640 delegated_caps,
641 wrapping_key,
642 } => CommandReturnValue::PutWrapKey(
643 client
644 .put_wrap_key(
645 key_id.into(),
646 Default::default(),
647 domains.into(),
648 caps.into(),
649 delegated_caps.into(),
650 WrapAlgorithm::Aes256Ccm,
651 wrapping_key.as_ref().as_secret_slice(),
652 )
653 .map_err(|source| Error::Client {
654 context: "putting wrap key",
655 source,
656 })?,
657 ),
658 Command::ExportWrapped {
659 wrap_key_id,
660 object,
661 } => CommandReturnValue::ExportWrapped(
662 client
663 .export_wrapped(wrap_key_id.into(), object.object_type(), object.id().into())
664 .map_err(|source| Error::Client {
665 context: "exporting wrapped key",
666 source,
667 })?,
668 ),
669 Command::ImportWrapped {
670 wrap_key_id,
671 message,
672 } => CommandReturnValue::ImportWrapped(
673 client
674 .import_wrapped(wrap_key_id.into(), message.clone())
675 .map_err(|source| Error::Client {
676 context: "importing wrapped key",
677 source,
678 })?,
679 ),
680 Command::DeleteObject(object) => {
681 client
682 .delete_object(object.id().into(), object.object_type())
683 .map_err(|source| Error::Client {
684 context: "deleting object",
685 source,
686 })?;
687 CommandReturnValue::DeleteObject
688 }
689 Command::GetObjectInfo(object) => CommandReturnValue::GetObjectInfo(
690 client
691 .get_object_info(object.id().into(), object.object_type())
692 .map_err(|source| Error::Client {
693 context: "getting object info",
694 source,
695 })?,
696 ),
697 Command::SetForceAuditOption(setting) => {
698 client
699 .set_force_audit_option((*setting).into())
700 .map_err(|source| Error::Client {
701 context: "setting force audit option",
702 source,
703 })?;
704 CommandReturnValue::SetForceAuditOption
705 }
706 Command::SetCommandAuditOption { command, setting } => {
707 client
708 .set_command_audit_option(*command, (*setting).into())
709 .map_err(|source| Error::Client {
710 context: "setting command audit option",
711 source,
712 })?;
713 CommandReturnValue::SetCommandAuditOption
714 }
715 Command::GetLogEntries => {
716 let log_entries = client.get_log_entries().map_err(|source| Error::Client {
717 context: "getting log entries",
718 source,
719 })?;
720
721 CommandReturnValue::GetLogEntries(LogEntries {
722 unlogged_boot_events: log_entries.unlogged_boot_events,
723 unlogged_auth_events: log_entries.unlogged_auth_events,
724 num_entries: log_entries.num_entries,
725 entries: log_entries
726 .entries
727 .into_iter()
728 .map(|entry| LogEntry {
729 item: entry.item,
730 cmd: entry.cmd,
731 length: entry.length,
732 session_key: entry.session_key,
733 target_key: entry.target_key,
734 second_key: entry.second_key,
735 result: entry.result,
736 tick: entry.tick,
737 digest: LogDigest(entry.digest.0),
738 })
739 .collect::<Vec<_>>(),
740 })
741 }
742 })
743 }
744}
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749
750 #[test]
751 fn ed25519_signature() {
752 let signature = Ed25519Signature {
753 r: vec![],
754 s: vec![],
755 };
756
757 println!("r: {:?}, s: {:?}", signature.r, signature.s);
758 }
759
760 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
761 mod scenario {
762 use std::{
763 fs::File,
764 io::stdout,
765 path::{Path, PathBuf},
766 };
767
768 use rstest::rstest;
769 use testresult::TestResult;
770
771 use super::*;
772 use crate::automation::{FileBackedScenario, Scenario};
773
774 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
775 fn run_scenario(scenario_file: impl AsRef<Path>) -> TestResult {
776 let scenario_file = scenario_file.as_ref();
777 eprintln!(
778 "Running scenario file {scenario_file}",
779 scenario_file = scenario_file.display()
780 );
781 let file_backed_scenario: FileBackedScenario =
782 serde_json::from_reader(File::open(scenario_file)?)?;
783 let runner = ScenarioRunner::new(Connector::mockhsm());
784 let return_value = runner
785 .run_with_writer(&Scenario::try_from(&file_backed_scenario)?, &mut stdout())?;
786 return_value.persist_file_backed_scenario(&file_backed_scenario)?;
787
788 Ok(())
789 }
790
791 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
792 #[rstest]
793 fn scenario_test(#[files("tests/scenarios/*.json")] scenario_file: PathBuf) -> TestResult {
794 run_scenario(scenario_file)?;
795 Ok(())
796 }
797
798 #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
799 #[test]
800 fn wrapping_test() -> TestResult {
801 run_scenario("tests/scenarios/wrapping/export-wrapped.json")?;
803 run_scenario("tests/scenarios/wrapping/import-wrapped.json")?;
804 Ok(())
805 }
806 }
807}