gatehouse-runtime-protocol 0.4.0

Strict data contract for pinned container runtime adapters
Documentation
use super::*;
use serde_json::{json, Value};
use std::os::unix::fs::PermissionsExt;
struct Fixture {
    dir: tempfile::TempDir,
    host: Value,
    installation: Value,
    config: Value,
}
fn save(path: &Path, value: &Value) -> String {
    let bytes = canonical::encode(value).unwrap();
    fs::write(path, &bytes).unwrap();
    fs::set_permissions(path, fs::Permissions::from_mode(0o600)).unwrap();
    format!("{:x}", Sha256::digest(bytes))
}
impl Fixture {
    fn new() -> Self {
        let root = std::env::var_os("GATEHOUSE_JOURNAL_TEST_ROOT")
            .or_else(|| std::env::var_os("HOME"))
            .unwrap();
        let dir = tempfile::Builder::new()
            .prefix("installation-test-")
            .tempdir_in(root)
            .unwrap();
        fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o700)).unwrap();
        let executable = dir.path().join("adapter");
        fs::write(&executable, b"fixture never executed").unwrap();
        fs::set_permissions(&executable, fs::Permissions::from_mode(0o500)).unwrap();
        let expected = super::super::super::wire::parse_request(include_bytes!(
            "../../wire/status-request.json"
        ))
        .unwrap();
        let mut engine = expected.binding.engine_binding;
        engine.principal_uid = unsafe { libc::geteuid() };
        let config = json!({"schema":"gate-runtime/lifecycle-config-v1","engine_binding":engine});
        let installation = json!({"schema":"gate-runtime/lifecycle-installation-v1","product":"docker-lifecycle","version":"0.1.0","protocol":PROTOCOL,"executable_path":executable,"executable_sha256":format!("{:x}",Sha256::digest(b"fixture never executed")),"config_schema":"gate-runtime/lifecycle-config-v1"});
        let host = json!({"schema":"gate-runtime/lifecycle-host-v1","installation_path":dir.path().join("installation.json"),"installation_sha256":"","config_path":dir.path().join("config.json"),"config_sha256":""});
        let mut f = Self {
            dir,
            host,
            installation,
            config,
        };
        f.flush();
        f
    }
    fn path(&self) -> PathBuf {
        self.dir.path().join("host.json")
    }
    fn flush(&mut self) {
        self.host["installation_sha256"] = save(
            &self.dir.path().join("installation.json"),
            &self.installation,
        )
        .into();
        self.host["config_sha256"] =
            save(&self.dir.path().join("config.json"), &self.config).into();
        save(&self.path(), &self.host);
    }
    fn request(&self) -> Request {
        let mut r = super::super::super::wire::parse_request(include_bytes!(
            "../../wire/status-request.json"
        ))
        .unwrap();
        r.binding.installation_sha256 = self.host["installation_sha256"].as_str().unwrap().into();
        r.binding.config_sha256 = self.host["config_sha256"].as_str().unwrap().into();
        r.binding.executable_sha256 = self.installation["executable_sha256"]
            .as_str()
            .unwrap()
            .into();
        r.binding.engine_binding =
            serde_json::from_value(self.config["engine_binding"].clone()).unwrap();
        r.binding.operation_key = r.operation_key().unwrap();
        r
    }
}
#[test]
fn exact_snapshot_and_request_binding_are_required() {
    let f = Fixture::new();
    let loaded = VerifiedInstallation::load(&f.path()).unwrap();
    let r = f.request();
    loaded.validate_request(&r).unwrap();
    let snapshot = loaded.snapshot().unwrap();
    assert_eq!(snapshot.installation_sha256, r.binding.installation_sha256);
    assert_eq!(snapshot.executable_sha256, r.binding.executable_sha256);
    assert_eq!(snapshot.config_sha256, r.binding.config_sha256);
    assert_eq!(snapshot.engine_binding, r.binding.engine_binding);
    assert!(loaded.executable().is_ok());
    for field in ["installation_sha256", "config_sha256", "executable_sha256"] {
        let mut v = serde_json::to_value(&r).unwrap();
        v["binding"][field] = json!("a".repeat(64));
        let mut wrong: Request = serde_json::from_value(v).unwrap();
        wrong.binding.operation_key = wrong.operation_key().unwrap();
        assert!(loaded.validate_request(&wrong).is_err());
    }
}
#[test]
fn old_hosts_public_expected_and_bad_engine_cannot_be_loaded() {
    for schema in ["gate-runtime/installed-host-v2", "gate-runtime/v3"] {
        let mut f = Fixture::new();
        f.host["schema"] = schema.into();
        f.flush();
        assert!(VerifiedInstallation::load(&f.path()).is_err());
    }
    let mut f = Fixture::new();
    f.host["expected"] = "/tmp/public.json".into();
    f.flush();
    assert!(VerifiedInstallation::load(&f.path()).is_err());
    let mut f = Fixture::new();
    f.config["engine_binding"]["engine"] = "podman".into();
    f.flush();
    assert!(VerifiedInstallation::load(&f.path()).is_err());
}
#[test]
fn executable_substitution_even_identical_bytes_invalidates_held_identity() {
    let f = Fixture::new();
    let loaded = VerifiedInstallation::load(&f.path()).unwrap();
    let replacement = f.dir.path().join("replacement");
    fs::write(&replacement, b"fixture never executed").unwrap();
    fs::set_permissions(&replacement, fs::Permissions::from_mode(0o500)).unwrap();
    fs::rename(replacement, f.dir.path().join("adapter")).unwrap();
    assert!(loaded.revalidate().is_err());
    assert!(loaded.executable().is_err());
    assert!(loaded.snapshot().is_err());
}
#[test]
fn config_tamper_symlink_hardlink_and_unsafe_permissions_fail() {
    let f = Fixture::new();
    fs::write(f.dir.path().join("config.json"), b"{}").unwrap();
    assert!(VerifiedInstallation::load(&f.path()).is_err());
    let f = Fixture::new();
    let link = f.dir.path().join("host-link");
    std::os::unix::fs::symlink(f.path(), &link).unwrap();
    assert!(VerifiedInstallation::load(&link).is_err());
    let f = Fixture::new();
    fs::hard_link(f.dir.path().join("adapter"), f.dir.path().join("second")).unwrap();
    assert!(VerifiedInstallation::load(&f.path()).is_err());
    let f = Fixture::new();
    fs::set_permissions(f.path(), fs::Permissions::from_mode(0o644)).unwrap();
    assert!(VerifiedInstallation::load(&f.path()).is_err());
}
#[test]
fn duplicate_descriptor_fields_and_wrong_executable_digest_fail() {
    let f = Fixture::new();
    let bytes = fs::read_to_string(f.path()).unwrap();
    fs::write(
        f.path(),
        bytes.replace(
            "\"schema\":",
            "\"schema\":\"gate-runtime/lifecycle-host-v1\",\"schema\":",
        ),
    )
    .unwrap();
    assert!(VerifiedInstallation::load(&f.path()).is_err());
    let mut f = Fixture::new();
    f.installation["executable_sha256"] = "0".repeat(64).into();
    f.flush();
    assert!(VerifiedInstallation::load(&f.path()).is_err());
}

#[test]
fn held_config_snapshot_and_every_descriptor_reject_same_byte_replacement() {
    for member in ["host.json", "installation.json", "config.json"] {
        let f = Fixture::new();
        let loaded = VerifiedInstallation::load(&f.path()).unwrap();
        let r = f.request();
        assert_eq!(
            loaded.config_bytes().unwrap(),
            fs::read(f.dir.path().join("config.json")).unwrap()
        );
        assert_eq!(loaded.engine_binding().unwrap(), &r.binding.engine_binding);
        let path = f.dir.path().join(member);
        let replacement = f.dir.path().join("replacement");
        fs::write(&replacement, fs::read(&path).unwrap()).unwrap();
        fs::set_permissions(&replacement, fs::Permissions::from_mode(0o600)).unwrap();
        fs::rename(replacement, path).unwrap();
        assert!(loaded.validate_request(&r).is_err());
        assert!(loaded.config_bytes().is_err());
        assert!(loaded.engine_binding().is_err());
        assert!(loaded.executable().is_err());
        assert!(loaded.snapshot().is_err());
    }
}