Skip to main content

signstar_yubihsm2/
user.rs

1//! User handling for YubiHSM2 devices.
2
3use std::path::PathBuf;
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7use signstar_crypto::{passphrase::Passphrase, traits::UserWithPassphrase};
8use yubihsm::object::Id;
9
10/// Credentials for a YubiHSM2 device, that are backed by a UTF-8 encoded passphrase file.
11///
12/// Credentials are mapped to the authentication key ID and the passphrase file.
13/// The contents of the passphrase file are meant to be used as input to the key derivation function
14/// (KDF) for an authentication key.
15#[derive(Debug)]
16#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
17pub struct FileBackedCredentials {
18    pub(crate) id: Id,
19    passphrase_file: PathBuf,
20}
21
22impl TryFrom<&FileBackedCredentials> for yubihsm::Credentials {
23    type Error = crate::Error;
24
25    /// Creates a new [`yubihsm::Credentials`] from a [`FileBackedCredentials`].
26    ///
27    /// # Errors
28    ///
29    /// Returns an error if a [`Credentials`] cannot be created from the provided
30    /// [`FileBackedCredentials`].
31    fn try_from(value: &FileBackedCredentials) -> Result<Self, Self::Error> {
32        Ok(Self::from(&Credentials::try_from(value)?))
33    }
34}
35
36/// Credentials for a YubiHSM2 device.
37///
38/// Credentials are mapped to the authentication key ID and the passphrase used as key derivation
39/// function (KDF) for an authentication key.
40#[derive(Clone, Debug)]
41#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
42pub struct Credentials {
43    id: Id,
44    passphrase: Passphrase,
45}
46
47impl Credentials {
48    /// Creates a new [`Credentials`].
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// use signstar_crypto::passphrase::Passphrase;
54    /// use signstar_yubihsm2::Credentials;
55    ///
56    /// # fn main() -> testresult::TestResult {
57    /// let creds = Credentials::new("1".parse()?, "this-is-a-passphrase".parse()?);
58    /// # Ok(())
59    /// # }
60    /// ```
61    pub fn new(id: Id, passphrase: Passphrase) -> Self {
62        Self { id, passphrase }
63    }
64
65    /// Returns the [`Id`] of the [`Credentials`].
66    pub fn id(&self) -> Id {
67        self.id
68    }
69}
70
71impl UserWithPassphrase for Credentials {
72    fn user(&self) -> String {
73        self.id.to_string()
74    }
75
76    fn passphrase(&self) -> &Passphrase {
77        &self.passphrase
78    }
79}
80
81impl From<&Credentials> for yubihsm::Credentials {
82    fn from(value: &Credentials) -> Self {
83        Self::from_password(value.id, value.passphrase.expose_borrowed().as_bytes())
84    }
85}
86
87impl TryFrom<&FileBackedCredentials> for Credentials {
88    type Error = crate::Error;
89
90    /// Creates a new [`Credentials`] from a [`FileBackedCredentials`].
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if a [`Passphrase`] cannot be read from the passphrase file path of the
95    /// provided [`FileBackedCredentials`].
96    fn try_from(value: &FileBackedCredentials) -> Result<Self, Self::Error> {
97        Ok(Credentials::new(
98            value.id,
99            Passphrase::try_from(value.passphrase_file.as_path())?,
100        ))
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use std::io::Write;
107
108    use tempfile::{NamedTempFile, TempDir};
109    use testresult::TestResult;
110
111    use super::*;
112
113    #[test]
114    fn credentials_user_with_passphrase() -> TestResult {
115        let credentials = Credentials::new("1".parse()?, Passphrase::generate(None));
116        assert_eq!(credentials.user(), "1");
117        assert_eq!(
118            credentials.passphrase().expose_borrowed().len(),
119            Passphrase::DEFAULT_LENGTH
120        );
121
122        Ok(())
123    }
124
125    /// Ensures, that a [`yubihsm::Credentials`] can be created from a [`FileBackedCredentials`].
126    #[test]
127    fn yubihsm_credentials_try_from_file_backed_credentials_succeeds() -> TestResult {
128        let temp_file = {
129            let mut temp_file = NamedTempFile::new()?;
130            temp_file.write_all("passphrase".as_bytes())?;
131            temp_file
132        };
133        let file_backed_credentials = FileBackedCredentials {
134            id: "1".parse()?,
135            passphrase_file: temp_file.path().to_path_buf(),
136        };
137        let _creds = yubihsm::Credentials::try_from(&file_backed_credentials)?;
138
139        Ok(())
140    }
141
142    /// Ensures, that a [`yubihsm::Credentials`] cannot be created from a [`FileBackedCredentials`]
143    /// tracking a directory.
144    #[test]
145    fn yubihsm_credentials_try_from_file_backed_credentials_fails_on_dir() -> TestResult {
146        let temp_file = TempDir::new()?;
147        let file_backed_credentials = FileBackedCredentials {
148            id: "1".parse()?,
149            passphrase_file: temp_file.path().to_path_buf(),
150        };
151        assert!(yubihsm::Credentials::try_from(&file_backed_credentials).is_err());
152
153        Ok(())
154    }
155}