Skip to main content

signstar_yubihsm2/automation/
command.rs

1//! Scenario commands.
2
3use std::time::Duration;
4#[cfg(feature = "cli")]
5use std::{
6    fs::{File, read},
7    io::Read,
8    path::{Path, PathBuf},
9};
10
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13#[cfg(feature = "cli")]
14use signstar_crypto::passphrase::Passphrase;
15use yubihsm::{
16    Capability as YubiHsmCapability,
17    command::Code,
18    object::{Filter, Id, Type},
19    opaque::Algorithm,
20    wrap::Message,
21};
22
23use crate::{
24    Credentials,
25    automation::CommandReturnValue,
26    backup::Label,
27    object::{AuthenticationKey, Capabilities, Domains, KeyInfo, ObjectId, WrapKey},
28};
29#[cfg(feature = "cli")]
30use crate::{
31    object::{WrapKeyFromPassphrase, WrapKeyKind},
32    user::FileBackedCredentials,
33};
34
35/// Indicates the setting of the auditing.
36#[derive(Clone, Copy, Debug)]
37#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
38#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
39pub enum AuditOption {
40    /// Auditing is enabled but can be disabled.
41    On,
42
43    /// Auditing is disabled.
44    Off,
45
46    /// Auditing is permanently enabled and cannot be disabled.
47    Fix,
48}
49
50impl From<AuditOption> for yubihsm::AuditOption {
51    fn from(value: AuditOption) -> Self {
52        match value {
53            AuditOption::On => Self::On,
54            AuditOption::Off => Self::Off,
55            AuditOption::Fix => Self::Fix,
56        }
57    }
58}
59
60/// The printable name of a [`Command`].
61#[derive(Debug, strum::Display)]
62#[strum(serialize_all = "snake_case")]
63#[cfg_attr(feature = "serde", derive(Serialize))]
64#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
65pub enum CommandName {
66    /// Query the device state.
67    DeviceInfo,
68
69    /// Reset the device to factory settings and reconnect afterwards.
70    ResetDeviceAndReconnect,
71
72    /// Query the command log of the device and print it to standard output.
73    GetLogEntries,
74
75    /// Change audit settings.
76    SetForceAuditOption,
77
78    /// Changes command audit settings.
79    SetCommandAuditOption,
80
81    /// Put authentication key on the device.
82    PutAuthenticationKey,
83
84    /// Change the currently used authentication key on the device.
85    ChangeAuthenticationKey,
86
87    /// Generates a new asymmetric key on the device.
88    GenerateAsymmetricKey,
89
90    /// Signs data using a `ed25519` key.
91    SignEd25519,
92
93    /// Puts opaque data on the device.
94    PutOpaque,
95
96    /// Retrieves opaque data from the device.
97    GetOpaque,
98
99    /// Puts new wrapping key on the device.
100    PutWrapKey,
101
102    /// Export object under wrap (encrypted).
103    ExportWrapped,
104
105    /// Imports objects under wrap (encrypted).
106    ImportWrapped,
107
108    /// Permanently remove an object from the device.
109    DeleteObject,
110
111    /// Query data about the object and print it to standard output.
112    GetObjectInfo,
113
114    /// Lists objects visible from the authenticated session based on a list of filters.
115    ListObjects,
116}
117
118impl From<&Command> for CommandName {
119    fn from(value: &Command) -> Self {
120        match value {
121            Command::DeviceInfo => Self::DeviceInfo,
122            Command::ResetDeviceAndReconnect { .. } => Self::ResetDeviceAndReconnect,
123            Command::GetLogEntries => Self::GetLogEntries,
124            Command::SetForceAuditOption(_) => Self::SetForceAuditOption,
125            Command::SetCommandAuditOption { .. } => Self::SetCommandAuditOption,
126            Command::PutAuthenticationKey { .. } => Self::PutAuthenticationKey,
127            Command::ChangeAuthenticationKey { .. } => Self::ChangeAuthenticationKey,
128            Command::GenerateAsymmetricKey { .. } => Self::GenerateAsymmetricKey,
129            Command::SignEd25519 { .. } => Self::SignEd25519,
130            Command::PutOpaque { .. } => Self::PutOpaque,
131            Command::GetOpaque { .. } => Self::GetOpaque,
132            Command::PutWrapKey { .. } => Self::PutWrapKey,
133            Command::ExportWrapped { .. } => Self::ExportWrapped,
134            Command::ImportWrapped { .. } => Self::ImportWrapped,
135            Command::DeleteObject(_) => Self::DeleteObject,
136            Command::GetObjectInfo(_) => Self::GetObjectInfo,
137            Command::ListObjects(_) => Self::ListObjects,
138        }
139    }
140}
141
142impl From<&CommandReturnValue> for CommandName {
143    fn from(value: &CommandReturnValue) -> Self {
144        match value {
145            CommandReturnValue::DeviceInfo(_) => Self::DeviceInfo,
146            CommandReturnValue::ResetDeviceAndReconnect => Self::ResetDeviceAndReconnect,
147            CommandReturnValue::GetLogEntries(_) => Self::GetLogEntries,
148            CommandReturnValue::SetForceAuditOption => Self::SetForceAuditOption,
149            CommandReturnValue::SetCommandAuditOption => Self::SetCommandAuditOption,
150            CommandReturnValue::PutAuthenticationKey { .. } => Self::PutAuthenticationKey,
151            CommandReturnValue::ChangeAuthenticationKey { .. } => Self::ChangeAuthenticationKey,
152            CommandReturnValue::GenerateAsymmetricKey { .. } => Self::GenerateAsymmetricKey,
153            CommandReturnValue::SignEd25519 { .. } => Self::SignEd25519,
154            CommandReturnValue::PutOpaque { .. } => Self::PutOpaque,
155            CommandReturnValue::GetOpaque { .. } => Self::GetOpaque,
156            CommandReturnValue::PutWrapKey { .. } => Self::PutWrapKey,
157            CommandReturnValue::ExportWrapped { .. } => Self::ExportWrapped,
158            CommandReturnValue::ImportWrapped { .. } => Self::ImportWrapped,
159            CommandReturnValue::DeleteObject => Self::DeleteObject,
160            CommandReturnValue::GetObjectInfo(_) => Self::GetObjectInfo,
161            CommandReturnValue::ListObjects(_) => Self::ListObjects,
162        }
163    }
164}
165
166#[cfg(feature = "cli")]
167impl From<&FileBackedCommand> for CommandName {
168    fn from(value: &FileBackedCommand) -> Self {
169        match value {
170            FileBackedCommand::DeviceInfo => Self::DeviceInfo,
171            FileBackedCommand::ResetDeviceAndReconnect { .. } => Self::ResetDeviceAndReconnect,
172            FileBackedCommand::GetLogEntries => Self::GetLogEntries,
173            FileBackedCommand::SetForceAuditOption(_) => Self::SetForceAuditOption,
174            FileBackedCommand::SetCommandAuditOption { .. } => Self::SetCommandAuditOption,
175            FileBackedCommand::PutAuthenticationKey { .. } => Self::PutAuthenticationKey,
176            FileBackedCommand::ChangeAuthenticationKey { .. } => Self::ChangeAuthenticationKey,
177            FileBackedCommand::GenerateAsymmetricKey { .. } => Self::GenerateAsymmetricKey,
178            FileBackedCommand::SignEd25519 { .. } => Self::SignEd25519,
179            FileBackedCommand::PutOpaque { .. } => Self::PutOpaque,
180            FileBackedCommand::GetOpaque { .. } => Self::GetOpaque,
181            FileBackedCommand::PutWrapKey { .. } => Self::PutWrapKey,
182            FileBackedCommand::ExportWrapped { .. } => Self::ExportWrapped,
183            FileBackedCommand::ImportWrapped { .. } => Self::ImportWrapped,
184            FileBackedCommand::DeleteObject(_) => Self::DeleteObject,
185            FileBackedCommand::GetObjectInfo(_) => Self::GetObjectInfo,
186            FileBackedCommand::ListObjects(_) => Self::ListObjects,
187        }
188    }
189}
190
191/// An object type in the YubiHSM2.
192///
193/// # Note
194///
195/// This type is only needed because [`Type`] uses a custom serde implementation based on bytes.
196#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
197#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
198#[derive(Clone, Copy, Debug, strum::Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
199#[strum(serialize_all = "kebab-case")]
200pub enum ObjectType {
201    /// Raw data.
202    Opaque,
203
204    /// Authentication keys.
205    AuthenticationKey,
206
207    /// Asymmetric private keys.
208    AsymmetricKey,
209
210    /// Key for exporting and importing of keys and data.
211    WrapKey,
212
213    /// HMAC private key.
214    HmacKey,
215
216    /// A template for validating SSH certificate requests.
217    Template,
218
219    /// A Yubike-AES OTP encryption and decryption key.
220    OtpAeakey,
221
222    /// Symmetric private keys for encryption and decryption.
223    SymmetricKey,
224}
225
226impl From<Type> for ObjectType {
227    fn from(value: Type) -> Self {
228        match value {
229            Type::Opaque => Self::Opaque,
230            Type::AuthenticationKey => Self::AuthenticationKey,
231            Type::AsymmetricKey => Self::AsymmetricKey,
232            Type::WrapKey => Self::WrapKey,
233            Type::HmacKey => Self::HmacKey,
234            Type::Template => Self::Template,
235            Type::OtpAeadKey => Self::OtpAeakey,
236            Type::SymmetricKey => Self::SymmetricKey,
237        }
238    }
239}
240
241impl From<&ObjectType> for Type {
242    fn from(value: &ObjectType) -> Self {
243        match value {
244            ObjectType::Opaque => Self::Opaque,
245            ObjectType::AuthenticationKey => Self::AuthenticationKey,
246            ObjectType::AsymmetricKey => Self::AsymmetricKey,
247            ObjectType::WrapKey => Self::WrapKey,
248            ObjectType::HmacKey => Self::HmacKey,
249            ObjectType::Template => Self::Template,
250            ObjectType::OtpAeakey => Self::OtpAeadKey,
251            ObjectType::SymmetricKey => Self::SymmetricKey,
252        }
253    }
254}
255
256/// A filter to apply when retrieving information about objects in a YubiHSM2.
257///
258/// # Note
259///
260/// This type is only needed because [`Filter`] neither implements [`Debug`] nor serde: <https://github.com/iqlusioninc/yubihsm.rs/pull/672>.
261///
262/// In addition, we only implement a subset of the [`Filter`].
263#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
264#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
265#[derive(Clone, Debug)]
266pub enum ListObjectFilter {
267    /// Filter by capabilities.
268    Capabilities(Capabilities),
269
270    /// Filter by domains.
271    Domains(Domains),
272
273    /// Filter by ID.
274    Id(Id),
275
276    /// Filter by type.
277    Type(ObjectType),
278}
279
280impl From<&ListObjectFilter> for Filter {
281    fn from(value: &ListObjectFilter) -> Self {
282        match value {
283            ListObjectFilter::Capabilities(capabilities) => {
284                Filter::Capabilities(capabilities.into())
285            }
286            ListObjectFilter::Domains(domains) => Filter::Domains(domains.into()),
287            ListObjectFilter::Id(id) => Filter::Id(*id),
288            ListObjectFilter::Type(typ) => Filter::Type(typ.into()),
289        }
290    }
291}
292
293/// A file containing opaque data.
294///
295/// The file is guaranteed to be not larger than [`OpaqueData::MAX_DATA_SIZE`] bytes during time
296/// of creation.
297#[derive(Clone, Debug)]
298#[cfg(feature = "cli")]
299#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
300#[cfg_attr(feature = "serde", serde(try_from = "PathBuf", into = "PathBuf"))]
301pub struct OpaqueDataFile(PathBuf);
302
303#[cfg(feature = "cli")]
304impl OpaqueDataFile {
305    /// Creates a new [`OpaqueDataFile`] from a path.
306    ///
307    /// # Error
308    ///
309    /// Returns an error, if
310    ///
311    /// - `path` is not a file
312    /// - `path` cannot be opened for reading
313    /// - the file size of `path` is larger than [`OpaqueData::MAX_DATA_SIZE`]
314    pub fn new(path: impl AsRef<Path>) -> Result<Self, crate::Error> {
315        let path = path.as_ref();
316        if !path.is_file() {
317            return Err(crate::automation::Error::OpaqueDataNotAFile {
318                path: path.to_path_buf(),
319            }
320            .into());
321        }
322        let file = File::open(path).map_err(|source| crate::Error::IoPath {
323            path: path.to_path_buf(),
324            context: "opening an opaque data file for reading",
325            source,
326        })?;
327        let data_length = file
328            .metadata()
329            .map_err(|source| crate::Error::IoPath {
330                path: path.to_path_buf(),
331                context: "retrieving metadata of an opaque data file",
332                source,
333            })?
334            .len() as usize;
335        if data_length > OpaqueData::MAX_DATA_SIZE {
336            return Err(crate::automation::Error::OpaqueDataFileLength {
337                path: path.to_path_buf(),
338                data_length,
339            }
340            .into());
341        }
342
343        Ok(Self(path.to_path_buf()))
344    }
345}
346
347#[cfg(feature = "cli")]
348impl TryFrom<PathBuf> for OpaqueDataFile {
349    type Error = crate::Error;
350
351    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
352        Self::new(&value)
353    }
354}
355
356#[cfg(feature = "cli")]
357impl From<OpaqueDataFile> for PathBuf {
358    fn from(value: OpaqueDataFile) -> Self {
359        value.0
360    }
361}
362
363#[cfg(feature = "cli")]
364impl TryFrom<&OpaqueDataFile> for Vec<u8> {
365    type Error = crate::Error;
366
367    /// Creates a new vector of bytes from a [`OpaqueDataFile`].
368    ///
369    /// # Note
370    ///
371    /// This conversion does not fail on `value` tracking a file that is larger than
372    /// [`OpaqueData::MAX_DATA_SIZE`] bytes.
373    ///
374    /// # Errors
375    ///
376    /// Returns an error, if
377    ///
378    /// - the file tracked by `value` cannot be opened for reading
379    /// - the file tracked by `value` cannot be read
380    fn try_from(value: &OpaqueDataFile) -> Result<Self, Self::Error> {
381        let mut file = File::open(value.0.as_path()).map_err(|source| crate::Error::IoPath {
382            path: value.0.clone(),
383            context: "opening an opaque data file for reading",
384            source,
385        })?;
386        let mut buffer = Vec::new();
387        file.read_to_end(&mut buffer)
388            .map_err(|source| crate::Error::IoPath {
389                path: value.0.clone(),
390                context: "reading the contents of an opaque data file",
391                source,
392            })?;
393
394        Ok(buffer)
395    }
396}
397
398/// Data for an opaque object, which is guaranteed to be not larger than
399/// [`OpaqueData::MAX_DATA_SIZE`] bytes.
400///
401/// # Note
402///
403/// The [`PUT OPAQUE` command] documentation states, that the maximum message size is 2048 bytes
404/// (including message headers).
405///
406/// [`PUT OPAQUE` command]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-cmd-reference.html#put-opaque-command
407#[derive(Clone, Debug)]
408pub struct OpaqueData(Vec<u8>);
409
410impl OpaqueData {
411    /// The maximum allowed message size.
412    ///
413    /// # Note
414    ///
415    /// According to the documentation, the maximum message size
416    /// [`MAX_MSG_SIZE`][`yubihsm::command::MAX_MSG_SIZE`] includes the headers for a message.
417    /// After testing, we concluded, that the headers do not take up more than 54 bytes.
418    pub const MAX_DATA_SIZE: usize = 1980;
419
420    /// Creates a new [`OpaqueData`] from a byte vector.
421    ///
422    /// # Errors
423    ///
424    /// Returns an error, if `data` is longer than [`Self::MAX_DATA_SIZE`].
425    pub fn new(data: Vec<u8>) -> Result<Self, crate::Error> {
426        if data.len() > OpaqueData::MAX_DATA_SIZE {
427            return Err(crate::automation::Error::OpaqueDataLength {
428                data_length: data.len(),
429            }
430            .into());
431        }
432
433        Ok(Self(data))
434    }
435}
436
437#[cfg(feature = "cli")]
438impl TryFrom<&OpaqueDataFile> for OpaqueData {
439    type Error = crate::Error;
440
441    fn try_from(value: &OpaqueDataFile) -> Result<Self, Self::Error> {
442        let data: Vec<u8> = value.try_into()?;
443        Self::new(data)
444    }
445}
446
447impl From<&OpaqueData> for Vec<u8> {
448    fn from(value: &OpaqueData) -> Self {
449        value.0.clone()
450    }
451}
452
453/// The "algorithm" (or type) of an opaque data object.
454///
455/// This type is required when putting opaque data onto a YubiHSM2 (using the [`PUT OPAQUE`
456/// command]) and is returned when retrieving object info (using the [`GET OBJECT INFO` command]).
457///
458/// [`PUT OPAQUE` command]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-cmd-reference.html#put-opaque-command
459/// [`GET OBJECT INFO` command]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-cmd-reference.html#get-object-info-command
460#[derive(Clone, Copy, Debug, strum::Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
461#[strum(serialize_all = "kebab-case")]
462#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
463#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
464pub enum OpaqueDataAlgorithm {
465    /// Opaque data.
466    OpaqueData,
467
468    /// An X590 certificate.
469    OpaqueX590Certificate,
470}
471
472impl From<Algorithm> for OpaqueDataAlgorithm {
473    fn from(value: Algorithm) -> Self {
474        match value {
475            Algorithm::Data => Self::OpaqueData,
476            Algorithm::X509Certificate => Self::OpaqueX590Certificate,
477        }
478    }
479}
480
481impl From<&OpaqueDataAlgorithm> for Algorithm {
482    fn from(value: &OpaqueDataAlgorithm) -> Self {
483        match value {
484            OpaqueDataAlgorithm::OpaqueData => Self::Data,
485            OpaqueDataAlgorithm::OpaqueX590Certificate => Self::X509Certificate,
486        }
487    }
488}
489
490/// The valid capabilities for an opaque data object.
491#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
492#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
493#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
494pub enum OpaqueDataCapabilities {
495    /// No capabilities.
496    None,
497
498    /// The opaque data is exportable under wrap.
499    ExportableUnderWrap,
500}
501
502impl From<&OpaqueDataCapabilities> for YubiHsmCapability {
503    fn from(value: &OpaqueDataCapabilities) -> Self {
504        match value {
505            OpaqueDataCapabilities::None => YubiHsmCapability::empty(),
506            OpaqueDataCapabilities::ExportableUnderWrap => YubiHsmCapability::EXPORTABLE_UNDER_WRAP,
507        }
508    }
509}
510
511/// A single command that is atomically executed against a YubiHSM2.
512#[derive(Debug)]
513pub enum Command {
514    /// Query the device state.
515    DeviceInfo,
516
517    /// Reset the device to factory settings and reconnect afterwards.
518    ///
519    /// Note that this is a destructive operation and the authenticating user will need to have
520    /// appropriate capabilities.
521    ResetDeviceAndReconnect {
522        /// The time frame in which the reconnect has to happen.
523        timeout: Duration,
524    },
525
526    /// Query the command log of the device and print it to standard output.
527    GetLogEntries,
528
529    /// Change audit settings.
530    ///
531    /// This mode prevents the device from performing additional operations when the Logs and Error
532    /// Codes is full.
533    ///
534    /// See [Force Audit](https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#force-audit) for more details.
535    SetForceAuditOption(AuditOption),
536
537    /// Changes command audit settings.
538    ///
539    /// This is used to manage auditing options for specific commands. By default all commands are
540    /// logged.
541    ///
542    /// See [Force Audit](https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#command-audit) for more details.
543    SetCommandAuditOption {
544        /// Command of which the setting should be changed.
545        command: Code,
546
547        /// New setting value.
548        setting: AuditOption,
549    },
550
551    /// Put authentication key on the device.
552    ///
553    /// This command is used to append new authentication keys.
554    PutAuthenticationKey {
555        /// The key identity and capabilities.
556        info: KeyInfo,
557
558        /// Additional delegated capabilities which would apply to objects that are created or
559        /// imported.
560        delegated_caps: Capabilities,
561
562        /// The authentication key to put onto the YubiHSM2.
563        authentication_key: AuthenticationKey,
564    },
565
566    /// Change the authentication key used for the current session.
567    ///
568    /// This command is used to change an authentication key that is currently used for the
569    /// connection.
570    ChangeAuthenticationKey {
571        /// The key ID.
572        key_id: Id,
573
574        /// The authentication key to put onto the YubiHSM2 instead of the currently used one.
575        authentication_key: AuthenticationKey,
576    },
577
578    /// Generates new `ed25519` signing key on the device.
579    GenerateAsymmetricKey {
580        /// The key identity and capabilities.
581        info: KeyInfo,
582    },
583
584    /// Signs data using provided `ed25519` key.
585    SignEd25519 {
586        /// The key to be used for signing.
587        key_id: Id,
588
589        /// Raw data blob which should be signed.
590        data: Vec<u8>,
591    },
592
593    /// Puts new wrapping key on the device.
594    ///
595    /// This command is used to append new wrapping keys which serve as encryption keys for other
596    /// objects.
597    PutWrapKey {
598        /// The key identity and capabilities.
599        info: KeyInfo,
600
601        /// Additional delegated capabilities which would apply to objects that are created or
602        /// imported.
603        delegated_caps: Capabilities,
604
605        /// The wrapping key.
606        wrapping_key: WrapKey,
607    },
608
609    /// Stores opaque data (e.g. a certificate) in the device.
610    ///
611    /// # Note
612    ///
613    /// The size of the object is limited to 2028 bytes, which is the maximum message size.
614    PutOpaque {
615        /// The ID of the object.
616        id: Id,
617
618        /// A label describing the object.
619        label: Label,
620
621        /// The domains the opaque data will be available in.
622        domains: Domains,
623
624        /// The capabilities which will apply to the opaque data.
625        capabilities: OpaqueDataCapabilities,
626
627        /// The type of data.
628        algorithm: OpaqueDataAlgorithm,
629
630        /// The data.
631        data: OpaqueData,
632    },
633
634    /// Retrieves an opaque data object.
635    GetOpaque {
636        /// The ID of the opaque data object to retrieve.
637        id: Id,
638    },
639
640    /// Export object under wrap (encrypted).
641    ExportWrapped {
642        /// Wrapping key which should encrypt the exported object.
643        wrap_key_id: Id,
644
645        /// Object that will be exported.
646        object: ObjectId,
647    },
648
649    /// Imports objects under wrap (encrypted).
650    ImportWrapped {
651        /// Wrapping key which would decrypt the imported object.
652        wrap_key_id: Id,
653
654        /// The encrypted message which should be imported.
655        message: Message,
656    },
657
658    /// Permanently remove an object from the device.
659    DeleteObject(ObjectId),
660
661    /// Query data about the object and print it to standard output.
662    GetObjectInfo(ObjectId),
663
664    /// Lists objects visible from the authenticated session based on a list of filters.
665    ListObjects(Vec<ListObjectFilter>),
666}
667
668#[cfg(feature = "cli")]
669impl TryFrom<&FileBackedCommand> for Command {
670    type Error = crate::Error;
671
672    /// Creates a new [`Command`] from this [`FileBackedCommand`].
673    ///
674    /// # Errors
675    ///
676    /// Returns an error, if reading/creating the required data from input files fails.
677    fn try_from(value: &FileBackedCommand) -> Result<Self, Self::Error> {
678        Ok(match value {
679            FileBackedCommand::DeviceInfo => Command::DeviceInfo,
680            FileBackedCommand::ResetDeviceAndReconnect { timeout_ms } => {
681                Command::ResetDeviceAndReconnect {
682                    timeout: Duration::from_millis(*timeout_ms),
683                }
684            }
685            FileBackedCommand::GetLogEntries => Command::GetLogEntries,
686            FileBackedCommand::SetForceAuditOption(audit_option) => {
687                Command::SetForceAuditOption(*audit_option)
688            }
689            FileBackedCommand::SetCommandAuditOption { command, setting } => {
690                Command::SetCommandAuditOption {
691                    command: (*command),
692                    setting: (*setting),
693                }
694            }
695            FileBackedCommand::PutAuthenticationKey {
696                info,
697                delegated_caps,
698                passphrase_file,
699            } => Command::PutAuthenticationKey {
700                info: info.clone(),
701                delegated_caps: delegated_caps.clone(),
702                authentication_key: AuthenticationKey::try_from(passphrase_file.as_path())?,
703            },
704            FileBackedCommand::ChangeAuthenticationKey {
705                key_id,
706                passphrase_file,
707            } => Command::ChangeAuthenticationKey {
708                key_id: *key_id,
709                authentication_key: AuthenticationKey::try_from(passphrase_file.as_path())?,
710            },
711            FileBackedCommand::GenerateAsymmetricKey { info } => {
712                Command::GenerateAsymmetricKey { info: info.clone() }
713            }
714            FileBackedCommand::SignEd25519 { key_id, data } => Command::SignEd25519 {
715                key_id: (*key_id),
716                data: data.to_vec(),
717            },
718            FileBackedCommand::PutOpaque {
719                id,
720                label,
721                domains,
722                capabilities,
723                algorithm,
724                data_file,
725            } => Command::PutOpaque {
726                id: *id,
727                label: label.clone(),
728                domains: domains.clone(),
729                capabilities: *capabilities,
730                algorithm: *algorithm,
731                data: OpaqueData::try_from(data_file)?,
732            },
733            FileBackedCommand::GetOpaque { id, .. } => Command::GetOpaque { id: *id },
734            FileBackedCommand::PutWrapKey {
735                info,
736                delegated_caps,
737                passphrase_file,
738            } => Command::PutWrapKey {
739                info: info.clone(),
740                delegated_caps: delegated_caps.clone(),
741                wrapping_key: WrapKey::try_from(WrapKeyFromPassphrase::new(
742                    &Passphrase::try_from(passphrase_file.as_path())?,
743                    WrapKeyKind::Aes256,
744                )?)?,
745            },
746            FileBackedCommand::ExportWrapped {
747                wrap_key_id,
748                object,
749                wrapped_file: _,
750            } => Command::ExportWrapped {
751                wrap_key_id: (*wrap_key_id),
752                object: (*object),
753            },
754            FileBackedCommand::ImportWrapped {
755                wrap_key_id,
756                wrapped_file,
757            } => {
758                let message =
759                    Message::from_vec(read(wrapped_file.as_path()).map_err(|source| {
760                        Self::Error::IoPath {
761                            path: wrapped_file.clone(),
762                            context: "reading a file under wrap",
763                            source,
764                        }
765                    })?)
766                    .map_err(|source| Self::Error::InvalidWrap {
767                        context: "reading the wrapped file",
768                        source,
769                    })?;
770
771                Command::ImportWrapped {
772                    wrap_key_id: (*wrap_key_id),
773                    message,
774                }
775            }
776            FileBackedCommand::DeleteObject(id) => Command::DeleteObject(*id),
777            FileBackedCommand::GetObjectInfo(id) => Command::GetObjectInfo(*id),
778            FileBackedCommand::ListObjects(filters) => Command::ListObjects(filters.clone()),
779        })
780    }
781}
782
783/// A single command that is atomically executed against a YubiHSM2.
784///
785/// Different from [`Command`], this enum does not assign data directly in its variants, but instead
786/// relies on paths to files to read from or write to.
787#[derive(Debug)]
788#[cfg(feature = "cli")]
789#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
790#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
791pub enum FileBackedCommand {
792    /// Query the device state.
793    DeviceInfo,
794
795    /// Reset the device to factory settings and reconnect afterwards.
796    ///
797    ///
798    /// # Note
799    ///
800    /// This is a destructive operation and the authenticating user will need to have appropriate
801    /// capabilities.
802    ResetDeviceAndReconnect {
803        /// The timeframe (in ms) in which the reconnect has to happen.
804        timeout_ms: u64,
805    },
806
807    /// Query the command log of the device and print it to standard output.
808    GetLogEntries,
809
810    /// Change audit settings.
811    ///
812    /// This mode prevents the device from performing additional operations when the Logs and Error
813    /// Codes is full.
814    ///
815    /// See [Force Audit](https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#force-audit) for more details.
816    SetForceAuditOption(AuditOption),
817
818    /// Changes command audit settings.
819    ///
820    /// This is used to manage auditing options for specific commands. By default all commands are
821    /// logged.
822    ///
823    /// See [Force Audit](https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#command-audit) for more details.
824    SetCommandAuditOption {
825        /// Command of which the setting should be changed.
826        command: Code,
827
828        /// New setting value.
829        setting: AuditOption,
830    },
831
832    /// Put authentication key on the device.
833    ///
834    /// This command is used to append new authentication keys.
835    PutAuthenticationKey {
836        /// The key identity and capabilities.
837        #[cfg_attr(feature = "serde", serde(flatten))]
838        info: KeyInfo,
839
840        /// Additional delegated capabilities which would apply to objects that are created or
841        /// imported.
842        delegated_caps: Capabilities,
843
844        /// The file containing passphrase of the authenticating user.
845        passphrase_file: PathBuf,
846    },
847
848    /// Change the authentication key on the device, which is currently used for the session.
849    ///
850    /// This command is used to replace the currently used authentication key.
851    ChangeAuthenticationKey {
852        /// The key ID.
853        key_id: Id,
854
855        /// The file containing the passphrase of the authenticating user.
856        passphrase_file: PathBuf,
857    },
858
859    /// Generates new `ed25519` signing key on the device.
860    GenerateAsymmetricKey {
861        /// The key identity and capabilities.
862        #[cfg_attr(feature = "serde", serde(flatten))]
863        info: KeyInfo,
864    },
865
866    /// Signs data using provided `ed25519` key.
867    SignEd25519 {
868        /// The key to be used for signing.
869        key_id: Id,
870
871        /// Raw data blob which should be signed.
872        data: Vec<u8>,
873    },
874
875    /// Stores opaque data (e.g. a certificate) in the device.
876    ///
877    /// # Note
878    ///
879    /// The size of the object is limited to 2028 bytes, which is the maximum message size.
880    PutOpaque {
881        /// The ID of the object.
882        id: Id,
883
884        /// A label describing the object.
885        label: Label,
886
887        /// The domains the opaque data will be available in.
888        domains: Domains,
889
890        /// The capabilities which will apply to the opaque data.
891        capabilities: OpaqueDataCapabilities,
892
893        /// The type of data.
894        algorithm: OpaqueDataAlgorithm,
895
896        /// The file containing the data.
897        data_file: OpaqueDataFile,
898    },
899
900    /// Puts new wrapping key on the device.
901    ///
902    /// This command is used to append new wrapping keys which serve as encryption keys for other
903    /// objects.
904    PutWrapKey {
905        /// The key identity and capabilities.
906        #[cfg_attr(feature = "serde", serde(flatten))]
907        info: KeyInfo,
908
909        /// Additional delegated capabilities which would apply to objects that are created or
910        /// imported.
911        delegated_caps: Capabilities,
912
913        /// The file containing the passphrase from which the wrapping key is generated.
914        passphrase_file: PathBuf,
915    },
916
917    /// Retrieves an opaque data object.
918    GetOpaque {
919        /// The path to write the data to.
920        data_file: PathBuf,
921
922        /// The ID of the opaque data object to retrieve.
923        id: Id,
924    },
925
926    /// Export object under wrap (encrypted).
927    ExportWrapped {
928        /// Wrapping key which should encrypt the exported object.
929        wrap_key_id: Id,
930
931        /// Object that will be exported.
932        #[cfg_attr(feature = "serde", serde(flatten))]
933        object: ObjectId,
934
935        /// Output file which will contain the exported object encrypted with the wrapping key.
936        wrapped_file: PathBuf,
937    },
938
939    /// Imports objects under wrap (encrypted).
940    ImportWrapped {
941        /// Wrapping key which would decrypt the imported object.
942        wrap_key_id: Id,
943
944        /// Input file which contains the imported object encrypted with the wrapping key.
945        wrapped_file: PathBuf,
946    },
947
948    /// Permanently remove an object from the device.
949    DeleteObject(ObjectId),
950
951    /// Query data about the object and print it to standard output.
952    GetObjectInfo(ObjectId),
953
954    /// Lists objects visible from the authenticated session based on a list of filters.
955    ListObjects(Vec<ListObjectFilter>),
956}
957
958/// A list of [`Command`]s that are run with a specific authentication.
959///
960/// A single [`Credentials`] is used for authentication of each command towards the YubiHSM2
961/// backend.
962#[derive(Debug)]
963pub struct AuthenticatedCommandChain {
964    auth: Credentials,
965    commands: Vec<Command>,
966}
967
968impl AuthenticatedCommandChain {
969    /// Creates a new [`AuthenticatedCommandChain`] from authentication data and a list of commands.
970    pub fn new(auth: Credentials, commands: Vec<Command>) -> Self {
971        Self { auth, commands }
972    }
973
974    /// Returns the authentication details for the authenticated commands.
975    pub fn auth(&self) -> &Credentials {
976        &self.auth
977    }
978
979    /// Returns the commands for the authenticated commands.
980    pub fn commands(&self) -> &[Command] {
981        &self.commands
982    }
983}
984
985/// A list of [`Command`]s that are run with a specific authentication.
986///
987/// A single [`FileBackedCredentials`] is used for authentication of each command towards the
988/// YubiHSM2 backend.
989#[cfg(feature = "cli")]
990#[derive(Debug)]
991#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
992pub struct FileBackedAuthenticatedCommandChain {
993    pub(crate) auth: FileBackedCredentials,
994    pub(crate) commands: Vec<FileBackedCommand>,
995}
996
997#[cfg(test)]
998mod tests {
999    #[cfg(feature = "cli")]
1000    use std::io::Write;
1001
1002    #[cfg(feature = "cli")]
1003    use tempfile::{NamedTempFile, TempDir};
1004    use testresult::TestResult;
1005
1006    use super::*;
1007
1008    const LARGE_DATA_LENGTH: usize = OpaqueData::MAX_DATA_SIZE + 1;
1009
1010    /// Ensures, that [`OpaqueData::new`] fails on input data that is too large.
1011    #[test]
1012    fn opaque_data_new_fails_on_large_data() -> TestResult {
1013        let data = Vec::from_iter([1; LARGE_DATA_LENGTH]);
1014        match OpaqueData::new(data) {
1015            Err(crate::Error::Automation(crate::automation::Error::OpaqueDataLength {
1016                ..
1017            })) => {}
1018            Err(error) => panic!(
1019                "Expected to fail with Error::OpaqueDataLength, but got a different error instead: {error}"
1020            ),
1021            Ok(opaque_data) => panic!(
1022                "Expected to fail with Error::OpaqueDataLength, succeeded instead: {opaque_data:?}"
1023            ),
1024        };
1025
1026        Ok(())
1027    }
1028
1029    /// Ensures, that a [`PathBuf`] can be created from an [`OpaqueDataFile`].
1030    #[cfg(feature = "cli")]
1031    #[test]
1032    fn path_from_opaque_data_file() -> TestResult {
1033        let data_file = {
1034            let mut data_file = NamedTempFile::new()?;
1035            let data: Vec<u8> = Vec::from_iter([1; 1]);
1036            data_file.write_all(data.as_slice())?;
1037            data_file
1038        };
1039        let opaque_data_file = OpaqueDataFile::new(data_file.path())?;
1040        let _path: PathBuf = opaque_data_file.into();
1041
1042        Ok(())
1043    }
1044
1045    /// Ensures, that [`OpaqueDataFile::new`] fails on file path being a directory.
1046    #[cfg(feature = "cli")]
1047    #[test]
1048    fn opaque_data_file_new_fails_on_dir() -> TestResult {
1049        let temp_dir = TempDir::new()?;
1050
1051        match OpaqueDataFile::new(temp_dir.path()) {
1052            Err(crate::Error::Automation(crate::automation::Error::OpaqueDataNotAFile {
1053                ..
1054            })) => {}
1055            Err(error) => panic!(
1056                "Expected to fail with Error::OpaqueDataNotAFile, but got a different error instead: {error}"
1057            ),
1058            Ok(opaque_data) => panic!(
1059                "Expected to fail with Error::OpaqueDataNotAFile, succeeded instead: {opaque_data:?}"
1060            ),
1061        };
1062
1063        Ok(())
1064    }
1065
1066    /// Ensures, that [`OpaqueDataFile::new`] fails on file path of a file that is too large.
1067    #[cfg(feature = "cli")]
1068    #[test]
1069    fn opaque_data_file_new_fails_on_large_data() -> TestResult {
1070        let data_file = {
1071            let mut data_file = NamedTempFile::new()?;
1072            let data: Vec<u8> = Vec::from_iter([1; LARGE_DATA_LENGTH]);
1073            data_file.write_all(data.as_slice())?;
1074            data_file
1075        };
1076
1077        match OpaqueDataFile::new(data_file.path()) {
1078            Err(crate::Error::Automation(crate::automation::Error::OpaqueDataFileLength {
1079                ..
1080            })) => {}
1081            Err(error) => panic!(
1082                "Expected to fail with Error::OpaqueDataFileLength, but got a different error instead: {error}"
1083            ),
1084            Ok(opaque_data) => panic!(
1085                "Expected to fail with Error::OpaqueDataFileLength, succeeded instead: {opaque_data:?}"
1086            ),
1087        };
1088
1089        Ok(())
1090    }
1091}