gatehouse_runtime_protocol/lifecycle_v2/
installation.rs1use super::{
3 plan_record::PlanRecord,
4 profile::{Profile, SlotIdentity, PRODUCT, PROTOCOL, VERSION},
5 protected_context::{protected, Identity, ProtectedContext, CONTEXT_SCHEMA},
6};
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9use std::{
10 fs::{self, File, OpenOptions},
11 os::unix::fs::{FileExt, MetadataExt, OpenOptionsExt},
12 path::{Path, PathBuf},
13};
14type Result<T> = std::result::Result<T, &'static str>;
15fn io<T>(r: std::io::Result<T>) -> Result<T> {
16 r.map_err(|_| "installation filesystem failure")
17}
18fn digest(b: &[u8]) -> String {
19 format!("{:x}", Sha256::digest(b))
20}
21#[derive(Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23struct Host {
24 schema: String,
25 installation_path: String,
26 installation_sha256: String,
27 config_path: String,
28 config_sha256: String,
29}
30#[derive(Serialize, Deserialize)]
31#[serde(deny_unknown_fields)]
32struct Installation {
33 schema: String,
34 product: String,
35 version: String,
36 protocol: String,
37 executable_path: String,
38 executable_sha256: String,
39 config_schema: String,
40}
41fn parse<T: serde::de::DeserializeOwned + Serialize>(bytes: &[u8]) -> Result<T> {
42 let value: T = serde_json::from_slice(bytes).map_err(|_| "installation fields rejected")?;
44 if serde_json::to_vec(&value).map_err(|_| "installation encode")? != bytes {
45 return Err("noncanonical installation");
46 }
47 Ok(value)
48}
49struct Snapshot {
50 path: PathBuf,
51 file: File,
52 identity: Identity,
53 bytes: Vec<u8>,
54}
55impl Snapshot {
56 fn open(path: &Path, executable: bool) -> Result<Self> {
57 protected(path)?;
58 let file = io(OpenOptions::new()
59 .read(true)
60 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK)
61 .open(path))?;
62 let m = io(file.metadata())?;
63 let uid = unsafe { libc::geteuid() };
64 let mode = m.mode() & 0o7777;
65 let mode_ok = if executable {
66 if m.uid() == 0 {
67 matches!(mode, 0o555 | 0o755)
68 } else {
69 m.uid() == uid && matches!(mode, 0o500 | 0o700)
70 }
71 } else {
72 m.uid() == uid && matches!(mode, 0o400 | 0o600)
73 };
74 let limit = if executable {
75 128 * 1024 * 1024
76 } else {
77 1024 * 1024
78 };
79 if !m.is_file() || m.nlink() != 1 || !mode_ok || m.len() > limit {
80 return Err("unprotected installation member");
81 }
82 let bytes = read(&file, limit)?;
83 if executable && !bytes.starts_with(b"\x7fELF") {
84 return Err("ELF executable required");
85 }
86 let held = Self {
87 path: path.into(),
88 file,
89 identity: Identity::of(&m),
90 bytes,
91 };
92 held.check()?;
93 Ok(held)
94 }
95 fn check(&self) -> Result<()> {
96 protected(&self.path)?;
97 if Identity::of(&io(self.file.metadata())?) != self.identity
98 || Identity::of(&io(fs::symlink_metadata(&self.path))?) != self.identity
99 {
100 return Err("installation member changed");
101 }
102 if read(&self.file, self.bytes.len() as u64)? != self.bytes {
103 return Err("installation bytes changed");
104 }
105 if Identity::of(&io(self.file.metadata())?) != self.identity
106 || Identity::of(&io(fs::symlink_metadata(&self.path))?) != self.identity
107 {
108 return Err("installation member changed during read");
109 }
110 Ok(())
111 }
112}
113fn read(file: &File, limit: u64) -> Result<Vec<u8>> {
114 let mut bytes = Vec::new();
115 let mut buf = [0; 8192];
116 while bytes.len() as u64 <= limit {
117 let n = io(file.read_at(&mut buf, bytes.len() as u64))?;
118 if n == 0 {
119 break;
120 }
121 bytes.extend_from_slice(&buf[..n]);
122 }
123 if bytes.len() as u64 > limit {
124 return Err("installation byte limit");
125 }
126 Ok(bytes)
127}
128pub struct VerifiedPodmanInstallation {
130 host: Snapshot,
131 installation: Snapshot,
132 config: Snapshot,
133 executable: Snapshot,
134 context: ProtectedContext,
135 identity: SlotIdentity,
136}
137impl VerifiedPodmanInstallation {
138 pub fn load(host_path: &Path) -> Result<Self> {
139 let host = Snapshot::open(host_path, false)?;
140 let h: Host = parse(&host.bytes)?;
141 if h.schema != "gate-runtime/podman-host-v1" {
142 return Err("Podman host schema required");
143 }
144 let installation = Snapshot::open(Path::new(&h.installation_path), false)?;
145 if digest(&installation.bytes) != h.installation_sha256 {
146 return Err("installation digest mismatch");
147 }
148 let i: Installation = parse(&installation.bytes)?;
149 if i.schema != "gate-runtime/podman-installation-v1"
150 || i.product != PRODUCT
151 || i.version != VERSION
152 || i.protocol != PROTOCOL
153 || i.config_schema != CONTEXT_SCHEMA
154 {
155 return Err("Podman installation tuple required");
156 }
157 let config = Snapshot::open(Path::new(&h.config_path), false)?;
158 if digest(&config.bytes) != h.config_sha256 {
159 return Err("config digest mismatch");
160 }
161 let context =
162 ProtectedContext::from_held_versioned(&config.path, io(config.file.try_clone())?)?;
163 if context.snapshot()?.1 != config.bytes {
164 return Err("held config snapshot mismatch");
165 }
166 let executable = Snapshot::open(Path::new(&i.executable_path), true)?;
167 if digest(&executable.bytes) != i.executable_sha256 {
168 return Err("executable digest mismatch");
169 }
170 let identity = SlotIdentity {
171 profile: Profile {
172 protocol: PROTOCOL.into(),
173 product: PRODUCT.into(),
174 version: VERSION.into(),
175 engine: "podman".into(),
176 },
177 installation_sha256: digest(&installation.bytes),
178 config_sha256: digest(&config.bytes),
179 executable_sha256: digest(&executable.bytes),
180 };
181 identity.validate()?;
182 let result = Self {
183 host,
184 installation,
185 config,
186 executable,
187 context,
188 identity,
189 };
190 result.revalidate()?;
191 Ok(result)
192 }
193 pub fn revalidate(&self) -> Result<()> {
194 self.host.check()?;
195 self.installation.check()?;
196 self.config.check()?;
197 self.executable.check()?;
198 self.context.revalidate()
199 }
200 pub fn context(&self) -> Result<&ProtectedContext> {
201 self.revalidate()?;
202 Ok(&self.context)
203 }
204 pub fn identity(&self) -> Result<&SlotIdentity> {
205 self.revalidate()?;
206 Ok(&self.identity)
207 }
208 pub fn executable_file(&self) -> Result<&File> {
210 self.revalidate()?;
211 Ok(&self.executable.file)
212 }
213 pub fn capture_expected(
214 &self,
215 request: &super::capture_wire::CaptureRequest,
216 ) -> Result<super::capture_wire::CaptureExpected> {
217 self.revalidate()?;
218 super::capture_wire::CaptureExpected::bind(
219 request,
220 &self.identity,
221 digest(&self.host.bytes),
222 self.config.path.to_str().ok_or("config path UTF8")?.into(),
223 )
224 }
225 pub fn config_file(&self) -> Result<&File> {
226 self.revalidate()?;
227 Ok(&self.config.file)
228 }
229 pub fn match_record<'a>(
230 &'a self,
231 record: &'a PlanRecord,
232 ) -> Result<RecordInstallationMatch<'a>> {
233 let matched = RecordInstallationMatch {
234 installation: self,
235 record,
236 };
237 matched.revalidate()?;
238 Ok(matched)
239 }
240}
241pub struct RecordInstallationMatch<'a> {
243 installation: &'a VerifiedPodmanInstallation,
244 record: &'a PlanRecord,
245}
246impl RecordInstallationMatch<'_> {
247 pub fn revalidate(&self) -> Result<()> {
248 self.installation.revalidate()?;
249 self.record.match_installation_snapshot(
250 &self.installation.identity,
251 &self.installation.config.bytes,
252 )
253 }
254}