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