gatehouse_runtime_protocol/lifecycle/installation/
files.rs1use super::*;
2use sha2::{Digest, Sha256};
3use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
4use std::{
5 fs::{self, File, OpenOptions},
6 io::Read,
7 path::{Path, PathBuf},
8};
9
10fn io<T>(r: std::io::Result<T>) -> Result<T> {
11 r.map_err(|_| Invalid("protected lifecycle installation rejected"))
12}
13#[derive(PartialEq, Eq)]
14struct Identity {
15 dev: u64,
16 ino: u64,
17 mode: u32,
18 uid: u32,
19 gid: u32,
20 links: u64,
21 len: u64,
22 mtime: i64,
23 mtime_ns: i64,
24 ctime: i64,
25 ctime_ns: i64,
26}
27impl Identity {
28 fn of(m: &fs::Metadata) -> Self {
29 Self {
30 dev: m.dev(),
31 ino: m.ino(),
32 mode: m.mode(),
33 uid: m.uid(),
34 gid: m.gid(),
35 links: m.nlink(),
36 len: m.len(),
37 mtime: m.mtime(),
38 mtime_ns: m.mtime_nsec(),
39 ctime: m.ctime(),
40 ctime_ns: m.ctime_nsec(),
41 }
42 }
43}
44fn protected(path: &Path) -> Result<()> {
45 if !path.is_absolute() || io(fs::canonicalize(path))? != path {
46 return Err(Invalid(
47 "lifecycle installation path must be protected absolute",
48 ));
49 }
50 for parent in path.ancestors().skip(1) {
51 let m = io(fs::symlink_metadata(parent))?;
52 if !m.is_dir()
54 || m.mode() & 0o022 != 0
55 || ![0, unsafe { libc::geteuid() }].contains(&m.uid())
56 {
57 return Err(Invalid("lifecycle installation ancestor is unprotected"));
58 }
59 }
60 Ok(())
61}
62struct Held {
63 path: PathBuf,
64 file: File,
65 identity: Identity,
66 bytes: Vec<u8>,
67 sha256: String,
68}
69impl Held {
70 fn open(path: &Path, executable: bool) -> Result<Self> {
71 protected(path)?;
72 let file = io(OpenOptions::new()
73 .read(true)
74 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK)
75 .open(path))?;
76 let m = io(file.metadata())?;
77 let mode = m.mode() & 0o7777;
78 let permitted = if executable {
79 matches!(mode, 0o500 | 0o700 | 0o555 | 0o755)
80 } else {
81 matches!(mode, 0o400 | 0o600)
82 };
83 if !m.is_file() || m.nlink() != 1 || m.uid() != unsafe { libc::geteuid() } || !permitted {
85 return Err(Invalid("invalid protected lifecycle file"));
86 }
87 let max = if executable {
88 64 * 1024 * 1024
89 } else {
90 crate::MAX_MESSAGE_BYTES as u64
91 };
92 if m.len() > max {
93 return Err(Invalid("lifecycle installation file too large"));
94 }
95 let mut bytes = vec![];
96 io((&file).take(max + 1).read_to_end(&mut bytes))?;
97 if bytes.len() as u64 > max {
98 return Err(Invalid("lifecycle installation file too large"));
99 }
100 let sha256 = format!("{:x}", Sha256::digest(&bytes));
101 let held = Self {
102 path: path.into(),
103 file,
104 identity: Identity::of(&m),
105 bytes: if executable { vec![] } else { bytes },
106 sha256,
107 };
108 held.revalidate()?;
109 Ok(held)
110 }
111 fn revalidate(&self) -> Result<()> {
112 protected(&self.path)?;
113 if Identity::of(&io(self.file.metadata())?) != self.identity
114 || Identity::of(&io(fs::symlink_metadata(&self.path))?) != self.identity
115 {
116 return Err(Invalid("lifecycle installation changed"));
117 }
118 Ok(())
119 }
120}
121#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct InstallationSnapshot {
125 pub installation_sha256: String,
126 pub executable_sha256: String,
127 pub config_sha256: String,
128 pub engine_binding: EngineBinding,
129}
130pub struct VerifiedInstallation {
132 host: Held,
133 installation: Held,
134 config: Held,
135 executable: Held,
136 engine: EngineBinding,
137}
138impl VerifiedInstallation {
139 pub fn load(host_path: &Path) -> Result<Self> {
140 let host = Held::open(host_path, false)?;
141 let h: Host = parse(&host.bytes)?;
142 if h.schema != "gate-runtime/lifecycle-host-v1" {
143 return Err(Invalid("unsupported lifecycle host"));
144 }
145 digest(&h.installation_sha256)?;
146 digest(&h.config_sha256)?;
147 let installation = Held::open(Path::new(&h.installation_path), false)?;
148 if installation.sha256 != h.installation_sha256 {
149 return Err(Invalid("lifecycle installation digest mismatch"));
150 }
151 let i: Installation = parse(&installation.bytes)?;
152 let profile = match (i.product.as_str(), i.version.as_str()) {
153 ("docker-lifecycle", "0.1.0") => "docker",
154 ("podman-lifecycle", "0.3.0") => "podman",
155 _ => return Err(Invalid("unsupported lifecycle installation")),
156 };
157 if i.schema != "gate-runtime/lifecycle-installation-v1"
158 || i.protocol != PROTOCOL
159 || i.config_schema != "gate-runtime/lifecycle-config-v1"
160 {
161 return Err(Invalid("unsupported lifecycle installation"));
162 }
163 digest(&i.executable_sha256)?;
164 let config = Held::open(Path::new(&h.config_path), false)?;
165 if config.sha256 != h.config_sha256 {
166 return Err(Invalid("lifecycle config digest mismatch"));
167 }
168 let c: Config = parse(&config.bytes)?;
169 if c.schema != i.config_schema {
170 return Err(Invalid("unsupported lifecycle config"));
171 }
172 c.engine_binding.validate()?;
173 if c.engine_binding.engine != profile {
174 return Err(Invalid("lifecycle product/engine mismatch"));
175 }
176 if c.engine_binding.principal_uid != unsafe { libc::geteuid() } {
177 return Err(Invalid("lifecycle principal mismatch"));
178 }
179 let executable = Held::open(Path::new(&i.executable_path), true)?;
180 if executable.sha256 != i.executable_sha256 {
181 return Err(Invalid("lifecycle executable digest mismatch"));
182 }
183 let verified = Self {
184 host,
185 installation,
186 config,
187 executable,
188 engine: c.engine_binding,
189 };
190 verified.revalidate()?;
191 Ok(verified)
192 }
193 pub fn revalidate(&self) -> Result<()> {
194 for held in [
195 &self.host,
196 &self.installation,
197 &self.config,
198 &self.executable,
199 ] {
200 held.revalidate()?;
201 }
202 Ok(())
203 }
204 pub fn validate_request(&self, r: &Request) -> Result<()> {
207 r.validate()?;
208 self.revalidate()?;
209 if r.binding.installation_sha256 != self.installation.sha256
211 || r.binding.executable_sha256 != self.executable.sha256
212 || r.binding.config_sha256 != self.config.sha256
213 || r.binding.engine_binding != self.engine
214 || r.binding.engine_binding.principal_uid != unsafe { libc::geteuid() }
215 {
216 return Err(Invalid("request does not match protected installation"));
217 }
218 Ok(())
219 }
220 pub fn snapshot(&self) -> Result<InstallationSnapshot> {
223 self.revalidate()?;
224 Ok(InstallationSnapshot {
225 installation_sha256: self.installation.sha256.clone(),
226 executable_sha256: self.executable.sha256.clone(),
227 config_sha256: self.config.sha256.clone(),
228 engine_binding: self.engine.clone(),
229 })
230 }
231 pub fn config_bytes(&self) -> Result<&[u8]> {
232 self.revalidate()?;
233 Ok(&self.config.bytes)
234 }
235 pub fn engine_binding(&self) -> Result<&EngineBinding> {
236 self.revalidate()?;
237 Ok(&self.engine)
238 }
239 pub fn executable(&self) -> Result<&File> {
240 self.revalidate()?;
241 Ok(&self.executable.file)
242 }
243}
244#[cfg(test)]
245mod tests;