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    command::Code as CommandCode,
20    device::Info as DeviceInfo,
21    ed25519::Signature,
22    object::{Handle, Id as YubiHsmObjectId, Info as ObjectInfo},
23    response::Code as ResponseCode,
24    wrap::{Algorithm as WrapAlgorithm, Message},
25};
26
27#[cfg(feature = "cli")]
28use crate::automation::{
29    Error as AutomationError,
30    FileBackedCommand,
31    FileBackedScenario,
32    error::FileBackedScenarioReturnValueMismatch,
33};
34use crate::{
35    Error,
36    automation::{Command, Scenario},
37    object::KeyInfo,
38};
39
40/// Signature made using the ed25519 signing algorithm.
41///
42/// # Note
43///
44/// This type exists to augment [`yubihsm::ed25519::Signature`], which does not use serde.
45#[derive(Debug)]
46#[cfg_attr(feature = "serde", derive(Serialize))]
47#[cfg_attr(
48    any(
49        all(not(feature = "serde"), feature = "_yubihsm2-mockhsm"),
50        all(
51            not(feature = "serde"),
52            not(feature = "_yubihsm2-mockhsm"),
53            not(feature = "cli")
54        )
55    ),
56    allow(unused)
57)]
58pub struct Ed25519Signature {
59    /// Raw bytes of the `R` component of the signature.
60    r: Vec<u8>,
61    /// Raw bytes of the `S` component of the signature.
62    s: Vec<u8>,
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/// Response from [`Client::get_log_entries`].
75///
76/// # Note
77///
78/// This type exists to augment a non-public return type of a public function.
79/// <https://github.com/iqlusioninc/yubihsm.rs/issues/617>
80#[derive(Debug)]
81#[cfg_attr(feature = "serde", derive(Serialize))]
82pub struct LogEntries {
83    /// Number of boot events which weren't logged (if buffer is full and audit enforce is set)
84    pub unlogged_boot_events: u16,
85
86    /// Number of unlogged authentication events (if buffer is full and audit enforce is set)
87    pub unlogged_auth_events: u16,
88
89    /// Number of entries in the response
90    pub num_entries: u8,
91
92    /// Entries in the log
93    pub entries: Vec<LogEntry>,
94}
95
96/// Entry in the log response.
97///
98/// # Note
99///
100/// This type exists to augment a non-public return type of a public function.
101/// <https://github.com/iqlusioninc/yubihsm.rs/issues/617>
102#[derive(Debug, Eq, PartialEq)]
103#[cfg_attr(feature = "serde", derive(Serialize))]
104pub struct LogEntry {
105    /// Entry number
106    pub item: u16,
107
108    /// Command type
109    pub cmd: CommandCode,
110
111    /// Command length
112    pub length: u16,
113
114    /// Session key ID
115    pub session_key: YubiHsmObjectId,
116
117    /// Target key ID
118    pub target_key: YubiHsmObjectId,
119
120    /// Second key affected
121    pub second_key: YubiHsmObjectId,
122
123    /// Result of the operation
124    pub result: ResponseCode,
125
126    /// Tick count of the HSM's internal clock
127    pub tick: u32,
128
129    /// 16-byte truncated SHA-256 digest of this log entry and the digest of the previous entry
130    pub digest: LogDigest,
131}
132
133/// Size of a truncated digest in the log
134///
135/// # Note
136///
137/// This type exists to augment a non-public return type of a public function.
138/// <https://github.com/iqlusioninc/yubihsm.rs/issues/617>
139pub const LOG_DIGEST_SIZE: usize = 16;
140
141/// Truncated SHA-256 digest of a log entry and the previous log digest
142///
143/// # Note
144///
145/// This type exists to augment a non-public return type of a public function.
146/// <https://github.com/iqlusioninc/yubihsm.rs/issues/617>
147#[derive(Eq, PartialEq)]
148#[cfg_attr(feature = "serde", derive(Serialize))]
149pub struct LogDigest(pub [u8; LOG_DIGEST_SIZE]);
150
151impl AsRef<[u8]> for LogDigest {
152    fn as_ref(&self) -> &[u8] {
153        &self.0[..]
154    }
155}
156
157impl Debug for LogDigest {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        write!(f, "LogDigest(")?;
160        for (i, byte) in self.0.iter().enumerate() {
161            write!(f, "{byte:02x}")?;
162            write!(f, "{}", if i == LOG_DIGEST_SIZE - 1 { ")" } else { ":" })?;
163        }
164        Ok(())
165    }
166}
167
168/// Serializes an `object` to JSON, suffixed by a newline.
169///
170/// # Errors
171///
172/// Returns an error if
173/// - serialization fails
174/// - writing to the `writer` fails
175#[cfg(feature = "serde")]
176fn serialize_with_newline(mut writer: &mut dyn Write, object: impl Serialize) -> Result<(), Error> {
177    serde_json::to_writer(&mut writer, &object).map_err(|source| Error::Json {
178        context: "serializing response",
179        source,
180    })?;
181    writer.write_all(b"\n").map_err(|source| Error::Io {
182        context: "writing record delimiter",
183        source,
184    })?;
185    Ok(())
186}
187
188/// The return value of a [`Command`].
189#[derive(Debug)]
190#[cfg_attr(feature = "serde", derive(Serialize))]
191#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
192pub enum CommandReturnValue {
193    /// The return value of [`Client::device_info`].
194    DeviceInfo(DeviceInfo),
195
196    /// The return value of [`Client::reset_device_and_reconnect`].
197    ResetDeviceAndReconnect,
198
199    /// The return value of [`Client::put_authentication_key`].
200    PutAuthenticationKey(YubiHsmObjectId),
201
202    /// The return value of [`Client::generate_asymmetric_key`]
203    GenerateAsymmetricKey(YubiHsmObjectId),
204
205    /// The return value of [`Client::sign_ed25519`].
206    SignEd25519(Ed25519Signature),
207
208    /// The return value of [`Client::put_wrap_key`].
209    PutWrapKey(YubiHsmObjectId),
210
211    /// The return value of [`Client::export_wrapped`].
212    ExportWrapped(Message),
213
214    /// The return value of [`Client::import_wrapped`].
215    ImportWrapped(Handle),
216
217    /// The return value of [`Client::delete_object`].
218    DeleteObject,
219
220    /// The return value of [`Client::get_object_info`].
221    GetObjectInfo(ObjectInfo),
222
223    /// The return value of [`Client::set_force_audit_option`].
224    SetForceAuditOption,
225
226    /// The return value of [`Client::set_command_audit_option`].
227    SetCommandAuditOption,
228
229    /// The return value of [`Client::get_log_entries`].
230    GetLogEntries(LogEntries),
231}
232
233impl PartialEq<Command> for &CommandReturnValue {
234    /// Compares [`CommandReturnValue`] and [`Command`].
235    ///
236    /// # Note
237    ///
238    /// Comparison is done using the enum variants on a best effort basis.
239    /// No data is compared directly.
240    fn eq(&self, other: &Command) -> bool {
241        match (self, other) {
242            (CommandReturnValue::DeviceInfo(_), Command::DeviceInfo)
243            | (CommandReturnValue::ResetDeviceAndReconnect, Command::ResetDeviceAndReconnect)
244            | (CommandReturnValue::PutAuthenticationKey(_), Command::PutAuthenticationKey { .. })
245            | (
246                CommandReturnValue::GenerateAsymmetricKey(_),
247                Command::GenerateAsymmetricKey { .. },
248            )
249            | (CommandReturnValue::SignEd25519(_), Command::SignEd25519 { .. })
250            | (CommandReturnValue::PutWrapKey(_), Command::PutWrapKey { .. })
251            | (CommandReturnValue::ExportWrapped(_), Command::ExportWrapped { .. })
252            | (CommandReturnValue::ImportWrapped(_), Command::ImportWrapped { .. })
253            | (CommandReturnValue::DeleteObject, Command::DeleteObject(_))
254            | (CommandReturnValue::GetObjectInfo(_), Command::GetObjectInfo(_))
255            | (CommandReturnValue::SetForceAuditOption, Command::SetForceAuditOption(_))
256            | (CommandReturnValue::SetCommandAuditOption, Command::SetCommandAuditOption { .. })
257            | (CommandReturnValue::GetLogEntries(_), Command::GetLogEntries) => true,
258            (CommandReturnValue::DeviceInfo(_), _)
259            | (CommandReturnValue::ResetDeviceAndReconnect, _)
260            | (CommandReturnValue::PutAuthenticationKey(_), _)
261            | (CommandReturnValue::GenerateAsymmetricKey(_), _)
262            | (CommandReturnValue::SignEd25519(_), _)
263            | (CommandReturnValue::PutWrapKey(_), _)
264            | (CommandReturnValue::ExportWrapped(_), _)
265            | (CommandReturnValue::ImportWrapped(_), _)
266            | (CommandReturnValue::DeleteObject, _)
267            | (CommandReturnValue::GetObjectInfo(_), _)
268            | (CommandReturnValue::SetForceAuditOption, _)
269            | (CommandReturnValue::SetCommandAuditOption, _)
270            | (CommandReturnValue::GetLogEntries(_), _) => false,
271        }
272    }
273}
274
275#[cfg(feature = "cli")]
276impl PartialEq<FileBackedCommand> for &CommandReturnValue {
277    /// Compares [`CommandReturnValue`] and [`FileBackedCommand`].
278    ///
279    /// # Note
280    ///
281    /// Comparison is done using the enum variants on a best effort basis.
282    /// No data is compared directly.
283    fn eq(&self, other: &FileBackedCommand) -> bool {
284        match (self, other) {
285            (CommandReturnValue::DeviceInfo(_), FileBackedCommand::DeviceInfo)
286            | (
287                CommandReturnValue::ResetDeviceAndReconnect,
288                FileBackedCommand::ResetDeviceAndReconnect,
289            )
290            | (
291                CommandReturnValue::PutAuthenticationKey(_),
292                FileBackedCommand::PutAuthenticationKey { .. },
293            )
294            | (
295                CommandReturnValue::GenerateAsymmetricKey(_),
296                FileBackedCommand::GenerateAsymmetricKey { .. },
297            )
298            | (CommandReturnValue::SignEd25519(_), FileBackedCommand::SignEd25519 { .. })
299            | (CommandReturnValue::PutWrapKey(_), FileBackedCommand::PutWrapKey { .. })
300            | (CommandReturnValue::ExportWrapped(_), FileBackedCommand::ExportWrapped { .. })
301            | (CommandReturnValue::ImportWrapped(_), FileBackedCommand::ImportWrapped { .. })
302            | (CommandReturnValue::DeleteObject, FileBackedCommand::DeleteObject(_))
303            | (CommandReturnValue::GetObjectInfo(_), FileBackedCommand::GetObjectInfo(_))
304            | (
305                CommandReturnValue::SetForceAuditOption,
306                FileBackedCommand::SetForceAuditOption(_),
307            )
308            | (
309                CommandReturnValue::SetCommandAuditOption,
310                FileBackedCommand::SetCommandAuditOption { .. },
311            )
312            | (CommandReturnValue::GetLogEntries(_), FileBackedCommand::GetLogEntries) => true,
313            (CommandReturnValue::DeviceInfo(_), _)
314            | (CommandReturnValue::ResetDeviceAndReconnect, _)
315            | (CommandReturnValue::PutAuthenticationKey(_), _)
316            | (CommandReturnValue::GenerateAsymmetricKey(_), _)
317            | (CommandReturnValue::SignEd25519(_), _)
318            | (CommandReturnValue::PutWrapKey(_), _)
319            | (CommandReturnValue::ExportWrapped(_), _)
320            | (CommandReturnValue::ImportWrapped(_), _)
321            | (CommandReturnValue::DeleteObject, _)
322            | (CommandReturnValue::GetObjectInfo(_), _)
323            | (CommandReturnValue::SetForceAuditOption, _)
324            | (CommandReturnValue::SetCommandAuditOption, _)
325            | (CommandReturnValue::GetLogEntries(_), _) => false,
326        }
327    }
328}
329
330/// The return value of a [`Scenario`].
331///
332/// Tracks the return value for each command executed as part of a [`Scenario`].
333#[derive(Debug)]
334pub struct ScenarioReturnValue {
335    authenticated_command_chains: Vec<Vec<CommandReturnValue>>,
336}
337
338impl ScenarioReturnValue {
339    /// Returns a reference to the return values of the authenticated command chains.
340    pub fn chains(&self) -> &[Vec<CommandReturnValue>] {
341        self.authenticated_command_chains.as_slice()
342    }
343
344    /// Compares this [`ScenarioReturnValue`] with a [`FileBackedScenario`].
345    ///
346    /// # Errors
347    ///
348    /// Returns an error if
349    ///
350    /// - the number of command chains in the `file_backed_scenario` does not match those in `self`
351    /// - the number of commands in a chain of commands in the `file_backed_scenario` does not match
352    ///   their equivalent in `self`
353    /// - one or more commands in the `file_backed_scenario` do not match a return value command in
354    ///   `self` (the associated commands differ)
355    #[cfg(feature = "cli")]
356    fn compare_with_file_backed_scenario(
357        &self,
358        file_backed_scenario: &FileBackedScenario,
359    ) -> Result<(), Error> {
360        debug!(
361            "Comparing the return values of the scenario with the requested commands of the file backed scenario"
362        );
363
364        let mut mismatches = Vec::new();
365
366        if file_backed_scenario.as_ref().len() != self.authenticated_command_chains.len() {
367            return Err(
368                AutomationError::MismatchingNumberOfAuthenticatedCommandChains {
369                    scenario: file_backed_scenario.as_ref().len(),
370                    scenario_return_value: self.authenticated_command_chains.len(),
371                }
372                .into(),
373            );
374        }
375
376        for (file_backed_authenticated_command_chain, command_return_values) in file_backed_scenario
377            .as_ref()
378            .iter()
379            .zip(self.authenticated_command_chains.iter())
380        {
381            if file_backed_authenticated_command_chain.commands.len() != command_return_values.len()
382            {
383                return Err(AutomationError::MismatchingNumberOfCommands {
384                    authenticated_command_chain: file_backed_authenticated_command_chain
385                        .commands
386                        .len(),
387                    command_return_values: command_return_values.len(),
388                }
389                .into());
390            }
391
392            for (file_backed_command, command_return_value) in
393                file_backed_authenticated_command_chain
394                    .commands
395                    .iter()
396                    .zip(command_return_values.iter())
397            {
398                if command_return_value.ne(file_backed_command) {
399                    mismatches.push(FileBackedScenarioReturnValueMismatch {
400                        file_backed_scenario_command: file_backed_command.into(),
401                        command_return_value: command_return_value.into(),
402                    });
403                }
404            }
405        }
406
407        if !mismatches.is_empty() {
408            return Err(
409                AutomationError::MismatchingReturnValueForFileBackedScenario { mismatches }.into(),
410            );
411        }
412
413        Ok(())
414    }
415
416    /// Persists the data of a [`ScenarioReturnValue`] according to a [`FileBackedScenario`].
417    ///
418    /// # Errors
419    ///
420    /// Returns an error if
421    ///
422    /// - the `file_backed_scenario` cannot be compared with `self`
423    /// - data from the `file_backed_scenario` fails to be persisted
424    #[cfg(feature = "cli")]
425    pub fn persist_file_backed_scenario(
426        &self,
427        file_backed_scenario: &FileBackedScenario,
428    ) -> Result<(), Error> {
429        self.compare_with_file_backed_scenario(file_backed_scenario)?;
430
431        for (file_backed_authenticated_command_chain, command_return_values) in file_backed_scenario
432            .as_ref()
433            .iter()
434            .zip(self.authenticated_command_chains.iter())
435        {
436            for (file_backed_command, command_return_value) in
437                file_backed_authenticated_command_chain
438                    .commands
439                    .iter()
440                    .zip(command_return_values.iter())
441            {
442                if let (
443                    FileBackedCommand::ExportWrapped { wrapped_file, .. },
444                    CommandReturnValue::ExportWrapped(message),
445                ) = (file_backed_command, command_return_value)
446                {
447                    write(wrapped_file.as_path(), message.clone().into_vec()).map_err(|source| {
448                        Error::IoPath {
449                            path: wrapped_file.clone(),
450                            context: "writing an encrypted message to the file",
451                            source,
452                        }
453                    })?
454                }
455            }
456        }
457
458        Ok(())
459    }
460}
461
462/// Runs commands against a physical or in-memory YubiHSM2 token.
463pub struct ScenarioRunner {
464    #[cfg_attr(
465        any(
466            all(not(feature = "serde"), feature = "_yubihsm2-mockhsm"),
467            all(
468                not(feature = "serde"),
469                not(feature = "_yubihsm2-mockhsm"),
470                not(feature = "cli")
471            )
472        ),
473        allow(unused)
474    )]
475    connector: Connector,
476}
477
478impl Debug for ScenarioRunner {
479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480        // Client is not Debug so we cannot derive Debug for ScenarioRunner
481        f.debug_struct("ScenarioRunner").finish()
482    }
483}
484
485impl ScenarioRunner {
486    /// Creates a new [`ScenarioRunner`] for a [`Connector`].
487    pub fn new(connector: Connector) -> Self {
488        Self { connector }
489    }
490
491    /// Runs a [`Scenario`].
492    ///
493    /// # Errors
494    ///
495    /// Returns an error if executing one of the commands in the scenario fails.
496    ///
497    /// Before returning the error, the return values of successfully executed commands will be
498    /// emitted in an error message to the log.
499    pub fn run(&self, scenario: &Scenario) -> Result<ScenarioReturnValue, Error> {
500        let mut authenticated_command_chains = Vec::new();
501
502        for authenticated_commands in scenario.as_ref().iter() {
503            let mut client = Client::open(
504                self.connector.clone(),
505                Credentials::from(authenticated_commands.auth()),
506                true,
507            )
508            .map_err(|source| Error::Client {
509                context: "opening new client",
510                source,
511            })?;
512            let mut command_return_values = Vec::new();
513
514            for command in authenticated_commands.commands().iter() {
515                info!("Executing {command:?}");
516                match self.run_command(&mut client, command) {
517                    Ok(return_value) => command_return_values.push(return_value),
518                    Err(error) => {
519                        // Emit the already collected output as an error.
520                        error!(
521                            "{}",
522                            authenticated_command_chains
523                                .iter()
524                                .flatten()
525                                .map(|return_value| format!("{return_value:?}"))
526                                .chain(
527                                    command_return_values
528                                        .iter()
529                                        .map(|return_value| format!("{return_value:?}"))
530                                )
531                                .collect::<Vec<_>>()
532                                .join("\n")
533                        );
534                        return Err(error);
535                    }
536                }
537            }
538
539            authenticated_command_chains.push(command_return_values);
540        }
541
542        Ok(ScenarioReturnValue {
543            authenticated_command_chains,
544        })
545    }
546
547    /// Runs a [`Scenario`].
548    ///
549    /// The `writer` will receive [JSONL]-formatted responses for commands which generate them.
550    ///
551    /// # Errors
552    ///
553    /// Returns an error if
554    ///
555    /// - executing the scenario fails
556    /// - the return value of a command cannot be serialized and written to the writer.
557    ///
558    /// [JSONL]: https://jsonlines.org/
559    #[cfg(feature = "serde")]
560    pub fn run_with_writer(
561        &self,
562        scenario: &Scenario,
563        writer: &mut dyn Write,
564    ) -> Result<ScenarioReturnValue, Error> {
565        let scenario_return_value = self.run(scenario)?;
566        for return_value in scenario_return_value
567            .authenticated_command_chains
568            .iter()
569            .flatten()
570        {
571            serialize_with_newline(writer, return_value)?;
572        }
573
574        Ok(scenario_return_value)
575    }
576
577    /// Runs a single [`Command`] and returns a [`CommandReturnValue`] for it.
578    ///
579    /// # Errors
580    ///
581    /// Returns an error if
582    /// - executing the command on device fails
583    /// - reading or writing associated files fails
584    fn run_command(
585        &self,
586        client: &mut Client,
587        command: &Command,
588    ) -> Result<CommandReturnValue, Error> {
589        Ok(match command {
590            Command::DeviceInfo => {
591                CommandReturnValue::DeviceInfo(client.device_info().map_err(|source| {
592                    Error::Client {
593                        context: "executing device info command",
594                        source,
595                    }
596                })?)
597            }
598            Command::ResetDeviceAndReconnect => {
599                client
600                    .reset_device_and_reconnect(Duration::from_secs(2))
601                    .map_err(|source| Error::Client {
602                        context: "executing device info command",
603                        source,
604                    })?;
605                CommandReturnValue::ResetDeviceAndReconnect
606            }
607            Command::PutAuthenticationKey {
608                info:
609                    KeyInfo {
610                        key_id,
611                        domains,
612                        caps,
613                    },
614                delegated_caps,
615                authentication_key,
616            } => CommandReturnValue::PutAuthenticationKey(
617                client
618                    .put_authentication_key(
619                        key_id.into(),
620                        Default::default(),
621                        domains.into(),
622                        caps.into(),
623                        delegated_caps.into(),
624                        Default::default(),
625                        authentication_key,
626                    )
627                    .map_err(|source| Error::Client {
628                        context: "putting authentication key",
629                        source,
630                    })?,
631            ),
632            Command::GenerateAsymmetricKey {
633                info:
634                    KeyInfo {
635                        key_id,
636                        domains,
637                        caps,
638                    },
639            } => CommandReturnValue::GenerateAsymmetricKey(
640                client
641                    .generate_asymmetric_key(
642                        key_id.into(),
643                        Default::default(),
644                        domains.into(),
645                        caps.into(),
646                        AsymmetricAlgorithm::Ed25519,
647                    )
648                    .map_err(|source| Error::Client {
649                        context: "generating asymmetric key",
650                        source,
651                    })?,
652            ),
653            Command::SignEd25519 { key_id, data } => CommandReturnValue::SignEd25519(
654                client
655                    .sign_ed25519(key_id.into(), &data[..])
656                    .map_err(|source| Error::Client {
657                        context: "signing with ed25519 key",
658                        source,
659                    })?
660                    .into(),
661            ),
662            Command::PutWrapKey {
663                info:
664                    KeyInfo {
665                        key_id,
666                        domains,
667                        caps,
668                    },
669                delegated_caps,
670                wrapping_key,
671            } => CommandReturnValue::PutWrapKey(
672                client
673                    .put_wrap_key(
674                        key_id.into(),
675                        Default::default(),
676                        domains.into(),
677                        caps.into(),
678                        delegated_caps.into(),
679                        WrapAlgorithm::Aes256Ccm,
680                        wrapping_key,
681                    )
682                    .map_err(|source| Error::Client {
683                        context: "putting wrap key",
684                        source,
685                    })?,
686            ),
687            Command::ExportWrapped {
688                wrap_key_id,
689                object,
690            } => CommandReturnValue::ExportWrapped(
691                client
692                    .export_wrapped(wrap_key_id.into(), object.object_type(), object.id().into())
693                    .map_err(|source| Error::Client {
694                        context: "exporting wrapped key",
695                        source,
696                    })?,
697            ),
698            Command::ImportWrapped {
699                wrap_key_id,
700                message,
701            } => CommandReturnValue::ImportWrapped(
702                client
703                    .import_wrapped(wrap_key_id.into(), message.clone())
704                    .map_err(|source| Error::Client {
705                        context: "importing wrapped key",
706                        source,
707                    })?,
708            ),
709            Command::DeleteObject(object) => {
710                client
711                    .delete_object(object.id().into(), object.object_type())
712                    .map_err(|source| Error::Client {
713                        context: "deleting object",
714                        source,
715                    })?;
716                CommandReturnValue::DeleteObject
717            }
718            Command::GetObjectInfo(object) => CommandReturnValue::GetObjectInfo(
719                client
720                    .get_object_info(object.id().into(), object.object_type())
721                    .map_err(|source| Error::Client {
722                        context: "getting object info",
723                        source,
724                    })?,
725            ),
726            Command::SetForceAuditOption(setting) => {
727                client
728                    .set_force_audit_option((*setting).into())
729                    .map_err(|source| Error::Client {
730                        context: "setting force audit option",
731                        source,
732                    })?;
733                CommandReturnValue::SetForceAuditOption
734            }
735            Command::SetCommandAuditOption { command, setting } => {
736                client
737                    .set_command_audit_option(*command, (*setting).into())
738                    .map_err(|source| Error::Client {
739                        context: "setting command audit option",
740                        source,
741                    })?;
742                CommandReturnValue::SetCommandAuditOption
743            }
744            Command::GetLogEntries => {
745                let log_entries = client.get_log_entries().map_err(|source| Error::Client {
746                    context: "getting log entries",
747                    source,
748                })?;
749
750                CommandReturnValue::GetLogEntries(LogEntries {
751                    unlogged_boot_events: log_entries.unlogged_boot_events,
752                    unlogged_auth_events: log_entries.unlogged_auth_events,
753                    num_entries: log_entries.num_entries,
754                    entries: log_entries
755                        .entries
756                        .into_iter()
757                        .map(|entry| LogEntry {
758                            item: entry.item,
759                            cmd: entry.cmd,
760                            length: entry.length,
761                            session_key: entry.session_key,
762                            target_key: entry.target_key,
763                            second_key: entry.second_key,
764                            result: entry.result,
765                            tick: entry.tick,
766                            digest: LogDigest(entry.digest.0),
767                        })
768                        .collect::<Vec<_>>(),
769                })
770            }
771        })
772    }
773}
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778
779    #[test]
780    fn ed25519_signature() {
781        let signature = Ed25519Signature {
782            r: vec![],
783            s: vec![],
784        };
785
786        println!("r: {:?}, s: {:?}", signature.r, signature.s);
787    }
788
789    #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde", feature = "cli"))]
790    mod scenario {
791        use std::{
792            fs::File,
793            io::stdout,
794            path::{Path, PathBuf},
795        };
796
797        use rstest::rstest;
798        use testresult::TestResult;
799
800        use super::*;
801        use crate::automation::{FileBackedScenario, Scenario};
802
803        #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
804        fn run_scenario(scenario_file: impl AsRef<Path>) -> TestResult {
805            let scenario_file = scenario_file.as_ref();
806            eprintln!(
807                "Running scenario file {scenario_file}",
808                scenario_file = scenario_file.display()
809            );
810            let file_backed_scenario: FileBackedScenario =
811                serde_json::from_reader(File::open(scenario_file)?)?;
812            let runner = ScenarioRunner::new(Connector::mockhsm());
813            let return_value = runner
814                .run_with_writer(&Scenario::try_from(&file_backed_scenario)?, &mut stdout())?;
815            return_value.persist_file_backed_scenario(&file_backed_scenario)?;
816
817            Ok(())
818        }
819
820        #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
821        #[rstest]
822        fn scenario_test(#[files("tests/scenarios/*.json")] scenario_file: PathBuf) -> TestResult {
823            run_scenario(scenario_file)?;
824            Ok(())
825        }
826
827        #[cfg(all(feature = "_yubihsm2-mockhsm", feature = "serde"))]
828        #[test]
829        fn wrapping_test() -> TestResult {
830            // these two need to run in order: first exporting to a file, then importing that file
831            run_scenario("tests/scenarios/wrapping/export-wrapped.json")?;
832            run_scenario("tests/scenarios/wrapping/import-wrapped.json")?;
833            Ok(())
834        }
835    }
836}