Skip to main content

signstar_yubihsm2/object/
key.rs

1//! YubiHSM2 key metadata.
2
3use std::{collections::BTreeSet, fmt::Display, fs::read_to_string, hash::Hash, path::Path};
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7#[cfg(feature = "serde")]
8use serde_repr::{Deserialize_repr, Serialize_repr};
9use signstar_crypto::passphrase::{Passphrase, PassphrasePolicy};
10use strum::{AsRefStr, IntoStaticStr};
11use yubihsm::authentication::Key as YubiHsmAuthenticationKey;
12
13use crate::object::{Capabilities, Id};
14
15/// YubiHSM2 object domain.
16///
17/// Objects can belong to one or many domains on the YubiHSM2.
18/// See [Core Concepts - Domains](https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#domains) for more details.
19#[derive(
20    AsRefStr,
21    Clone,
22    Copy,
23    Debug,
24    strum::Display,
25    Eq,
26    Hash,
27    IntoStaticStr,
28    Ord,
29    PartialEq,
30    PartialOrd,
31)]
32#[cfg_attr(feature = "serde", derive(Deserialize_repr, Serialize_repr))]
33#[repr(u8)]
34pub enum Domain {
35    /// First domain.
36    #[strum(serialize = "1")]
37    One = 1,
38    /// Second domain.
39    #[strum(serialize = "2")]
40    Two = 2,
41    /// Third domain.
42    #[strum(serialize = "3")]
43    Three = 3,
44    /// Fourth domain.
45    #[strum(serialize = "4")]
46    Four = 4,
47    /// Fifth domain.
48    #[strum(serialize = "5")]
49    Five = 5,
50    /// Sixth domain.
51    #[strum(serialize = "6")]
52    Six = 6,
53    /// Seventh domain.
54    #[strum(serialize = "7")]
55    Seven = 7,
56    /// Eighth domain.
57    #[strum(serialize = "8")]
58    Eight = 8,
59    /// Ninth domain.
60    #[strum(serialize = "9")]
61    Nine = 9,
62    /// Tenth domain.
63    #[strum(serialize = "10")]
64    Ten = 10,
65    /// Eleventh domain.
66    #[strum(serialize = "11")]
67    Eleven = 11,
68    /// Twelfth domain.
69    #[strum(serialize = "12")]
70    Twelve = 12,
71    /// Thirteenth domain.
72    #[strum(serialize = "13")]
73    Thirteen = 13,
74    /// Fourteenth domain.
75    #[strum(serialize = "14")]
76    Fourteen = 14,
77    /// Fifteenth domain.
78    #[strum(serialize = "15")]
79    Fifteen = 15,
80    /// Sixteenth domain.
81    #[strum(serialize = "16")]
82    Sixteen = 16,
83}
84
85#[cfg(feature = "cli")]
86impl clap::ValueEnum for Domain {
87    fn value_variants<'a>() -> &'a [Self] {
88        static VARIANTS: &[Domain] = &[
89            Domain::One,
90            Domain::Two,
91            Domain::Three,
92            Domain::Four,
93            Domain::Five,
94            Domain::Six,
95            Domain::Seven,
96            Domain::Eight,
97            Domain::Nine,
98            Domain::Ten,
99            Domain::Eleven,
100            Domain::Twelve,
101            Domain::Thirteen,
102            Domain::Fourteen,
103            Domain::Fifteen,
104            Domain::Sixteen,
105        ];
106        VARIANTS
107    }
108
109    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
110        let str: &'static str = self.into();
111        Some(clap::builder::PossibleValue::new(str))
112    }
113}
114
115impl From<Domain> for yubihsm::Domain {
116    fn from(value: Domain) -> Self {
117        match value {
118            Domain::One => Self::DOM1,
119            Domain::Two => Self::DOM2,
120            Domain::Three => Self::DOM3,
121            Domain::Four => Self::DOM4,
122            Domain::Five => Self::DOM5,
123            Domain::Six => Self::DOM6,
124            Domain::Seven => Self::DOM7,
125            Domain::Eight => Self::DOM8,
126            Domain::Nine => Self::DOM9,
127            Domain::Ten => Self::DOM10,
128            Domain::Eleven => Self::DOM11,
129            Domain::Twelve => Self::DOM12,
130            Domain::Thirteen => Self::DOM13,
131            Domain::Fourteen => Self::DOM14,
132            Domain::Fifteen => Self::DOM15,
133            Domain::Sixteen => Self::DOM16,
134        }
135    }
136}
137
138/// A set of domains of an object on a YubiHSM2.
139///
140/// Each object is assigned to at least one [`Domain`].
141#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
142#[cfg_attr(
143    feature = "serde",
144    derive(Serialize, Deserialize),
145    serde(try_from = "BTreeSet<Domain>")
146)]
147pub struct Domains(BTreeSet<Domain>);
148
149impl Domains {
150    /// Converts this object into raw big-endian bytes.
151    pub fn to_be_bytes(&self) -> [u8; 2] {
152        self.bits().to_be_bytes()
153    }
154
155    /// Returns set of domains containing all available domains.
156    pub fn all() -> Self {
157        yubihsm::Domain::all().bits().into()
158    }
159
160    /// Returns the underlying bits value.
161    pub fn bits(&self) -> u16 {
162        yubihsm::Domain::from(self).bits()
163    }
164}
165
166impl Display for Domains {
167    /// Formats a [`Domains`] as a string.
168    ///
169    /// Here, the domains in `self` are represented as a comma-separated list (e.g. `1, 2, 3` or
170    /// `1`).
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        write!(
173            f,
174            "{}",
175            self.0
176                .iter()
177                .map(|domain| domain.as_ref())
178                .collect::<Vec<_>>()
179                .join(", ")
180        )
181    }
182}
183
184impl From<Domain> for Domains {
185    fn from(value: Domain) -> Self {
186        Self(BTreeSet::from_iter([value]))
187    }
188}
189
190impl From<yubihsm::Domain> for Domains {
191    fn from(value: yubihsm::Domain) -> Self {
192        let lookup = [
193            (yubihsm::Domain::DOM1, Domain::One),
194            (yubihsm::Domain::DOM2, Domain::Two),
195            (yubihsm::Domain::DOM3, Domain::Three),
196            (yubihsm::Domain::DOM4, Domain::Four),
197            (yubihsm::Domain::DOM5, Domain::Five),
198            (yubihsm::Domain::DOM6, Domain::Six),
199            (yubihsm::Domain::DOM7, Domain::Seven),
200            (yubihsm::Domain::DOM8, Domain::Eight),
201            (yubihsm::Domain::DOM9, Domain::Nine),
202            (yubihsm::Domain::DOM10, Domain::Ten),
203            (yubihsm::Domain::DOM11, Domain::Eleven),
204            (yubihsm::Domain::DOM12, Domain::Twelve),
205            (yubihsm::Domain::DOM13, Domain::Thirteen),
206            (yubihsm::Domain::DOM14, Domain::Fourteen),
207            (yubihsm::Domain::DOM15, Domain::Fifteen),
208            (yubihsm::Domain::DOM16, Domain::Sixteen),
209        ];
210
211        Domains(BTreeSet::from_iter(lookup.iter().filter_map(
212            |(yubi_dom, dom)| {
213                if value.contains(*yubi_dom) {
214                    Some(*dom)
215                } else {
216                    None
217                }
218            },
219        )))
220    }
221}
222
223impl From<u16> for Domains {
224    fn from(value: u16) -> Self {
225        yubihsm::Domain::from_bits_retain(value).into()
226    }
227}
228
229impl From<&[Domain]> for Domains {
230    fn from(value: &[Domain]) -> Self {
231        Self(value.iter().copied().collect())
232    }
233}
234
235impl TryFrom<BTreeSet<Domain>> for Domains {
236    type Error = crate::object::Error;
237
238    fn try_from(domains: BTreeSet<Domain>) -> Result<Self, Self::Error> {
239        if domains.is_empty() {
240            return Err(Self::Error::EmptySetOfDomains);
241        }
242        Ok(Self(domains))
243    }
244}
245
246impl From<&Domains> for yubihsm::Domain {
247    fn from(value: &Domains) -> Self {
248        value
249            .0
250            .iter()
251            .map(|cap| yubihsm::Domain::from(*cap))
252            .fold(yubihsm::Domain::empty(), |acc, c| acc | c)
253    }
254}
255
256/// An authentication key.
257#[derive(Debug)]
258pub struct AuthenticationKey(YubiHsmAuthenticationKey);
259
260impl AuthenticationKey {
261    /// The default [`PassphrasePolicy`] for an [`AuthenticationKey`].
262    pub const PASSPHRASE_POLICY: PassphrasePolicy = PassphrasePolicy { minimum_length: 30 };
263}
264
265impl AsRef<YubiHsmAuthenticationKey> for AuthenticationKey {
266    fn as_ref(&self) -> &YubiHsmAuthenticationKey {
267        &self.0
268    }
269}
270
271impl From<AuthenticationKey> for YubiHsmAuthenticationKey {
272    fn from(value: AuthenticationKey) -> Self {
273        value.0
274    }
275}
276
277impl From<&AuthenticationKey> for YubiHsmAuthenticationKey {
278    fn from(value: &AuthenticationKey) -> Self {
279        value.0.clone()
280    }
281}
282
283impl TryFrom<&Path> for AuthenticationKey {
284    type Error = crate::Error;
285
286    /// Creates a new [`AuthenticationKey`] from the contents of `file`.
287    ///
288    /// The contents of `file` must be a valid UTF-8 string that satisfies the default
289    /// [`PassphrasePolicy`].
290    ///
291    /// # Errors
292    ///
293    /// Returns an error if
294    ///
295    /// - the contents of `file` cannot be read to a valid UTF-8 encoded string
296    /// - the contents of `file` do not satisfy the requirements of [`Self::PASSPHRASE_POLICY`]
297    fn try_from(file: &Path) -> Result<Self, Self::Error> {
298        let passphrase = Passphrase::new_with_policy(
299            read_to_string(file).map_err(|source| crate::Error::IoPath {
300                path: file.into(),
301                context: "reading the passphrase for an authentication key derivation from file",
302                source,
303            })?,
304            &Self::PASSPHRASE_POLICY,
305        )?;
306
307        Ok(Self(YubiHsmAuthenticationKey::derive_from_password(
308            passphrase.expose_borrowed().as_bytes(),
309        )))
310    }
311}
312
313impl TryFrom<&Passphrase> for AuthenticationKey {
314    type Error = crate::Error;
315
316    /// Creates a new [`AuthenticationKey`] from a [`Passphrase`].
317    ///
318    /// # Errors
319    ///
320    /// Returns an error, if
321    ///
322    /// - the `passphrase` does not satisfy the requirements of [`Self::PASSPHRASE_POLICY`]
323    fn try_from(passphrase: &Passphrase) -> Result<Self, Self::Error> {
324        passphrase.check_against_policy(&Self::PASSPHRASE_POLICY)?;
325
326        Ok(Self(YubiHsmAuthenticationKey::derive_from_password(
327            passphrase.expose_borrowed().as_bytes(),
328        )))
329    }
330}
331
332/// Metadata about a key stored on a YubiHSM2.
333///
334/// This struct stores common parameters of keys regardless of their usage may describe
335/// authentication, wrapping and signing keys.
336#[derive(Clone, Debug)]
337#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
338pub struct KeyInfo {
339    /// Inner identifier used to track the key on the YubiHSM2.
340    pub key_id: Id,
341
342    /// Key domain.
343    ///
344    /// Must be in range `1..16`.
345    /// See [Core Concepts - Domains](https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-core-concepts.html#domains).
346    pub domains: Domains,
347
348    /// Capabilities of this key.
349    pub caps: Capabilities,
350}
351
352#[cfg(test)]
353mod tests {
354    use std::io::Write;
355
356    use rand::{
357        distributions::{Alphanumeric, DistString},
358        thread_rng,
359    };
360    use tempfile::{NamedTempFile, TempDir};
361    use testresult::TestResult;
362
363    use super::*;
364
365    /// Ensures that [`Domains::to_string`] works as expected.
366    #[test]
367    fn domains_to_string() {
368        let domain_list = vec![Domain::One];
369        let domains = Domains::from(domain_list.as_slice());
370        assert_eq!("1", domains.to_string());
371
372        let domain_list = vec![Domain::One, Domain::Two];
373        let domains = Domains::from(domain_list.as_slice());
374        assert_eq!("1, 2", domains.to_string());
375    }
376
377    #[test]
378    fn authentication_key_try_from_path_succeeds() -> TestResult {
379        let file = {
380            let mut file = NamedTempFile::new()?;
381            let passphrase = Alphanumeric.sample_string(&mut thread_rng(), 30);
382            file.write_all(passphrase.as_bytes())?;
383            file
384        };
385
386        match AuthenticationKey::try_from(file.path()) {
387            Ok(_) => {}
388            Err(error) => panic!(
389                "Expected to create an authentication key from the contents of a file, but got an error instead: {error}"
390            ),
391        }
392
393        Ok(())
394    }
395
396    #[test]
397    fn authentication_key_try_from_path_fails_on_short_passphrase() -> TestResult {
398        let file = {
399            let mut file = NamedTempFile::new()?;
400            let passphrase = Alphanumeric.sample_string(&mut thread_rng(), 10);
401            file.write_all(passphrase.as_bytes())?;
402            file
403        };
404
405        match AuthenticationKey::try_from(file.path()) {
406            Ok(_) => panic!(
407                "Expected to fail with Error::Length, but succeeded in creating an authentication key from a passphrase file instead."
408            ),
409            Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(_))) => {}
410            Err(error) => panic!(
411                "Expected to fail with Error::Length, but failed with a different error instead: {error}"
412            ),
413        }
414
415        Ok(())
416    }
417
418    #[test]
419    fn authentication_key_try_from_path_fails_on_file_is_dir() -> TestResult {
420        let file = TempDir::new()?;
421
422        match AuthenticationKey::try_from(file.path()) {
423            Ok(_) => panic!(
424                "Expected to fail with Error::IoPath, but succeeded in creating an authentication key from a passphrase file instead."
425            ),
426            Err(crate::Error::IoPath { .. }) => {}
427            Err(error) => panic!(
428                "Expected to fail with Error::IoPath, but failed with a different error instead: {error}"
429            ),
430        }
431
432        Ok(())
433    }
434
435    #[test]
436    fn authentication_key_try_from_passphrase_succeeds() -> TestResult {
437        let passphrase = Passphrase::generate(Some(30));
438
439        match AuthenticationKey::try_from(&passphrase) {
440            Ok(_) => {}
441            Err(error) => panic!(
442                "Expected to create an authentication key from a passphrase, but got an error instead: {error}"
443            ),
444        }
445
446        Ok(())
447    }
448
449    #[test]
450    fn authentication_key_try_from_passphrase_fails_on_passphrase_too_short() -> TestResult {
451        let passphrase = Passphrase::new("passphrase".to_string());
452
453        match AuthenticationKey::try_from(&passphrase) {
454            Ok(_) => panic!("Expected to fail with Error::Length, but succeeded instead."),
455            Err(crate::Error::SignstarCrypto(signstar_crypto::Error::Passphrase(_))) => {}
456            Err(error) => panic!(
457                "Expected to fail with Error::Length, but failed with a different error instead: {error}"
458            ),
459        }
460
461        Ok(())
462    }
463}