Skip to main content

varve_core/
realm.rs

1//! Realms (REQ-REALM-001) — the pin names its trust universe.
2//!
3//! A machine can serve several *independent* toolchain universes — different
4//! organizations, different trust roots, different registries — in parallel.
5//! A realm binds a name to (registry, trust root); the pin references the
6//! name; a committed `varve-realms.toml` (discovered by the same walk-up as
7//! the pin, so trust travels with the code) carries the definitions.
8//!
9//! Isolation is by construction, not convention: every piece of per-realm
10//! state lives under an effective root namespaced by the TRUST-ROOT
11//! FINGERPRINT — two realms cannot cross-talk even with identical layer
12//! names and counters, and a realm's layers can only ever verify against
13//! that realm's root. When a pin names a realm, the realm is authoritative:
14//! the ambient environment cannot substitute a different trust root.
15
16use std::collections::BTreeMap;
17use std::path::{Path, PathBuf};
18
19use serde::Deserialize;
20
21/// The realms file name, discovered by walking up from the working
22/// directory (it may sit beside the pin or above it).
23pub const REALMS_FILE: &str = "varve-realms.toml";
24
25/// A resolved realm: everything needed to fetch and verify its layers.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Realm {
28    pub name: String,
29    pub registry: String,
30    /// Raw ed25519 root public key bytes.
31    pub trust_root: Vec<u8>,
32    /// The realm asserts that it publishes a signed line index
33    /// (REQ-INDEXAUTH-001 clause 5). Where true, a missing index is an ERROR
34    /// rather than a silent fall back to the registry's unauthenticated
35    /// listing — otherwise an attacker need only delete the index to disable
36    /// the check. Defaults to false so every existing realm keeps working:
37    /// failing closed by default would break all of them at once.
38    pub signed_index: bool,
39}
40
41impl Realm {
42    /// Short fingerprint of the trust root — the store namespace. Sixteen
43    /// hex chars of sha256(pubkey): collision-safe for a namespace while
44    /// staying readable in paths.
45    pub fn fingerprint(&self) -> String {
46        crate::store::manifest_digest(&self.trust_root)
47            .strip_prefix("sha256:")
48            .expect("digest shape")[..16]
49            .to_string()
50    }
51
52    /// The per-realm effective root under which core/state/status live.
53    pub fn effective_root(&self, varve_root: &Path) -> PathBuf {
54        varve_root.join("realms").join(self.fingerprint())
55    }
56}
57
58#[derive(Debug, thiserror::Error)]
59pub enum RealmError {
60    #[error(
61        "no {REALMS_FILE} found walking up from {start} — the pin names realm '{realm}' but no realm definitions exist; commit a {REALMS_FILE} defining it"
62    )]
63    NoRealmsFile { start: String, realm: String },
64    #[error("{path}: not a valid realms file: {reason}")]
65    Parse { path: String, reason: String },
66    #[error(
67        "realm '{realm}' is not defined in {path} — defined realms: {defined:?}. Fix the pin or add the realm."
68    )]
69    Undefined {
70        realm: String,
71        path: String,
72        defined: Vec<String>,
73    },
74    #[error("realm '{realm}' in {path}: {reason}")]
75    BadDefinition {
76        realm: String,
77        path: String,
78        reason: String,
79    },
80    #[error("io error at {path}")]
81    Io {
82        path: String,
83        #[source]
84        source: std::io::Error,
85    },
86}
87
88#[derive(Deserialize)]
89#[serde(deny_unknown_fields)]
90struct RawRealmsFile {
91    #[serde(default)]
92    realm: BTreeMap<String, RawRealm>,
93}
94
95#[derive(Deserialize)]
96#[serde(deny_unknown_fields)]
97struct RawRealm {
98    registry: String,
99    /// Inline hex-encoded ed25519 public key…
100    #[serde(rename = "trust-root", default)]
101    trust_root: Option<String>,
102    /// …or a key file, relative to the realms file.
103    #[serde(rename = "trust-root-file", default)]
104    trust_root_file: Option<String>,
105    /// `signed-index = true` — this realm publishes a signed line index and
106    /// consumers must not accept an unauthenticated listing for it.
107    #[serde(rename = "signed-index", default)]
108    signed_index: bool,
109}
110
111/// Find the realms file by walking up from `start`.
112pub fn find_realms_file(start: &Path) -> Option<PathBuf> {
113    let mut dir = Some(start);
114    while let Some(d) = dir {
115        let candidate = d.join(REALMS_FILE);
116        if candidate.is_file() {
117            return Some(candidate);
118        }
119        dir = d.parent();
120    }
121    None
122}
123
124/// Every realm name the discovered realms file defines. Used to label store
125/// partitions by realm rather than by trust-root fingerprint — a fingerprint is
126/// unambiguous but tells a human nothing.
127pub fn realm_names(start: &Path) -> Result<Vec<String>, RealmError> {
128    let Some(path) = find_realms_file(start) else {
129        return Ok(Vec::new());
130    };
131    let text = std::fs::read_to_string(&path).map_err(|source| RealmError::Io {
132        path: path.display().to_string(),
133        source,
134    })?;
135    let file: RawRealmsFile = toml::from_str(&text).map_err(|e| RealmError::Parse {
136        path: path.display().to_string(),
137        reason: e.to_string(),
138    })?;
139    Ok(file.realm.into_keys().collect())
140}
141
142/// Load one realm by name from the realms file discovered from `start`.
143pub fn resolve_realm(start: &Path, name: &str) -> Result<Realm, RealmError> {
144    let Some(path) = find_realms_file(start) else {
145        return Err(RealmError::NoRealmsFile {
146            start: start.display().to_string(),
147            realm: name.to_string(),
148        });
149    };
150    let text = std::fs::read_to_string(&path).map_err(|source| RealmError::Io {
151        path: path.display().to_string(),
152        source,
153    })?;
154    let raw: RawRealmsFile = toml::from_str(&text).map_err(|e| RealmError::Parse {
155        path: path.display().to_string(),
156        reason: e.to_string(),
157    })?;
158    let Some(def) = raw.realm.get(name) else {
159        return Err(RealmError::Undefined {
160            realm: name.to_string(),
161            path: path.display().to_string(),
162            defined: raw.realm.keys().cloned().collect(),
163        });
164    };
165    let bad = |reason: String| RealmError::BadDefinition {
166        realm: name.to_string(),
167        path: path.display().to_string(),
168        reason,
169    };
170    let hex_key = match (&def.trust_root, &def.trust_root_file) {
171        (Some(_), Some(_)) => {
172            return Err(bad(
173                "both trust-root and trust-root-file given — pick one".into()
174            ));
175        }
176        (Some(inline), None) => inline.trim().to_string(),
177        (None, Some(file)) => {
178            let key_path = path.parent().unwrap_or(Path::new(".")).join(file);
179            std::fs::read_to_string(&key_path)
180                .map_err(|e| {
181                    bad(format!(
182                        "cannot read trust-root-file {}: {e}",
183                        key_path.display()
184                    ))
185                })?
186                .trim()
187                .to_string()
188        }
189        (None, None) => return Err(bad("no trust-root or trust-root-file".into())),
190    };
191    if hex_key.len() != 64 || !hex_key.chars().all(|c| c.is_ascii_hexdigit()) {
192        return Err(bad(
193            "trust root is not a 64-hex-char ed25519 public key".into()
194        ));
195    }
196    let trust_root = (0..hex_key.len())
197        .step_by(2)
198        .map(|i| u8::from_str_radix(&hex_key[i..i + 2], 16).expect("checked hex"))
199        .collect();
200    Ok(Realm {
201        name: name.to_string(),
202        registry: def.registry.clone(),
203        trust_root,
204        signed_index: def.signed_index,
205    })
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    fn realms_dir(content: &str) -> tempfile::TempDir {
213        let tmp = tempfile::tempdir().unwrap();
214        std::fs::write(tmp.path().join(REALMS_FILE), content).unwrap();
215        tmp
216    }
217
218    // rivet: verifies REQ-STORE-001
219    #[test]
220    fn every_defined_realm_is_named() {
221        // `list` labels store partitions by realm name rather than by
222        // trust-root fingerprint, which is unambiguous but tells a human
223        // nothing. Mutation testing found this helper replaceable by an empty
224        // vec with nothing noticing: the CLI test that covers it cannot kill
225        // mutants, because the gate runs `--workspace --lib`.
226        let dir = realms_dir(TWO_REALMS);
227        let mut names = realm_names(dir.path()).unwrap();
228        names.sort();
229        assert_eq!(names, ["acme", "pulseengine"], "both realms named");
230
231        // No realms file is not an error — a project may define none.
232        let empty = tempfile::tempdir().unwrap();
233        assert!(realm_names(empty.path()).unwrap().is_empty());
234
235        // A malformed file IS an error: labelling must not paper over a file
236        // the user believes is being read.
237        let bad = realms_dir("this is not toml {{{");
238        assert!(realm_names(bad.path()).is_err());
239    }
240
241    const TWO_REALMS: &str = r#"
242[realm.pulseengine]
243registry = "oci://ghcr.io/pulseengine/varve/layers"
244trust-root = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
245
246[realm.acme]
247registry = "oci://ghcr.io/acme/layers"
248trust-root = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
249"#;
250
251    // rivet: verifies REQ-REALM-001
252    #[test]
253    fn realms_resolve_by_name_with_walk_up_discovery() {
254        let tmp = realms_dir(TWO_REALMS);
255        let deep = tmp.path().join("a/b");
256        std::fs::create_dir_all(&deep).unwrap();
257        let realm = resolve_realm(&deep, "acme").unwrap();
258        assert_eq!(realm.registry, "oci://ghcr.io/acme/layers");
259        assert_eq!(realm.trust_root, vec![0xbb; 32]);
260    }
261
262    // rivet: verifies REQ-REALM-001
263    #[test]
264    fn different_roots_mean_different_namespaces() {
265        let tmp = realms_dir(TWO_REALMS);
266        let pe = resolve_realm(tmp.path(), "pulseengine").unwrap();
267        let acme = resolve_realm(tmp.path(), "acme").unwrap();
268        assert_ne!(pe.fingerprint(), acme.fingerprint());
269        let root = Path::new("/var/root");
270        assert_ne!(pe.effective_root(root), acme.effective_root(root));
271        assert!(pe.effective_root(root).starts_with("/var/root/realms"));
272    }
273
274    // rivet: verifies REQ-REALM-001
275    #[test]
276    fn an_undefined_realm_fails_closed_naming_what_exists() {
277        let tmp = realms_dir(TWO_REALMS);
278        let err = resolve_realm(tmp.path(), "evil-corp").unwrap_err();
279        let msg = err.to_string();
280        assert!(msg.contains("evil-corp") && msg.contains("pulseengine") && msg.contains("acme"));
281    }
282
283    // rivet: verifies REQ-REALM-001
284    #[test]
285    fn a_missing_realms_file_fails_closed_with_guidance() {
286        let tmp = tempfile::tempdir().unwrap();
287        let err = resolve_realm(tmp.path(), "pulseengine").unwrap_err();
288        assert!(err.to_string().contains(REALMS_FILE));
289    }
290
291    // rivet: verifies REQ-REALM-001
292    #[test]
293    fn trust_root_file_is_read_relative_to_the_realms_file() {
294        let tmp = tempfile::tempdir().unwrap();
295        std::fs::create_dir_all(tmp.path().join("keys")).unwrap();
296        std::fs::write(tmp.path().join("keys/root.pub"), "cc".repeat(32)).unwrap();
297        std::fs::write(
298            tmp.path().join(REALMS_FILE),
299            "[realm.filekey]\nregistry = \"oci://r/x\"\ntrust-root-file = \"keys/root.pub\"\n",
300        )
301        .unwrap();
302        let realm = resolve_realm(tmp.path(), "filekey").unwrap();
303        assert_eq!(realm.trust_root, vec![0xcc; 32]);
304    }
305
306    // rivet: verifies REQ-REALM-001
307    #[test]
308    fn malformed_definitions_are_refused() {
309        for (name, body) in [
310            ("nokey", "[realm.nokey]\nregistry = \"oci://r/x\"\n"),
311            (
312                "badkey",
313                "[realm.badkey]\nregistry = \"oci://r/x\"\ntrust-root = \"zz\"\n",
314            ),
315            // Wrong-length but PURE-HEX: length and charset must each
316            // reject independently.
317            (
318                "shorthex",
319                "[realm.shorthex]\nregistry = \"oci://r/x\"\ntrust-root = \"cccccccccccccccccccccccccccccccc\"\n",
320            ),
321            (
322                "bothkeys",
323                "[realm.bothkeys]\nregistry = \"oci://r/x\"\ntrust-root = \"aa\"\ntrust-root-file = \"f\"\n",
324            ),
325        ] {
326            let tmp = realms_dir(body);
327            assert!(
328                resolve_realm(tmp.path(), name).is_err(),
329                "{name} must refuse"
330            );
331        }
332    }
333
334    // rivet: verifies REQ-INDEXAUTH-001
335    #[test]
336    fn a_realm_declares_whether_it_publishes_a_signed_index() {
337        // Clause 5. Failing closed by default would break every realm that
338        // exists; failing open with no way to opt in would let an attacker
339        // disable the check by deleting the index. The realm decides, which is
340        // where every other trust question is already settled.
341        let tmp = realms_dir(
342            r#"
343[realm.declaring]
344registry     = "oci://example.test/layers"
345trust-root   = "4e771dc62a08be89e3450f8cd807da58ff70af4a4e124ebf2d2b71684cfd9973"
346signed-index = true
347
348[realm.silent]
349registry   = "oci://example.test/other"
350trust-root = "4e771dc62a08be89e3450f8cd807da58ff70af4a4e124ebf2d2b71684cfd9973"
351"#,
352        );
353        assert!(
354            resolve_realm(tmp.path(), "declaring").unwrap().signed_index,
355            "a realm that declares an index must be recorded as declaring it"
356        );
357        assert!(
358            !resolve_realm(tmp.path(), "silent").unwrap().signed_index,
359            "the default must be false, or every existing realm breaks at once"
360        );
361    }
362}