Skip to main content

signstar_yubihsm2/automation/
runner.rs

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