Skip to main content

gatehouse_runtime_protocol/lifecycle/installation/
files.rs

1use 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        // SAFETY: geteuid takes no arguments.
53        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        // SAFETY: geteuid takes no arguments.
84        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/// Revalidated installation facts for Core request construction. These facts
122/// confer no dispatch authority; the host still requires a durable journal token.
123#[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}
130/// A held file snapshot, not a runnable host or mutation capability.
131pub 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    /// Binds an already journal-authorized request to this exact installed
199    /// snapshot; does not infer a request, endpoint or identity from environment.
200    pub fn validate_request(&self, r: &Request) -> Result<()> {
201        r.validate()?;
202        self.revalidate()?;
203        // SAFETY: geteuid takes no arguments.
204        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    /// Future host must execute this held descriptor, not reopen an ambient path.
215    /// Reading this descriptor is not permission to bypass journal authorization.
216    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;