signstar_config/config/
credentials.rs

1//! Credentials handling for [`SignstarConfig`].
2
3use std::{fmt::Display, str::FromStr};
4
5use nix::unistd::User;
6use serde::{Deserialize, Serialize};
7use ssh_key::authorized_keys::Entry;
8use zeroize::Zeroize;
9
10use crate::ConfigError;
11#[cfg(doc)]
12use crate::SignstarConfig;
13
14/// The name of a user on a Unix system
15///
16/// The username may only contain characters in the set of alphanumeric ASCII characters and the
17/// `-`, or `_` character.
18#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Zeroize)]
19#[serde(into = "String", try_from = "String")]
20pub struct SystemUserId(String);
21
22impl SystemUserId {
23    /// Creates a new [`SystemUserId`]
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if `user` contains chars other than alphanumeric ones, `-`, or `_`.
28    ///
29    /// # Examples
30    ///
31    /// ```
32    /// use signstar_config::SystemUserId;
33    ///
34    /// # fn main() -> testresult::TestResult {
35    /// SystemUserId::new("user1".to_string())?;
36    /// SystemUserId::new("User_1".to_string())?;
37    /// assert!(SystemUserId::new("?ser-1".to_string()).is_err());
38    /// # Ok(())
39    /// # }
40    /// ```
41    pub fn new(user: String) -> Result<Self, crate::Error> {
42        if user.is_empty()
43            || !(user
44                .chars()
45                .all(|char| char.is_ascii_alphanumeric() || char == '_' || char == '-'))
46        {
47            return Err(ConfigError::InvalidSystemUserName { name: user }.into());
48        }
49        Ok(Self(user))
50    }
51}
52
53impl AsRef<str> for SystemUserId {
54    fn as_ref(&self) -> &str {
55        &self.0
56    }
57}
58
59impl Display for SystemUserId {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        self.0.fmt(f)
62    }
63}
64
65impl From<SystemUserId> for String {
66    fn from(value: SystemUserId) -> Self {
67        value.0
68    }
69}
70
71impl FromStr for SystemUserId {
72    type Err = crate::Error;
73
74    fn from_str(s: &str) -> Result<Self, Self::Err> {
75        Self::new(s.to_string())
76    }
77}
78
79impl TryFrom<String> for SystemUserId {
80    type Error = crate::Error;
81
82    fn try_from(value: String) -> Result<Self, Self::Error> {
83        Self::new(value)
84    }
85}
86
87impl TryFrom<User> for SystemUserId {
88    type Error = crate::Error;
89
90    fn try_from(value: User) -> Result<Self, Self::Error> {
91        Self::new(value.name)
92    }
93}
94
95/// An entry of an authorized_keys file
96///
97/// This type ensures compliance with SSH's [AuhtorizedKeysFile] format.
98///
99/// [AuhtorizedKeysFile]: https://man.archlinux.org/man/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT
100#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
101#[serde(into = "String", try_from = "String")]
102pub struct AuthorizedKeyEntry(Entry);
103
104impl AuthorizedKeyEntry {
105    /// Creates a new [`AuthorizedKeyEntry`]
106    ///
107    /// # Errors
108    ///
109    /// Returns an error, if `data` can not be converted to an
110    /// [`ssh_key::authorized_keys::Entry`].
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use signstar_config::AuthorizedKeyEntry;
116    ///
117    /// # fn main() -> testresult::TestResult {
118    /// let auth_key = AuthorizedKeyEntry::new("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".to_string())?;
119    /// assert_eq!("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host", auth_key.to_string());
120    ///
121    /// // this fails because the empty string is not a valid AuthorizedKeyEntry
122    /// assert!(AuthorizedKeyEntry::new("".to_string()).is_err());
123    /// # Ok(())
124    /// # }
125    /// ```
126    pub fn new(entry: String) -> Result<Self, crate::Error> {
127        Ok(Self(Entry::from_str(&entry).map_err(|_source| {
128            ConfigError::InvalidAuthorizedKeyEntry { entry }
129        })?))
130    }
131}
132
133impl AsRef<Entry> for AuthorizedKeyEntry {
134    fn as_ref(&self) -> &Entry {
135        &self.0
136    }
137}
138
139impl Display for AuthorizedKeyEntry {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        write!(f, "{}", self.0.to_string())
142    }
143}
144
145impl From<AuthorizedKeyEntry> for String {
146    fn from(value: AuthorizedKeyEntry) -> Self {
147        value.to_string()
148    }
149}
150
151impl FromStr for AuthorizedKeyEntry {
152    type Err = crate::Error;
153
154    fn from_str(s: &str) -> Result<Self, Self::Err> {
155        Self::new(s.to_string())
156    }
157}
158
159impl From<&AuthorizedKeyEntry> for Entry {
160    fn from(value: &AuthorizedKeyEntry) -> Self {
161        value.0.clone()
162    }
163}
164
165impl TryFrom<String> for AuthorizedKeyEntry {
166    type Error = crate::Error;
167
168    fn try_from(value: String) -> Result<Self, crate::Error> {
169        Self::new(value)
170    }
171}
172
173impl std::hash::Hash for AuthorizedKeyEntry {
174    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
175        self.0.to_string().hash(state);
176    }
177}
178
179impl Ord for AuthorizedKeyEntry {
180    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
181        self.0.to_string().cmp(&other.0.to_string())
182    }
183}
184
185impl PartialOrd for AuthorizedKeyEntry {
186    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
187        Some(self.cmp(other))
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use testresult::TestResult;
194
195    use super::*;
196
197    #[test]
198    fn system_user_id_new_fails() {
199        assert!(SystemUserId::new("üser".to_string()).is_err());
200    }
201
202    #[test]
203    fn authorized_key_entry_new_fails() {
204        assert!(AuthorizedKeyEntry::new("foo".to_string()).is_err());
205    }
206
207    #[test]
208    fn authorized_key_to_string() -> TestResult {
209        let entry = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host";
210        let authorized_key = AuthorizedKeyEntry::new(entry.to_string())?;
211
212        assert_eq!(authorized_key.to_string(), entry);
213        Ok(())
214    }
215}