Skip to main content

hardware_enclave/
exec.rs

1// Copyright 2026 Jay Gowdy
2// SPDX-License-Identifier: MIT
3
4use std::collections::BTreeMap;
5use std::ffi::OsString;
6use std::path::PathBuf;
7use std::process::ExitStatus;
8
9use crate::internal::app_adapter::{launcher, LaunchRequest, ResolutionStrategy, ResolvedProgram};
10
11use crate::error::{Error, Result};
12
13pub use crate::internal::app_adapter::IntegrationType;
14
15/// Launch a child process with hardware-backed secrets injected.
16///
17/// The [`run()`][SecureProcess::run] method provides full security guarantees:
18/// - Secret env var values are mlocked before spawn and zeroized after the child exits.
19/// - The spawned child inherits `RLIMIT_CORE=0` on Unix (preventing core dumps of the
20///   secret-laden environment).
21///
22/// The [`exec()`][SecureProcess::exec] method provides **weaker** guarantees:
23/// - Secrets are NOT mlocked (they are passed via `Command::env` without locking).
24/// - Secrets are NOT zeroized (the current process is replaced; no cleanup runs).
25/// - Prefer [`run()`][SecureProcess::run] for Type 2 secret delivery. Use
26///   [`exec()`][SecureProcess::exec] only when you need to replace the current
27///   process image and accept the weaker guarantees.
28pub struct SecureProcess {
29    program: PathBuf,
30    args: Vec<OsString>,
31    secret_env: BTreeMap<String, String>,
32    env_additions: BTreeMap<String, String>,
33    env_removals: Vec<String>,
34    scrub_patterns: Vec<String>,
35}
36
37impl std::fmt::Debug for SecureProcess {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("SecureProcess")
40            .field("program", &self.program)
41            .field("args", &self.args)
42            .field("env_additions", &self.env_additions)
43            .field("env_removals", &self.env_removals)
44            .field("scrub_patterns", &self.scrub_patterns)
45            // secret_env intentionally omitted
46            .finish()
47    }
48}
49
50impl SecureProcess {
51    pub fn new(program: impl Into<PathBuf>) -> Self {
52        Self {
53            program: program.into(),
54            args: Vec::new(),
55            secret_env: BTreeMap::new(),
56            env_additions: BTreeMap::new(),
57            env_removals: Vec::new(),
58            scrub_patterns: Vec::new(),
59        }
60    }
61
62    pub fn arg(mut self, a: impl Into<OsString>) -> Self {
63        self.args.push(a.into());
64        self
65    }
66
67    pub fn args(mut self, args: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
68        self.args.extend(args.into_iter().map(Into::into));
69        self
70    }
71
72    /// Inject a secret value as an environment variable (Type 2 delivery).
73    /// The value is mlocked and zeroized after the child exits.
74    pub fn secret_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
75        self.secret_env.insert(key.into(), value.into());
76        self
77    }
78
79    /// Add a non-secret environment variable (e.g. a config file path).
80    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
81        self.env_additions.insert(key.into(), value.into());
82        self
83    }
84
85    /// Remove an environment variable from the child's environment.
86    pub fn env_remove(mut self, key: impl Into<String>) -> Self {
87        self.env_removals.push(key.into());
88        self
89    }
90
91    /// Scrub inherited env vars matching this pattern before spawning.
92    /// Accepts exact names or prefix patterns ending in `*`.
93    pub fn scrub(mut self, pattern: impl Into<String>) -> Self {
94        self.scrub_patterns.push(pattern.into());
95        self
96    }
97
98    /// Spawn the child and wait for it to exit. Zeroizes secret env vars after child returns.
99    pub fn run(self) -> Result<ExitStatus> {
100        let mut env_overrides: BTreeMap<String, String> = BTreeMap::new();
101        for (k, v) in self.secret_env {
102            env_overrides.insert(k, v);
103        }
104        for (k, v) in self.env_additions {
105            env_overrides.insert(k, v);
106        }
107
108        let request = LaunchRequest {
109            program: ResolvedProgram {
110                path: self.program,
111                fixed_args: Vec::new(),
112                strategy: ResolutionStrategy::ExplicitPath,
113                shell_hint: None,
114            },
115            args: self
116                .args
117                .into_iter()
118                .map(|s| s.to_string_lossy().into_owned())
119                .collect(),
120            env_overrides,
121            env_removals: self.env_removals,
122            env_scrub_patterns: self.scrub_patterns,
123        };
124
125        launcher::run(request).map_err(|e| Error::KeyOperation {
126            operation: "exec".into(),
127            detail: e.to_string(),
128        })
129    }
130
131    /// Replace the current process image via execve() (Unix).
132    /// On Windows, falls back to run() since CreateProcess cannot replace the calling image.
133    ///
134    /// WARNING: secret env var zeroization is NOT possible after exec() because
135    /// the current process no longer exists. Use run() when zeroization matters.
136    #[allow(clippy::needless_return, unreachable_code)]
137    pub fn exec(self) -> Result<std::convert::Infallible> {
138        #[cfg(unix)]
139        {
140            use std::os::unix::process::CommandExt;
141            let mut cmd = std::process::Command::new(&self.program);
142            cmd.args(&self.args);
143            for (k, v) in &self.secret_env {
144                cmd.env(k, v);
145            }
146            for (k, v) in &self.env_additions {
147                cmd.env(k, v);
148            }
149            for k in &self.env_removals {
150                cmd.env_remove(k);
151            }
152            let err = cmd.exec();
153            return Err(Error::KeyOperation {
154                operation: "exec".into(),
155                detail: err.to_string(),
156            });
157        }
158        #[cfg(not(unix))]
159        {
160            let status = self.run()?;
161            let code = status.code().unwrap_or(1);
162            std::process::exit(code);
163        }
164    }
165}
166
167/// A temporary file containing secret content, shredded (zeroed) on drop.
168///
169/// Platform selection:
170/// - Linux/WSL2: `memfd_create` (anonymous in-memory file, no filesystem path).
171/// - macOS: 0o600 temp file in a 0o700 temp directory, shredded on drop.
172/// - Windows: restricted-permission temp directory, shredded on drop.
173pub struct TempSecretFile {
174    #[cfg(target_os = "linux")]
175    _inner: TempSecretInner,
176    #[cfg(not(target_os = "linux"))]
177    _inner: crate::internal::app_adapter::TempConfig,
178    path_str: String,
179}
180
181// Fields held only for their Drop side-effects (auto-cleanup of the fd/file).
182#[cfg(target_os = "linux")]
183#[allow(dead_code)]
184enum TempSecretInner {
185    Memfd(crate::internal::app_adapter::MemfdConfig),
186    Fallback(crate::internal::app_adapter::TempConfig),
187}
188
189impl std::fmt::Debug for TempSecretFile {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        f.debug_struct("TempSecretFile")
192            .field("path", &self.path_str)
193            .finish()
194    }
195}
196
197impl TempSecretFile {
198    /// Write text content to a platform-appropriate secret temp location.
199    pub fn create(content: &str) -> Result<Self> {
200        Self::create_bytes(content.as_bytes())
201    }
202
203    /// Write binary content.
204    pub fn create_bytes(content: &[u8]) -> Result<Self> {
205        #[cfg(target_os = "linux")]
206        {
207            match crate::internal::app_adapter::create_memfd_config(
208                "enclave-secret",
209                "secret",
210                content,
211            ) {
212                Ok(memfd) => {
213                    let path_str = memfd.path().to_string_lossy().into_owned();
214                    Ok(Self {
215                        _inner: TempSecretInner::Memfd(memfd),
216                        path_str,
217                    })
218                }
219                Err(_) => {
220                    // memfd not available (e.g. older kernel); fall back to temp file.
221                    let tc = crate::internal::app_adapter::TempConfig::write(
222                        "enclave-secret",
223                        "secret",
224                        content,
225                    )
226                    .map_err(|e| Error::KeyOperation {
227                        operation: "temp_secret".into(),
228                        detail: e.to_string(),
229                    })?;
230                    let path_str = tc.path().to_string_lossy().into_owned();
231                    Ok(Self {
232                        _inner: TempSecretInner::Fallback(tc),
233                        path_str,
234                    })
235                }
236            }
237        }
238        #[cfg(not(target_os = "linux"))]
239        {
240            let tc = crate::internal::app_adapter::TempConfig::write(
241                "enclave-secret",
242                "secret",
243                content,
244            )
245            .map_err(|e| Error::KeyOperation {
246                operation: "temp_secret".into(),
247                detail: e.to_string(),
248            })?;
249            let path_str = tc.path().to_string_lossy().into_owned();
250            Ok(Self {
251                _inner: tc,
252                path_str,
253            })
254        }
255    }
256
257    /// The path to pass to the target process.
258    pub fn path(&self) -> &str {
259        &self.path_str
260    }
261}