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    /// The realm's primary source. Kept as the first element of `sources` too,
30    /// so existing callers that read `registry` keep working unchanged
31    /// (REQ-MIRROR-001 clause 5).
32    pub registry: String,
33    /// Every source, in the realm's stated order of preference, primary first.
34    ///
35    /// Ordered, not raced: an operator must be able to predict which source
36    /// served them, and a run that picked differently each time would make an
37    /// incident unreproducible.
38    pub sources: Vec<String>,
39    /// Raw ed25519 root public key bytes.
40    pub trust_root: Vec<u8>,
41    /// The realm asserts that it publishes a signed line index
42    /// (REQ-INDEXAUTH-001 clause 5). Where true, a missing index is an ERROR
43    /// rather than a silent fall back to the registry's unauthenticated
44    /// listing — otherwise an attacker need only delete the index to disable
45    /// the check. Defaults to false so every existing realm keeps working:
46    /// failing closed by default would break all of them at once.
47    pub signed_index: bool,
48}
49
50impl Realm {
51    /// Short fingerprint of the trust root — the store namespace. Sixteen
52    /// hex chars of sha256(pubkey): collision-safe for a namespace while
53    /// staying readable in paths.
54    pub fn fingerprint(&self) -> String {
55        crate::store::manifest_digest(&self.trust_root)
56            .strip_prefix("sha256:")
57            .expect("digest shape")[..16]
58            .to_string()
59    }
60
61    /// The per-realm effective root under which core/state/status live.
62    pub fn effective_root(&self, varve_root: &Path) -> PathBuf {
63        varve_root.join("realms").join(self.fingerprint())
64    }
65}
66
67#[derive(Debug, thiserror::Error)]
68pub enum RealmError {
69    #[error(
70        "no {REALMS_FILE} found walking up from {start} — the pin names realm '{realm}' but no realm definitions exist; commit a {REALMS_FILE} defining it"
71    )]
72    NoRealmsFile { start: String, realm: String },
73    #[error("{path}: not a valid realms file: {reason}")]
74    Parse { path: String, reason: String },
75    #[error(
76        "realm '{realm}' is not defined in {path} — defined realms: {defined:?}. Fix the pin or add the realm."
77    )]
78    Undefined {
79        realm: String,
80        path: String,
81        defined: Vec<String>,
82    },
83    #[error("realm '{realm}' in {path}: {reason}")]
84    BadDefinition {
85        realm: String,
86        path: String,
87        reason: String,
88    },
89    #[error("io error at {path}")]
90    Io {
91        path: String,
92        #[source]
93        source: std::io::Error,
94    },
95}
96
97#[derive(Deserialize)]
98#[serde(deny_unknown_fields)]
99struct RawRealmsFile {
100    #[serde(default)]
101    realm: BTreeMap<String, RawRealm>,
102}
103
104#[derive(Deserialize)]
105#[serde(deny_unknown_fields)]
106struct RawRealm {
107    registry: String,
108    /// Additional sources, tried in order after `registry`, when it cannot be
109    /// reached (REQ-MIRROR-001).
110    ///
111    /// Safe by construction: a layer is accepted because its manifest verifies
112    /// against this realm's trust root, so a mirror is transport and not
113    /// authority. A tampered mirror fails the signature check and a truncated
114    /// one fails the digest check — a second source widens availability, never
115    /// the trust surface.
116    #[serde(default)]
117    mirrors: Vec<String>,
118    /// Inline hex-encoded ed25519 public key…
119    #[serde(rename = "trust-root", default)]
120    trust_root: Option<String>,
121    /// …or a key file, relative to the realms file.
122    #[serde(rename = "trust-root-file", default)]
123    trust_root_file: Option<String>,
124    /// `signed-index = true` — this realm publishes a signed line index and
125    /// consumers must not accept an unauthenticated listing for it.
126    #[serde(rename = "signed-index", default)]
127    signed_index: bool,
128}
129
130/// Find the realms file by walking up from `start`.
131pub fn find_realms_file(start: &Path) -> Option<PathBuf> {
132    let mut dir = Some(start);
133    while let Some(d) = dir {
134        let candidate = d.join(REALMS_FILE);
135        if candidate.is_file() {
136            return Some(candidate);
137        }
138        dir = d.parent();
139    }
140    None
141}
142
143/// Every realm name the discovered realms file defines. Used to label store
144/// partitions by realm rather than by trust-root fingerprint — a fingerprint is
145/// unambiguous but tells a human nothing.
146pub fn realm_names(start: &Path) -> Result<Vec<String>, RealmError> {
147    let Some(path) = find_realms_file(start) else {
148        return Ok(Vec::new());
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 file: RawRealmsFile = toml::from_str(&text).map_err(|e| RealmError::Parse {
155        path: path.display().to_string(),
156        reason: e.to_string(),
157    })?;
158    Ok(file.realm.into_keys().collect())
159}
160
161/// Load one realm by name from the realms file discovered from `start`.
162pub fn resolve_realm(start: &Path, name: &str) -> Result<Realm, RealmError> {
163    let Some(path) = find_realms_file(start) else {
164        return Err(RealmError::NoRealmsFile {
165            start: start.display().to_string(),
166            realm: name.to_string(),
167        });
168    };
169    let text = std::fs::read_to_string(&path).map_err(|source| RealmError::Io {
170        path: path.display().to_string(),
171        source,
172    })?;
173    let raw: RawRealmsFile = toml::from_str(&text).map_err(|e| RealmError::Parse {
174        path: path.display().to_string(),
175        reason: e.to_string(),
176    })?;
177    let Some(def) = raw.realm.get(name) else {
178        return Err(RealmError::Undefined {
179            realm: name.to_string(),
180            path: path.display().to_string(),
181            defined: raw.realm.keys().cloned().collect(),
182        });
183    };
184    let bad = |reason: String| RealmError::BadDefinition {
185        realm: name.to_string(),
186        path: path.display().to_string(),
187        reason,
188    };
189    let hex_key = match (&def.trust_root, &def.trust_root_file) {
190        (Some(_), Some(_)) => {
191            return Err(bad(
192                "both trust-root and trust-root-file given — pick one".into()
193            ));
194        }
195        (Some(inline), None) => inline.trim().to_string(),
196        (None, Some(file)) => {
197            let key_path = path.parent().unwrap_or(Path::new(".")).join(file);
198            std::fs::read_to_string(&key_path)
199                .map_err(|e| {
200                    bad(format!(
201                        "cannot read trust-root-file {}: {e}",
202                        key_path.display()
203                    ))
204                })?
205                .trim()
206                .to_string()
207        }
208        (None, None) => return Err(bad("no trust-root or trust-root-file".into())),
209    };
210    if hex_key.len() != 64 || !hex_key.chars().all(|c| c.is_ascii_hexdigit()) {
211        return Err(bad(
212            "trust root is not a 64-hex-char ed25519 public key".into()
213        ));
214    }
215    let trust_root = (0..hex_key.len())
216        .step_by(2)
217        .map(|i| u8::from_str_radix(&hex_key[i..i + 2], 16).expect("checked hex"))
218        .collect();
219    Ok(Realm {
220        name: name.to_string(),
221        registry: def.registry.clone(),
222        sources: std::iter::once(def.registry.clone())
223            .chain(def.mirrors.iter().cloned())
224            .collect(),
225        trust_root,
226        signed_index: def.signed_index,
227    })
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    fn realms_dir(content: &str) -> tempfile::TempDir {
235        let tmp = tempfile::tempdir().unwrap();
236        std::fs::write(tmp.path().join(REALMS_FILE), content).unwrap();
237        tmp
238    }
239
240    // rivet: verifies REQ-STORE-001
241    #[test]
242    fn every_defined_realm_is_named() {
243        // `list` labels store partitions by realm name rather than by
244        // trust-root fingerprint, which is unambiguous but tells a human
245        // nothing. Mutation testing found this helper replaceable by an empty
246        // vec with nothing noticing: the CLI test that covers it cannot kill
247        // mutants, because the gate runs `--workspace --lib`.
248        let dir = realms_dir(TWO_REALMS);
249        let mut names = realm_names(dir.path()).unwrap();
250        names.sort();
251        assert_eq!(names, ["acme", "pulseengine"], "both realms named");
252
253        // No realms file is not an error — a project may define none.
254        let empty = tempfile::tempdir().unwrap();
255        assert!(realm_names(empty.path()).unwrap().is_empty());
256
257        // A malformed file IS an error: labelling must not paper over a file
258        // the user believes is being read.
259        let bad = realms_dir("this is not toml {{{");
260        assert!(realm_names(bad.path()).is_err());
261    }
262
263    const TWO_REALMS: &str = r#"
264[realm.pulseengine]
265registry = "oci://ghcr.io/pulseengine/varve/layers"
266trust-root = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
267
268[realm.acme]
269registry = "oci://ghcr.io/acme/layers"
270trust-root = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
271"#;
272
273    // rivet: verifies REQ-REALM-001
274    #[test]
275    fn realms_resolve_by_name_with_walk_up_discovery() {
276        let tmp = realms_dir(TWO_REALMS);
277        let deep = tmp.path().join("a/b");
278        std::fs::create_dir_all(&deep).unwrap();
279        let realm = resolve_realm(&deep, "acme").unwrap();
280        assert_eq!(realm.registry, "oci://ghcr.io/acme/layers");
281        assert_eq!(realm.trust_root, vec![0xbb; 32]);
282    }
283
284    // rivet: verifies REQ-REALM-001
285    #[test]
286    fn different_roots_mean_different_namespaces() {
287        let tmp = realms_dir(TWO_REALMS);
288        let pe = resolve_realm(tmp.path(), "pulseengine").unwrap();
289        let acme = resolve_realm(tmp.path(), "acme").unwrap();
290        assert_ne!(pe.fingerprint(), acme.fingerprint());
291        let root = Path::new("/var/root");
292        assert_ne!(pe.effective_root(root), acme.effective_root(root));
293        assert!(pe.effective_root(root).starts_with("/var/root/realms"));
294    }
295
296    // rivet: verifies REQ-REALM-001
297    #[test]
298    fn an_undefined_realm_fails_closed_naming_what_exists() {
299        let tmp = realms_dir(TWO_REALMS);
300        let err = resolve_realm(tmp.path(), "evil-corp").unwrap_err();
301        let msg = err.to_string();
302        assert!(msg.contains("evil-corp") && msg.contains("pulseengine") && msg.contains("acme"));
303    }
304
305    // rivet: verifies REQ-REALM-001
306    #[test]
307    fn a_missing_realms_file_fails_closed_with_guidance() {
308        let tmp = tempfile::tempdir().unwrap();
309        let err = resolve_realm(tmp.path(), "pulseengine").unwrap_err();
310        assert!(err.to_string().contains(REALMS_FILE));
311    }
312
313    // rivet: verifies REQ-REALM-001
314    #[test]
315    fn trust_root_file_is_read_relative_to_the_realms_file() {
316        let tmp = tempfile::tempdir().unwrap();
317        std::fs::create_dir_all(tmp.path().join("keys")).unwrap();
318        std::fs::write(tmp.path().join("keys/root.pub"), "cc".repeat(32)).unwrap();
319        std::fs::write(
320            tmp.path().join(REALMS_FILE),
321            "[realm.filekey]\nregistry = \"oci://r/x\"\ntrust-root-file = \"keys/root.pub\"\n",
322        )
323        .unwrap();
324        let realm = resolve_realm(tmp.path(), "filekey").unwrap();
325        assert_eq!(realm.trust_root, vec![0xcc; 32]);
326    }
327
328    // rivet: verifies REQ-REALM-001
329    #[test]
330    fn malformed_definitions_are_refused() {
331        for (name, body) in [
332            ("nokey", "[realm.nokey]\nregistry = \"oci://r/x\"\n"),
333            (
334                "badkey",
335                "[realm.badkey]\nregistry = \"oci://r/x\"\ntrust-root = \"zz\"\n",
336            ),
337            // Wrong-length but PURE-HEX: length and charset must each
338            // reject independently.
339            (
340                "shorthex",
341                "[realm.shorthex]\nregistry = \"oci://r/x\"\ntrust-root = \"cccccccccccccccccccccccccccccccc\"\n",
342            ),
343            (
344                "bothkeys",
345                "[realm.bothkeys]\nregistry = \"oci://r/x\"\ntrust-root = \"aa\"\ntrust-root-file = \"f\"\n",
346            ),
347        ] {
348            let tmp = realms_dir(body);
349            assert!(
350                resolve_realm(tmp.path(), name).is_err(),
351                "{name} must refuse"
352            );
353        }
354    }
355
356    // rivet: verifies REQ-INDEXAUTH-001
357    #[test]
358    fn a_realm_declares_whether_it_publishes_a_signed_index() {
359        // Clause 5. Failing closed by default would break every realm that
360        // exists; failing open with no way to opt in would let an attacker
361        // disable the check by deleting the index. The realm decides, which is
362        // where every other trust question is already settled.
363        let tmp = realms_dir(
364            r#"
365[realm.declaring]
366registry     = "oci://example.test/layers"
367trust-root   = "4e771dc62a08be89e3450f8cd807da58ff70af4a4e124ebf2d2b71684cfd9973"
368signed-index = true
369
370[realm.silent]
371registry   = "oci://example.test/other"
372trust-root = "4e771dc62a08be89e3450f8cd807da58ff70af4a4e124ebf2d2b71684cfd9973"
373"#,
374        );
375        assert!(
376            resolve_realm(tmp.path(), "declaring").unwrap().signed_index,
377            "a realm that declares an index must be recorded as declaring it"
378        );
379        assert!(
380            !resolve_realm(tmp.path(), "silent").unwrap().signed_index,
381            "the default must be false, or every existing realm breaks at once"
382        );
383    }
384}
385
386#[cfg(test)]
387mod mirror_tests {
388    use super::*;
389
390    fn parse(text: &str, name: &str) -> Realm {
391        let dir = std::env::temp_dir().join(format!("varve-realm-mirror-{name}"));
392        let _ = std::fs::remove_dir_all(&dir);
393        std::fs::create_dir_all(&dir).expect("scratch");
394        std::fs::write(dir.join(REALMS_FILE), text).expect("write");
395        resolve_realm(&dir, name).expect("parses")
396    }
397
398    /// Clause 5. Every realms file in existence names one registry and no
399    /// mirrors; all of them must keep working with no edit.
400    // rivet: verifies REQ-MIRROR-001
401    #[test]
402    fn a_realm_naming_one_registry_still_works_and_has_one_source() {
403        let r = parse(
404            "[realm.solo]\nregistry = \"oci://ghcr.io/o/r\"\n\
405             trust-root = \"4e771dc62a08be89e3450f8cd807da58ff70af4a4e124ebf2d2b71684cfd9973\"\n",
406            "solo",
407        );
408        assert_eq!(r.registry, "oci://ghcr.io/o/r");
409        assert_eq!(r.sources, vec!["oci://ghcr.io/o/r".to_string()]);
410    }
411
412    /// Clause 1 and the ordering in clause 2: primary first, then the stated
413    /// mirrors in the order written.
414    // rivet: verifies REQ-MIRROR-001
415    #[test]
416    fn mirrors_follow_the_primary_in_the_order_they_are_written() {
417        let r = parse(
418            "[realm.many]\nregistry = \"oci://primary\"\n\
419             mirrors = [\"oci://second\", \"oci://third\"]\n\
420             trust-root = \"4e771dc62a08be89e3450f8cd807da58ff70af4a4e124ebf2d2b71684cfd9973\"\n",
421            "many",
422        );
423        assert_eq!(
424            r.sources,
425            vec![
426                "oci://primary".to_string(),
427                "oci://second".to_string(),
428                "oci://third".to_string()
429            ]
430        );
431        // `registry` still names the primary, so nothing that reads it changes.
432        assert_eq!(r.registry, "oci://primary");
433    }
434
435    /// The trust root is per REALM, not per source. A mirrors list cannot
436    /// introduce a second authority — that is what makes mirroring safe here
437    /// rather than a trust decision.
438    // rivet: verifies REQ-MIRROR-001
439    #[test]
440    fn mirrors_cannot_carry_a_trust_root_of_their_own() {
441        let dir = std::env::temp_dir().join("varve-realm-mirror-root");
442        let _ = std::fs::remove_dir_all(&dir);
443        std::fs::create_dir_all(&dir).expect("scratch");
444        std::fs::write(
445            dir.join(REALMS_FILE),
446            "[realm.x]\nregistry = \"oci://a\"\n\
447             mirrors = [{ registry = \"oci://b\", trust-root = \"dead\" }]\n\
448             trust-root = \"4e771dc62a08be89e3450f8cd807da58ff70af4a4e124ebf2d2b71684cfd9973\"\n",
449        )
450        .expect("write");
451        assert!(
452            resolve_realm(&dir, "x").is_err(),
453            "a mirror must not be able to declare its own trust root"
454        );
455    }
456}