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::generate_asymmetric_key`]
109    GenerateAsymmetricKey(YubiHsmObjectId),
110
111    /// The return value of [`Client::sign_ed25519`].
112    SignEd25519(Ed25519Signature),
113
114    /// The return value of [`Client::put_opaque`].
115    PutOpaque(YubiHsmObjectId),
116
117    /// The return value of [`Client::get_opaque`].
118    GetOpaque(Vec<u8>),
119
120    /// The return value of [`Client::put_wrap_key`].
121    PutWrapKey(YubiHsmObjectId),
122
123    /// The return value of [`Client::export_wrapped`].
124    ExportWrapped(Message),
125
126    /// The return value of [`Client::import_wrapped`].
127    ImportWrapped(Handle),
128
129    /// The return value of [`Client::delete_object`].
130    DeleteObject,
131
132    /// The return value of [`Client::get_object_info`].
133    GetObjectInfo(ObjectInfo),
134
135    /// The return value of [`Client::set_force_audit_option`].
136    SetForceAuditOption,
137
138    /// The return value of [`Client::set_command_audit_option`].
139    SetCommandAuditOption,
140
141    /// The return value of [`Client::get_log_entries`].
142    GetLogEntries(LogEntries),
143
144    /// The return value of [`Client::list_objects`].
145    ListObjects(Vec<Entry>),
146}
147
148impl PartialEq<Command> for &CommandReturnValue {
149    /// Compares [`CommandReturnValue`] and [`Command`].
150    ///
151    /// # Note
152    ///
153    /// Comparison is done using the enum variants on a best effort basis.
154    /// No data is compared directly.
155    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    /// Compares [`CommandReturnValue`] and [`FileBackedCommand`].
199    ///
200    /// # Note
201    ///
202    /// Comparison is done using the enum variants on a best effort basis.
203    /// No data is compared directly.
204    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/// The return value of a [`Scenario`].
258///
259/// Tracks the return value for each command executed as part of a [`Scenario`].
260#[derive(Debug)]
261pub struct ScenarioReturnValue {
262    authenticated_command_chains: Vec<Vec<CommandReturnValue>>,
263}
264
265impl ScenarioReturnValue {
266    /// Returns a reference to the return values of the authenticated command chains.
267    pub fn chains(&self) -> &[Vec<CommandReturnValue>] {
268        self.authenticated_command_chains.as_slice()
269    }
270
271    /// Compares this [`ScenarioReturnValue`] with a [`FileBackedScenario`].
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if
276    ///
277    /// - the number of command chains in the `file_backed_scenario` does not match those in `self`
278    /// - the number of commands in a chain of commands in the `file_backed_scenario` does not match
279    ///   their equivalent in `self`
280    /// - one or more commands in the `file_backed_scenario` do not match a return value command in
281    ///   `self` (the associated commands differ)
282    #[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    /// Persists the data of a [`ScenarioReturnValue`] according to a [`FileBackedScenario`].
344    ///
345    /// # Errors
346    ///
347    /// Returns an error if
348    ///
349    /// - the `file_backed_scenario` cannot be compared with `self`
350    /// - data from the `file_backed_scenario` fails to be persisted
351    #[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
403/// Runs commands against a physical or in-memory YubiHSM2 token.
404pub struct ScenarioRunner {
405    connector: Connector,
406}
407
408impl Debug for ScenarioRunner {
409    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410        // Client is not Debug so we cannot derive Debug for ScenarioRunner
411        f.debug_struct("ScenarioRunner").finish()
412    }
413}
414
415impl ScenarioRunner {
416    /// Creates a new [`ScenarioRunner`] for a [`Connector`].
417    pub fn new(connector: Connector) -> Self {
418        Self { connector }
419    }
420
421    /// Runs a [`Scenario`].
422    ///
423    /// # Errors
424    ///
425    /// Returns an error if executing one of the commands in the scenario fails.
426    ///
427    /// Before returning the error, the return values of successfully executed commands will be
428    /// emitted in an error message to the log.
429    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                        // Emit the already collected output as an error.
450                        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    /// Runs a [`Scenario`].
478    ///
479    /// The `writer` will receive [JSONL]-formatted responses for commands which generate them.
480    ///
481    /// # Errors
482    ///
483    /// Returns an error if
484    ///
485    /// - executing the scenario fails
486    /// - the return value of a command cannot be serialized and written to the writer.
487    ///
488    /// [JSONL]: https://jsonlines.org/
489    #[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    /// Runs a single [`Command`] and returns a [`CommandReturnValue`] for it.
508    ///
509    /// # Errors
510    ///
511    /// Returns an error if
512    /// - executing the command on device fails
513    /// - reading or writing associated files fails
514    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            // these two need to run in order: first exporting to a file, then importing that file
787            run_scenario("tests/scenarios/wrapping/export-wrapped.json")?;
788            run_scenario("tests/scenarios/wrapping/import-wrapped.json")?;
789            Ok(())
790        }
791    }
792}