From c28d3f0e57e7a7017e081e459f3e5485b9addc11 Mon Sep 17 00:00:00 2001
From: David Runge <dvzrv@archlinux.org>
Date: Tue, 14 Jan 2025 14:32:11 +0100
Subject: [PATCH 1/2] feat: Add `signstar-common` crate to provide common
 facilities

- Adds all public data required for the `signstar-configure-build`
  crate, as it will be shared by other crates.
- Adds NetHSM specific defaults.
- Adds basic configuration file retrieval and directory handling for
  Signstar hosts.
- Adds administrative credentials file retrieval and directory handling
  for Signstar hosts.

Signed-off-by: David Runge <dvzrv@archlinux.org>
---
 .lycheeignore                            |   4 +
 Cargo.lock                               |   8 +
 Cargo.toml                               |   2 +
 signstar-common/Cargo.toml               |  14 ++
 signstar-common/README.md                |  22 +++
 signstar-common/src/admin_credentials.rs | 113 +++++++++++++
 signstar-common/src/common.rs            |  26 +++
 signstar-common/src/config.rs            | 205 +++++++++++++++++++++++
 signstar-common/src/lib.rs               |   8 +
 signstar-common/src/nethsm.rs            |  26 +++
 signstar-common/src/ssh.rs               |  31 ++++
 signstar-common/src/system_user.rs       |  62 +++++++
 12 files changed, 521 insertions(+)
 create mode 100644 signstar-common/Cargo.toml
 create mode 100644 signstar-common/README.md
 create mode 100644 signstar-common/src/admin_credentials.rs
 create mode 100644 signstar-common/src/common.rs
 create mode 100644 signstar-common/src/config.rs
 create mode 100644 signstar-common/src/lib.rs
 create mode 100644 signstar-common/src/nethsm.rs
 create mode 100644 signstar-common/src/ssh.rs
 create mode 100644 signstar-common/src/system_user.rs

diff --git a/.lycheeignore b/.lycheeignore
index 26f8b06f..75cbd132 100644
--- a/.lycheeignore
+++ b/.lycheeignore
@@ -3,3 +3,7 @@
 # an URL fragment that's used in format! macro
 # see: https://github.com/lycheeverse/lychee/issues/1492
 https://raw.githubusercontent.com/Nitrokey/nethsm-sdk-py/main/tests/%7B%7D
+
+# URLs that only become available after release of a component
+https://signstar.archlinux.page/rustdoc/signstar_common/
+https://docs.rs/signstar_common/latest/signstar_common/
diff --git a/Cargo.lock b/Cargo.lock
index fa094f4d..d9cdcaef 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3133,6 +3133,14 @@ dependencies = [
  "rand_core",
 ]
 
+[[package]]
+name = "signstar-common"
+version = "0.1.0"
+dependencies = [
+ "nethsm",
+ "thiserror 2.0.11",
+]
+
 [[package]]
 name = "signstar-configure-build"
 version = "0.1.2"
diff --git a/Cargo.toml b/Cargo.toml
index 54407d2a..6b970a36 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
   "nethsm-config",
   "nethsm-tests",
   "signstar-configure-build",
+  "signstar-common",
   "signstar-request-signature",
 ]
 
@@ -31,6 +32,7 @@ serde = { version = "1.0.215", features = ["derive"] }
 # Then adjust the tests in signstar-request-signature/src/lib.rs to additionally test
 # inputs that reference the "old_sha2".
 sha2 = "0.11.0-pre.4"
+signstar-common = { path = "signstar-common", version = "0.1.0" }
 signstar-request-signature = { path = "signstar-request-signature", version = "0.1.0" }
 strum = { version = "0.27.0", features = ["derive"] }
 testdir = "0.9.3"
diff --git a/signstar-common/Cargo.toml b/signstar-common/Cargo.toml
new file mode 100644
index 00000000..c07c559c
--- /dev/null
+++ b/signstar-common/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+authors.workspace = true
+description = "Common components for Signstar libraries and command-line interfaces"
+edition.workspace = true
+homepage.workspace = true
+keywords = ["user", "signstar", "nethsm"]
+license.workspace = true
+name = "signstar-common"
+repository.workspace = true
+version = "0.1.0"
+
+[dependencies]
+nethsm.workspace = true
+thiserror.workspace = true
diff --git a/signstar-common/README.md b/signstar-common/README.md
new file mode 100644
index 00000000..9f04895b
--- /dev/null
+++ b/signstar-common/README.md
@@ -0,0 +1,22 @@
+# Signstar common
+
+Shared components and data types for Signstar tools and libraries
+
+## Documentation
+
+- <https://signstar.archlinux.page/rustdoc/signstar_common/> for development version of the crate
+- <https://docs.rs/signstar_common/latest/signstar_common/> for released versions of the crate
+
+## Contributing
+
+Please refer to the [contributing guidelines] to learn how to contribute to this project.
+
+## License
+
+This project may be used under the terms of the [Apache-2.0] or [MIT] license.
+
+Changes to this project - unless stated otherwise - automatically fall under the terms of both of the aforementioned licenses.
+
+[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0
+[MIT]: https://opensource.org/licenses/MIT
+[contributing guidelines]: ../CONTRIBUTING.md
diff --git a/signstar-common/src/admin_credentials.rs b/signstar-common/src/admin_credentials.rs
new file mode 100644
index 00000000..eedc834e
--- /dev/null
+++ b/signstar-common/src/admin_credentials.rs
@@ -0,0 +1,113 @@
+//! Data and functions for using administrative credentials on a Signstar host.
+//!
+//! # Examples
+//!
+//! ```
+//! use signstar_common::admin_credentials::get_credentials_dir;
+//!
+//! // Get the directory path in which administrative credentials reside.
+//! println!("{:?}", get_credentials_dir());
+//! ```
+
+use std::{
+    fs::{Permissions, create_dir_all, set_permissions},
+    os::unix::fs::{PermissionsExt, chown},
+    path::PathBuf,
+};
+
+use crate::common::{CREDENTIALS_DIR_MODE, get_data_home};
+
+/// File name of plaintext administrative credentials.
+const PLAINTEXT_CREDENTIALS_FILE: &str = "admin-credentials.toml";
+
+/// File name of systemd-creds encrypted administrative credentials.
+const SYSTEMD_CREDS_CREDENTIALS_FILE: &str = "admin-credentials.creds";
+
+/// The directory for administrative credentials (encrypted and unencrypted).
+const CREDENTIALS_DIR: &str = "creds/";
+
+/// An error that may occur when handling credentials.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    /// Applying permissions to a file failed.
+    #[error("Unable to apply permissions {permissions} to {path}:\n{source}")]
+    ApplyPermissions {
+        permissions: u32,
+        path: PathBuf,
+        source: std::io::Error,
+    },
+
+    /// No plaintext administrative credentials file can be found
+    #[error("Unable to create directory {dir}:\n{source}")]
+    CreateDirectory {
+        dir: &'static str,
+        source: std::io::Error,
+    },
+
+    /// The ownership of a directory can not be set.
+    #[error("Ownership of directory {dir} can not be changed to user {system_user}: {source}")]
+    DirChangeOwner {
+        dir: PathBuf,
+        system_user: String,
+        source: std::io::Error,
+    },
+}
+
+/// Returns the path of the directory in which administrative credentials reside.
+pub fn get_credentials_dir() -> PathBuf {
+    get_data_home().join(PathBuf::from(CREDENTIALS_DIR))
+}
+
+/// Returns the file path for plaintext administrative credentials.
+pub fn get_plaintext_credentials_file() -> PathBuf {
+    get_credentials_dir().join(PathBuf::from(PLAINTEXT_CREDENTIALS_FILE))
+}
+
+/// Returns the file path for systemd-creds encrypted administrative credentials.
+pub fn get_systemd_creds_credentials_file() -> PathBuf {
+    get_credentials_dir().join(PathBuf::from(SYSTEMD_CREDS_CREDENTIALS_FILE))
+}
+
+/// Creates the directory for administrative credentials.
+///
+/// # Errors
+///
+/// Returns an error if the directory or one of its parents can not be created.
+/// Refer to [`create_dir_all`] for further information on failure scenarios.
+pub fn create_credentials_dir() -> Result<(), Error> {
+    let credentials_dir = get_credentials_dir();
+    create_dir_all(credentials_dir.as_path()).map_err(|source| Error::CreateDirectory {
+        dir: CREDENTIALS_DIR,
+        source,
+    })?;
+
+    // Set the permissions of the credentials directory to `CREDENTIALS_DIR_MODE`.
+    set_permissions(
+        credentials_dir.as_path(),
+        Permissions::from_mode(CREDENTIALS_DIR_MODE),
+    )
+    .map_err(|source| Error::ApplyPermissions {
+        permissions: CREDENTIALS_DIR_MODE,
+        path: credentials_dir.clone(),
+        source,
+    })?;
+
+    // Recursively chown all directories to root, until `DATA_HOME` is
+    // reached.
+    let data_home = get_data_home();
+    let mut chown_dir = credentials_dir.clone();
+    while chown_dir != data_home {
+        chown(&chown_dir, Some(0), Some(0)).map_err(|source| Error::DirChangeOwner {
+            dir: chown_dir.to_path_buf(),
+            system_user: "root".to_string(),
+            source,
+        })?;
+        if let Some(parent) = &chown_dir.parent() {
+            chown_dir = parent.to_path_buf()
+        } else {
+            break;
+        }
+    }
+
+    Ok(())
+}
diff --git a/signstar-common/src/common.rs b/signstar-common/src/common.rs
new file mode 100644
index 00000000..8dd86eb9
--- /dev/null
+++ b/signstar-common/src/common.rs
@@ -0,0 +1,26 @@
+//! Common data for all Signstar functionalities.
+//!
+//! # Examples
+//!
+//! ```
+//! use signstar_common::common::get_data_home;
+//!
+//! // Get the directory below which all Signstar related data is stored.
+//! println!("{:?}", get_data_home());
+//! ```
+
+use std::path::PathBuf;
+
+/// The directory below which o store all Signstar related data.
+const DATA_HOME: &str = "/var/lib/signstar/";
+
+/// The file mode of directories containing credentials.
+pub const CREDENTIALS_DIR_MODE: u32 = 0o100700;
+
+/// The file mode of secret files.
+pub const SECRET_FILE_MODE: u32 = 0o100600;
+
+/// Get the directory below which all Signstar related data is stored.
+pub fn get_data_home() -> PathBuf {
+    PathBuf::from(DATA_HOME)
+}
diff --git a/signstar-common/src/config.rs b/signstar-common/src/config.rs
new file mode 100644
index 00000000..5cba4ce1
--- /dev/null
+++ b/signstar-common/src/config.rs
@@ -0,0 +1,205 @@
+//! Default locations for Signstar configuration files.
+//!
+//! # Examples
+//!
+//! ```
+//! use signstar_common::config::{
+//!     get_config_file_or_default,
+//!     get_config_file_paths,
+//!     get_config_file,
+//!     get_default_config_dir_path,
+//!     get_default_config_file_path,
+//!     get_etc_override_config_file_path,
+//!     get_etc_override_dir_path,
+//!     get_run_override_config_file_path,
+//!     get_run_override_dir_path,
+//!     get_usr_local_override_config_file_path,
+//!     get_usr_local_override_dir_path,
+//! };
+//!
+//! // Get directory paths for Signstar configuration files.
+//! println!("{:?}", get_etc_override_dir_path());
+//! println!("{:?}", get_run_override_dir_path());
+//! println!("{:?}", get_usr_local_override_dir_path());
+//! println!("{:?}", get_default_config_dir_path());
+//!
+//! // Get file paths for Signstar configuration files.
+//! println!("{:?}", get_etc_override_config_file_path());
+//! println!("{:?}", get_run_override_config_file_path());
+//! println!("{:?}", get_usr_local_override_config_file_path());
+//! println!("{:?}", get_default_config_file_path());
+//!
+//! // Get the first config file found, according to directory precedence.
+//! println!("{:?}", get_config_file());
+//!
+//! // Get the first config file found, according to directory precedence, or the default if none are found.
+//! println!("{:?}", get_config_file_or_default());
+//!
+//! // Get all configuration file paths, sorted by directory precedence.
+//! println!("{:?}", get_config_file_paths());
+//! ```
+
+use std::{fs::create_dir_all, path::PathBuf};
+
+/// The default config directory below "/usr" for Signstar hosts.
+const DEFAULT_CONFIG_DIR: &str = "/usr/share/signstar/";
+
+/// The override config directory below "/etc" for Signstar hosts.
+const ETC_OVERRIDE_CONFIG_DIR: &str = "/etc/signstar/";
+
+/// The override config directory below "/run" for Signstar hosts.
+const RUN_OVERRIDE_CONFIG_DIR: &str = "/run/signstar/";
+
+/// The override config directory below "/usr/local" for Signstar hosts.
+const USR_LOCAL_OVERRIDE_CONFIG_DIR: &str = "/usr/local/share/signstar/";
+
+/// The filename of a Signstar configuration file.
+const CONFIG_FILE: &str = "config.toml";
+
+/// An error that may occur when handling configuration directories or files.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    /// A directory can not be created.
+    #[error("Unable to create directory {dir}:\n{source}")]
+    CreateDirectory { dir: String, source: std::io::Error },
+}
+
+/// Returns the first Signstar configuration file available, or [`None`] if none found.
+///
+/// Considers files named `config.toml` in the following directories in descending priority:
+/// - `/etc/signstar`
+/// - `/run/signstar`
+/// - `/usr/local/share/signstar`
+/// - `/usr/share/signstar`
+///
+/// The first existing file is returned.
+/// If no file is found [`None`] is returned.
+pub fn get_config_file() -> Option<PathBuf> {
+    [
+        get_etc_override_config_file_path(),
+        get_run_override_config_file_path(),
+        get_usr_local_override_config_file_path(),
+        get_default_config_file_path(),
+    ]
+    .into_iter()
+    .find(|file| file.is_file())
+}
+
+/// Returns the first Signstar configuration file available, or the default if none found.
+///
+/// Considers files named `config.toml` in the following directories in descending priority:
+/// - `/etc/signstar`
+/// - `/run/signstar`
+/// - `/usr/local/share/signstar`
+/// - `/usr/share/signstar`
+///
+/// The first existing file is returned.
+/// If no file is found, the default location `/usr/share/signstar/config.toml` is returned.
+pub fn get_config_file_or_default() -> PathBuf {
+    let Some(config) = get_config_file() else {
+        return get_default_config_file_path();
+    };
+    config
+}
+
+/// Returns a list of all configuration file locations, sorted by precedence.
+pub fn get_config_file_paths() -> Vec<PathBuf> {
+    vec![
+        get_etc_override_config_file_path(),
+        get_run_override_config_file_path(),
+        get_usr_local_override_config_file_path(),
+        get_default_config_file_path(),
+    ]
+}
+
+/// Returns the file path of the configuration file override below /etc.
+pub fn get_etc_override_config_file_path() -> PathBuf {
+    PathBuf::from([ETC_OVERRIDE_CONFIG_DIR, CONFIG_FILE].concat())
+}
+
+/// Returns the directory path of the configuration override directory below /etc.
+pub fn get_etc_override_dir_path() -> PathBuf {
+    PathBuf::from(ETC_OVERRIDE_CONFIG_DIR)
+}
+
+/// Returns the file path of the configuration file override below /run.
+pub fn get_run_override_config_file_path() -> PathBuf {
+    PathBuf::from([RUN_OVERRIDE_CONFIG_DIR, CONFIG_FILE].concat())
+}
+
+/// Returns the directory path of the configuration override directory below /run.
+pub fn get_run_override_dir_path() -> PathBuf {
+    PathBuf::from(RUN_OVERRIDE_CONFIG_DIR)
+}
+
+/// Returns the file path of the configuration file override below /usr/local.
+pub fn get_usr_local_override_config_file_path() -> PathBuf {
+    PathBuf::from([USR_LOCAL_OVERRIDE_CONFIG_DIR, CONFIG_FILE].concat())
+}
+
+/// Returns the directory path of the configuration override directory below /usr/local.
+pub fn get_usr_local_override_dir_path() -> PathBuf {
+    PathBuf::from(USR_LOCAL_OVERRIDE_CONFIG_DIR)
+}
+
+/// Returns the file path of the default configuration file /usr.
+pub fn get_default_config_file_path() -> PathBuf {
+    PathBuf::from([DEFAULT_CONFIG_DIR, CONFIG_FILE].concat())
+}
+
+/// Returns the directory path of the default configuration directory below /usr.
+pub fn get_default_config_dir_path() -> PathBuf {
+    PathBuf::from(DEFAULT_CONFIG_DIR)
+}
+
+/// Creates the default configuration directory below /usr.
+///
+/// # Errors
+///
+/// Returns an error if the directory or one of its parents can not be created.
+/// Refer to [`create_dir_all`] for further information on failure scenarios.
+pub fn create_default_config_dir() -> Result<(), Error> {
+    create_dir_all(get_default_config_dir_path()).map_err(|source| Error::CreateDirectory {
+        dir: DEFAULT_CONFIG_DIR.to_string(),
+        source,
+    })
+}
+
+/// Creates the configuration override dir below /etc.
+///
+/// # Errors
+///
+/// Returns an error if the directory or one of its parents can not be created.
+/// Refer to [`create_dir_all`] for further information on failure scenarios.
+pub fn create_etc_override_config_dir() -> Result<(), Error> {
+    create_dir_all(get_etc_override_dir_path()).map_err(|source| Error::CreateDirectory {
+        dir: ETC_OVERRIDE_CONFIG_DIR.to_string(),
+        source,
+    })
+}
+
+/// Creates the configuration override dir below /run.
+///
+/// # Errors
+///
+/// Returns an error if the directory or one of its parents can not be created.
+/// Refer to [`create_dir_all`] for further information on failure scenarios.
+pub fn create_run_override_config_dir() -> Result<(), Error> {
+    create_dir_all(get_run_override_dir_path()).map_err(|source| Error::CreateDirectory {
+        dir: RUN_OVERRIDE_CONFIG_DIR.to_string(),
+        source,
+    })
+}
+
+/// Creates the configuration override dir below /usr/local.
+///
+/// # Errors
+///
+/// Returns an error if the directory or one of its parents can not be created.
+/// Refer to [`create_dir_all`] for further information on failure scenarios.
+pub fn create_usr_local_override_config_dir() -> Result<(), Error> {
+    create_dir_all(get_usr_local_override_dir_path()).map_err(|source| Error::CreateDirectory {
+        dir: USR_LOCAL_OVERRIDE_CONFIG_DIR.to_string(),
+        source,
+    })
+}
diff --git a/signstar-common/src/lib.rs b/signstar-common/src/lib.rs
new file mode 100644
index 00000000..0f446ff0
--- /dev/null
+++ b/signstar-common/src/lib.rs
@@ -0,0 +1,8 @@
+//! Common components and data for Signstar crates.
+
+pub mod admin_credentials;
+pub mod common;
+pub mod config;
+pub mod nethsm;
+pub mod ssh;
+pub mod system_user;
diff --git a/signstar-common/src/nethsm.rs b/signstar-common/src/nethsm.rs
new file mode 100644
index 00000000..a746d85c
--- /dev/null
+++ b/signstar-common/src/nethsm.rs
@@ -0,0 +1,26 @@
+//! Defaults for NetHSM backends.
+
+use std::net::Ipv4Addr;
+
+use nethsm::LogLevel;
+
+/// The default admin user name of newly provisioned NetHSM.
+pub const USER_DEFAULT_ADMIN: &str = "admin";
+
+/// The default IP address of an unprovisioned NetHSM.
+pub const NETWORK_DEFAULT_IP_ADDRESS: Ipv4Addr = Ipv4Addr::new(192, 168, 1, 1);
+
+/// The default netmask of an unprovisioned NetHSM.
+pub const NETWORK_DEFAULT_NETMASK: Ipv4Addr = Ipv4Addr::new(255, 255, 255, 0);
+
+/// The default gateway of an unprovisioned NetHSM.
+pub const NETWORK_DEFAULT_GATEWAY: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
+
+/// The default logging IP address of an unprovisioned NetHSM.
+pub const LOGGING_DEFAULT_IP_ADDRESS: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
+
+/// The default logging port of an unprovisioned NetHSM.
+pub const LOGGING_DEFAULT_PORT: u32 = 514;
+
+/// The default logging level of an unprovisioned NetHSM.
+pub const LOGGING_DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Info;
diff --git a/signstar-common/src/ssh.rs b/signstar-common/src/ssh.rs
new file mode 100644
index 00000000..9ca2d3fe
--- /dev/null
+++ b/signstar-common/src/ssh.rs
@@ -0,0 +1,31 @@
+//! Defaults for SSH.
+//!
+//! # Examples
+//!
+//! ```
+//! use signstar_common::ssh::{get_ssh_authorized_key_base_dir, get_sshd_config_dropin_dir};
+//!
+//! // Get directory path for SSH authorized_keys files for Signstar users.
+//! println!("{:?}", get_ssh_authorized_key_base_dir());
+//!
+//! // Get directory path for sshd_config drop-in files.
+//! println!("{:?}", get_sshd_config_dropin_dir());
+//! ```
+
+use std::path::PathBuf;
+
+/// The base directory below which SSH authorized_keys files for users are located.
+const SSH_AUTHORIZED_KEY_BASE_DIR: &str = "/etc/ssh/";
+
+/// The directory below which sshd_config drop-in files are located.
+const SSHD_CONFIG_DROPIN_DIR: &str = "/etc/ssh/sshd_config.d/";
+
+/// Returns the directory path below which SSH authorized_keys files for Signstar users are located.
+pub fn get_ssh_authorized_key_base_dir() -> PathBuf {
+    PathBuf::from(SSH_AUTHORIZED_KEY_BASE_DIR)
+}
+
+/// Returns the directory path below which SSH authorized_keys files for Signstar users are located.
+pub fn get_sshd_config_dropin_dir() -> PathBuf {
+    PathBuf::from(SSHD_CONFIG_DROPIN_DIR)
+}
diff --git a/signstar-common/src/system_user.rs b/signstar-common/src/system_user.rs
new file mode 100644
index 00000000..b0406e10
--- /dev/null
+++ b/signstar-common/src/system_user.rs
@@ -0,0 +1,62 @@
+//! Defaults for system users.
+//!
+//! ```
+//! use signstar_common::system_user::{get_home_base_dir_path, get_relative_user_secrets_dir};
+//!
+//! // Get the base directory below which Signstar system user homes are located.
+//! println!("{:?}", get_home_base_dir_path());
+//!
+//! // Get the relative directory below which Signstar secrets are located per system user.
+//! println!("{:?}", get_relative_user_secrets_dir());
+//! ```
+
+use std::path::PathBuf;
+
+use crate::common::get_data_home;
+
+/// The relative base directory below which system user homes are located.
+///
+/// This directory resides relative to the data home on the system.
+const HOME_BASE_DIR: &str = "home/";
+
+/// The directory name below which credentials files are stored.
+///
+/// The directory is evaluated relative to a user's home.
+const USER_SECRETS_DIR: &str = ".local/state/signstar/secrets/";
+
+/// The file extension of plaintext credential files.
+const PLAINTEXT_SECRETS_EXTENSION: &str = "txt";
+
+/// The file extension of systemd-creds encrypted credential files.
+const SYSTEMD_CREDS_SECRETS_EXTENSION: &str = "creds";
+
+/// Returns the base directory below which Signstar system user homes are located.
+pub fn get_home_base_dir_path() -> PathBuf {
+    get_data_home().join(PathBuf::from(HOME_BASE_DIR))
+}
+
+/// Returns the relative directory below which Signstar secrets are located per system user.
+pub fn get_relative_user_secrets_dir() -> PathBuf {
+    PathBuf::from(USER_SECRETS_DIR)
+}
+
+/// Returns the path to the secrets directory for a specific system user.
+pub fn get_user_secrets_dir(system_user: &str) -> PathBuf {
+    get_home_base_dir_path()
+        .join(PathBuf::from(system_user))
+        .join(get_relative_user_secrets_dir())
+}
+
+/// Returns the path to a plaintext secrets file for a system user and backend user.
+pub fn get_plaintext_secret_file(system_user: &str, backend_user: &str) -> PathBuf {
+    get_user_secrets_dir(system_user).join(PathBuf::from(
+        [backend_user, ".", PLAINTEXT_SECRETS_EXTENSION].concat(),
+    ))
+}
+
+/// Returns the path to a systemd-creds encrypted secrets file for a system user and backend user.
+pub fn get_systemd_creds_secret_file(system_user: &str, backend_user: &str) -> PathBuf {
+    get_user_secrets_dir(system_user).join(PathBuf::from(
+        [backend_user, ".", SYSTEMD_CREDS_SECRETS_EXTENSION].concat(),
+    ))
+}
-- 
GitLab


From 891dcbf937a3bc6428a96153ac23426dd0688dbe Mon Sep 17 00:00:00 2001
From: David Runge <dvzrv@archlinux.org>
Date: Tue, 14 Jan 2025 17:07:30 +0100
Subject: [PATCH 2/2] feat: Rely on `signstar-common` crate for default
 locations

Signed-off-by: David Runge <dvzrv@archlinux.org>
---
 Cargo.lock                          |   1 +
 signstar-configure-build/Cargo.toml |   1 +
 signstar-configure-build/src/cli.rs |  59 +++++++++------
 signstar-configure-build/src/lib.rs | 112 ++++++++++++++--------------
 4 files changed, 95 insertions(+), 78 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index d9cdcaef..8a00f64c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3148,6 +3148,7 @@ dependencies = [
  "clap",
  "nethsm-config",
  "nix",
+ "signstar-common",
  "strum 0.27.1",
  "sysinfo 0.33.0",
  "thiserror 2.0.11",
diff --git a/signstar-configure-build/Cargo.toml b/signstar-configure-build/Cargo.toml
index 762d138b..f3da82e0 100644
--- a/signstar-configure-build/Cargo.toml
+++ b/signstar-configure-build/Cargo.toml
@@ -13,6 +13,7 @@ version = "0.1.2"
 clap = { workspace = true, features = ["cargo", "derive", "env"] }
 nethsm-config.workspace = true
 nix = { version = "0.29.0", features = ["user"] }
+signstar-common.workspace = true
 strum.workspace = true
 sysinfo = "0.33.0"
 thiserror.workspace = true
diff --git a/signstar-configure-build/src/cli.rs b/signstar-configure-build/src/cli.rs
index 4f1808bd..95340eb9 100644
--- a/signstar-configure-build/src/cli.rs
+++ b/signstar-configure-build/src/cli.rs
@@ -1,17 +1,17 @@
 use clap::{Parser, crate_name};
+use signstar_common::{
+    config::{
+        get_default_config_file_path,
+        get_etc_override_config_file_path,
+        get_run_override_config_file_path,
+        get_usr_local_override_config_file_path,
+    },
+    ssh::{get_ssh_authorized_key_base_dir, get_sshd_config_dropin_dir},
+    system_user::get_home_base_dir_path,
+};
 use strum::VariantNames;
 
-use crate::{
-    ConfigPath,
-    DEFAULT_CONFIG_FILE,
-    ETC_OVERRIDE_CONFIG_FILE,
-    HOME_BASE_DIR,
-    RUN_OVERRIDE_CONFIG_FILE,
-    SSH_AUTHORIZED_KEY_BASE_DIR,
-    SSHD_DROPIN_CONFIG_DIR,
-    SshForceCommand,
-    USR_LOCAL_OVERRIDE_CONFIG_FILE,
-};
+use crate::{ConfigPath, SshForceCommand};
 
 pub const BIN_NAME: &str = crate_name!();
 const SSH_FORCE_COMMAND_VARIANTS: &[&str] = SshForceCommand::VARIANTS;
@@ -30,25 +30,32 @@ It creates system users and their integration based on a central configuration f
 
 By default, one of the following configuration files is used if it exists, in the following order:
 
-- \"{USR_LOCAL_OVERRIDE_CONFIG_FILE}\"
+- {:?}
 
-- \"{RUN_OVERRIDE_CONFIG_FILE}\"
+- {:?}
 
-- \"{ETC_OVERRIDE_CONFIG_FILE}\"
+- {:?}
 
-If none of the above are found, the default location \"{DEFAULT_CONFIG_FILE}\" is used.
+If none of the above are found, the default location {:?} is used.
 Alternatively a custom configuration file location can be specified using the \"--config\"/ \"-c\" option.
 
 System users, if they don't exist already, are created with the help of `useradd`.
-The users are created without a passphrase and setup with a home below \"{HOME_BASE_DIR}\".
+The users are created without a passphrase and setup with a home below {:?}.
 However, their home directory is not created automatically.
 The system user accounts are then unlocked with the help of `usermod`.
 For each system user a tmpfiles.d integration is provided below \"/usr/lib/tmpfiles.d\", to allow automatic creation of their home directory.
 
-If the used configuration file associates the system user with SSH public keys, a dedicated \"authorized_keys\" file containing the SSH public keys for the user is created below \"{SSH_AUTHORIZED_KEY_BASE_DIR}\".
-Additionally, an \"sshd_config\" drop-in configuration is created below \"{SSHD_DROPIN_CONFIG_DIR}\".
+If the used configuration file associates the system user with SSH public keys, a dedicated \"authorized_keys\" file containing the SSH public keys for the user is created below {:?}.
+Additionally, an \"sshd_config\" drop-in configuration is created below {:?}.
 This \"sshd_config\" drop-in configuration enforces the use of the user's \"authorized_keys\" and the use of a specific command (i.e. one of {SSH_FORCE_COMMAND_VARIANTS:?}) depending on the user's role.",
-    ),
+    get_usr_local_override_config_file_path(),
+    get_run_override_config_file_path(),
+    get_etc_override_config_file_path(),
+    get_default_config_file_path(),
+    get_home_base_dir_path(),
+    get_ssh_authorized_key_base_dir(),
+    get_sshd_config_dropin_dir(),
+    )
 )]
 pub struct Cli {
     #[arg(
@@ -61,14 +68,18 @@ If specified, the custom configuration file is used instead of the default confi
 
 If unspecified, one of the following configuration files is used if it exists, in the following order:
 
-- \"{USR_LOCAL_OVERRIDE_CONFIG_FILE}\"
+- {:?}
 
-- \"{RUN_OVERRIDE_CONFIG_FILE}\"
+- {:?}
 
-- \"{ETC_OVERRIDE_CONFIG_FILE}\"
+- {:?}
 
-If none of the above are found, the default location \"{DEFAULT_CONFIG_FILE}\" is used.
-"),
+If none of the above are found, the default location {:?} is used.",
+    get_usr_local_override_config_file_path(),
+    get_run_override_config_file_path(),
+    get_etc_override_config_file_path(),
+    get_default_config_file_path(),
+),
         long,
         short
     )]
diff --git a/signstar-configure-build/src/lib.rs b/signstar-configure-build/src/lib.rs
index 219fec9c..cf840248 100644
--- a/signstar-configure-build/src/lib.rs
+++ b/signstar-configure-build/src/lib.rs
@@ -8,18 +8,15 @@ use std::{
 
 use nethsm_config::{HermeticParallelConfig, SystemUserId, UserMapping};
 use nix::unistd::User;
+use signstar_common::{
+    config::get_config_file_or_default,
+    ssh::{get_ssh_authorized_key_base_dir, get_sshd_config_dropin_dir},
+    system_user::get_home_base_dir_path,
+};
 use sysinfo::{Pid, System};
 
 pub mod cli;
 
-pub static ETC_OVERRIDE_CONFIG_FILE: &str = "/etc/signstar/config.toml";
-pub static RUN_OVERRIDE_CONFIG_FILE: &str = "/run/signstar/config.toml";
-pub static USR_LOCAL_OVERRIDE_CONFIG_FILE: &str = "/usr/local/share/signstar/config.toml";
-pub static DEFAULT_CONFIG_FILE: &str = "/usr/share/signstar/config.toml";
-pub static SSH_AUTHORIZED_KEY_BASE_DIR: &str = "/etc/ssh";
-pub static SSHD_DROPIN_CONFIG_DIR: &str = "/etc/ssh/sshd_config.d";
-pub static HOME_BASE_DIR: &str = "/var/lib/signstar/home";
-
 #[derive(Debug, thiserror::Error)]
 pub enum Error {
     /// A config error
@@ -64,6 +61,16 @@ pub enum Error {
     #[error("The string {0} could not be converted to a \"sysinfo::Uid\"")]
     SysUidFromStr(String),
 
+    /// A `Path` value for a tmpfiles.d integration is not valid.
+    #[error(
+        "The Path value {path} for the tmpfiles.d integration for {user} is not valid:\n{reason}"
+    )]
+    TmpfilesDPath {
+        path: String,
+        user: SystemUserId,
+        reason: &'static str,
+    },
+
     /// Adding a user failed
     #[error("Adding user {user} failed:\n{source}")]
     UserAdd {
@@ -109,12 +116,7 @@ pub enum Error {
 
 /// The configuration file path for the application.
 ///
-/// If the path exists and is a file, one of the following configuration file locations is used:
-/// - [`ETC_OVERRIDE_CONFIG_FILE`]
-/// - [`RUN_OVERRIDE_CONFIG_FILE`]
-/// - [`USR_LOCAL_OVERRIDE_CONFIG_FILE`]
-///
-/// If none of the above is found, [`DEFAULT_CONFIG_FILE`] is used (even if it doesn't exist!).
+/// The configuration file location is defined by the behavior of [`get_config_file_or_default`].
 #[derive(Clone, Debug)]
 pub struct ConfigPath(PathBuf);
 
@@ -133,25 +135,10 @@ impl AsRef<Path> for ConfigPath {
 impl Default for ConfigPath {
     /// Returns the default [`ConfigPath`].
     ///
-    /// If the path exists and is a file, one of the following configuration file locations is used:
-    /// - [`ETC_OVERRIDE_CONFIG_FILE`]
-    /// - [`RUN_OVERRIDE_CONFIG_FILE`]
-    /// - [`USR_LOCAL_OVERRIDE_CONFIG_FILE`]
-    ///
-    /// If none of the above is found, [`DEFAULT_CONFIG_FILE`] is used (even if it doesn't exist!).
+    /// Uses [`get_config_file_or_default`] to find the first usable configuration file path, or the
+    /// default if none is found.
     fn default() -> Self {
-        for config_file in [
-            ETC_OVERRIDE_CONFIG_FILE,
-            RUN_OVERRIDE_CONFIG_FILE,
-            USR_LOCAL_OVERRIDE_CONFIG_FILE,
-        ] {
-            let config = PathBuf::from(config_file);
-            if config.is_file() {
-                return Self(config);
-            }
-        }
-
-        Self(PathBuf::from(DEFAULT_CONFIG_FILE))
+        Self(get_config_file_or_default())
     }
 }
 
@@ -308,8 +295,8 @@ pub fn ensure_root() -> Result<(), Error> {
 ///
 /// Works on the [`UserMapping`]s of the provided `config` and creates system users for all
 /// mappings, that define system users, if they don't exist on the system yet.
-/// System users are created unlocked, without passphrase, with their homes located in
-/// [`HOME_BASE_DIR`].
+/// System users are created unlocked, without passphrase, with their homes located in the directory
+/// returned by [`get_home_base_dir_path`].
 /// The home directories of users are not created upon user creation, but instead a [tmpfiles.d]
 /// configuration is added for them to automate their creation upon system boot.
 ///
@@ -354,17 +341,17 @@ pub fn create_system_users(config: &HermeticParallelConfig) -> Result<(), Error>
             continue;
         }
 
+        let home_base_dir = get_home_base_dir_path();
+
         // add user, but do not create its home
         print!("Creating user \"{user}\"...");
         let user_add = Command::new("useradd")
-            .args([
-                "--base-dir",
-                HOME_BASE_DIR,
-                "--user-group",
-                "--shell",
-                "/usr/bin/bash",
-                user.as_ref(),
-            ])
+            .arg("--base-dir")
+            .arg(home_base_dir.as_path())
+            .arg("--user-group")
+            .arg("--shell")
+            .arg("/usr/bin/bash")
+            .arg(user.as_ref())
             .output()
             .map_err(|error| Error::UserAdd {
                 user: user.clone(),
@@ -407,8 +394,25 @@ pub fn create_system_users(config: &HermeticParallelConfig) -> Result<(), Error>
                     user: user.clone(),
                     source,
                 })?;
+
+            // ensure that the `Path` component in the tmpfiles.d file
+            // - has whitespace replaced with a c-style escape
+            // - does not contain specifiers
+            let home_dir = {
+                let home_dir =
+                    format!("{}/{user}", home_base_dir.to_string_lossy()).replace(" ", "\\x20");
+                if home_dir.contains("%") {
+                    return Err(Error::TmpfilesDPath {
+                        path: home_dir.clone(),
+                        user: user.clone(),
+                        reason: "Specifiers (%) are not supported at this point.",
+                    });
+                }
+                home_dir
+            };
+
             buffer
-                .write_all(format!("d {HOME_BASE_DIR}/{user} 700 {user} {user}\n").as_bytes())
+                .write_all(format!("d {home_dir} 700 {user} {user}\n",).as_bytes())
                 .map_err(|source| Error::WriteTmpfilesD {
                     user: user.clone(),
                     source,
@@ -422,14 +426,14 @@ pub fn create_system_users(config: &HermeticParallelConfig) -> Result<(), Error>
                 // add SSH authorized keys file user in system-wide location
                 print!("Adding SSH authorized_keys file for user \"{user}\"...");
                 {
-                    let filename = format!(
-                        "{SSH_AUTHORIZED_KEY_BASE_DIR}/signstar-user-{user}.authorized_keys"
-                    );
-                    let mut buffer =
-                        File::create(filename).map_err(|source| Error::WriteAuthorizedKeys {
-                            user: user.clone(),
-                            source,
-                        })?;
+                    let mut buffer = File::create(
+                        get_ssh_authorized_key_base_dir()
+                            .join(format!("signstar-user-{user}.authorized_keys")),
+                    )
+                    .map_err(|source| Error::WriteAuthorizedKeys {
+                        user: user.clone(),
+                        source,
+                    })?;
                     buffer
                         .write_all(
                             (authorized_keys
@@ -450,9 +454,9 @@ pub fn create_system_users(config: &HermeticParallelConfig) -> Result<(), Error>
                 // add sshd_config drop-in configuration for user
                 print!("Adding sshd_config drop-in configuration for user \"{user}\"...");
                 {
-                    let mut buffer = File::create(format!(
-                        "{SSHD_DROPIN_CONFIG_DIR}/10-signstar-user-{user}.conf"
-                    ))
+                    let mut buffer = File::create(
+                        get_sshd_config_dropin_dir().join(format!("10-signstar-user-{user}.conf")),
+                    )
                     .map_err(|source| Error::WriteSshdConfig {
                         user: user.clone(),
                         source,
-- 
GitLab