signstar_crypto/
test.rs

1//! Utilities used for test setups.
2
3use std::{
4    fs::{Permissions, create_dir_all, set_permissions, write},
5    os::{linux::fs::MetadataExt, unix::fs::PermissionsExt},
6    path::PathBuf,
7    process::{Child, Command},
8    thread,
9    time,
10};
11
12use change_user_run::get_command;
13use log::debug;
14
15/// An error that may occur when using test utils.
16#[derive(Debug, thiserror::Error)]
17pub enum Error {
18    /// Applying permissions to a file failed.
19    #[error("Unable to apply permissions to {path}:\n{source}")]
20    ApplyPermissions {
21        /// The file that was being modified.
22        path: PathBuf,
23
24        /// The source error.
25        source: std::io::Error,
26    },
27
28    /// A directory can not be created.
29    #[error("Unable to create directory {dir}:\n{source}")]
30    CreateDirectory {
31        /// The directory which was about to be created.
32        dir: PathBuf,
33
34        /// The source error.
35        source: std::io::Error,
36    },
37
38    /// The socket for io.systemd.Credentials could not be started.
39    #[error("Unable to start socket for io.systemd.Credentials:\n{0}")]
40    CredentialsSocket(#[source] std::io::Error),
41
42    /// An I/O error.
43    #[error("I/O error while {context}:\n{source}")]
44    Io {
45        /// The short description of the operation.
46        context: &'static str,
47
48        /// The source error.
49        source: std::io::Error,
50    },
51
52    /// An I/O error with a specific path.
53    #[error("I/O error at {path} while {context}:\n{source}")]
54    IoPath {
55        /// The file that was being accessed.
56        path: PathBuf,
57
58        /// The short description of the operation.
59        context: &'static str,
60
61        /// The source error.
62        source: std::io::Error,
63    },
64
65    /// A timeout has been reached.
66    #[error("Timeout of {timeout}ms reached while {context}")]
67    Timeout {
68        /// The value of the timeout in milliseconds.
69        timeout: u64,
70
71        /// The short description of the operation.
72        context: String,
73    },
74}
75
76/// Writes a dummy `/etc/machine-id`
77///
78/// # Note
79///
80/// This is a requirement for using systemd-creds.
81///
82/// # Errors
83///
84/// Returns an error if
85///
86/// - a static machine-id can not be written to `/etc/machine-id`,
87/// - or metadata on the created `/etc/machine-id` can not be retrieved.
88pub(crate) fn write_machine_id() -> Result<(), Error> {
89    debug!("Write dummy /etc/machine-id, required for systemd-creds");
90    let machine_id = PathBuf::from("/etc/machine-id");
91    write(&machine_id, "d3b07384d113edec49eaa6238ad5ff00").map_err(|source| Error::IoPath {
92        path: machine_id.to_path_buf(),
93        context: "writing machine-id",
94        source,
95    })?;
96
97    let metadata = machine_id.metadata().map_err(|source| Error::IoPath {
98        path: machine_id,
99        context: "getting metadata of file",
100        source,
101    })?;
102    debug!(
103        "/etc/machine-id\nmode: {}\nuid: {}\ngid: {}",
104        metadata.permissions().mode(),
105        metadata.st_uid(),
106        metadata.st_gid()
107    );
108    Ok(())
109}
110
111/// A background process.
112///
113/// Tracks a [`Child`] which represents a process that runs in the background.
114/// The background process is automatically killed upon dropping the [`BackgroundProcess`].
115#[derive(Debug)]
116pub struct BackgroundProcess {
117    child: Child,
118    command: String,
119}
120
121impl BackgroundProcess {
122    /// Kills the tracked background process.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the process could not be killed.
127    pub fn kill(&mut self) -> Result<(), Error> {
128        self.child.kill().map_err(|source| Error::Io {
129            context: "killing process",
130            source,
131        })
132    }
133}
134
135impl Drop for BackgroundProcess {
136    /// Kills the tracked background process when destructing the [`BackgroundProcess`].
137    fn drop(&mut self) {
138        if let Err(error) = self.kill() {
139            log::debug!(
140                "Unable to kill background process of command {}:\n{error}",
141                self.command
142            )
143        }
144    }
145}
146
147/// Starts a socket for `io.systemd.Credentials` using `systemd-socket-activate`.
148///
149/// Sets the file mode of the socket to `666` so that all users on the system have access.
150///
151/// # Errors
152///
153/// Returns an error if
154///
155/// - `systemd-socket-activate` is unable to start the required socket,
156/// - one or more files in `/run/systemd` can not be listed,
157/// - applying of permissions on `/run/systemd/io.systemd.Credentials` fails,
158/// - or the socket has not been made available within 10000ms.
159pub fn start_credentials_socket() -> Result<BackgroundProcess, crate::Error> {
160    write_machine_id()?;
161    let systemd_run_path = PathBuf::from("/run/systemd");
162    let socket_path = PathBuf::from("/run/systemd/io.systemd.Credentials");
163    create_dir_all(&systemd_run_path).map_err(|source| Error::CreateDirectory {
164        dir: systemd_run_path,
165        source,
166    })?;
167
168    // Run systemd-socket-activate to provide /run/systemd/io.systemd.Credentials
169    let command = "systemd-socket-activate";
170    let systemd_socket_activate = get_command(command)?;
171    let mut command = Command::new(systemd_socket_activate);
172    let command = command.args([
173        "--listen",
174        "/run/systemd/io.systemd.Credentials",
175        "--accept",
176        "--fdname=varlink",
177        "systemd-creds",
178    ]);
179    let child = command.spawn().map_err(Error::CredentialsSocket)?;
180
181    // Set the socket to be writable by all, once it's available.
182    let timeout = 10000;
183    let step = 100;
184    let mut elapsed = 0;
185    let mut permissions_set = false;
186    while elapsed < timeout {
187        if socket_path.exists() {
188            debug!("Found {socket_path:?}");
189            set_permissions(socket_path.as_path(), Permissions::from_mode(0o666)).map_err(
190                |source| Error::ApplyPermissions {
191                    path: socket_path.to_path_buf(),
192                    source,
193                },
194            )?;
195            permissions_set = true;
196            break;
197        } else {
198            thread::sleep(time::Duration::from_millis(step));
199            elapsed += step;
200        }
201    }
202    if !permissions_set {
203        return Err(Error::Timeout {
204            timeout,
205            context: format!("waiting for {socket_path:?}"),
206        }
207        .into());
208    }
209
210    Ok(BackgroundProcess {
211        child,
212        command: format!("{command:?}"),
213    })
214}