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 if i.schema != "gate-runtime/lifecycle-installation-v1"
153 || i.product != "docker-lifecycle"
154 || i.version != "0.1.0"
155 || i.protocol != PROTOCOL
156 || i.config_schema != "gate-runtime/lifecycle-config-v1"
157 {
158 return Err(Invalid("unsupported lifecycle installation"));
159 }
160 digest(&i.executable_sha256)?;
161 let config = Held::open(Path::new(&h.config_path), false)?;
162 if config.sha256 != h.config_sha256 {
163 return Err(Invalid("lifecycle config digest mismatch"));
164 }
165 let c: Config = parse(&config.bytes)?;
166 if c.schema != i.config_schema {
167 return Err(Invalid("unsupported lifecycle config"));
168 }
169 c.engine_binding.validate()?;
170 if c.engine_binding.principal_uid != unsafe { libc::geteuid() } {
171 return Err(Invalid("lifecycle principal mismatch"));
172 }
173 let executable = Held::open(Path::new(&i.executable_path), true)?;
174 if executable.sha256 != i.executable_sha256 {
175 return Err(Invalid("lifecycle executable digest mismatch"));
176 }
177 let verified = Self {
178 host,
179 installation,
180 config,
181 executable,
182 engine: c.engine_binding,
183 };
184 verified.revalidate()?;
185 Ok(verified)
186 }
187 pub fn revalidate(&self) -> Result<()> {
188 for held in [
189 &self.host,
190 &self.installation,
191 &self.config,
192 &self.executable,
193 ] {
194 held.revalidate()?;
195 }
196 Ok(())
197 }
198 pub fn validate_request(&self, r: &Request) -> Result<()> {
201 r.validate()?;
202 self.revalidate()?;
203 if r.binding.installation_sha256 != self.installation.sha256
205 || r.binding.executable_sha256 != self.executable.sha256
206 || r.binding.config_sha256 != self.config.sha256
207 || r.binding.engine_binding != self.engine
208 || r.binding.engine_binding.principal_uid != unsafe { libc::geteuid() }
209 {
210 return Err(Invalid("request does not match protected installation"));
211 }
212 Ok(())
213 }
214 pub fn snapshot(&self) -> Result<InstallationSnapshot> {
217 self.revalidate()?;
218 Ok(InstallationSnapshot {
219 installation_sha256: self.installation.sha256.clone(),
220 executable_sha256: self.executable.sha256.clone(),
221 config_sha256: self.config.sha256.clone(),
222 engine_binding: self.engine.clone(),
223 })
224 }
225 pub fn config_bytes(&self) -> Result<&[u8]> {
226 self.revalidate()?;
227 Ok(&self.config.bytes)
228 }
229 pub fn engine_binding(&self) -> Result<&EngineBinding> {
230 self.revalidate()?;
231 Ok(&self.engine)
232 }
233 pub fn executable(&self) -> Result<&File> {
234 self.revalidate()?;
235 Ok(&self.executable.file)
236 }
237}
238#[cfg(test)]
239mod tests;