Skip to main content

signstar_yubihsm2/
backup.rs

1//! Utilities for parsing and creating YubiHSM2 wrap files.
2//!
3//! Wrap files are used for [backup and restore] actions with a YubiHSM2 device.
4//! This module provides support for the proprietary YHW data format, used by Yubico tooling.
5//!
6//! The module supports backup of the following types of objects:
7//! - ed25519 private keys (both seeded and expanded form),
8//! - AES-128 authentication keys,
9//! - opaque byte vectors.
10//!
11//! # YHW format
12//!
13//! YubiHSM wrap files (`*.yhw`) consist of an inner and an outer format.
14//!
15//! ## Outer
16//!
17//! The outer format is represented by a base64-encoded file.
18//! Its contents consist of 13 bytes of [nonce] at the start and AES-CCM encrypted data until the
19//! end of the file.
20//!
21//! ## Inner
22//!
23//! Decrypting the AES-CCM encrypted outer data reveals the inner format which has the following
24//! structure:
25//!
26//! - 1 byte for [`WrapAlgorithm`]
27//! - 8 bytes for [`Capabilities`]
28//! - 2 bytes for encoding the object's identifier
29//! - 2 bytes for encoding the wrapped object length without framing
30//! - 2 bytes for [`Domains`]
31//! - 1 byte for the object type (e.g. asymmetric key, opaque)
32//! - 1 byte for the subtype of the object (e.g. ed25519 key)
33//! - 1 byte for a sequence number, which is used internally and always `0`
34//! - 1 byte for encoding the origin (this is only relevant when exporting)
35//! - 40 bytes for a UTF-8 encoded [`Label`]
36//! - the rest of the inner format is specific to each object type (e.g. opaque byte vectors are
37//!   embedded in their entirety here)
38//!
39//! [backup and restore]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-backup-restore.html
40//! [nonce]: https://en.wikipedia.org/wiki/Cryptographic_nonce
41
42use std::{
43    array::TryFromSliceError,
44    fmt::{Debug, Display},
45    fs::read,
46    path::Path,
47    str::FromStr,
48};
49
50use aes::{Aes128, cipher::typenum::Unsigned};
51use base64ct::{Base64, Encoding as _};
52use ccm::{
53    Ccm,
54    Nonce,
55    aead::{Aead, KeyInit, rand_core::RngCore},
56    consts::{U13, U16},
57};
58use curve25519_dalek::Scalar;
59use ed25519_dalek::{SigningKey, hazmat::ExpandedSecretKey};
60use num_enum::{FromPrimitive, IntoPrimitive};
61#[cfg(feature = "serde")]
62use serde::{Deserialize, Serialize};
63use yubihsm::object::{Handle, Id, Label as YubiHsmObjectLabel, Type};
64
65use crate::object::{Capabilities, Domains, ObjectId};
66
67/// Backup error.
68#[derive(Debug, thiserror::Error)]
69pub enum Error {
70    /// Base64 decoding error.
71    #[error("Decoding Base64 failed: {0}")]
72    Base64Decode(#[from] base64ct::Error),
73
74    /// Decryption error.
75    #[error("Decryption error: {0}")]
76    Decrypt(#[from] ccm::Error),
77
78    /// Slice length error.
79    #[error("Incorrect slice length: {0}")]
80    SliceLength(#[from] TryFromSliceError),
81
82    /// Unexpected Ed25519 serialized form length.
83    ///
84    /// The only supported values are [ExpandedEd25519KeyData::LEN] and [SeedEd25519KeyData::LEN].
85    #[error("Unexpected Ed25519 serialized form length: {actual}")]
86    UnexpectedEd25519SerializedLength {
87        /// Length of the serialized form encountered.
88        actual: usize,
89    },
90
91    /// Unsupported object type.
92    #[error("Cannot parse data of unknown type: {0:?}")]
93    UnknownObjectType(ObjectType),
94
95    /// Object error.
96    #[error("YubiHSM2 object error: {0:?}")]
97    YubiHsmObject(#[from] yubihsm::object::Error),
98
99    /// Parsing failed because the buffer does not contain enough data.
100    #[error("Parsing buffer: not enough data.")]
101    InsufficientDataInBuffer,
102
103    /// Label length error.
104    #[error(
105        "The string '{label}' could not be converted to a label as it exceeds the 40 bytes limit."
106    )]
107    LabelLength {
108        /// The label string that exceeded the 40-byte limit.
109        label: String,
110    },
111
112    /// Label length error.
113    #[error(
114        "The label '{label}' is invalid, because it contains the invalid character '{char:?}'."
115    )]
116    InvalidLabelCharacter {
117        /// The label string that exceeded the 40-byte limit.
118        label: String,
119
120        /// The invalid label character.
121        char: char,
122    },
123}
124
125/// The representation of data about to be wrapped (encrypted) with key.
126pub struct PlainWrappedDataWithKey<'a, 'b> {
127    /// Data that is being wrapped.
128    pub data: &'a [u8],
129
130    /// Wrapping key.
131    pub key: &'b [u8],
132}
133
134impl Debug for PlainWrappedDataWithKey<'_, '_> {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        f.debug_struct("PlainWrappedDataWithKey")
137            .field("data", &self.data)
138            .field("key", &"[REDACTED]")
139            .finish()
140    }
141}
142
143impl TryFrom<PlainWrappedDataWithKey<'_, '_>> for YubiHsm2Wrap {
144    type Error = Error;
145
146    /// Encrypts `value.data` using a `value.key` and returns it as a new [`YubiHsm2Wrap`].
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if encryption of `wrapped_data` with `wrapping_key` fails.
151    fn try_from(value: PlainWrappedDataWithKey<'_, '_>) -> Result<Self, Self::Error> {
152        let cipher = Aes128Ccm::new(value.key.into());
153        let mut nonce = [0; 13];
154        let mut rng = aes::cipher::crypto_common::rand_core::OsRng;
155        rng.fill_bytes(&mut nonce);
156        let mut wrapped = cipher.encrypt(Nonce::from_slice(&nonce), value.data)?;
157        wrapped.splice(0..0, nonce);
158
159        Ok(Self { wrapped })
160    }
161}
162
163type Aes128Ccm = Ccm<Aes128, U16, U13>;
164
165/// The representation of wrapped (encrypted) data of a YubiHSM2.
166#[derive(Debug)]
167pub struct YubiHsm2Wrap {
168    wrapped: Vec<u8>,
169}
170
171impl YubiHsm2Wrap {
172    /// Creates a new [`YubiHsm2Wrap`] from raw binary bytes.
173    pub fn new(wrapped: Vec<u8>) -> Self {
174        Self { wrapped }
175    }
176
177    /// Creates a new [`YubiHsm2Wrap`] from bytes containing the proprietary Yubico YHW format.
178    ///
179    /// # Note
180    ///
181    /// Leading and trailing whitespace are stripped.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if `wrapped` cannot be decoded from base64.
186    pub fn from_yhw(wrapped: &str) -> Result<Self, Error> {
187        let wrapped = wrapped.trim_ascii();
188        let wrapped = Base64::decode_vec(wrapped)?;
189        Ok(Self { wrapped })
190    }
191
192    /// Creates a [`String`] containing the representation of [`Self`] in the proprietary Yubico YHW
193    /// format.
194    pub fn to_yhw(&self) -> String {
195        Base64::encode_string(&self.wrapped)
196    }
197
198    /// Decrypts the [`YubiHsm2Wrap`] using the provided `wrapping_key`.
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if decrypting the data using `wrapping_key` fails.
203    pub fn decrypt(&self, wrapping_key: &[u8]) -> Result<Vec<u8>, Error> {
204        let cipher = Aes128Ccm::new(wrapping_key.into());
205        let (nonce, ciphertext) = self.wrapped.split_at(U13::to_usize());
206        let plaintext = cipher.decrypt(nonce.into(), ciphertext)?;
207
208        Ok(plaintext)
209    }
210}
211
212impl AsRef<[u8]> for YubiHsm2Wrap {
213    fn as_ref(&self) -> &[u8] {
214        &self.wrapped
215    }
216}
217
218/// The supported algorithms available for wrapping (encryption) of data.
219///
220/// See <https://github.com/Yubico/yubihsm-shell/blob/5a0447b9786d0e6149b67529789bd67530b38d6b/lib/yubihsm.h#L488-L515>.
221#[derive(Clone, Copy, Debug, Eq, FromPrimitive, IntoPrimitive, Ord, PartialEq, PartialOrd)]
222#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
223#[repr(u8)]
224pub enum WrapAlgorithm {
225    /// CCM using AES-128 keys.
226    Aes128Ccm = 29,
227
228    /// CCM using AES-192 keys.
229    Aes192Ccm = 41,
230
231    /// CCM using AES-256 keys.
232    Aes256Ccm = 42,
233
234    /// Unknown wrap algorithm.
235    #[num_enum(catch_all)]
236    Unknown(u8),
237}
238
239/// The object type contained in the backup.
240///
241/// All variants that are known (that is, all with the exception of [`ObjectType::Unknown`]) are
242/// supported.
243#[derive(Clone, Copy, Debug, Eq, FromPrimitive, IntoPrimitive, PartialEq)]
244#[repr(u8)]
245pub enum ObjectType {
246    /// Ed25519.
247    ///
248    /// See <https://github.com/Yubico/yubihsm-shell/blob/5a0447b9786d0e6149b67529789bd67530b38d6b/lib/yubihsm.h#L520>.
249    Ed25519 = 46,
250
251    /// AES-128 used for authentication keys.
252    ///
253    /// See <https://github.com/Yubico/yubihsm-shell/blob/5a0447b9786d0e6149b67529789bd67530b38d6b/lib/yubihsm.h#L507C3-L507C45>.
254    Aes128Auth = 38,
255
256    /// Raw byte data.
257    ///
258    /// See <https://github.com/Yubico/yubihsm-shell/blob/5a0447b9786d0e6149b67529789bd67530b38d6b/lib/yubihsm.h#L491>.
259    Opaque = 30,
260
261    /// Unknown object type.
262    #[num_enum(catch_all)]
263    Unknown(u8),
264}
265
266/// Expanded form of an ed25519 private key without seed.
267#[derive(Clone, Debug, Eq, PartialEq)]
268pub struct ExpandedEd25519KeyData<'a> {
269    /// Private scalar.
270    pub private_scalar: &'a [u8; 32],
271
272    /// Private hash prefix.
273    pub private_hash_prefix: &'a [u8; 32],
274
275    /// Public key.
276    pub public: &'a [u8; 32],
277}
278
279impl ExpandedEd25519KeyData<'_> {
280    /// The number of bytes tracked in an [`ExpandedEd25519KeyData`].
281    pub const LEN: usize = 32 * 3;
282}
283
284impl<'a> From<ExpandedEd25519KeyData<'a>> for ExpandedSecretKey {
285    fn from(value: ExpandedEd25519KeyData<'a>) -> Self {
286        let mut private_scalar = *value.private_scalar;
287        private_scalar.reverse();
288        ExpandedSecretKey {
289            scalar: Scalar::from_bytes_mod_order(private_scalar),
290            hash_prefix: *value.private_hash_prefix,
291        }
292    }
293}
294
295impl<'a> TryFrom<&'a [u8]> for ExpandedEd25519KeyData<'a> {
296    type Error = TryFromSliceError;
297    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
298        Ok(Self {
299            private_scalar: value[0..32].try_into()?,
300            private_hash_prefix: value[32..64].try_into()?,
301            public: value[64..].try_into()?,
302        })
303    }
304}
305
306/// The private parts of an ed25519 key.
307///
308/// # Note
309///
310/// The data includes the private key seed.
311#[derive(Clone, Debug, Eq, PartialEq)]
312pub struct SeedEd25519KeyData<'a> {
313    /// Private scalar.
314    pub private_scalar: &'a [u8; 32],
315
316    /// Private hash prefix.
317    pub private_hash_prefix: &'a [u8; 32],
318
319    /// Public key.
320    pub public: &'a [u8; 32],
321
322    /// Private key seed.
323    pub private_seed: &'a [u8; 32],
324}
325
326impl SeedEd25519KeyData<'_> {
327    /// The number of bytes tracked in a [`SeedEd25519KeyData`].
328    pub const LEN: usize = 32 * 4;
329}
330
331impl<'a> TryFrom<&'a [u8]> for SeedEd25519KeyData<'a> {
332    type Error = TryFromSliceError;
333    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
334        Ok(Self {
335            private_seed: value[0..32].try_into()?,
336            private_scalar: value[32..64].try_into()?,
337            private_hash_prefix: value[64..96].try_into()?,
338            public: value[96..].try_into()?,
339        })
340    }
341}
342
343impl<'a> From<SeedEd25519KeyData<'a>> for ExpandedSecretKey {
344    fn from(value: SeedEd25519KeyData<'a>) -> Self {
345        let mut private_scalar = *value.private_scalar;
346        private_scalar.reverse();
347
348        // NOTE: `ExpandedSecretKey::from_slice` unnecessarily clamps the scalar
349        ExpandedSecretKey {
350            scalar: Scalar::from_bytes_mod_order(private_scalar),
351            hash_prefix: *value.private_hash_prefix,
352        }
353    }
354}
355
356impl<'a> From<&'a SeedEd25519KeyData<'a>> for SigningKey {
357    fn from(value: &'a SeedEd25519KeyData<'a>) -> Self {
358        SigningKey::from(value.private_seed)
359    }
360}
361
362/// An Ed25519 key serialized in YubiHSM2 specific format.
363///
364/// The serialized form, as accepted by the YubiHSM2, consists of four 32-byte values:
365/// - secret key seed, from with the scalar and hash-prefix are derived,
366/// - scalar value, used directly for signing,
367/// - hash prefix, which is a domain separator used when hashing the message to generate the
368///   pseudorandom `r` value,
369/// - public key, used for verifying signed data.
370#[derive(Debug)]
371pub struct SerializedEd25519([u8; 32 * 4]);
372
373impl AsRef<[u8]> for SerializedEd25519 {
374    fn as_ref(&self) -> &[u8] {
375        &self.0
376    }
377}
378
379impl From<&SigningKey> for SerializedEd25519 {
380    fn from(value: &SigningKey) -> Self {
381        let mut result = [0; _];
382        let expanded = ExpandedSecretKey::from(&value.to_bytes());
383        result[0..32].copy_from_slice(value.as_bytes());
384        result[32..64].copy_from_slice(expanded.scalar.as_bytes());
385        result[32..64].reverse();
386        result[64..96].copy_from_slice(&expanded.hash_prefix);
387        result[96..].copy_from_slice(value.verifying_key().as_bytes());
388        Self(result)
389    }
390}
391
392/// An AES-128 based authentication key.
393#[derive(Clone, Debug, Eq, PartialEq)]
394pub struct AuthAes128<'a> {
395    /// Delegated capabilities of the key.
396    pub delegated_capabilities: &'a [u8; 8],
397
398    /// Pair of symmetric keys used for encryption and MAC.
399    pub symmetric_keys: &'a [u8; 32],
400}
401
402impl AuthAes128<'_> {
403    /// The number of bytes tracked in an [`AuthAes128`].
404    const LEN: usize = 8 + 32;
405}
406
407/// The deserialized body of a wrapped object.
408///
409/// This usually is the private key material for a signing or authentication object.
410/// However, it can also represent [raw binary data][WrappedPayload::Opaque], which may have no
411/// specific purpose in the context of the cryptographic functionalities of the YubiHSM2 hardware.
412#[derive(Clone, Debug, Eq, PartialEq)]
413pub enum WrappedPayload<'a> {
414    /// Ed25519 private key parts without the private key seed.
415    ExpandedEd25519(ExpandedEd25519KeyData<'a>),
416
417    /// Ed25519 private key parts with the private key seed.
418    SeedEd25519(SeedEd25519KeyData<'a>),
419
420    /// AES-128-based authentication key.
421    AuthAes128(AuthAes128<'a>),
422
423    /// Raw binary data.
424    Opaque(&'a [u8]),
425}
426
427impl<'a> WrappedPayload<'a> {
428    /// Parses raw bytes of specified object type into a [`WrappedPayload`] structure.
429    ///
430    /// Depending on the [`ObjectType`] the expected shape of `bytes` differs:
431    /// - for ed25519 keys two forms are accepted: expanded (exactly 96 bytes) and seeded (128
432    ///   bytes)
433    /// - for AES-128 authentication keys, `bytes` need to be exactly 40 bytes long (8 bytes for
434    ///   delecated capabilities and 32 for a pair of AES-128 keys)
435    /// - opaque does not make any restrictions and will accept any `bytes`
436    ///
437    /// # Errors
438    ///
439    /// Returns an [`Error`] if:
440    /// - private key material length is incorrect
441    fn parse(object_type: ObjectType, bytes: &'a [u8]) -> Result<WrappedPayload<'a>, Error> {
442        Ok(match object_type {
443            ObjectType::Ed25519 => match bytes.len() {
444                ExpandedEd25519KeyData::LEN => Self::ExpandedEd25519(bytes.try_into()?),
445                SeedEd25519KeyData::LEN => Self::SeedEd25519(bytes.try_into()?),
446                len => return Err(Error::UnexpectedEd25519SerializedLength { actual: len }),
447            },
448            ObjectType::Aes128Auth => {
449                let (delegated_capabilities, symmetric_keys) = bytes.split_at(8);
450                Self::AuthAes128(AuthAes128 {
451                    delegated_capabilities: delegated_capabilities.try_into()?,
452                    symmetric_keys: symmetric_keys.try_into()?,
453                })
454            }
455            ObjectType::Opaque => Self::Opaque(bytes),
456            object_type => return Err(Error::UnknownObjectType(object_type)),
457        })
458    }
459
460    /// Serializes itself into the provided buffer.
461    fn serialize_into(&self, buffer: &mut Vec<u8>) {
462        match self {
463            WrappedPayload::ExpandedEd25519(key_data) => {
464                buffer.extend_from_slice(key_data.private_scalar);
465                buffer.extend_from_slice(key_data.private_hash_prefix);
466                buffer.extend_from_slice(key_data.public);
467            }
468            WrappedPayload::SeedEd25519(key_data) => {
469                buffer.extend_from_slice(key_data.private_seed);
470                buffer.extend_from_slice(key_data.private_scalar);
471                buffer.extend_from_slice(key_data.private_hash_prefix);
472                buffer.extend_from_slice(key_data.public);
473            }
474            WrappedPayload::AuthAes128(key_data) => {
475                buffer.extend_from_slice(key_data.delegated_capabilities);
476                buffer.extend_from_slice(key_data.symmetric_keys);
477            }
478            WrappedPayload::Opaque(key_data) => buffer.extend_from_slice(key_data),
479        }
480    }
481
482    /// Returns the expected length of the serialized form.
483    fn len(&self) -> usize {
484        match self {
485            WrappedPayload::ExpandedEd25519(_) => ExpandedEd25519KeyData::LEN,
486            WrappedPayload::SeedEd25519(_) => SeedEd25519KeyData::LEN,
487            WrappedPayload::AuthAes128(_) => AuthAes128::LEN,
488            WrappedPayload::Opaque(key_data) => key_data.len(),
489        }
490    }
491}
492
493/// Reader of big-endian encoded bytes.
494struct BeReader<'a> {
495    pos: usize,
496    buf: &'a [u8],
497}
498
499impl<'a> BeReader<'a> {
500    /// Constructs a new reader backed by the specified buffer.
501    fn new(buf: &'a [u8]) -> Self {
502        Self { buf, pos: 0 }
503    }
504
505    /// Returns the current position of this reader.
506    fn position(&self) -> usize {
507        self.pos
508    }
509
510    /// Reads one byte and forwards the reader's position.
511    ///
512    /// # Errors
513    ///
514    /// Returns an [error][Error::InsufficientDataInBuffer] if there are no more bytes to read.
515    fn read_u8(&mut self) -> Result<u8, Error> {
516        if self.pos + 1 >= self.buf.len() {
517            return Err(Error::InsufficientDataInBuffer);
518        }
519        let byte = self.buf[self.pos];
520        self.pos += 1;
521        Ok(byte)
522    }
523
524    /// Reads a [`u16`] and forwards the reader's position.
525    ///
526    /// # Errors
527    ///
528    /// Returns an [error][Error::InsufficientDataInBuffer] if there are insufficient bytes in the
529    /// buffer.
530    fn read_u16(&mut self) -> Result<u16, Error> {
531        Ok(u16::from_be_bytes([self.read_u8()?, self.read_u8()?]))
532    }
533
534    /// Reads a constant-size array and forwards the reader's position.
535    ///
536    /// # Errors
537    ///
538    /// Returns an [error][Error::InsufficientDataInBuffer] if there are insufficient bytes in the
539    /// buffer.
540    fn read<const N: usize>(&mut self) -> Result<&'a [u8; N], Error> {
541        if self.pos + N >= self.buf.len() {
542            return Err(Error::InsufficientDataInBuffer);
543        }
544        let bytes = &self.buf[self.pos..self.pos + N];
545        self.pos += N;
546        bytes
547            .try_into()
548            .map_err(|_| Error::InsufficientDataInBuffer)
549    }
550
551    /// Reads a constant-size array and forwards the reader's position.
552    ///
553    /// # Errors
554    ///
555    /// Returns an [error][Error::InsufficientDataInBuffer] if the reader has already been fully
556    /// read.
557    fn read_to_end(&mut self) -> Result<&'a [u8], Error> {
558        if self.pos > self.buf.len() {
559            return Err(Error::InsufficientDataInBuffer);
560        }
561        let bytes = &self.buf[self.pos..];
562        self.pos = self.buf.len() + 1;
563        Ok(bytes)
564    }
565}
566
567/// 40-bytes long textual description of the object.
568///
569/// # Examples
570///
571/// Converting a string to a [`Label`]:
572///
573/// ```
574/// # fn main() -> testresult::TestResult {
575/// use signstar_yubihsm2::backup::Label;
576///
577/// let label: Label = "test".parse()?;
578///
579/// assert_eq!(label.to_string(), "test");
580/// # Ok(()) }
581/// ```
582#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
583#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
584#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
585pub struct Label([u8; 40]);
586
587impl FromStr for Label {
588    type Err = Error;
589
590    /// Creates a new [`Label`] from a string slice.
591    ///
592    /// The text must be no longer than 40 bytes and may be empty.
593    ///
594    /// # Examples
595    ///
596    /// Converting a string to [`Label`]:
597    ///
598    /// ```
599    /// # fn main() -> testresult::TestResult {
600    /// use signstar_yubihsm2::backup::{Error, Label};
601    ///
602    /// let label: Label = "test".parse()?;
603    ///
604    /// assert_eq!(label.to_string(), "test");
605    ///
606    /// // When the string is too long [`Error::LabelLength`] is returned:
607    ///
608    /// assert!(matches!(
609    ///     "a".repeat(50).parse::<Label>(),
610    ///     Err(Error::LabelLength { .. })
611    /// ));
612    /// # Ok(()) }
613    /// ```
614    ///
615    /// # Errors
616    ///
617    /// Returns an error if the string is longer than 40 bytes.
618    fn from_str(s: &str) -> Result<Self, Self::Err> {
619        if s.len() > 40 {
620            return Err(Error::LabelLength { label: s.into() });
621        }
622        if s.contains('\0') {
623            return Err(Error::InvalidLabelCharacter {
624                label: s.to_string(),
625                char: '\0',
626            });
627        }
628        let mut buf = [0; 40];
629        buf[..s.len()].copy_from_slice(s.as_bytes());
630        Ok(Self(buf))
631    }
632}
633
634impl From<&[u8; 40]> for Label {
635    /// Creates a new [`Label`] from a slice of 40 bytes.
636    ///
637    /// # Examples
638    ///
639    /// ```
640    /// # fn main() -> testresult::TestResult {
641    /// use signstar_yubihsm2::backup::Label;
642    ///
643    /// let label = Label::from(&[0; 40]);
644    ///
645    /// assert_eq!(label.to_string(), "");
646    /// # Ok(()) }
647    /// ```
648    fn from(value: &[u8; 40]) -> Self {
649        let mut buf = [0; 40];
650        buf.copy_from_slice(value);
651        Self(buf)
652    }
653}
654
655impl From<YubiHsmObjectLabel> for Label {
656    fn from(value: YubiHsmObjectLabel) -> Self {
657        Label::from(&value.0)
658    }
659}
660
661// NOTE: This is only relevant for serde.
662impl TryFrom<String> for Label {
663    type Error = Error;
664
665    fn try_from(value: String) -> Result<Self, Self::Error> {
666        Self::from_str(&value)
667    }
668}
669
670// NOTE: This is only relevant for serde.
671impl From<Label> for String {
672    /// Creates a new [`String`] from a [`Label`].
673    fn from(value: Label) -> Self {
674        format!("{value}")
675    }
676}
677
678impl AsRef<[u8; 40]> for Label {
679    /// Returns a reference to the underlying buffer.
680    ///
681    /// # Examples
682    ///
683    /// ```
684    /// # fn main() -> testresult::TestResult {
685    /// use signstar_yubihsm2::backup::Label;
686    ///
687    /// let label: Label = "test".parse()?;
688    ///
689    /// assert_eq!(label.as_ref().len(), 40);
690    /// # Ok(()) }
691    /// ```
692    fn as_ref(&self) -> &[u8; 40] {
693        &self.0
694    }
695}
696
697impl Display for Label {
698    /// Converts the label to a string and writes it to a given formatter.
699    ///
700    /// Note that if the underlying buffer does not contain valid UTF-8 data, the conversion is
701    /// lossy.
702    ///
703    /// # Examples
704    ///
705    /// ```
706    /// # fn main() -> testresult::TestResult {
707    /// use std::fmt::Write;
708    ///
709    /// use signstar_yubihsm2::backup::Label;
710    ///
711    /// let label: Label = "test".parse()?;
712    ///
713    /// let mut str = String::new();
714    /// write!(str, "{label}")?;
715    ///
716    /// assert_eq!(str, "test");
717    /// # Ok(()) }
718    /// ```
719    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
720        let len = self.0.iter().position(|&b| b == 0).unwrap_or(self.0.len());
721        let label = String::from_utf8_lossy(&self.0[..len]);
722        write!(f, "{label}")
723    }
724}
725
726impl From<&Label> for YubiHsmObjectLabel {
727    fn from(value: &Label) -> Self {
728        Self(*value.as_ref())
729    }
730}
731
732/// Parsed representation of the backup's inner format.
733#[derive(Debug)]
734pub struct InnerFormat<'a> {
735    /// Algorithm used for creating this wrap.
736    pub wrap_algorithm: WrapAlgorithm,
737
738    /// Capabilities of the wrapped object.
739    pub capabilities: Capabilities,
740
741    /// Identifier of the wrapped object.
742    pub object_id: ObjectId,
743
744    /// Domains of the wrapped object.
745    pub domains: Domains,
746
747    /// Type of the object.
748    pub object_type: ObjectType,
749
750    /// Sequence number, which is an internal number and is always `0`.
751    pub sequence: u8,
752
753    /// Key origin.
754    pub origin: u8,
755
756    /// Key label.
757    pub label: Label,
758
759    /// Payload of the key.
760    pub key_data: WrappedPayload<'a>,
761}
762
763impl<'a> InnerFormat<'a> {
764    /// Parses the inner format from `raw`.
765    ///
766    /// # Errors
767    ///
768    /// Returns an error if
769    /// - the buffer does not contain enough bytes for parsing
770    /// - the data in the buffer is inconsistent
771    /// - parsing private key material fails
772    pub fn parse(raw: &'a [u8]) -> Result<Self, crate::Error> {
773        let mut reader = BeReader::new(raw);
774
775        let wrap_algorithm = WrapAlgorithm::from(reader.read_u8()?);
776        let capabilities = Capabilities::from(*reader.read::<8>()?);
777        let id = reader.read_u16()?;
778        let datalen = reader.read_u16()?;
779        let domains = reader.read_u16()?.into();
780        let object_id = ObjectId::from(Handle::new(
781            id,
782            Type::from_u8(reader.read_u8()?).map_err(Error::YubiHsmObject)?,
783        ));
784        let object_type = ObjectType::from(reader.read_u8()?);
785        let sequence = reader.read_u8()?;
786        let origin = reader.read_u8()?;
787
788        let label = reader.read::<40>()?.into();
789
790        // check if the datalen is consistent with the buffer's length
791        if reader.position() + datalen as usize != raw.len() {
792            Err(Error::InsufficientDataInBuffer)?;
793        }
794
795        Ok(Self {
796            wrap_algorithm,
797            capabilities,
798            object_id,
799            domains,
800            object_type,
801            sequence,
802            origin,
803            label,
804            key_data: WrappedPayload::parse(object_type, reader.read_to_end()?)?,
805        })
806    }
807
808    /// Serializes this format into a list of bytes.
809    pub fn serialize_into(&self, buffer: &mut Vec<u8>) {
810        buffer.push(self.wrap_algorithm.into());
811        buffer.extend_from_slice(&<[u8; 8]>::from(&self.capabilities));
812        buffer.extend_from_slice(&self.object_id.id().to_be_bytes());
813        buffer.extend_from_slice(&(self.key_data.len() as u16).to_be_bytes());
814        buffer.extend_from_slice(&self.domains.to_be_bytes());
815        buffer.push(self.object_id.object_type().to_u8());
816        buffer.push(self.object_type.into());
817        buffer.push(self.sequence);
818        buffer.push(self.origin);
819        buffer.extend_from_slice(self.label.as_ref());
820        self.key_data.serialize_into(buffer);
821    }
822}
823
824/// Wraps an ed25519 private key file using a wrapping key and returns it in YHW format.
825///
826/// # Errors
827///
828/// Returns an error if
829/// - reading the key file fails
830/// - reading the wrapping key file fails
831/// - encryption of the backup fails
832/// - the inner format structure is incorrect
833pub fn wrap_ed25519(
834    private_key_file: impl AsRef<Path>,
835    wrapping_key: impl AsRef<Path>,
836    object_id: Id,
837    domains: Domains,
838    capabilities: Capabilities,
839    label: Label,
840) -> Result<String, crate::Error> {
841    let wrapping_key = read(&wrapping_key).map_err(|source| crate::Error::IoPath {
842        path: wrapping_key.as_ref().into(),
843        context: "reading wrapping key file",
844        source,
845    })?;
846    let key = SerializedEd25519::from(&SigningKey::from_bytes(
847        &read(&private_key_file)
848            .map_err(|source| crate::Error::IoPath {
849                path: private_key_file.as_ref().into(),
850                context: "reading an ed25519 private key file",
851                source,
852            })?
853            .try_into()
854            .map_err(|_| crate::Error::IncorrectDataLength {
855                context: "reading an ed25519 key file",
856            })?,
857    ));
858    let inner = InnerFormat {
859        wrap_algorithm: WrapAlgorithm::Aes128Ccm,
860        capabilities,
861        object_id: ObjectId::AsymmetricKey(object_id),
862        domains,
863        object_type: ObjectType::Ed25519,
864        sequence: 0,
865        origin: 1,
866        label,
867        key_data: WrappedPayload::SeedEd25519(key.as_ref().try_into().map_err(|_| {
868            crate::Error::IncorrectDataLength {
869                context: "converting key formats",
870            }
871        })?),
872    };
873    let buffer = {
874        let mut buffer = vec![];
875        inner.serialize_into(&mut buffer);
876        buffer
877    };
878    let data_with_key = PlainWrappedDataWithKey {
879        data: &buffer,
880        key: &wrapping_key,
881    };
882    Ok(YubiHsm2Wrap::try_from(data_with_key)?.to_yhw())
883}
884
885#[cfg(test)]
886mod tests {
887
888    use std::{assert_matches, fs::write};
889
890    use ed25519_dalek::VerifyingKey;
891    use tempfile::TempDir;
892    use testresult::TestResult;
893
894    use super::*;
895    use crate::object::{Capability, Domain};
896
897    const WRAP_KEY: &[u8] = &[
898        0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
899    ];
900
901    #[test]
902    fn decrypt_ed25519() -> TestResult {
903        let wrap = YubiHsm2Wrap::from_yhw(include_str!("../tests/backup/private-ed25519.yhw"))?;
904        let decrypted = wrap.decrypt(WRAP_KEY)?;
905        assert!(!decrypted.is_empty());
906        let inner = InnerFormat::parse(&decrypted)?;
907        let mut buffer = vec![];
908        inner.serialize_into(&mut buffer);
909        assert_eq!(buffer, decrypted);
910        assert_eq!(inner.object_type, ObjectType::Ed25519);
911        assert_eq!(inner.wrap_algorithm, WrapAlgorithm::Aes128Ccm);
912        assert_eq!(inner.object_id.id(), 0x1f_u16);
913        assert_eq!(inner.domains, Domain::One.into());
914        assert_eq!(inner.sequence, 0);
915        assert_eq!(inner.origin, 2);
916        assert_eq!(inner.label.to_string(), "Ed25519_Key");
917        let WrappedPayload::ExpandedEd25519(key_data) = inner.key_data else {
918            panic!("Expected Ed25519 key data");
919        };
920        let ExpandedEd25519KeyData {
921            private_scalar,
922            private_hash_prefix,
923            public,
924        } = key_data;
925
926        assert_eq!(
927            private_scalar,
928            &[
929                117, 188, 78, 175, 249, 221, 207, 75, 177, 26, 92, 146, 43, 19, 156, 7, 87, 43,
930                173, 199, 232, 63, 249, 230, 100, 131, 86, 147, 80, 229, 193, 192
931            ]
932        );
933        assert_eq!(
934            private_hash_prefix,
935            &[
936                182, 113, 137, 6, 206, 62, 108, 30, 26, 138, 65, 215, 178, 10, 9, 215, 181, 55,
937                132, 37, 162, 172, 202, 169, 56, 150, 245, 195, 212, 232, 235, 183
938            ]
939        );
940        assert_eq!(
941            public,
942            &[
943                185, 235, 254, 46, 190, 171, 17, 45, 56, 27, 211, 240, 69, 46, 39, 226, 53, 109,
944                50, 78, 181, 96, 30, 177, 162, 240, 122, 187, 82, 30, 156, 242
945            ]
946        );
947        let signing_key: ExpandedSecretKey = key_data.into();
948        let verifying_key = VerifyingKey::from(&signing_key);
949        assert_eq!(public, &verifying_key.to_bytes());
950        Ok(())
951    }
952
953    #[test]
954    fn decrypt_ed25519_with_seed() -> TestResult {
955        let wrap =
956            YubiHsm2Wrap::from_yhw(include_str!("../tests/backup/private-ed25519-seed.yhw"))?;
957        let decrypted = wrap.decrypt(WRAP_KEY)?;
958        assert!(!decrypted.is_empty());
959        let inner = InnerFormat::parse(&decrypted)?;
960        let mut buffer = vec![];
961        inner.serialize_into(&mut buffer);
962        assert_eq!(buffer, decrypted);
963        assert_eq!(inner.object_type, ObjectType::Ed25519);
964        assert_eq!(inner.wrap_algorithm, WrapAlgorithm::Aes128Ccm);
965        assert_eq!(inner.object_id.id(), 13);
966        assert_eq!(inner.domains, Domains::all());
967        assert_eq!(inner.sequence, 0);
968        assert_eq!(inner.origin, 1);
969        assert_eq!(inner.label.to_string(), "Signature_Key_Ed_2");
970        let WrappedPayload::SeedEd25519(key_data) = inner.key_data.clone() else {
971            panic!("Expected Ed25519 key data");
972        };
973
974        let SeedEd25519KeyData {
975            private_scalar,
976            private_hash_prefix,
977            public,
978            private_seed,
979        } = key_data;
980
981        assert_eq!(
982            private_seed,
983            &[
984                73, 122, 141, 156, 79, 125, 147, 201, 97, 207, 112, 15, 133, 155, 17, 216, 4, 254,
985                88, 71, 207, 139, 63, 170, 229, 246, 54, 32, 206, 12, 84, 86
986            ]
987        );
988        assert_eq!(
989            private_scalar,
990            &[
991                7, 81, 112, 122, 75, 85, 173, 6, 20, 181, 199, 29, 147, 191, 157, 102, 147, 157,
992                133, 249, 149, 223, 14, 41, 17, 51, 179, 38, 146, 102, 210, 15
993            ]
994        );
995        assert_eq!(
996            private_hash_prefix,
997            &[
998                161, 55, 166, 21, 136, 215, 184, 182, 181, 62, 143, 223, 62, 159, 19, 228, 179, 87,
999                101, 158, 129, 137, 207, 186, 191, 206, 220, 148, 44, 83, 203, 115
1000            ]
1001        );
1002        assert_eq!(
1003            public,
1004            &[
1005                252, 157, 136, 36, 18, 36, 60, 188, 181, 153, 78, 169, 136, 74, 14, 210, 150, 203,
1006                47, 42, 79, 2, 238, 0, 103, 237, 202, 100, 87, 40, 252, 44
1007            ]
1008        );
1009        let signing_key = SigningKey::from(&key_data);
1010        let serialized = SerializedEd25519::from(&signing_key);
1011        assert_eq!(
1012            inner.key_data,
1013            WrappedPayload::parse(ObjectType::Ed25519, serialized.as_ref())?
1014        );
1015
1016        assert_eq!(public, &signing_key.verifying_key().to_bytes());
1017        let exp = ExpandedSecretKey::from(private_seed);
1018
1019        let mut private_scalar = *private_scalar;
1020        private_scalar.reverse();
1021
1022        assert_eq!(exp.scalar.as_bytes(), &private_scalar);
1023        assert_eq!(&exp.hash_prefix, private_hash_prefix);
1024
1025        let signing_key: ExpandedSecretKey = key_data.into();
1026        assert_eq!(exp.scalar, signing_key.scalar);
1027        assert_eq!(exp.hash_prefix, signing_key.hash_prefix);
1028
1029        let verifying_key = VerifyingKey::from(&signing_key);
1030        assert_eq!(public, &verifying_key.to_bytes());
1031        Ok(())
1032    }
1033
1034    #[test]
1035    fn auth_key() -> TestResult {
1036        let wrap = YubiHsm2Wrap::from_yhw(include_str!("../tests/backup/auth.yhw"))?;
1037        let decrypted = wrap.decrypt(WRAP_KEY)?;
1038        assert!(!decrypted.is_empty());
1039        let inner = InnerFormat::parse(&decrypted)?;
1040        let mut buffer = vec![];
1041        inner.serialize_into(&mut buffer);
1042        assert_eq!(decrypted, buffer);
1043        assert_eq!(inner.object_type, ObjectType::Aes128Auth);
1044        assert_eq!(
1045            inner.capabilities,
1046            Capabilities::from(&[Capability::ExportableUnderWrap][..])
1047        );
1048        assert_eq!(inner.domains, Domain::One.into());
1049        assert_eq!(inner.object_id.id(), 14);
1050        assert_eq!(
1051            inner.key_data,
1052            WrappedPayload::AuthAes128(AuthAes128 {
1053                delegated_capabilities: &[0; 8],
1054                symmetric_keys: &[
1055                    152, 123, 73, 154, 181, 120, 84, 139, 48, 32, 41, 176, 213, 53, 39, 232, 122,
1056                    150, 131, 153, 10, 233, 98, 202, 67, 12, 27, 245, 184, 198, 41, 93
1057                ]
1058            })
1059        );
1060        assert_eq!(inner.object_id.object_type(), Type::AuthenticationKey);
1061        assert_eq!(inner.label.to_string(), "");
1062        assert_eq!(inner.origin, 2);
1063        assert_eq!(inner.sequence, 0);
1064        Ok(())
1065    }
1066
1067    #[test]
1068    fn opaque_data() -> TestResult {
1069        let wrap = YubiHsm2Wrap::from_yhw(include_str!("../tests/backup/opaque.yhw"))?;
1070        let decrypted = wrap.decrypt(WRAP_KEY)?;
1071        assert!(!decrypted.is_empty());
1072        let inner = InnerFormat::parse(&decrypted)?;
1073        let mut buffer = vec![];
1074        inner.serialize_into(&mut buffer);
1075        assert_eq!(decrypted, buffer);
1076        assert_eq!(inner.object_type, ObjectType::Opaque);
1077        assert_eq!(
1078            inner.capabilities,
1079            Capabilities::from(&[Capability::ExportableUnderWrap][..])
1080        );
1081        assert_eq!(inner.domains, Domain::One.into());
1082        assert_eq!(inner.object_id.id(), 13);
1083        assert_eq!(inner.key_data, WrappedPayload::Opaque(&[1, 2, 3]));
1084        assert_eq!(inner.object_id.object_type(), Type::Opaque);
1085        assert_eq!(inner.label.to_string(), "random");
1086        assert_eq!(inner.origin, 2);
1087        assert_eq!(inner.sequence, 0);
1088        Ok(())
1089    }
1090
1091    #[test]
1092    fn roundtrip_yhw() -> TestResult {
1093        let input = include_str!("../tests/backup/private-ed25519-seed.yhw");
1094        let wrap = YubiHsm2Wrap::from_yhw(input)?;
1095        assert_eq!(input, wrap.to_yhw());
1096        Ok(())
1097    }
1098
1099    #[test]
1100    fn encrypt_decrypt() -> TestResult {
1101        let input = include_str!("../tests/backup/opaque.yhw");
1102        let wrap = YubiHsm2Wrap::from_yhw(input)?;
1103        let decrypted_original = wrap.decrypt(WRAP_KEY)?;
1104        let plain = PlainWrappedDataWithKey {
1105            data: &decrypted_original,
1106            key: WRAP_KEY,
1107        };
1108        let wrap: YubiHsm2Wrap = plain.try_into()?;
1109        let decrypted_from_plain = wrap.decrypt(WRAP_KEY)?;
1110        assert_eq!(decrypted_original, decrypted_from_plain);
1111        Ok(())
1112    }
1113
1114    #[test]
1115    fn roundtrip_wrap() -> TestResult {
1116        let temp_dir = TempDir::new()?;
1117        let private_key_file = temp_dir.path().join("private.key");
1118        let wrapping_key_file = temp_dir.path().join("wrap.key");
1119        write(&private_key_file, [0; 32])?;
1120        write(&wrapping_key_file, WRAP_KEY)?;
1121
1122        let object_id = 1;
1123        let wrapped = wrap_ed25519(
1124            private_key_file,
1125            wrapping_key_file,
1126            object_id,
1127            Domains::all(),
1128            Capabilities::from(&[Capability::SignEddsa][..]),
1129            "test".parse()?,
1130        )?;
1131
1132        let yhw = YubiHsm2Wrap::from_yhw(&wrapped)?;
1133        let raw = yhw.decrypt(WRAP_KEY)?;
1134        let inner = InnerFormat::parse(&raw)?;
1135        assert_eq!(inner.object_id, ObjectId::AsymmetricKey(object_id));
1136        Ok(())
1137    }
1138
1139    /// Ensures, that [`Label::from_str`] fails on a string slice containing invalid characters
1140    /// (e.g. `\0`).
1141    #[test]
1142    fn label_from_str_fails_on_invalid_char() -> TestResult {
1143        let text = "some label\0text";
1144        assert_matches!(Label::from_str(text), Err(Error::InvalidLabelCharacter { char, .. }) if char == '\0' );
1145
1146        Ok(())
1147    }
1148
1149    /// Ensures that a (lossy) [`String`] can be created from [`Label`].
1150    #[test]
1151    fn string_from_label() -> TestResult {
1152        let text = "this is a label";
1153        let label = Label::from_str(text)?;
1154        let string_label: String = label.into();
1155
1156        assert_eq!(string_label, text);
1157        Ok(())
1158    }
1159}