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, 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(Clone, Copy, Debug, Default, Eq, Hash, IntoStaticStr, Ord, PartialEq, PartialOrd)]
358pub enum WrapKeyKind {
359    /// AES-128 in Counter with CBC-MAC (CCM) mode.
360    Aes128,
361
362    /// AES-192 in Counter with CBC-MAC (CCM) mode.
363    Aes192,
364
365    /// AES-256 in Counter with CBC-MAC (CCM) mode.
366    ///
367    /// # Note
368    ///
369    /// This is the default, as it is considered resistant against [quantum attacks].
370    ///
371    /// [quantum attacks]: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard#Quantum_attacks
372    #[default]
373    Aes256,
374}
375
376impl WrapKeyKind {
377    /// Returns the size of the wrap key kind in bytes.
378    pub fn key_len(&self) -> usize {
379        match self {
380            Self::Aes128 => 16,
381            Self::Aes192 => 24,
382            Self::Aes256 => 32,
383        }
384    }
385}
386
387impl From<&WrapKeyKind> for YubiHsmWrapAlgorithm {
388    fn from(value: &WrapKeyKind) -> Self {
389        match value {
390            WrapKeyKind::Aes128 => Self::Aes128Ccm,
391            WrapKeyKind::Aes192 => Self::Aes192Ccm,
392            WrapKeyKind::Aes256 => Self::Aes256Ccm,
393        }
394    }
395}
396
397impl From<YubiHsmWrapAlgorithm> for WrapKeyKind {
398    fn from(value: YubiHsmWrapAlgorithm) -> Self {
399        match value {
400            YubiHsmWrapAlgorithm::Aes128Ccm => Self::Aes128,
401            YubiHsmWrapAlgorithm::Aes192Ccm => Self::Aes192,
402            YubiHsmWrapAlgorithm::Aes256Ccm => Self::Aes256,
403        }
404    }
405}
406
407/// A wrap key.
408///
409/// Wrap keys are used to wrap (encrypt) objects (e.g. other keys or data) in a YubiHSM2.
410pub struct WrapKey {
411    kind: WrapKeyKind,
412    data: Zeroizing<Vec<u8>>,
413}
414
415impl WrapKey {
416    /// The default [`PassphrasePolicy`] for a [`WrapKey`].
417    pub const PASSPHRASE_POLICY: PassphrasePolicy = PassphrasePolicy {
418        minimum_length: 100,
419    };
420
421    /// Creates a new [`WrapKey`] of a specific kind.
422    ///
423    /// # Errors
424    ///
425    /// Returns an error if generating random bytes for the new wrap key fails
426    pub fn generate_random(kind: WrapKeyKind) -> Result<Self, crate::Error> {
427        let data = {
428            let mut bytes = Zeroizing::new(vec![0u8; kind.key_len()]);
429            fill(&mut bytes).map_err(|source| crate::object::Error::GetRandom {
430                context: "generating a random wrapping key",
431                source,
432            })?;
433            bytes
434        };
435
436        Ok(Self { kind, data })
437    }
438}
439
440impl Debug for WrapKey {
441    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442        f.debug_struct("WrapKey")
443            .field("kind", &self.kind)
444            .field("data", &"[REDACTED]")
445            .finish()
446    }
447}
448
449impl From<&WrapKey> for Vec<u8> {
450    fn from(value: &WrapKey) -> Self {
451        value.data.to_vec()
452    }
453}
454
455/// A helper struct for the creation of a [`WrapKey`] from a [`Passphrase`].
456///
457/// The struct tracks a [`Passphrase`] and a [`WrapKeyKind`].
458///
459/// The passphrase is guaranteed to be validated against the passphrase policy imposed by
460/// [`WrapKey`].
461#[derive(Debug)]
462pub struct WrapKeyFromPassphrase<'passphrase> {
463    passphrase: &'passphrase Passphrase,
464    kind: WrapKeyKind,
465}
466
467impl<'passphrase> WrapKeyFromPassphrase<'passphrase> {
468    /// The static salt used for argon2, when hashing a passphrase.
469    pub(crate) const ARGON2_SALT: &'static [u8] = b"Salt for a Signstar backup key";
470
471    /// Creates a new [`WrapKeyFromPassphrase`].
472    ///
473    /// # Note
474    ///
475    /// It is recommended to use [`WrapKeyKind::Aes256`] for `kind`, as it is considered resistant
476    /// against [quantum attacks].
477    ///
478    /// # Errors
479    ///
480    /// Returns an error, if checking `passphrase` against [`WrapKey::PASSPHRASE_POLICY`] fails.
481    ///
482    /// [quantum attacks]: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard#Quantum_attacks
483    pub fn new(
484        passphrase: &'passphrase Passphrase,
485        kind: WrapKeyKind,
486    ) -> Result<Self, crate::Error> {
487        passphrase.check_against_policy(&WrapKey::PASSPHRASE_POLICY)?;
488
489        Ok(Self { passphrase, kind })
490    }
491}
492
493impl<'passphrase> TryFrom<WrapKeyFromPassphrase<'passphrase>> for WrapKey {
494    type Error = crate::Error;
495
496    /// Creates a new [`WrapKey`] from a [`WrapKeyFromPassphrase`].
497    ///
498    /// Uses the [argon2] key derivation function to create the [`WrapKey`] from the `passphrase` of
499    /// `value` and a static salt.
500    ///
501    /// # Errors
502    ///
503    /// Returns an error, if hashing the passphrase of `value` into the targeted `data` of a
504    /// [`WrapKey`] fails.
505    ///
506    /// [argon2]: https://en.wikipedia.org/wiki/Argon2
507    fn try_from(value: WrapKeyFromPassphrase<'passphrase>) -> Result<Self, Self::Error> {
508        let mut data = Zeroizing::new(vec![0u8; value.kind.key_len()]);
509        Argon2::default()
510            .hash_password_into(
511                value.passphrase.expose_borrowed().as_bytes(),
512                WrapKeyFromPassphrase::ARGON2_SALT,
513                &mut data,
514            )
515            .map_err(|source| crate::object::Error::Argon2 {
516                context: "creating a wrap key from a passphrase",
517                source,
518            })?;
519
520        Ok(WrapKey {
521            kind: value.kind,
522            data,
523        })
524    }
525}
526
527/// A helper struct for the creation of a [`YubiHsmWrapKey`].
528///
529/// The struct tracks an [`Id`] and a reference to a [`WrapKey`].
530#[derive(Debug)]
531pub struct YubiHsmWrapKeyFromWrapKey<'wrap_key> {
532    pub(crate) id: Id,
533    pub(crate) wrap_key: &'wrap_key WrapKey,
534}
535
536impl<'wrap_key> TryFrom<&YubiHsmWrapKeyFromWrapKey<'wrap_key>> for YubiHsmWrapKey {
537    type Error = crate::Error;
538
539    /// Creates a [`YubiHsmWrapKey`] from a [`YubiHsmWrapKeyFromWrapKey`].
540    ///
541    /// # Errors
542    ///
543    /// Returns an error if [`YubiHsmWrapKey::from_bytes`] fails.
544    fn try_from(value: &YubiHsmWrapKeyFromWrapKey) -> Result<Self, Self::Error> {
545        Self::from_bytes(value.id, &value.wrap_key.data).map_err(|source| crate::Error::Device {
546            context: "creating a YubiHSM2 wrap key from bytes",
547            source,
548        })
549    }
550}
551
552/// Metadata about a key stored on a YubiHSM2.
553///
554/// This struct stores common parameters of keys regardless of their usage may describe
555/// authentication, wrapping and signing keys.
556#[derive(Clone, Debug)]
557#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
558pub struct KeyInfo {
559    /// Inner identifier used to track the key on the YubiHSM2.
560    pub key_id: Id,
561
562    /// Key domain.
563    ///
564    /// Must be in range `1..16`.
565    /// See [Core Concepts - Domains](https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#domains).
566    pub domains: Domains,
567
568    /// Capabilities of this key.
569    pub caps: Capabilities,
570}
571
572/// An asymmetric key algorithm.
573///
574/// # Note
575///
576/// This type is only required because [`yubihsm::asymmetric::Algorithm`] does not implement the
577/// interfaces that we need: <https://github.com/iqlusioninc/yubihsm.rs/pull/665>
578///
579/// As such, this type is less specific than [`yubihsm::Algorithm`], because using it we are only
580/// interested in comparing with e.g. [`KeyType`] and not in the underlying data structure.
581#[derive(Clone, Copy, Debug, strum::Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
582pub enum AsymmetricAlgorithm {
583    /// 2048-bit RSA
584    Rsa2048,
585
586    /// 3072-bit RSA
587    Rsa3072,
588
589    /// 4096-bit RSA
590    Rsa4096,
591
592    /// Ed25519
593    Ed25519,
594
595    /// NIST P-224 (secp224r1)
596    EcP224,
597
598    /// NIST P-256 (secp256r1, prime256v1)
599    EcP256,
600
601    /// NIST P-384 (secp384r1)
602    EcP384,
603
604    /// P-521 (secp521r1)
605    EcP521,
606
607    /// secp256k1
608    EcK256,
609
610    /// brainpool256r1
611    EcBp256,
612
613    /// brainpool384r1
614    EcBp384,
615
616    /// brainpool512r1
617    EcBp512,
618}
619
620impl From<YubiHsmAsymmetricAlgorithm> for AsymmetricAlgorithm {
621    fn from(value: YubiHsmAsymmetricAlgorithm) -> Self {
622        match value {
623            YubiHsmAsymmetricAlgorithm::Rsa2048 => Self::Rsa2048,
624            YubiHsmAsymmetricAlgorithm::Rsa3072 => Self::Rsa3072,
625            YubiHsmAsymmetricAlgorithm::Rsa4096 => Self::Rsa4096,
626            YubiHsmAsymmetricAlgorithm::Ed25519 => Self::Ed25519,
627            YubiHsmAsymmetricAlgorithm::EcP224 => Self::EcP224,
628            YubiHsmAsymmetricAlgorithm::EcP256 => Self::EcP256,
629            YubiHsmAsymmetricAlgorithm::EcP384 => Self::EcP384,
630            YubiHsmAsymmetricAlgorithm::EcP521 => Self::EcP521,
631            YubiHsmAsymmetricAlgorithm::EcK256 => Self::EcK256,
632            YubiHsmAsymmetricAlgorithm::EcBp256 => Self::EcBp256,
633            YubiHsmAsymmetricAlgorithm::EcBp384 => Self::EcBp384,
634            YubiHsmAsymmetricAlgorithm::EcBp512 => Self::EcBp512,
635        }
636    }
637}
638
639impl PartialEq<KeyType> for AsymmetricAlgorithm {
640    fn eq(&self, other: &KeyType) -> bool {
641        matches!(
642            (other, self),
643            (KeyType::Rsa, Self::Rsa2048)
644                | (KeyType::Rsa, Self::Rsa3072)
645                | (KeyType::Rsa, Self::Rsa4096)
646                | (KeyType::Curve25519, Self::Ed25519)
647                | (KeyType::EcP224, Self::EcP224)
648                | (KeyType::EcP256, Self::EcP256)
649                | (KeyType::EcP384, Self::EcP384)
650                | (KeyType::EcP521, Self::EcP521)
651                | (KeyType::EcK256, Self::EcK256)
652                | (KeyType::EcBp256, Self::EcBp256)
653                | (KeyType::EcBp384, Self::EcBp384)
654        )
655    }
656}
657
658/// The "algorithm" used by a YubiHSM2 object.
659///
660/// # Note
661///
662/// This type is only required because [`yubihsm::Algorithm`] does not implement the interfaces that
663/// we need: <https://github.com/iqlusioninc/yubihsm.rs/pull/665>
664///
665/// As such, this type is less specific than [`yubihsm::Algorithm`], because we are not using some
666/// of its variants.
667#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
668pub enum ObjectAlgorithm {
669    /// Asymmetric algorithms
670    Asymmetric(AsymmetricAlgorithm),
671
672    /// YubiHSM 2 symmetric PSK authentication
673    Authentication,
674
675    /// Elliptic Curve Diffie-Hellman (i.e. key exchange) algorithms
676    Ecdh,
677
678    /// ECDSA algorithms
679    Ecdsa,
680
681    /// HMAC algorithms
682    Hmac,
683
684    /// RSA-PSS mask generating functions
685    Mgf,
686
687    /// Opaque data types
688    Opaque(OpaqueDataAlgorithm),
689
690    /// RSA algorithms (signing and encryption)
691    Rsa,
692
693    /// SSH template algorithms
694    Template,
695
696    /// Object wrap (i.e. HSM-to-HSM encryption) algorithms
697    Wrap(WrapKeyKind),
698
699    /// Yubico OTP algorithms
700    YubicoOtp,
701}
702
703impl From<YubiHsmAlgorithm> for ObjectAlgorithm {
704    fn from(value: YubiHsmAlgorithm) -> Self {
705        match value {
706            YubiHsmAlgorithm::Asymmetric(algorithm) => Self::Asymmetric(algorithm.into()),
707            YubiHsmAlgorithm::Authentication(_) => Self::Authentication,
708            YubiHsmAlgorithm::Ecdh(_) => Self::Ecdh,
709            YubiHsmAlgorithm::Ecdsa(_) => Self::Ecdsa,
710            YubiHsmAlgorithm::Hmac(_) => Self::Hmac,
711            YubiHsmAlgorithm::Mgf(_) => Self::Mgf,
712            YubiHsmAlgorithm::Opaque(algorithm) => Self::Opaque(algorithm.into()),
713            YubiHsmAlgorithm::Rsa(_) => Self::Rsa,
714            YubiHsmAlgorithm::Template(_) => Self::Template,
715            YubiHsmAlgorithm::Wrap(algorithm) => Self::Wrap(algorithm.into()),
716            YubiHsmAlgorithm::YubicoOtp(_) => Self::YubicoOtp,
717        }
718    }
719}
720
721#[cfg(test)]
722mod tests {
723    use std::io::Write;
724
725    use rand::{
726        distributions::{Alphanumeric, DistString},
727        thread_rng,
728    };
729    use rstest::{fixture, rstest};
730    use tempfile::{NamedTempFile, TempDir};
731    use testresult::TestResult;
732
733    use super::*;
734
735    /// Ensures that [`Domains::to_string`] works as expected.
736    #[test]
737    fn domains_to_string() {
738        let domain_list = vec![Domain::One];
739        let domains = Domains::from(domain_list.as_slice());
740        assert_eq!("1", domains.to_string());
741
742        let domain_list = vec![Domain::One, Domain::Two];
743        let domains = Domains::from(domain_list.as_slice());
744        assert_eq!("1, 2", domains.to_string());
745    }
746
747    #[test]
748    fn authentication_key_try_from_path_succeeds() -> TestResult {
749        let file = {
750            let mut file = NamedTempFile::new()?;
751            let passphrase = Alphanumeric.sample_string(&mut thread_rng(), 30);
752            file.write_all(passphrase.as_bytes())?;
753            file
754        };
755
756        match AuthenticationKey::try_from(file.path()) {
757            Ok(_) => {}
758            Err(error) => panic!(
759                "Expected to create an authentication key from the contents of a file, but got an error instead: {error}"
760            ),
761        }
762
763        Ok(())
764    }
765
766    #[test]
767    fn authentication_key_try_from_path_fails_on_short_passphrase() -> TestResult {
768        let file = {
769            let mut file = NamedTempFile::new()?;
770            let passphrase = Alphanumeric.sample_string(&mut thread_rng(), 10);
771            file.write_all(passphrase.as_bytes())?;
772            file
773        };
774
775        match AuthenticationKey::try_from(file.path()) {
776            Ok(_) => panic!(
777                "Expected to fail with Error::Length, but succeeded in creating an authentication key from a passphrase file instead."
778            ),
779            Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(_))) => {}
780            Err(error) => panic!(
781                "Expected to fail with Error::Length, but failed with a different error instead: {error}"
782            ),
783        }
784
785        Ok(())
786    }
787
788    #[test]
789    fn authentication_key_try_from_path_fails_on_file_is_dir() -> TestResult {
790        let file = TempDir::new()?;
791
792        match AuthenticationKey::try_from(file.path()) {
793            Ok(_) => panic!(
794                "Expected to fail with Error::IoPath, but succeeded in creating an authentication key from a passphrase file instead."
795            ),
796            Err(crate::Error::IoPath { .. }) => {}
797            Err(error) => panic!(
798                "Expected to fail with Error::IoPath, but failed with a different error instead: {error}"
799            ),
800        }
801
802        Ok(())
803    }
804
805    #[test]
806    fn authentication_key_try_from_passphrase_succeeds() -> TestResult {
807        let passphrase = Passphrase::generate(Some(30));
808
809        match AuthenticationKey::try_from(&passphrase) {
810            Ok(_) => {}
811            Err(error) => panic!(
812                "Expected to create an authentication key from a passphrase, but got an error instead: {error}"
813            ),
814        }
815
816        Ok(())
817    }
818
819    #[test]
820    fn authentication_key_try_from_passphrase_fails_on_passphrase_too_short() -> TestResult {
821        let passphrase = Passphrase::new("passphrase".to_string());
822
823        match AuthenticationKey::try_from(&passphrase) {
824            Ok(_) => panic!("Expected to fail with Error::Length, but succeeded instead."),
825            Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(_))) => {}
826            Err(error) => panic!(
827                "Expected to fail with Error::Length, but failed with a different error instead: {error}"
828            ),
829        }
830
831        Ok(())
832    }
833
834    /// Ensures that [`WrapKeyKind::key_len`] returns the correct number for each variant.
835    #[rstest]
836    #[case(WrapKeyKind::Aes128, 16)]
837    #[case(WrapKeyKind::Aes192, 24)]
838    #[case(WrapKeyKind::Aes256, 32)]
839    fn wrap_key_kind_key_len(#[case] wrap_key_kind: WrapKeyKind, #[case] len: usize) {
840        assert_eq!(wrap_key_kind.key_len(), len);
841    }
842
843    /// Ensures that variants of [`YubiHsmWrapAlgorithm`] can be created from [`WrapKeyKind`]
844    /// variants.
845    #[rstest]
846    #[case(WrapKeyKind::Aes128, YubiHsmWrapAlgorithm::Aes128Ccm)]
847    #[case(WrapKeyKind::Aes192, YubiHsmWrapAlgorithm::Aes192Ccm)]
848    #[case(WrapKeyKind::Aes256, YubiHsmWrapAlgorithm::Aes256Ccm)]
849    fn yubihsm_wrap_algorithm_from_wrap_key_kind(
850        #[case] wrap_key_kind: WrapKeyKind,
851        #[case] algorithm: YubiHsmWrapAlgorithm,
852    ) {
853        assert_eq!(YubiHsmWrapAlgorithm::from(&wrap_key_kind), algorithm);
854    }
855
856    /// Ensures that [`WrapKey::generate_random`] creates a [`WrapKey`] based on a [`WrapKeyKind`].
857    #[rstest]
858    #[case(WrapKeyKind::Aes128)]
859    #[case(WrapKeyKind::Aes192)]
860    #[case(WrapKeyKind::Aes256)]
861    fn wrap_key_generate_random_succeeds(#[case] wrap_key_kind: WrapKeyKind) -> TestResult {
862        let wrap_key = WrapKey::generate_random(wrap_key_kind)?;
863        let data: Vec<u8> = From::from(&wrap_key);
864
865        assert_eq!(data.len(), wrap_key_kind.key_len());
866
867        Ok(())
868    }
869
870    /// Ensures that the [`Debug`] representation of [`WrapKey`] contains the correct data.
871    #[rstest]
872    #[case(WrapKeyKind::Aes128)]
873    #[case(WrapKeyKind::Aes192)]
874    #[case(WrapKeyKind::Aes256)]
875    fn wrap_key_debug(#[case] wrap_key_kind: WrapKeyKind) -> TestResult {
876        let wrap_key = WrapKey::generate_random(wrap_key_kind)?;
877        let data_debug = format!("{:?}", wrap_key.data.to_vec());
878        let wrap_key_debug = format!("{wrap_key:?}");
879        let wrap_key_kind_debug = format!("{wrap_key_kind:?}");
880
881        assert!(wrap_key_debug.contains(&wrap_key_kind_debug));
882        assert!(wrap_key_debug.contains("[REDACTED]"));
883        assert!(!wrap_key_debug.contains(&data_debug));
884
885        Ok(())
886    }
887
888    /// A valid [`Passphrase`] for a [`WrapKey`].
889    #[fixture]
890    fn valid_wrap_key_passphrase() -> Passphrase {
891        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())
892    }
893
894    /// An invalid [`Passphrase`] for a [`WrapKey`].
895    #[fixture]
896    fn invalid_wrap_key_passphrase() -> Passphrase {
897        Passphrase::new("this passphrase is shorter than 100 chars".to_string())
898    }
899
900    /// Ensures that [`WrapKeyFromPassphrase::new`] succeeds with sufficiently long passphrases.
901    #[rstest]
902    #[case(WrapKeyKind::Aes128)]
903    #[case(WrapKeyKind::Aes192)]
904    #[case(WrapKeyKind::Aes256)]
905    fn wrap_key_from_passphrase_new_succeeds(
906        #[case] wrap_key_kind: WrapKeyKind,
907        valid_wrap_key_passphrase: Passphrase,
908    ) -> TestResult {
909        WrapKeyFromPassphrase::new(&valid_wrap_key_passphrase, wrap_key_kind)?;
910
911        Ok(())
912    }
913
914    /// Ensures that [`WrapKeyFromPassphrase::new`] fails on invalid passphrases.
915    #[rstest]
916    #[case(WrapKeyKind::Aes128)]
917    #[case(WrapKeyKind::Aes192)]
918    #[case(WrapKeyKind::Aes256)]
919    fn wrap_key_from_passphrase_new_fails_on_short_passphrase(
920        #[case] wrap_key_kind: WrapKeyKind,
921        invalid_wrap_key_passphrase: Passphrase,
922    ) -> TestResult {
923        assert!(WrapKeyFromPassphrase::new(&invalid_wrap_key_passphrase, wrap_key_kind).is_err());
924
925        Ok(())
926    }
927
928    /// Ensures that creating a [`WrapKey`] from a [`WrapKeyFromPassphrase`] succeeds on valid data.
929    #[rstest]
930    #[case(WrapKeyKind::Aes128)]
931    #[case(WrapKeyKind::Aes192)]
932    #[case(WrapKeyKind::Aes256)]
933    fn wrap_key_try_from_wrap_key_from_passphrase_succeeds(
934        #[case] wrap_key_kind: WrapKeyKind,
935        valid_wrap_key_passphrase: Passphrase,
936    ) -> TestResult {
937        let wrap_key_from_passphrase =
938            WrapKeyFromPassphrase::new(&valid_wrap_key_passphrase, wrap_key_kind)?;
939        let wrap_key = WrapKey::try_from(wrap_key_from_passphrase)?;
940        let data: Vec<u8> = From::from(&wrap_key);
941
942        assert_eq!(data.len(), wrap_key_kind.key_len());
943
944        Ok(())
945    }
946
947    /// Ensures that creating a [`YubiHsmWrapKey`] from a [`WrapKey`] succeeds with valid data.
948    #[rstest]
949    #[case(WrapKeyKind::Aes128)]
950    #[case(WrapKeyKind::Aes192)]
951    #[case(WrapKeyKind::Aes256)]
952    fn yubihsm_wrap_key_try_from_yubihsm_wrap_key_from_wrap_key_succeeds(
953        #[case] wrap_key_kind: WrapKeyKind,
954        valid_wrap_key_passphrase: Passphrase,
955    ) -> TestResult {
956        let wrap_key = {
957            let wrap_key_from_passphrase =
958                WrapKeyFromPassphrase::new(&valid_wrap_key_passphrase, wrap_key_kind)?;
959            WrapKey::try_from(wrap_key_from_passphrase)?
960        };
961        let yubihsm_wrap_key_from_wrap_key = YubiHsmWrapKeyFromWrapKey {
962            id: "1".parse()?,
963            wrap_key: &wrap_key,
964        };
965
966        YubiHsmWrapKey::try_from(&yubihsm_wrap_key_from_wrap_key)?;
967
968        Ok(())
969    }
970}