Skip to main content

signstar_yubihsm2/automation/
command.rs

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