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