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//! [backup and restore]: https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-backup-restore.html
12
13use std::{array::TryFromSliceError, fmt::Debug};
14
15use aes::{Aes128, cipher::typenum::Unsigned};
16use base64ct::{Base64, Encoding as _};
17use ccm::{
18    Ccm,
19    Nonce,
20    aead::{Aead, KeyInit, rand_core::RngCore},
21    consts::{U13, U16},
22};
23use curve25519_dalek::Scalar;
24use ed25519_dalek::{SigningKey, hazmat::ExpandedSecretKey};
25use num_enum::{FromPrimitive, IntoPrimitive};
26use yubihsm::object::{Handle, Type};
27
28use crate::object::{Domains, ObjectId};
29
30/// Backup error.
31#[derive(Debug, thiserror::Error)]
32pub enum Error {
33    /// Base64 decoding error.
34    #[error("Decoding Base64 failed: {0}")]
35    Base64Decode(#[from] base64ct::Error),
36
37    /// Decryption error.
38    #[error("Decryption error: {0}")]
39    Decrypt(#[from] ccm::Error),
40
41    /// Slice length error.
42    #[error("Incorrect slice length: {0}")]
43    SliceLength(#[from] TryFromSliceError),
44
45    /// Unexpected Ed25519 serialized form length.
46    ///
47    /// The only supported values are [ExpandedEd25519KeyData::LEN] and [SeedEd25519KeyData::LEN].
48    #[error("Unexpected Ed25519 serialized form length: {actual}")]
49    UnexpectedEd25519SerializedLength {
50        /// Length of the serialized form encountered.
51        actual: usize,
52    },
53
54    /// Unsupported object type.
55    #[error("Cannot parse data of unknown type: {0:?}")]
56    UnknownObjectType(ObjectType),
57
58    /// Object error.
59    #[error("YubiHSM2 object error: {0:?}")]
60    YubiHsmObject(#[from] yubihsm::object::Error),
61
62    /// Parsing failed because the buffer does not contain enough data.
63    #[error("Parsing buffer: not enough data.")]
64    InsufficientDataInBuffer,
65}
66
67/// The representation of data about to be wrapped (encrypted) with key.
68pub struct PlainWrappedDataWithKey<'a, 'b> {
69    /// Data that is being wrapped.
70    pub data: &'a [u8],
71
72    /// Wrapping key.
73    pub key: &'b [u8],
74}
75
76impl Debug for PlainWrappedDataWithKey<'_, '_> {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.debug_struct("PlainWrappedDataWithKey")
79            .field("data", &self.data)
80            .field("key", &"[REDACTED]")
81            .finish()
82    }
83}
84
85impl TryFrom<PlainWrappedDataWithKey<'_, '_>> for YubiHsm2Wrap {
86    type Error = Error;
87
88    /// Encrypts `value.data` using a `value.key` and returns it as a new [`YubiHsm2Wrap`].
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if encryption of `wrapped_data` with `wrapping_key` fails.
93    fn try_from(value: PlainWrappedDataWithKey<'_, '_>) -> Result<Self, Self::Error> {
94        let cipher = Aes128Ccm::new(value.key.into());
95        let mut nonce = [0; 13];
96        let mut rng = aes::cipher::crypto_common::rand_core::OsRng;
97        rng.fill_bytes(&mut nonce);
98        let mut wrapped = cipher.encrypt(Nonce::from_slice(&nonce), value.data)?;
99        wrapped.splice(0..0, nonce);
100
101        Ok(Self { wrapped })
102    }
103}
104
105type Aes128Ccm = Ccm<Aes128, U16, U13>;
106
107/// The representation of wrapped (encrypted) data of a YubiHSM2.
108#[derive(Debug)]
109pub struct YubiHsm2Wrap {
110    wrapped: Vec<u8>,
111}
112
113impl YubiHsm2Wrap {
114    /// Creates a new [`YubiHsm2Wrap`] from raw binary bytes.
115    pub fn new(wrapped: Vec<u8>) -> Self {
116        Self { wrapped }
117    }
118
119    /// Creates a new [`YubiHsm2Wrap`] from bytes containing the proprietary Yubico YHW format.
120    ///
121    /// # Note
122    ///
123    /// Leading and trailing whitespace are stripped.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if `wrapped` cannot be decoded from base64.
128    pub fn from_yhw(wrapped: &str) -> Result<Self, Error> {
129        let wrapped = wrapped.trim_ascii();
130        let wrapped = Base64::decode_vec(wrapped)?;
131        Ok(Self { wrapped })
132    }
133
134    /// Creates a [`String`] containing the representation of [`Self`] in the proprietary Yubico YHW
135    /// format.
136    pub fn to_yhw(&self) -> String {
137        Base64::encode_string(&self.wrapped)
138    }
139
140    /// Decrypts the [`YubiHsm2Wrap`] using the provided `wrapping_key`.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if decrypting the data using `wrapping_key` fails.
145    pub fn decrypt(&self, wrapping_key: &[u8]) -> Result<Vec<u8>, Error> {
146        let cipher = Aes128Ccm::new(wrapping_key.into());
147        let (nonce, ciphertext) = self.wrapped.split_at(U13::to_usize());
148        let plaintext = cipher.decrypt(nonce.into(), ciphertext)?;
149
150        Ok(plaintext)
151    }
152}
153
154impl AsRef<[u8]> for YubiHsm2Wrap {
155    fn as_ref(&self) -> &[u8] {
156        &self.wrapped
157    }
158}
159
160/// The supported algorithms available for wrapping (encryption) of data.
161///
162/// See <https://github.com/Yubico/yubihsm-shell/blob/5a0447b9786d0e6149b67529789bd67530b38d6b/lib/yubihsm.h#L488-L515>.
163#[derive(Clone, Copy, Debug, Eq, FromPrimitive, IntoPrimitive, PartialEq)]
164#[repr(u8)]
165pub enum WrapAlgorithm {
166    /// CCM using AES-128 keys.
167    Aes128Ccm = 29,
168
169    /// CCM using AES-192 keys.
170    Aes192Ccm = 41,
171
172    /// CCM using AES-256 keys.
173    Aes256Ccm = 42,
174
175    /// Unknown wrap algorithm.
176    #[num_enum(catch_all)]
177    Unknown(u8),
178}
179
180/// The object type contained in the backup.
181///
182/// All variants that are known (that is, all with the exception of [`ObjectType::Unknown`]) are
183/// supported.
184#[derive(Clone, Copy, Debug, Eq, FromPrimitive, IntoPrimitive, PartialEq)]
185#[repr(u8)]
186pub enum ObjectType {
187    /// Ed25519.
188    ///
189    /// See <https://github.com/Yubico/yubihsm-shell/blob/5a0447b9786d0e6149b67529789bd67530b38d6b/lib/yubihsm.h#L520>.
190    Ed25519 = 46,
191
192    /// AES-128 used for authentication keys.
193    ///
194    /// See <https://github.com/Yubico/yubihsm-shell/blob/5a0447b9786d0e6149b67529789bd67530b38d6b/lib/yubihsm.h#L507C3-L507C45>.
195    Aes128Auth = 38,
196
197    /// Raw byte data.
198    ///
199    /// See <https://github.com/Yubico/yubihsm-shell/blob/5a0447b9786d0e6149b67529789bd67530b38d6b/lib/yubihsm.h#L491>.
200    Opaque = 30,
201
202    /// Unknown object type.
203    #[num_enum(catch_all)]
204    Unknown(u8),
205}
206
207/// Expanded form of an ed25519 private key without seed.
208#[derive(Clone, Debug, Eq, PartialEq)]
209pub struct ExpandedEd25519KeyData<'a> {
210    /// Private scalar.
211    pub private_scalar: &'a [u8; 32],
212
213    /// Private hash prefix.
214    pub private_hash_prefix: &'a [u8; 32],
215
216    /// Public key.
217    pub public: &'a [u8; 32],
218}
219
220impl ExpandedEd25519KeyData<'_> {
221    /// The number of bytes tracked in an [`ExpandedEd25519KeyData`].
222    pub const LEN: usize = 32 * 3;
223}
224
225impl<'a> From<ExpandedEd25519KeyData<'a>> for ExpandedSecretKey {
226    fn from(value: ExpandedEd25519KeyData<'a>) -> Self {
227        let mut private_scalar = *value.private_scalar;
228        private_scalar.reverse();
229        ExpandedSecretKey {
230            scalar: Scalar::from_bytes_mod_order(private_scalar),
231            hash_prefix: *value.private_hash_prefix,
232        }
233    }
234}
235
236impl<'a> TryFrom<&'a [u8]> for ExpandedEd25519KeyData<'a> {
237    type Error = TryFromSliceError;
238    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
239        Ok(Self {
240            private_scalar: value[0..32].try_into()?,
241            private_hash_prefix: value[32..64].try_into()?,
242            public: value[64..].try_into()?,
243        })
244    }
245}
246
247/// The private parts of an ed25519 key.
248///
249/// # Note
250///
251/// The data includes the private key seed.
252#[derive(Clone, Debug, Eq, PartialEq)]
253pub struct SeedEd25519KeyData<'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    /// Private key seed.
264    pub private_seed: &'a [u8; 32],
265}
266
267impl SeedEd25519KeyData<'_> {
268    /// The number of bytes tracked in a [`SeedEd25519KeyData`].
269    pub const LEN: usize = 32 * 4;
270}
271
272impl<'a> TryFrom<&'a [u8]> for SeedEd25519KeyData<'a> {
273    type Error = TryFromSliceError;
274    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
275        Ok(Self {
276            private_seed: value[0..32].try_into()?,
277            private_scalar: value[32..64].try_into()?,
278            private_hash_prefix: value[64..96].try_into()?,
279            public: value[96..].try_into()?,
280        })
281    }
282}
283
284impl<'a> From<SeedEd25519KeyData<'a>> for ExpandedSecretKey {
285    fn from(value: SeedEd25519KeyData<'a>) -> Self {
286        let mut private_scalar = *value.private_scalar;
287        private_scalar.reverse();
288
289        // NOTE: `ExpandedSecretKey::from_slice` unnecessarily clamps the scalar
290        ExpandedSecretKey {
291            scalar: Scalar::from_bytes_mod_order(private_scalar),
292            hash_prefix: *value.private_hash_prefix,
293        }
294    }
295}
296
297impl<'a> From<&'a SeedEd25519KeyData<'a>> for SigningKey {
298    fn from(value: &'a SeedEd25519KeyData<'a>) -> Self {
299        SigningKey::from(value.private_seed)
300    }
301}
302
303/// An Ed25519 key serialized in YubiHSM2 specific format.
304///
305/// The serialized form, as accepted by the YubiHSM2, consists of four 32-byte values:
306/// - secret key seed, from with the scalar and hash-prefix are derived,
307/// - scalar value, used directly for signing,
308/// - hash prefix, which is a domain separator used when hashing the message to generate the
309///   pseudorandom `r` value,
310/// - public key, used for verifying signed data.
311#[derive(Debug)]
312pub struct SerializedEd25519([u8; 32 * 4]);
313
314impl AsRef<[u8]> for SerializedEd25519 {
315    fn as_ref(&self) -> &[u8] {
316        &self.0
317    }
318}
319
320impl From<&SigningKey> for SerializedEd25519 {
321    fn from(value: &SigningKey) -> Self {
322        let mut result = [0; _];
323        let expanded = ExpandedSecretKey::from(&value.to_bytes());
324        result[0..32].copy_from_slice(value.as_bytes());
325        result[32..64].copy_from_slice(expanded.scalar.as_bytes());
326        result[32..64].reverse();
327        result[64..96].copy_from_slice(&expanded.hash_prefix);
328        result[96..].copy_from_slice(value.verifying_key().as_bytes());
329        Self(result)
330    }
331}
332
333/// An AES-128 based authentication key.
334#[derive(Clone, Debug, Eq, PartialEq)]
335pub struct AuthAes128<'a> {
336    /// Delegated capabilities of the key.
337    pub delegated_capabilities: &'a [u8; 8],
338
339    /// Pair of symmetric keys used for encryption and MAC.
340    pub symmetric_keys: &'a [u8; 32],
341}
342
343impl AuthAes128<'_> {
344    /// The number of bytes tracked in an [`AuthAes128`].
345    const LEN: usize = 8 + 32;
346}
347
348/// The deserialized body of a wrapped object.
349///
350/// This usually is the private key material for a signing or authentication object.
351/// However, it can also represent [raw binary data][WrappedPayload::Opaque], which may have no
352/// specific purpose in the context of the cryptographic functionalities of the YubiHSM2 hardware.
353#[derive(Clone, Debug, Eq, PartialEq)]
354pub enum WrappedPayload<'a> {
355    /// Ed25519 private key parts without the private key seed.
356    ExpandedEd25519(ExpandedEd25519KeyData<'a>),
357
358    /// Ed25519 private key parts with the private key seed.
359    SeedEd25519(SeedEd25519KeyData<'a>),
360
361    /// AES-128-based authentication key.
362    AuthAes128(AuthAes128<'a>),
363
364    /// Raw binary data.
365    Opaque(&'a [u8]),
366}
367
368impl<'a> WrappedPayload<'a> {
369    /// Parses raw bytes of specified object type into a [`WrappedPayload`] structure.
370    ///
371    /// Depending on the [`ObjectType`] the expected shape of `bytes` differs:
372    /// - for ed25519 keys two forms are accepted: expanded (exactly 96 bytes) and seeded (128
373    ///   bytes)
374    /// - for AES-128 authentication keys, `bytes` need to be exactly 40 bytes long (8 bytes for
375    ///   delecated capabilities and 32 for a pair of AES-128 keys)
376    /// - opaque does not make any restrictions and will accept any `bytes`
377    ///
378    /// # Errors
379    ///
380    /// Returns an [`Error`] if:
381    /// - private key material length is incorrect
382    fn parse(object_type: ObjectType, bytes: &'a [u8]) -> Result<WrappedPayload<'a>, Error> {
383        Ok(match object_type {
384            ObjectType::Ed25519 => match bytes.len() {
385                ExpandedEd25519KeyData::LEN => Self::ExpandedEd25519(bytes.try_into()?),
386                SeedEd25519KeyData::LEN => Self::SeedEd25519(bytes.try_into()?),
387                len => return Err(Error::UnexpectedEd25519SerializedLength { actual: len }),
388            },
389            ObjectType::Aes128Auth => {
390                let (delegated_capabilities, symmetric_keys) = bytes.split_at(8);
391                Self::AuthAes128(AuthAes128 {
392                    delegated_capabilities: delegated_capabilities.try_into()?,
393                    symmetric_keys: symmetric_keys.try_into()?,
394                })
395            }
396            ObjectType::Opaque => Self::Opaque(bytes),
397            object_type => return Err(Error::UnknownObjectType(object_type)),
398        })
399    }
400
401    /// Serializes itself into the provided buffer.
402    fn serialize_into(&self, buffer: &mut Vec<u8>) {
403        match self {
404            WrappedPayload::ExpandedEd25519(key_data) => {
405                buffer.extend_from_slice(key_data.private_scalar);
406                buffer.extend_from_slice(key_data.private_hash_prefix);
407                buffer.extend_from_slice(key_data.public);
408            }
409            WrappedPayload::SeedEd25519(key_data) => {
410                buffer.extend_from_slice(key_data.private_seed);
411                buffer.extend_from_slice(key_data.private_scalar);
412                buffer.extend_from_slice(key_data.private_hash_prefix);
413                buffer.extend_from_slice(key_data.public);
414            }
415            WrappedPayload::AuthAes128(key_data) => {
416                buffer.extend_from_slice(key_data.delegated_capabilities);
417                buffer.extend_from_slice(key_data.symmetric_keys);
418            }
419            WrappedPayload::Opaque(key_data) => buffer.extend_from_slice(key_data),
420        }
421    }
422
423    /// Returns the expected length of the serialized form.
424    fn len(&self) -> usize {
425        match self {
426            WrappedPayload::ExpandedEd25519(_) => ExpandedEd25519KeyData::LEN,
427            WrappedPayload::SeedEd25519(_) => SeedEd25519KeyData::LEN,
428            WrappedPayload::AuthAes128(_) => AuthAes128::LEN,
429            WrappedPayload::Opaque(key_data) => key_data.len(),
430        }
431    }
432}
433
434/// Reader of big-endian encoded bytes.
435struct BeReader<'a> {
436    pos: usize,
437    buf: &'a [u8],
438}
439
440impl<'a> BeReader<'a> {
441    /// Constructs a new reader backed by the specified buffer.
442    fn new(buf: &'a [u8]) -> Self {
443        Self { buf, pos: 0 }
444    }
445
446    /// Returns the current position of this reader.
447    fn position(&self) -> usize {
448        self.pos
449    }
450
451    /// Reads one byte and forwards the reader's position.
452    ///
453    /// # Errors
454    ///
455    /// Returns an [error][Error::InsufficientDataInBuffer] if there are no more bytes to read.
456    fn read_u8(&mut self) -> Result<u8, Error> {
457        if self.pos + 1 >= self.buf.len() {
458            return Err(Error::InsufficientDataInBuffer);
459        }
460        let byte = self.buf[self.pos];
461        self.pos += 1;
462        Ok(byte)
463    }
464
465    /// Reads a [`u16`] and forwards the reader's position.
466    ///
467    /// # Errors
468    ///
469    /// Returns an [error][Error::InsufficientDataInBuffer] if there are insufficient bytes in the
470    /// buffer.
471    fn read_u16(&mut self) -> Result<u16, Error> {
472        Ok(u16::from_be_bytes([self.read_u8()?, self.read_u8()?]))
473    }
474
475    /// Reads a constant-size array and forwards the reader's position.
476    ///
477    /// # Errors
478    ///
479    /// Returns an [error][Error::InsufficientDataInBuffer] if there are insufficient bytes in the
480    /// buffer.
481    fn read<const N: usize>(&mut self) -> Result<&'a [u8; N], Error> {
482        if self.pos + N >= self.buf.len() {
483            return Err(Error::InsufficientDataInBuffer);
484        }
485        let bytes = &self.buf[self.pos..self.pos + N];
486        self.pos += N;
487        bytes
488            .try_into()
489            .map_err(|_| Error::InsufficientDataInBuffer)
490    }
491
492    /// Reads a constant-size array and forwards the reader's position.
493    ///
494    /// # Errors
495    ///
496    /// Returns an [error][Error::InsufficientDataInBuffer] if the reader has already been fully
497    /// read.
498    fn read_to_end(&mut self) -> Result<&'a [u8], Error> {
499        if self.pos > self.buf.len() {
500            return Err(Error::InsufficientDataInBuffer);
501        }
502        let bytes = &self.buf[self.pos..];
503        self.pos = self.buf.len() + 1;
504        Ok(bytes)
505    }
506}
507
508/// Parsed representation of the backup's inner format.
509#[derive(Debug)]
510pub struct InnerFormat<'a> {
511    /// Algorithm used for creating this wrap.
512    pub wrap_algorithm: WrapAlgorithm,
513
514    /// Capabilities of the wrapped object.
515    pub capabilities: &'a [u8; 8],
516
517    /// Identifier of the wrapped object.
518    pub object_id: ObjectId,
519
520    /// Domains of the wrapped object.
521    pub domains: Domains,
522
523    /// Type of the object.
524    pub object_type: ObjectType,
525
526    /// Sequence number, which is an internal number and is always `0`.
527    pub sequence: u8,
528
529    /// Key origin.
530    pub origin: u8,
531
532    /// Key label.
533    pub label: String,
534
535    /// Payload of the key.
536    pub key_data: WrappedPayload<'a>,
537}
538
539impl<'a> InnerFormat<'a> {
540    /// Parses the inner format from `raw`.
541    ///
542    /// # Errors
543    ///
544    /// Returns an error if
545    /// - the buffer does not contain enough bytes for parsing
546    /// - the data in the buffer is inconsistent
547    /// - parsing private key material fails
548    pub fn parse(raw: &'a [u8]) -> Result<Self, crate::Error> {
549        let mut reader = BeReader::new(raw);
550
551        let wrap_algorithm = WrapAlgorithm::from(reader.read_u8()?);
552        let capabilities = reader.read()?;
553        let id = reader.read_u16()?;
554        let datalen = reader.read_u16()?;
555        let domains = reader.read_u16()?.into();
556        let object_id = ObjectId::try_from(Handle::new(
557            id,
558            Type::from_u8(reader.read_u8()?).map_err(Error::YubiHsmObject)?,
559        ))?;
560        let object_type = ObjectType::from(reader.read_u8()?);
561        let sequence = reader.read_u8()?;
562        let origin = reader.read_u8()?;
563
564        let label = reader.read::<40>()?;
565        let len = label.iter().position(|&b| b == 0).unwrap_or(label.len());
566        let label = String::from_utf8_lossy(&label[..len]).into();
567
568        // check if the datalen is consistent with the buffer's length
569        if reader.position() + datalen as usize != raw.len() {
570            return Err(Error::InsufficientDataInBuffer)?;
571        }
572
573        Ok(Self {
574            wrap_algorithm,
575            capabilities,
576            object_id,
577            domains,
578            object_type,
579            sequence,
580            origin,
581            label,
582            key_data: WrappedPayload::parse(object_type, reader.read_to_end()?)?,
583        })
584    }
585
586    /// Serializes this format into a list of bytes.
587    pub fn serialize_into(&self, buffer: &mut Vec<u8>) {
588        buffer.push(self.wrap_algorithm.into());
589        buffer.extend_from_slice(self.capabilities);
590        buffer.extend_from_slice(&u16::from(self.object_id.id()).to_be_bytes());
591        buffer.extend_from_slice(&(self.key_data.len() as u16).to_be_bytes());
592        buffer.extend_from_slice(&self.domains.to_be_bytes());
593        buffer.push(self.object_id.object_type().to_u8());
594        buffer.push(self.object_type.into());
595        buffer.push(self.sequence);
596        buffer.push(self.origin);
597        let mut label: [u8; 40] = [0; 40];
598        let slice_len = self.label.len().min(label.len());
599        label[..slice_len].copy_from_slice(self.label.as_bytes());
600        buffer.extend_from_slice(&label);
601        self.key_data.serialize_into(buffer);
602    }
603}
604
605#[cfg(test)]
606mod tests {
607
608    use ed25519_dalek::VerifyingKey;
609    use testresult::TestResult;
610
611    use super::*;
612    use crate::object::Domain;
613
614    const WRAP_KEY: &[u8] = &[
615        0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
616    ];
617
618    #[test]
619    fn decrypt_ed25519() -> TestResult {
620        let wrap = YubiHsm2Wrap::from_yhw(include_str!("../tests/backup/private-ed25519.yhw"))?;
621        let decrypted = wrap.decrypt(WRAP_KEY)?;
622        assert!(!decrypted.is_empty());
623        let inner = InnerFormat::parse(&decrypted)?;
624        let mut buffer = vec![];
625        inner.serialize_into(&mut buffer);
626        assert_eq!(buffer, decrypted);
627        assert_eq!(inner.object_type, ObjectType::Ed25519);
628        assert_eq!(inner.wrap_algorithm, WrapAlgorithm::Aes128Ccm);
629        assert_eq!(u16::from(inner.object_id.id()), 0x1f_u16);
630        assert_eq!(inner.domains, Domain::One.into());
631        assert_eq!(inner.sequence, 0);
632        assert_eq!(inner.origin, 2);
633        assert_eq!(inner.label, "Ed25519_Key");
634        let WrappedPayload::ExpandedEd25519(key_data) = inner.key_data else {
635            panic!("Expected Ed25519 key data");
636        };
637        let ExpandedEd25519KeyData {
638            private_scalar,
639            private_hash_prefix,
640            public,
641        } = key_data;
642
643        assert_eq!(
644            private_scalar,
645            &[
646                117, 188, 78, 175, 249, 221, 207, 75, 177, 26, 92, 146, 43, 19, 156, 7, 87, 43,
647                173, 199, 232, 63, 249, 230, 100, 131, 86, 147, 80, 229, 193, 192
648            ]
649        );
650        assert_eq!(
651            private_hash_prefix,
652            &[
653                182, 113, 137, 6, 206, 62, 108, 30, 26, 138, 65, 215, 178, 10, 9, 215, 181, 55,
654                132, 37, 162, 172, 202, 169, 56, 150, 245, 195, 212, 232, 235, 183
655            ]
656        );
657        assert_eq!(
658            public,
659            &[
660                185, 235, 254, 46, 190, 171, 17, 45, 56, 27, 211, 240, 69, 46, 39, 226, 53, 109,
661                50, 78, 181, 96, 30, 177, 162, 240, 122, 187, 82, 30, 156, 242
662            ]
663        );
664        let signing_key: ExpandedSecretKey = key_data.into();
665        let verifying_key = VerifyingKey::from(&signing_key);
666        assert_eq!(public, &verifying_key.to_bytes());
667        Ok(())
668    }
669
670    #[test]
671    fn decrypt_ed25519_with_seed() -> TestResult {
672        let wrap =
673            YubiHsm2Wrap::from_yhw(include_str!("../tests/backup/private-ed25519-seed.yhw"))?;
674        let decrypted = wrap.decrypt(WRAP_KEY)?;
675        assert!(!decrypted.is_empty());
676        let inner = InnerFormat::parse(&decrypted)?;
677        let mut buffer = vec![];
678        inner.serialize_into(&mut buffer);
679        assert_eq!(buffer, decrypted);
680        assert_eq!(inner.object_type, ObjectType::Ed25519);
681        assert_eq!(inner.wrap_algorithm, WrapAlgorithm::Aes128Ccm);
682        assert_eq!(u16::from(inner.object_id.id()), 13);
683        assert_eq!(inner.domains, Domains::all());
684        assert_eq!(inner.sequence, 0);
685        assert_eq!(inner.origin, 1);
686        assert_eq!(inner.label, "Signature_Key_Ed_2");
687        let WrappedPayload::SeedEd25519(key_data) = inner.key_data.clone() else {
688            panic!("Expected Ed25519 key data");
689        };
690
691        let SeedEd25519KeyData {
692            private_scalar,
693            private_hash_prefix,
694            public,
695            private_seed,
696        } = key_data;
697
698        assert_eq!(
699            private_seed,
700            &[
701                73, 122, 141, 156, 79, 125, 147, 201, 97, 207, 112, 15, 133, 155, 17, 216, 4, 254,
702                88, 71, 207, 139, 63, 170, 229, 246, 54, 32, 206, 12, 84, 86
703            ]
704        );
705        assert_eq!(
706            private_scalar,
707            &[
708                7, 81, 112, 122, 75, 85, 173, 6, 20, 181, 199, 29, 147, 191, 157, 102, 147, 157,
709                133, 249, 149, 223, 14, 41, 17, 51, 179, 38, 146, 102, 210, 15
710            ]
711        );
712        assert_eq!(
713            private_hash_prefix,
714            &[
715                161, 55, 166, 21, 136, 215, 184, 182, 181, 62, 143, 223, 62, 159, 19, 228, 179, 87,
716                101, 158, 129, 137, 207, 186, 191, 206, 220, 148, 44, 83, 203, 115
717            ]
718        );
719        assert_eq!(
720            public,
721            &[
722                252, 157, 136, 36, 18, 36, 60, 188, 181, 153, 78, 169, 136, 74, 14, 210, 150, 203,
723                47, 42, 79, 2, 238, 0, 103, 237, 202, 100, 87, 40, 252, 44
724            ]
725        );
726        let signing_key = SigningKey::from(&key_data);
727        let serialized = SerializedEd25519::from(&signing_key);
728        assert_eq!(
729            inner.key_data,
730            WrappedPayload::parse(ObjectType::Ed25519, serialized.as_ref())?
731        );
732
733        assert_eq!(public, &signing_key.verifying_key().to_bytes());
734        let exp = ExpandedSecretKey::from(private_seed);
735
736        let mut private_scalar = *private_scalar;
737        private_scalar.reverse();
738
739        assert_eq!(exp.scalar.as_bytes(), &private_scalar);
740        assert_eq!(&exp.hash_prefix, private_hash_prefix);
741
742        let signing_key: ExpandedSecretKey = key_data.into();
743        assert_eq!(exp.scalar, signing_key.scalar);
744        assert_eq!(exp.hash_prefix, signing_key.hash_prefix);
745
746        let verifying_key = VerifyingKey::from(&signing_key);
747        assert_eq!(public, &verifying_key.to_bytes());
748        Ok(())
749    }
750
751    #[test]
752    fn auth_key() -> TestResult {
753        let wrap = YubiHsm2Wrap::from_yhw(include_str!("../tests/backup/auth.yhw"))?;
754        let decrypted = wrap.decrypt(WRAP_KEY)?;
755        assert!(!decrypted.is_empty());
756        let inner = InnerFormat::parse(&decrypted)?;
757        let mut buffer = vec![];
758        inner.serialize_into(&mut buffer);
759        assert_eq!(decrypted, buffer);
760        assert_eq!(inner.object_type, ObjectType::Aes128Auth);
761        assert_eq!(inner.capabilities, &[0, 0, 0, 0, 0, 1, 0, 0]);
762        assert_eq!(inner.domains, Domain::One.into());
763        assert_eq!(u16::from(inner.object_id.id()), 14);
764        assert_eq!(
765            inner.key_data,
766            WrappedPayload::AuthAes128(AuthAes128 {
767                delegated_capabilities: &[0; 8],
768                symmetric_keys: &[
769                    152, 123, 73, 154, 181, 120, 84, 139, 48, 32, 41, 176, 213, 53, 39, 232, 122,
770                    150, 131, 153, 10, 233, 98, 202, 67, 12, 27, 245, 184, 198, 41, 93
771                ]
772            })
773        );
774        assert_eq!(inner.object_id.object_type(), Type::AuthenticationKey);
775        assert_eq!(inner.label, "");
776        assert_eq!(inner.origin, 2);
777        assert_eq!(inner.sequence, 0);
778        Ok(())
779    }
780
781    #[test]
782    fn opaque_data() -> TestResult {
783        let wrap = YubiHsm2Wrap::from_yhw(include_str!("../tests/backup/opaque.yhw"))?;
784        let decrypted = wrap.decrypt(WRAP_KEY)?;
785        assert!(!decrypted.is_empty());
786        let inner = InnerFormat::parse(&decrypted)?;
787        let mut buffer = vec![];
788        inner.serialize_into(&mut buffer);
789        assert_eq!(decrypted, buffer);
790        assert_eq!(inner.object_type, ObjectType::Opaque);
791        assert_eq!(inner.capabilities, &[0, 0, 0, 0, 0, 1, 0, 0]);
792        assert_eq!(inner.domains, Domain::One.into());
793        assert_eq!(u16::from(inner.object_id.id()), 13);
794        assert_eq!(inner.key_data, WrappedPayload::Opaque(&[1, 2, 3]));
795        assert_eq!(inner.object_id.object_type(), Type::Opaque);
796        assert_eq!(inner.label, "random");
797        assert_eq!(inner.origin, 2);
798        assert_eq!(inner.sequence, 0);
799        Ok(())
800    }
801
802    #[test]
803    fn roundtrip_yhw() -> TestResult {
804        let input = include_str!("../tests/backup/private-ed25519-seed.yhw");
805        let wrap = YubiHsm2Wrap::from_yhw(input)?;
806        assert_eq!(input, wrap.to_yhw());
807        Ok(())
808    }
809
810    #[test]
811    fn encrypt_decrypt() -> TestResult {
812        let input = include_str!("../tests/backup/opaque.yhw");
813        let wrap = YubiHsm2Wrap::from_yhw(input)?;
814        let decrypted_original = wrap.decrypt(WRAP_KEY)?;
815        let plain = PlainWrappedDataWithKey {
816            data: &decrypted_original,
817            key: WRAP_KEY,
818        };
819        let wrap: YubiHsm2Wrap = plain.try_into()?;
820        let decrypted_from_plain = wrap.decrypt(WRAP_KEY)?;
821        assert_eq!(decrypted_original, decrypted_from_plain);
822        Ok(())
823    }
824}