Skip to main content

signstar_yubihsm2/automation/
runner.rs

1//! Scenario runner
2
3#[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/// Signature made using the ed25519 signing algorithm.
40///
41/// # Note
42///
43/// This type exists to augment [`yubihsm::ed25519::Signature`], which does not use serde.
44#[derive(Debug)]
45#[cfg_attr(feature = "serde", derive(Serialize))]
46pub struct Ed25519Signature {
47    /// Raw bytes of the `R` component of the signature.
48    r: Vec<u8>,
49    /// Raw bytes of the `S` component of the signature.
50    s: Vec<u8>,
51}
52
53impl Ed25519Signature {
54    /// Returns the raw bytes of the `R` component of the signature.
55    pub fn r(&self) -> &[u8] {
56        &self.r
57    }
58
59    /// Returns the raw bytes of the `S` component of the signature.
60    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/// Serializes an `object` to JSON, suffixed by a newline.
75///
76/// # Errors
77///
78/// Returns an error if
79/// - serialization fails
80/// - writing to the `writer` fails
81#[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/// The return value of a [`Command`].
95#[derive(Debug)]
96#[cfg_attr(feature = "serde", derive(Serialize))]
97#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
98pub enum CommandReturnValue {
99    /// The return value of [`Client::device_info`].
100    DeviceInfo(DeviceInfo),
101
102    /// The return value of [`Client::reset_device_and_reconnect`].
103    ResetDeviceAndReconnect,
104
105    /// The return value of [`Client::put_authentication_key`].
106    PutAuthenticationKey(YubiHsmObjectId),
107
108    /// The return value of [`Client::change_authentication_key`].
109    ChangeAuthenticationKey(YubiHsmObjectId),
110
111    /// The return value of [`Client::generate_asymmetric_key`]
112    GenerateAsymmetricKey(YubiHsmObjectId),
113
114    /// The return value of [`Client::sign_ed25519`].
115    SignEd25519(Ed25519Signature),
116
117    /// The return value of [`Client::put_opaque`].
118    PutOpaque(YubiHsmObjectId),
119
120    /// The return value of [`Client::get_opaque`].
121    GetOpaque(Vec<u8>),
122
123    /// The return value of [`Client::put_wrap_key`].
124    PutWrapKey(YubiHsmObjectId),
125
126    /// The return value of [`Client::export_wrapped`].
127    ExportWrapped(Message),
128
129    /// The return value of [`Client::import_wrapped`].
130    ImportWrapped(Handle),
131
132    /// The return value of [`Client::delete_object`].
133    DeleteObject,
134
135    /// The return value of [`Client::get_object_info`].
136    GetObjectInfo(ObjectInfo),
137
138    /// The return value of [`Client::set_force_audit_option`].
139    SetForceAuditOption,
140
141    /// The return value of [`Client::set_command_audit_option`].
142    SetCommandAuditOption,
143
144    /// The return value of [`Client::get_log_entries`].
145    GetLogEntries(LogEntries),
146
147    /// The return value of [`Client::list_objects`].
148    ListObjects(Vec<Entry>),
149}
150
151impl PartialEq<Command> for &CommandReturnValue {
152    /// Compares [`CommandReturnValue`] and [`Command`].
153    ///
154    /// # Note
155    ///
156    /// Comparison is done using the enum variants on a best effort basis.
157    /// No data is compared directly.
158    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    /// Compares [`CommandReturnValue`] and [`FileBackedCommand`].
207    ///
208    /// # Note
209    ///
210    /// Comparison is done using the enum variants on a best effort basis.
211    /// No data is compared directly.
212    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/// The return value of a [`Scenario`].
271///
272/// Tracks the return value for each command executed as part of a [`Scenario`].
273#[derive(Debug)]
274pub struct ScenarioReturnValue {
275    authenticated_command_chains: Vec<Vec<CommandReturnValue>>,
276}
277
278impl ScenarioReturnValue {
279    /// Returns a reference to the return values of the authenticated command chains.
280    pub fn chains(&self) -> &[Vec<CommandReturnValue>] {
281        self.authenticated_command_chains.as_slice()
282    }
283
284    /// Compares this [`ScenarioReturnValue`] with a [`FileBackedScenario`].
285    ///
286    /// # Errors
287    ///
288    /// Returns an error if
289    ///
290    /// - the number of command chains in the `file_backed_scenario` does not match those in `self`
291    /// - the number of commands in a chain of commands in the `file_backed_scenario` does not match
292    ///   their equivalent in `self`
293    /// - one or more commands in the `file_backed_scenario` do not match a return value command in
294    ///   `self` (the associated commands differ)
295    #[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    /// Persists the data of a [`ScenarioReturnValue`] according to a [`FileBackedScenario`].
357    ///
358    /// # Errors
359    ///
360    /// Returns an error if
361    ///
362    /// - the `file_backed_scenario` cannot be compared with `self`
363    /// - data from the `file_backed_scenario` fails to be persisted
364    #[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
416/// Runs commands against a physical or in-memory YubiHSM2 token.
417pub struct ScenarioRunner {
418    connector: Connector,
419}
420
421impl Debug for ScenarioRunner {
422    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423        // Client is not Debug so we cannot derive Debug for ScenarioRunner
424        f.debug_struct("ScenarioRunner").finish()
425    }
426}
427
428impl ScenarioRunner {
429    /// Creates a new [`ScenarioRunner`] for a [`Connector`].
430    pub fn new(connector: Connector) -> Self {
431        Self { connector }
432    }
433
434    /// Runs a [`Scenario`].
435    ///
436    /// # Errors
437    ///
438    /// Returns an error if executing one of the commands in the scenario fails.
439    ///
440    /// Before returning the error, the return values of successfully executed commands will be
441    /// emitted in an error message to the log.
442    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                        // Emit the already collected output as an error.
463                        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    /// Runs a [`Scenario`].
491    ///
492    /// The `writer` will receive [JSONL]-formatted responses for commands which generate them.
493    ///
494    /// # Errors
495    ///
496    /// Returns an error if
497    ///
498    /// - executing the scenario fails
499    /// - the return value of a command cannot be serialized and written to the writer.
500    ///
501    /// [JSONL]: https://jsonlines.org/
502    #[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    /// Runs a single [`Command`] and returns a [`CommandReturnValue`] for it.
521    ///
522    /// # Errors
523    ///
524    /// Returns an error if
525    /// - executing the command on device fails
526    /// - reading or writing associated files fails
527    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            // these two need to run in order: first exporting to a file, then importing that file
814            run_scenario("tests/scenarios/wrapping/export-wrapped.json")?;
815            run_scenario("tests/scenarios/wrapping/import-wrapped.json")?;
816            Ok(())
817        }
818    }
819}