Skip to main content

signstar_config/
admin_credentials.rs

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