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