signstar_config/admin_credentials.rs
1//! Administrative credentials handling for a NetHSM backend.
2
3use std::{
4 fs::{File, Permissions, read_to_string, set_permissions},
5 io::Write,
6 os::unix::fs::{PermissionsExt, chown},
7 path::{Path, PathBuf},
8 process::{Command, Stdio},
9};
10
11#[cfg(doc)]
12use nethsm::NetHsm;
13use serde::{de::DeserializeOwned, ser::Serialize};
14use signstar_common::{
15 admin_credentials::{
16 create_credentials_dir,
17 get_plaintext_credentials_file,
18 get_systemd_creds_credentials_file,
19 },
20 common::SECRET_FILE_MODE,
21};
22
23use crate::{
24 AdministrativeSecretHandling,
25 utils::{fail_if_not_root, get_command, get_current_system_user},
26};
27
28/// An error that may occur when handling administrative credentials for a NetHSM backend.
29#[derive(Debug, thiserror::Error)]
30pub enum Error {
31 /// There is no top-level administrator.
32 #[error("There is no top-level administrator but at least one is required")]
33 AdministratorMissing,
34
35 /// There is no top-level administrator with the name "admin".
36 #[error("The default top-level administrator \"admin\" is missing")]
37 AdministratorNoDefault,
38
39 /// A credentials file can not be created.
40 #[error("The credentials file {path} can not be created:\n{source}")]
41 CredsFileCreate {
42 /// The path to a credentials file administrative secrets can not be stored.
43 path: PathBuf,
44 /// The source error.
45 source: std::io::Error,
46 },
47
48 /// A credentials file does not exist.
49 #[error("The credentials file {path} does not exist")]
50 CredsFileMissing {
51 /// The path to a missing credentials file.
52 path: PathBuf,
53 },
54
55 /// A credentials file is not a file.
56 #[error("The credentials file {path} is not a file")]
57 CredsFileNotAFile {
58 /// The path to a credentials file that is not a file.
59 path: PathBuf,
60 },
61
62 /// A credentials file can not be written to.
63 #[error("The credentials file {path} can not be written to:\n{source}")]
64 CredsFileWrite {
65 /// The path to a credentials file that can not be written to.
66 path: PathBuf,
67 /// The source error
68 source: std::io::Error,
69 },
70
71 /// A passphrase is too short.
72 #[error(
73 "The passphrase for {context} is too short (should be at least {minimum_length} characters)"
74 )]
75 PassphraseTooShort {
76 /// The context in which the passphrase is used.
77 ///
78 /// This is inserted into the sentence "The _context_ passphrase is not long enough"
79 context: String,
80
81 /// The minimum length of a passphrase.
82 minimum_length: usize,
83 },
84}
85
86/// Administrative credentials.
87///
88/// Requires implementations to also derive [`DeserializeOwned`] and [`Serialize`].
89///
90/// Provides blanket implementations for loading of administrative credentials from default system
91/// locations ([`AdminCredentials::load`]) and specific paths
92/// ([`AdminCredentials::load_from_file`]), as well as storing of administrative credentials in the
93/// default system location ([`AdminCredentials::store`]).
94/// Technically, only the implementation of [`AdminCredentials::validate`] is required.
95pub trait AdminCredentials: DeserializeOwned + Serialize {
96 /// Loads an [`AdminCredentials`] from the default file location.
97 ///
98 /// # Errors
99 ///
100 /// Returns an error if [`AdminCredentials::load_from_file`] fails.
101 ///
102 /// # Panics
103 ///
104 /// This method panics when providing [`AdministrativeSecretHandling::ShamirsSecretSharing`]
105 /// as `secrets_handling`.
106 fn load(secrets_handling: AdministrativeSecretHandling) -> Result<Self, crate::Error> {
107 // fail if not running as root
108 fail_if_not_root(&get_current_system_user()?)?;
109
110 Self::load_from_file(
111 match secrets_handling {
112 AdministrativeSecretHandling::Plaintext => get_plaintext_credentials_file(),
113 AdministrativeSecretHandling::SystemdCreds => get_systemd_creds_credentials_file(),
114 AdministrativeSecretHandling::ShamirsSecretSharing => {
115 unimplemented!("Shamir's Secret Sharing is not yet supported")
116 }
117 },
118 secrets_handling,
119 )
120 }
121
122 /// Loads an [`AdminCredentials`] from file.
123 /// # Errors
124 ///
125 /// Returns an error if
126 ///
127 /// - the method is called by a system user that is not root,
128 /// - the file at `path` does not exist,
129 /// - the file at `path` is not a file,
130 /// - the file at `path` is considered as plaintext but can not be loaded,
131 /// - the file at `path` is considered as [systemd-creds] encrypted but can not be decrypted,
132 /// - or the file at `path` is considered as [systemd-creds] encrypted but can not be loaded
133 /// after decryption.
134 ///
135 /// # Panics
136 ///
137 /// This method panics when providing [`AdministrativeSecretHandling::ShamirsSecretSharing`]
138 /// as `secrets_handling`.
139 ///
140 /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
141 fn load_from_file(
142 path: impl AsRef<Path>,
143 secrets_handling: AdministrativeSecretHandling,
144 ) -> Result<Self, crate::Error> {
145 let path = path.as_ref();
146 if !path.exists() {
147 return Err(crate::Error::AdminSecretHandling(Error::CredsFileMissing {
148 path: path.to_path_buf(),
149 }));
150 }
151 if !path.is_file() {
152 return Err(crate::Error::AdminSecretHandling(
153 Error::CredsFileNotAFile {
154 path: path.to_path_buf(),
155 },
156 ));
157 }
158
159 let config: Self = match secrets_handling {
160 AdministrativeSecretHandling::Plaintext => toml::from_str(
161 &read_to_string(path).map_err(|source| crate::Error::IoPath {
162 path: path.to_path_buf(),
163 context: "reading administrative credentials",
164 source,
165 })?,
166 )
167 .map_err(|source| crate::Error::TomlRead {
168 path: path.to_path_buf(),
169 context: "deserializing a TOML string as administrative credentials",
170 source: Box::new(source),
171 })?,
172 AdministrativeSecretHandling::SystemdCreds => {
173 // Decrypt the credentials using systemd-creds.
174 let creds_command = get_command("systemd-creds")?;
175 let mut command = Command::new(creds_command);
176 let command = command.arg("decrypt").arg(path).arg("-");
177 let command_output =
178 command
179 .output()
180 .map_err(|source| crate::Error::CommandExec {
181 command: format!("{command:?}"),
182 source,
183 })?;
184 if !command_output.status.success() {
185 return Err(crate::Error::CommandNonZero {
186 command: format!("{command:?}"),
187 exit_status: command_output.status,
188 stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
189 });
190 }
191
192 // Read the resulting TOML string from stdout and construct an AdminCredentials
193 // from it.
194 let config_str = String::from_utf8(command_output.stdout).map_err(|source| {
195 crate::Error::Utf8String {
196 path: path.to_path_buf(),
197 context: "after decrypting".to_string(),
198 source,
199 }
200 })?;
201 toml::from_str(&config_str).map_err(|source| crate::Error::TomlRead {
202 path: path.to_path_buf(),
203 context: "deserializing a TOML string as administrative credentials",
204 source: Box::new(source),
205 })?
206 }
207 AdministrativeSecretHandling::ShamirsSecretSharing => {
208 unimplemented!("Shamir's Secret Sharing is not yet supported")
209 }
210 };
211 config.validate()?;
212 Ok(config)
213 }
214
215 /// Stores the [`AdminCredentials`] as a file in the default location.
216 ///
217 /// Depending on `secrets_handling`, the file path and contents differ:
218 ///
219 /// - [`AdministrativeSecretHandling::Plaintext`]: the file path is defined by
220 /// [`get_plaintext_credentials_file`] and the contents are plaintext,
221 /// - [`AdministrativeSecretHandling::SystemdCreds`]: the file path is defined by
222 /// [`get_systemd_creds_credentials_file`] and the contents are [systemd-creds] encrypted.
223 ///
224 /// Automatically creates the directory in which the administrative credentials are created.
225 /// After storing the [`AdminCredentials`] as file, its file permissions and ownership are
226 /// adjusted so that it is only accessible by root.
227 ///
228 /// # Errors
229 ///
230 /// Returns an error if
231 ///
232 /// - the method is called by a system user that is not root,
233 /// - the directory for administrative credentials cannot be created,
234 /// - `self` cannot be turned into its TOML representation,
235 /// - the [systemd-creds] command is not found,
236 /// - [systemd-creds] fails to encrypt the TOML representation of `self`,
237 /// - the target file can not be created,
238 /// - the plaintext or [systemd-creds] encrypted data can not be written to file,
239 /// - or the ownership or permissions of the target file can not be adjusted.
240 ///
241 /// # Panics
242 ///
243 /// This method panics when providing [`AdministrativeSecretHandling::ShamirsSecretSharing`]
244 /// as `secrets_handling`.
245 ///
246 /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
247 fn store(&self, secrets_handling: AdministrativeSecretHandling) -> Result<(), crate::Error> {
248 // fail if not running as root
249 fail_if_not_root(&get_current_system_user()?)?;
250
251 create_credentials_dir()?;
252
253 let (config_data, path) = {
254 // Get the TOML string representation of self.
255 let config_data =
256 toml::to_string_pretty(self).map_err(|source| crate::Error::TomlWrite {
257 path: PathBuf::new(),
258 context: "serializing administrative credentials",
259 source,
260 })?;
261 match secrets_handling {
262 AdministrativeSecretHandling::Plaintext => (
263 config_data.as_bytes().to_vec(),
264 get_plaintext_credentials_file(),
265 ),
266 AdministrativeSecretHandling::SystemdCreds => {
267 // Encrypt self as systemd-creds encrypted TOML file.
268 let creds_command = get_command("systemd-creds")?;
269 let mut command = Command::new(creds_command);
270 let command = command.args(["encrypt", "-", "-"]);
271
272 let mut command_child = command
273 .stdin(Stdio::piped())
274 .stdout(Stdio::piped())
275 .spawn()
276 .map_err(|source| crate::Error::CommandBackground {
277 command: format!("{command:?}"),
278 source,
279 })?;
280 let Some(mut stdin) = command_child.stdin.take() else {
281 return Err(crate::Error::CommandAttachToStdin {
282 command: format!("{command:?}"),
283 })?;
284 };
285
286 let handle = std::thread::spawn(move || {
287 stdin.write_all(config_data.as_bytes()).map_err(|source| {
288 crate::Error::CommandWriteToStdin {
289 command: "systemd-creds encrypt - -".to_string(),
290 source,
291 }
292 })
293 });
294
295 let _handle_result = handle.join().map_err(|source| crate::Error::Thread {
296 context: format!(
297 "storing systemd-creds encrypted administrative credentials: {source:?}"
298 ),
299 })?;
300
301 let command_output = command_child.wait_with_output().map_err(|source| {
302 crate::Error::CommandExec {
303 command: format!("{command:?}"),
304 source,
305 }
306 })?;
307 if !command_output.status.success() {
308 return Err(crate::Error::CommandNonZero {
309 command: format!("{command:?}"),
310 exit_status: command_output.status,
311 stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
312 });
313 }
314 (command_output.stdout, get_systemd_creds_credentials_file())
315 }
316 AdministrativeSecretHandling::ShamirsSecretSharing => {
317 unimplemented!("Shamir's Secret Sharing is not yet supported")
318 }
319 }
320 };
321
322 // Write administrative credentials to file and adjust permission and ownership
323 // of file
324 {
325 let mut file = File::create(path.as_path()).map_err(|source| {
326 crate::Error::AdminSecretHandling(Error::CredsFileCreate {
327 path: path.clone(),
328 source,
329 })
330 })?;
331 file.write_all(&config_data).map_err(|source| {
332 crate::Error::AdminSecretHandling(Error::CredsFileWrite {
333 path: path.to_path_buf(),
334 source,
335 })
336 })?;
337 }
338 chown(&path, Some(0), Some(0)).map_err(|source| crate::Error::Chown {
339 path: path.clone(),
340 user: "root".to_string(),
341 source,
342 })?;
343 set_permissions(path.as_path(), Permissions::from_mode(SECRET_FILE_MODE)).map_err(
344 |source| crate::Error::ApplyPermissions {
345 path: path.clone(),
346 mode: SECRET_FILE_MODE,
347 source,
348 },
349 )?;
350
351 Ok(())
352 }
353
354 /// Validates the [`AdminCredentials`].
355 ///
356 /// # Errors
357 ///
358 /// This method is supposed to return an error if an assumption about the integrity of the
359 /// administrative credentials cannot be met.
360 /// It is called in the blanket implementation of [`AdminCredentials::load_from_file`].
361 fn validate(&self) -> Result<(), crate::Error>;
362}