Skip to main content

signstar_yubihsm2/object/
key.rs

1//! YubiHSM2 key metadata.
2
3use std::{
4    collections::BTreeSet,
5    fmt::{Debug, Display},
6    fs::read_to_string,
7    hash::Hash,
8    path::Path,
9};
10
11use argon2::Argon2;
12use getrandom::fill;
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15#[cfg(feature = "serde")]
16use serde_repr::{Deserialize_repr, Serialize_repr};
17use signstar_crypto::passphrase::{Passphrase, PassphrasePolicy};
18use strum::{AsRefStr, IntoStaticStr};
19use yubihsm::{
20    authentication::Key as YubiHsmAuthenticationKey,
21    wrap::{Algorithm as YubiHsmWrapAlgorithm, Key as YubiHsmWrapKey},
22};
23use zeroize::Zeroizing;
24
25use crate::object::{Capabilities, Id};
26
27/// YubiHSM2 object domain.
28///
29/// Objects can belong to one or many domains on the YubiHSM2.
30/// See [Core Concepts - Domains](https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#domains) for more details.
31#[derive(
32    AsRefStr,
33    Clone,
34    Copy,
35    Debug,
36    strum::Display,
37    Eq,
38    Hash,
39    IntoStaticStr,
40    Ord,
41    PartialEq,
42    PartialOrd,
43)]
44#[cfg_attr(feature = "serde", derive(Deserialize_repr, Serialize_repr))]
45#[repr(u8)]
46pub enum Domain {
47    /// First domain.
48    #[strum(serialize = "1")]
49    One = 1,
50    /// Second domain.
51    #[strum(serialize = "2")]
52    Two = 2,
53    /// Third domain.
54    #[strum(serialize = "3")]
55    Three = 3,
56    /// Fourth domain.
57    #[strum(serialize = "4")]
58    Four = 4,
59    /// Fifth domain.
60    #[strum(serialize = "5")]
61    Five = 5,
62    /// Sixth domain.
63    #[strum(serialize = "6")]
64    Six = 6,
65    /// Seventh domain.
66    #[strum(serialize = "7")]
67    Seven = 7,
68    /// Eighth domain.
69    #[strum(serialize = "8")]
70    Eight = 8,
71    /// Ninth domain.
72    #[strum(serialize = "9")]
73    Nine = 9,
74    /// Tenth domain.
75    #[strum(serialize = "10")]
76    Ten = 10,
77    /// Eleventh domain.
78    #[strum(serialize = "11")]
79    Eleven = 11,
80    /// Twelfth domain.
81    #[strum(serialize = "12")]
82    Twelve = 12,
83    /// Thirteenth domain.
84    #[strum(serialize = "13")]
85    Thirteen = 13,
86    /// Fourteenth domain.
87    #[strum(serialize = "14")]
88    Fourteen = 14,
89    /// Fifteenth domain.
90    #[strum(serialize = "15")]
91    Fifteen = 15,
92    /// Sixteenth domain.
93    #[strum(serialize = "16")]
94    Sixteen = 16,
95}
96
97#[cfg(feature = "cli")]
98impl clap::ValueEnum for Domain {
99    fn value_variants<'a>() -> &'a [Self] {
100        static VARIANTS: &[Domain] = &[
101            Domain::One,
102            Domain::Two,
103            Domain::Three,
104            Domain::Four,
105            Domain::Five,
106            Domain::Six,
107            Domain::Seven,
108            Domain::Eight,
109            Domain::Nine,
110            Domain::Ten,
111            Domain::Eleven,
112            Domain::Twelve,
113            Domain::Thirteen,
114            Domain::Fourteen,
115            Domain::Fifteen,
116            Domain::Sixteen,
117        ];
118        VARIANTS
119    }
120
121    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
122        let str: &'static str = self.into();
123        Some(clap::builder::PossibleValue::new(str))
124    }
125}
126
127impl From<Domain> for yubihsm::Domain {
128    fn from(value: Domain) -> Self {
129        match value {
130            Domain::One => Self::DOM1,
131            Domain::Two => Self::DOM2,
132            Domain::Three => Self::DOM3,
133            Domain::Four => Self::DOM4,
134            Domain::Five => Self::DOM5,
135            Domain::Six => Self::DOM6,
136            Domain::Seven => Self::DOM7,
137            Domain::Eight => Self::DOM8,
138            Domain::Nine => Self::DOM9,
139            Domain::Ten => Self::DOM10,
140            Domain::Eleven => Self::DOM11,
141            Domain::Twelve => Self::DOM12,
142            Domain::Thirteen => Self::DOM13,
143            Domain::Fourteen => Self::DOM14,
144            Domain::Fifteen => Self::DOM15,
145            Domain::Sixteen => Self::DOM16,
146        }
147    }
148}
149
150/// A set of domains of an object on a YubiHSM2.
151///
152/// Each object is assigned to at least one [`Domain`].
153#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
154#[cfg_attr(
155    feature = "serde",
156    derive(Serialize, Deserialize),
157    serde(try_from = "BTreeSet<Domain>")
158)]
159pub struct Domains(BTreeSet<Domain>);
160
161impl Domains {
162    /// Converts this object into raw big-endian bytes.
163    pub fn to_be_bytes(&self) -> [u8; 2] {
164        self.bits().to_be_bytes()
165    }
166
167    /// Returns set of domains containing all available domains.
168    pub fn all() -> Self {
169        yubihsm::Domain::all().bits().into()
170    }
171
172    /// Returns the underlying bits value.
173    pub fn bits(&self) -> u16 {
174        yubihsm::Domain::from(self).bits()
175    }
176}
177
178impl Display for Domains {
179    /// Formats a [`Domains`] as a string.
180    ///
181    /// Here, the domains in `self` are represented as a comma-separated list (e.g. `1, 2, 3` or
182    /// `1`).
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        write!(
185            f,
186            "{}",
187            self.0
188                .iter()
189                .map(|domain| domain.as_ref())
190                .collect::<Vec<_>>()
191                .join(", ")
192        )
193    }
194}
195
196impl From<Domain> for Domains {
197    fn from(value: Domain) -> Self {
198        Self(BTreeSet::from_iter([value]))
199    }
200}
201
202impl From<yubihsm::Domain> for Domains {
203    fn from(value: yubihsm::Domain) -> Self {
204        let lookup = [
205            (yubihsm::Domain::DOM1, Domain::One),
206            (yubihsm::Domain::DOM2, Domain::Two),
207            (yubihsm::Domain::DOM3, Domain::Three),
208            (yubihsm::Domain::DOM4, Domain::Four),
209            (yubihsm::Domain::DOM5, Domain::Five),
210            (yubihsm::Domain::DOM6, Domain::Six),
211            (yubihsm::Domain::DOM7, Domain::Seven),
212            (yubihsm::Domain::DOM8, Domain::Eight),
213            (yubihsm::Domain::DOM9, Domain::Nine),
214            (yubihsm::Domain::DOM10, Domain::Ten),
215            (yubihsm::Domain::DOM11, Domain::Eleven),
216            (yubihsm::Domain::DOM12, Domain::Twelve),
217            (yubihsm::Domain::DOM13, Domain::Thirteen),
218            (yubihsm::Domain::DOM14, Domain::Fourteen),
219            (yubihsm::Domain::DOM15, Domain::Fifteen),
220            (yubihsm::Domain::DOM16, Domain::Sixteen),
221        ];
222
223        Domains(BTreeSet::from_iter(lookup.iter().filter_map(
224            |(yubi_dom, dom)| {
225                if value.contains(*yubi_dom) {
226                    Some(*dom)
227                } else {
228                    None
229                }
230            },
231        )))
232    }
233}
234
235impl From<u16> for Domains {
236    fn from(value: u16) -> Self {
237        yubihsm::Domain::from_bits_retain(value).into()
238    }
239}
240
241impl From<&[Domain]> for Domains {
242    fn from(value: &[Domain]) -> Self {
243        Self(value.iter().copied().collect())
244    }
245}
246
247impl TryFrom<BTreeSet<Domain>> for Domains {
248    type Error = crate::object::Error;
249
250    fn try_from(domains: BTreeSet<Domain>) -> Result<Self, Self::Error> {
251        if domains.is_empty() {
252            return Err(Self::Error::EmptySetOfDomains);
253        }
254        Ok(Self(domains))
255    }
256}
257
258impl From<&Domains> for yubihsm::Domain {
259    fn from(value: &Domains) -> Self {
260        value
261            .0
262            .iter()
263            .map(|cap| yubihsm::Domain::from(*cap))
264            .fold(yubihsm::Domain::empty(), |acc, c| acc | c)
265    }
266}
267
268/// An authentication key.
269#[derive(Debug)]
270pub struct AuthenticationKey(YubiHsmAuthenticationKey);
271
272impl AuthenticationKey {
273    /// The default [`PassphrasePolicy`] for an [`AuthenticationKey`].
274    pub const PASSPHRASE_POLICY: PassphrasePolicy = PassphrasePolicy { minimum_length: 30 };
275}
276
277impl AsRef<YubiHsmAuthenticationKey> for AuthenticationKey {
278    fn as_ref(&self) -> &YubiHsmAuthenticationKey {
279        &self.0
280    }
281}
282
283impl From<AuthenticationKey> for YubiHsmAuthenticationKey {
284    fn from(value: AuthenticationKey) -> Self {
285        value.0
286    }
287}
288
289impl From<&AuthenticationKey> for YubiHsmAuthenticationKey {
290    fn from(value: &AuthenticationKey) -> Self {
291        value.0.clone()
292    }
293}
294
295impl TryFrom<&Path> for AuthenticationKey {
296    type Error = crate::Error;
297
298    /// Creates a new [`AuthenticationKey`] from the contents of `file`.
299    ///
300    /// The contents of `file` must be a valid UTF-8 string that satisfies the default
301    /// [`PassphrasePolicy`].
302    ///
303    /// # Errors
304    ///
305    /// Returns an error if
306    ///
307    /// - the contents of `file` cannot be read to a valid UTF-8 encoded string
308    /// - the contents of `file` do not satisfy the requirements of [`Self::PASSPHRASE_POLICY`]
309    fn try_from(file: &Path) -> Result<Self, Self::Error> {
310        let passphrase = Passphrase::new_with_policy(
311            read_to_string(file).map_err(|source| crate::Error::IoPath {
312                path: file.into(),
313                context: "reading the passphrase for an authentication key derivation from file",
314                source,
315            })?,
316            &Self::PASSPHRASE_POLICY,
317        )?;
318
319        Ok(Self(YubiHsmAuthenticationKey::derive_from_password(
320            passphrase.expose_borrowed().as_bytes(),
321        )))
322    }
323}
324
325impl TryFrom<&Passphrase> for AuthenticationKey {
326    type Error = crate::Error;
327
328    /// Creates a new [`AuthenticationKey`] from a [`Passphrase`].
329    ///
330    /// # Errors
331    ///
332    /// Returns an error, if
333    ///
334    /// - the `passphrase` does not satisfy the requirements of [`Self::PASSPHRASE_POLICY`]
335    fn try_from(passphrase: &Passphrase) -> Result<Self, Self::Error> {
336        passphrase.check_against_policy(&Self::PASSPHRASE_POLICY)?;
337
338        Ok(Self(YubiHsmAuthenticationKey::derive_from_password(
339            passphrase.expose_borrowed().as_bytes(),
340        )))
341    }
342}
343
344/// The kind of a wrap key as used by the YubiHSM2.
345#[derive(Clone, Copy, Debug, Default, Eq, IntoStaticStr, PartialEq)]
346pub enum WrapKeyKind {
347    /// AES-128 in Counter with CBC-MAC (CCM) mode.
348    Aes128,
349
350    /// AES-192 in Counter with CBC-MAC (CCM) mode.
351    Aes192,
352
353    /// AES-256 in Counter with CBC-MAC (CCM) mode.
354    ///
355    /// # Note
356    ///
357    /// This is the default, as it is considered resistant against [quantum attacks].
358    ///
359    /// [quantum attacks]: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard#Quantum_attacks
360    #[default]
361    Aes256,
362}
363
364impl WrapKeyKind {
365    /// Returns the size of the wrap key kind in bytes.
366    pub fn key_len(&self) -> usize {
367        match self {
368            Self::Aes128 => 16,
369            Self::Aes192 => 24,
370            Self::Aes256 => 32,
371        }
372    }
373}
374
375impl From<&WrapKeyKind> for YubiHsmWrapAlgorithm {
376    fn from(value: &WrapKeyKind) -> Self {
377        match value {
378            WrapKeyKind::Aes128 => Self::Aes128Ccm,
379            WrapKeyKind::Aes192 => Self::Aes192Ccm,
380            WrapKeyKind::Aes256 => Self::Aes256Ccm,
381        }
382    }
383}
384
385/// A wrap key.
386///
387/// Wrap keys are used to wrap (encrypt) objects (e.g. other keys or data) in a YubiHSM2.
388pub struct WrapKey {
389    kind: WrapKeyKind,
390    data: Zeroizing<Vec<u8>>,
391}
392
393impl WrapKey {
394    /// The default [`PassphrasePolicy`] for a [`WrapKey`].
395    pub const PASSPHRASE_POLICY: PassphrasePolicy = PassphrasePolicy {
396        minimum_length: 100,
397    };
398
399    /// Creates a new [`WrapKey`] of a specific kind.
400    ///
401    /// # Errors
402    ///
403    /// Returns an error if generating random bytes for the new wrap key fails
404    pub fn generate_random(kind: WrapKeyKind) -> Result<Self, crate::Error> {
405        let data = {
406            let mut bytes = Zeroizing::new(vec![0u8; kind.key_len()]);
407            fill(&mut bytes).map_err(|source| crate::object::Error::GetRandom {
408                context: "generating a random wrapping key",
409                source,
410            })?;
411            bytes
412        };
413
414        Ok(Self { kind, data })
415    }
416}
417
418impl Debug for WrapKey {
419    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420        f.debug_struct("WrapKey")
421            .field("kind", &self.kind)
422            .field("data", &"[REDACTED]")
423            .finish()
424    }
425}
426
427impl From<&WrapKey> for Vec<u8> {
428    fn from(value: &WrapKey) -> Self {
429        value.data.to_vec()
430    }
431}
432
433/// A helper struct for the creation of a [`WrapKey`] from a [`Passphrase`].
434///
435/// The struct tracks a [`Passphrase`] and a [`WrapKeyKind`].
436///
437/// The passphrase is guaranteed to be validated against the passphrase policy imposed by
438/// [`WrapKey`].
439#[derive(Debug)]
440pub struct WrapKeyFromPassphrase<'passphrase> {
441    passphrase: &'passphrase Passphrase,
442    kind: WrapKeyKind,
443}
444
445impl<'passphrase> WrapKeyFromPassphrase<'passphrase> {
446    /// The static salt used for argon2, when hashing a passphrase.
447    pub(crate) const ARGON2_SALT: &'static [u8] = b"Salt for a Signstar backup key";
448
449    /// Creates a new [`WrapKeyFromPassphrase`].
450    ///
451    /// # Note
452    ///
453    /// It is recommended to use [`WrapKeyKind::Aes256`] for `kind`, as it is considered resistant
454    /// against [quantum attacks].
455    ///
456    /// # Errors
457    ///
458    /// Returns an error, if checking `passphrase` against [`WrapKey::PASSPHRASE_POLICY`] fails.
459    ///
460    /// [quantum attacks]: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard#Quantum_attacks
461    pub fn new(
462        passphrase: &'passphrase Passphrase,
463        kind: WrapKeyKind,
464    ) -> Result<Self, crate::Error> {
465        passphrase.check_against_policy(&WrapKey::PASSPHRASE_POLICY)?;
466
467        Ok(Self { passphrase, kind })
468    }
469}
470
471impl<'passphrase> TryFrom<WrapKeyFromPassphrase<'passphrase>> for WrapKey {
472    type Error = crate::Error;
473
474    /// Creates a new [`WrapKey`] from a [`WrapKeyFromPassphrase`].
475    ///
476    /// Uses the [argon2] key derivation function to create the [`WrapKey`] from the `passphrase` of
477    /// `value` and a static salt.
478    ///
479    /// # Errors
480    ///
481    /// Returns an error, if hashing the passphrase of `value` into the targeted `data` of a
482    /// [`WrapKey`] fails.
483    ///
484    /// [argon2]: https://en.wikipedia.org/wiki/Argon2
485    fn try_from(value: WrapKeyFromPassphrase<'passphrase>) -> Result<Self, Self::Error> {
486        let mut data = Zeroizing::new(vec![0u8; value.kind.key_len()]);
487        Argon2::default()
488            .hash_password_into(
489                value.passphrase.expose_borrowed().as_bytes(),
490                WrapKeyFromPassphrase::ARGON2_SALT,
491                &mut data,
492            )
493            .map_err(|source| crate::object::Error::Argon2 {
494                context: "creating a wrap key from a passphrase",
495                source,
496            })?;
497
498        Ok(WrapKey {
499            kind: value.kind,
500            data,
501        })
502    }
503}
504
505/// A helper struct for the creation of a [`YubiHsmWrapKey`].
506///
507/// The struct tracks an [`Id`] and a reference to a [`WrapKey`].
508#[derive(Debug)]
509pub struct YubiHsmWrapKeyFromWrapKey<'wrap_key> {
510    pub(crate) id: Id,
511    pub(crate) wrap_key: &'wrap_key WrapKey,
512}
513
514impl<'wrap_key> TryFrom<&YubiHsmWrapKeyFromWrapKey<'wrap_key>> for YubiHsmWrapKey {
515    type Error = crate::Error;
516
517    /// Creates a [`YubiHsmWrapKey`] from a [`YubiHsmWrapKeyFromWrapKey`].
518    ///
519    /// # Errors
520    ///
521    /// Returns an error if [`YubiHsmWrapKey::from_bytes`] fails.
522    fn try_from(value: &YubiHsmWrapKeyFromWrapKey) -> Result<Self, Self::Error> {
523        Self::from_bytes(value.id.into(), &value.wrap_key.data).map_err(|source| {
524            crate::Error::Device {
525                context: "creating a YubiHSM2 wrap key from bytes",
526                source,
527            }
528        })
529    }
530}
531
532/// Metadata about a key stored on a YubiHSM2.
533///
534/// This struct stores common parameters of keys regardless of their usage may describe
535/// authentication, wrapping and signing keys.
536#[derive(Clone, Debug)]
537#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
538pub struct KeyInfo {
539    /// Inner identifier used to track the key on the YubiHSM2.
540    pub key_id: Id,
541
542    /// Key domain.
543    ///
544    /// Must be in range `1..16`.
545    /// See [Core Concepts - Domains](https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#domains).
546    pub domains: Domains,
547
548    /// Capabilities of this key.
549    pub caps: Capabilities,
550}
551
552#[cfg(test)]
553mod tests {
554    use std::io::Write;
555
556    use rand::{
557        distributions::{Alphanumeric, DistString},
558        thread_rng,
559    };
560    use rstest::{fixture, rstest};
561    use tempfile::{NamedTempFile, TempDir};
562    use testresult::TestResult;
563
564    use super::*;
565
566    /// Ensures that [`Domains::to_string`] works as expected.
567    #[test]
568    fn domains_to_string() {
569        let domain_list = vec![Domain::One];
570        let domains = Domains::from(domain_list.as_slice());
571        assert_eq!("1", domains.to_string());
572
573        let domain_list = vec![Domain::One, Domain::Two];
574        let domains = Domains::from(domain_list.as_slice());
575        assert_eq!("1, 2", domains.to_string());
576    }
577
578    #[test]
579    fn authentication_key_try_from_path_succeeds() -> TestResult {
580        let file = {
581            let mut file = NamedTempFile::new()?;
582            let passphrase = Alphanumeric.sample_string(&mut thread_rng(), 30);
583            file.write_all(passphrase.as_bytes())?;
584            file
585        };
586
587        match AuthenticationKey::try_from(file.path()) {
588            Ok(_) => {}
589            Err(error) => panic!(
590                "Expected to create an authentication key from the contents of a file, but got an error instead: {error}"
591            ),
592        }
593
594        Ok(())
595    }
596
597    #[test]
598    fn authentication_key_try_from_path_fails_on_short_passphrase() -> TestResult {
599        let file = {
600            let mut file = NamedTempFile::new()?;
601            let passphrase = Alphanumeric.sample_string(&mut thread_rng(), 10);
602            file.write_all(passphrase.as_bytes())?;
603            file
604        };
605
606        match AuthenticationKey::try_from(file.path()) {
607            Ok(_) => panic!(
608                "Expected to fail with Error::Length, but succeeded in creating an authentication key from a passphrase file instead."
609            ),
610            Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(_))) => {}
611            Err(error) => panic!(
612                "Expected to fail with Error::Length, but failed with a different error instead: {error}"
613            ),
614        }
615
616        Ok(())
617    }
618
619    #[test]
620    fn authentication_key_try_from_path_fails_on_file_is_dir() -> TestResult {
621        let file = TempDir::new()?;
622
623        match AuthenticationKey::try_from(file.path()) {
624            Ok(_) => panic!(
625                "Expected to fail with Error::IoPath, but succeeded in creating an authentication key from a passphrase file instead."
626            ),
627            Err(crate::Error::IoPath { .. }) => {}
628            Err(error) => panic!(
629                "Expected to fail with Error::IoPath, but failed with a different error instead: {error}"
630            ),
631        }
632
633        Ok(())
634    }
635
636    #[test]
637    fn authentication_key_try_from_passphrase_succeeds() -> TestResult {
638        let passphrase = Passphrase::generate(Some(30));
639
640        match AuthenticationKey::try_from(&passphrase) {
641            Ok(_) => {}
642            Err(error) => panic!(
643                "Expected to create an authentication key from a passphrase, but got an error instead: {error}"
644            ),
645        }
646
647        Ok(())
648    }
649
650    #[test]
651    fn authentication_key_try_from_passphrase_fails_on_passphrase_too_short() -> TestResult {
652        let passphrase = Passphrase::new("passphrase".to_string());
653
654        match AuthenticationKey::try_from(&passphrase) {
655            Ok(_) => panic!("Expected to fail with Error::Length, but succeeded instead."),
656            Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(_))) => {}
657            Err(error) => panic!(
658                "Expected to fail with Error::Length, but failed with a different error instead: {error}"
659            ),
660        }
661
662        Ok(())
663    }
664
665    /// Ensures that [`WrapKeyKind::key_len`] returns the correct number for each variant.
666    #[rstest]
667    #[case(WrapKeyKind::Aes128, 16)]
668    #[case(WrapKeyKind::Aes192, 24)]
669    #[case(WrapKeyKind::Aes256, 32)]
670    fn wrap_key_kind_key_len(#[case] wrap_key_kind: WrapKeyKind, #[case] len: usize) {
671        assert_eq!(wrap_key_kind.key_len(), len);
672    }
673
674    /// Ensures that variants of [`YubiHsmWrapAlgorithm`] can be created from [`WrapKeyKind`]
675    /// variants.
676    #[rstest]
677    #[case(WrapKeyKind::Aes128, YubiHsmWrapAlgorithm::Aes128Ccm)]
678    #[case(WrapKeyKind::Aes192, YubiHsmWrapAlgorithm::Aes192Ccm)]
679    #[case(WrapKeyKind::Aes256, YubiHsmWrapAlgorithm::Aes256Ccm)]
680    fn yubihsm_wrap_algorithm_from_wrap_key_kind(
681        #[case] wrap_key_kind: WrapKeyKind,
682        #[case] algorithm: YubiHsmWrapAlgorithm,
683    ) {
684        assert_eq!(YubiHsmWrapAlgorithm::from(&wrap_key_kind), algorithm);
685    }
686
687    /// Ensures that [`WrapKey::generate_random`] creates a [`WrapKey`] based on a [`WrapKeyKind`].
688    #[rstest]
689    #[case(WrapKeyKind::Aes128)]
690    #[case(WrapKeyKind::Aes192)]
691    #[case(WrapKeyKind::Aes256)]
692    fn wrap_key_generate_random_succeeds(#[case] wrap_key_kind: WrapKeyKind) -> TestResult {
693        let wrap_key = WrapKey::generate_random(wrap_key_kind)?;
694        let data: Vec<u8> = From::from(&wrap_key);
695
696        assert_eq!(data.len(), wrap_key_kind.key_len());
697
698        Ok(())
699    }
700
701    /// Ensures that the [`Debug`] representation of [`WrapKey`] contains the correct data.
702    #[rstest]
703    #[case(WrapKeyKind::Aes128)]
704    #[case(WrapKeyKind::Aes192)]
705    #[case(WrapKeyKind::Aes256)]
706    fn wrap_key_debug(#[case] wrap_key_kind: WrapKeyKind) -> TestResult {
707        let wrap_key = WrapKey::generate_random(wrap_key_kind)?;
708        let data_debug = format!("{:?}", wrap_key.data.to_vec());
709        let wrap_key_debug = format!("{wrap_key:?}");
710        let wrap_key_kind_debug = format!("{wrap_key_kind:?}");
711
712        assert!(wrap_key_debug.contains(&wrap_key_kind_debug));
713        assert!(wrap_key_debug.contains("[REDACTED]"));
714        assert!(!wrap_key_debug.contains(&data_debug));
715
716        Ok(())
717    }
718
719    /// A valid [`Passphrase`] for a [`WrapKey`].
720    #[fixture]
721    fn valid_wrap_key_passphrase() -> Passphrase {
722        Passphrase::new("this is a long passphrase that is at least 100 chars long, very long omg, so long, really now, you gotta believe me".to_string())
723    }
724
725    /// An invalid [`Passphrase`] for a [`WrapKey`].
726    #[fixture]
727    fn invalid_wrap_key_passphrase() -> Passphrase {
728        Passphrase::new("this passphrase is shorter than 100 chars".to_string())
729    }
730
731    /// Ensures that [`WrapKeyFromPassphrase::new`] succeeds with sufficiently long passphrases.
732    #[rstest]
733    #[case(WrapKeyKind::Aes128)]
734    #[case(WrapKeyKind::Aes192)]
735    #[case(WrapKeyKind::Aes256)]
736    fn wrap_key_from_passphrase_new_succeeds(
737        #[case] wrap_key_kind: WrapKeyKind,
738        valid_wrap_key_passphrase: Passphrase,
739    ) -> TestResult {
740        WrapKeyFromPassphrase::new(&valid_wrap_key_passphrase, wrap_key_kind)?;
741
742        Ok(())
743    }
744
745    /// Ensures that [`WrapKeyFromPassphrase::new`] fails on invalid passphrases.
746    #[rstest]
747    #[case(WrapKeyKind::Aes128)]
748    #[case(WrapKeyKind::Aes192)]
749    #[case(WrapKeyKind::Aes256)]
750    fn wrap_key_from_passphrase_new_fails_on_short_passphrase(
751        #[case] wrap_key_kind: WrapKeyKind,
752        invalid_wrap_key_passphrase: Passphrase,
753    ) -> TestResult {
754        assert!(WrapKeyFromPassphrase::new(&invalid_wrap_key_passphrase, wrap_key_kind).is_err());
755
756        Ok(())
757    }
758
759    /// Ensures that creating a [`WrapKey`] from a [`WrapKeyFromPassphrase`] succeeds on valid data.
760    #[rstest]
761    #[case(WrapKeyKind::Aes128)]
762    #[case(WrapKeyKind::Aes192)]
763    #[case(WrapKeyKind::Aes256)]
764    fn wrap_key_try_from_wrap_key_from_passphrase_succeeds(
765        #[case] wrap_key_kind: WrapKeyKind,
766        valid_wrap_key_passphrase: Passphrase,
767    ) -> TestResult {
768        let wrap_key_from_passphrase =
769            WrapKeyFromPassphrase::new(&valid_wrap_key_passphrase, wrap_key_kind)?;
770        let wrap_key = WrapKey::try_from(wrap_key_from_passphrase)?;
771        let data: Vec<u8> = From::from(&wrap_key);
772
773        assert_eq!(data.len(), wrap_key_kind.key_len());
774
775        Ok(())
776    }
777
778    /// Ensures that creating a [`YubiHsmWrapKey`] from a [`WrapKey`] succeeds with valid data.
779    #[rstest]
780    #[case(WrapKeyKind::Aes128)]
781    #[case(WrapKeyKind::Aes192)]
782    #[case(WrapKeyKind::Aes256)]
783    fn yubihsm_wrap_key_try_from_yubihsm_wrap_key_from_wrap_key_succeeds(
784        #[case] wrap_key_kind: WrapKeyKind,
785        valid_wrap_key_passphrase: Passphrase,
786    ) -> TestResult {
787        let wrap_key = {
788            let wrap_key_from_passphrase =
789                WrapKeyFromPassphrase::new(&valid_wrap_key_passphrase, wrap_key_kind)?;
790            WrapKey::try_from(wrap_key_from_passphrase)?
791        };
792        let yubihsm_wrap_key_from_wrap_key = YubiHsmWrapKeyFromWrapKey {
793            id: "1".parse()?,
794            wrap_key: &wrap_key,
795        };
796
797        YubiHsmWrapKey::try_from(&yubihsm_wrap_key_from_wrap_key)?;
798
799        Ok(())
800    }
801}