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