Skip to main content

signstar_yubihsm2/automation/
runner.rs

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