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