1use std::collections::BTreeMap;
17use std::path::{Path, PathBuf};
18
19use serde::Deserialize;
20
21pub const REALMS_FILE: &str = "varve-realms.toml";
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Realm {
28 pub name: String,
29 pub registry: String,
33 pub sources: Vec<String>,
39 pub trust_root: Vec<u8>,
41 pub signed_index: bool,
48 pub retired_roots: Vec<RetiredRoot>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct RetiredRoot {
66 pub key: Vec<u8>,
69 pub retired: String,
71 pub last_layer: Option<String>,
74}
75
76impl RetiredRoot {
77 pub fn hex(&self) -> String {
80 self.key.iter().map(|b| format!("{b:02x}")).collect()
81 }
82
83 pub fn fingerprint(&self) -> String {
87 crate::store::manifest_digest(&self.key)
88 .strip_prefix("sha256:")
89 .expect("digest shape")[..16]
90 .to_string()
91 }
92}
93
94impl Realm {
95 pub fn fingerprint(&self) -> String {
99 crate::store::manifest_digest(&self.trust_root)
100 .strip_prefix("sha256:")
101 .expect("digest shape")[..16]
102 .to_string()
103 }
104
105 pub fn partition_label(&self, fingerprint: &str) -> Option<String> {
115 if self.fingerprint() == fingerprint {
116 return Some(self.name.clone());
117 }
118 self.retired_roots
119 .iter()
120 .find(|r| r.fingerprint() == fingerprint)
121 .map(|r| {
122 format!(
123 "{} (retired root, {} — layers here do not verify against the realm's \
124 current root)",
125 self.name, r.retired
126 )
127 })
128 }
129
130 pub fn explain_retired_signature(&self, envelope: &[u8], payload_type: &str) -> Option<String> {
145 let retired = self
146 .retired_roots
147 .iter()
148 .find(|r| crate::verify::dsse_verify_typed(envelope, payload_type, &r.key).is_ok())?;
149
150 let live: String = self.trust_root.iter().map(|b| format!("{b:02x}")).collect();
151 let mut why = format!(
152 "this signature verifies against a root the realm '{}' RETIRED on {} ({}), \
153 not against its current trust-root ({live}).",
154 self.name,
155 retired.retired,
156 retired.hex(),
157 );
158 if let Some(last) = &retired.last_layer {
159 why.push_str(&format!(
160 " Layers up to and including {last} were signed by the retired root."
161 ));
162 }
163 why.push_str(
164 " This is not a forgery and not a mistake on your part: the realm changed its \
165 root. Move your pin to a layer signed by the current root — the old layers \
166 are not recoverable under the new root, by design.",
167 );
168 Some(why)
169 }
170
171 pub fn effective_root(&self, varve_root: &Path) -> PathBuf {
173 varve_root.join("realms").join(self.fingerprint())
174 }
175}
176
177#[derive(Debug, thiserror::Error)]
178pub enum RealmError {
179 #[error(
180 "no {REALMS_FILE} found walking up from {start} — the pin names realm '{realm}' but no realm definitions exist; commit a {REALMS_FILE} defining it"
181 )]
182 NoRealmsFile { start: String, realm: String },
183 #[error("{path}: not a valid realms file: {reason}")]
184 Parse { path: String, reason: String },
185 #[error(
186 "realm '{realm}' is not defined in {path} — defined realms: {defined:?}. Fix the pin or add the realm."
187 )]
188 Undefined {
189 realm: String,
190 path: String,
191 defined: Vec<String>,
192 },
193 #[error("realm '{realm}' in {path}: {reason}")]
194 BadDefinition {
195 realm: String,
196 path: String,
197 reason: String,
198 },
199 #[error("io error at {path}")]
200 Io {
201 path: String,
202 #[source]
203 source: std::io::Error,
204 },
205}
206
207#[derive(Deserialize)]
208#[serde(deny_unknown_fields)]
209struct RawRealmsFile {
210 #[serde(default)]
211 realm: BTreeMap<String, RawRealm>,
212}
213
214#[derive(Deserialize)]
215#[serde(deny_unknown_fields)]
216struct RawRealm {
217 registry: String,
218 #[serde(default)]
227 mirrors: Vec<String>,
228 #[serde(rename = "trust-root", default)]
230 trust_root: Option<String>,
231 #[serde(rename = "trust-root-file", default)]
233 trust_root_file: Option<String>,
234 #[serde(rename = "signed-index", default)]
237 signed_index: bool,
238 #[serde(rename = "retired-roots", default)]
240 retired_roots: Vec<RawRetiredRoot>,
241}
242
243#[derive(Deserialize)]
244#[serde(deny_unknown_fields)]
245struct RawRetiredRoot {
246 key: String,
247 retired: String,
248 #[serde(rename = "last-layer", default)]
249 last_layer: Option<String>,
250}
251
252pub fn find_realms_file(start: &Path) -> Option<PathBuf> {
254 let mut dir = Some(start);
255 while let Some(d) = dir {
256 let candidate = d.join(REALMS_FILE);
257 if candidate.is_file() {
258 return Some(candidate);
259 }
260 dir = d.parent();
261 }
262 None
263}
264
265pub fn realm_names(start: &Path) -> Result<Vec<String>, RealmError> {
269 let Some(path) = find_realms_file(start) else {
270 return Ok(Vec::new());
271 };
272 let text = std::fs::read_to_string(&path).map_err(|source| RealmError::Io {
273 path: path.display().to_string(),
274 source,
275 })?;
276 let file: RawRealmsFile = toml::from_str(&text).map_err(|e| RealmError::Parse {
277 path: path.display().to_string(),
278 reason: e.to_string(),
279 })?;
280 Ok(file.realm.into_keys().collect())
281}
282
283pub fn resolve_realm(start: &Path, name: &str) -> Result<Realm, RealmError> {
285 let Some(path) = find_realms_file(start) else {
286 return Err(RealmError::NoRealmsFile {
287 start: start.display().to_string(),
288 realm: name.to_string(),
289 });
290 };
291 let text = std::fs::read_to_string(&path).map_err(|source| RealmError::Io {
292 path: path.display().to_string(),
293 source,
294 })?;
295 let raw: RawRealmsFile = toml::from_str(&text).map_err(|e| RealmError::Parse {
296 path: path.display().to_string(),
297 reason: e.to_string(),
298 })?;
299 let Some(def) = raw.realm.get(name) else {
300 return Err(RealmError::Undefined {
301 realm: name.to_string(),
302 path: path.display().to_string(),
303 defined: raw.realm.keys().cloned().collect(),
304 });
305 };
306 let bad = |reason: String| RealmError::BadDefinition {
307 realm: name.to_string(),
308 path: path.display().to_string(),
309 reason,
310 };
311 let hex_key = match (&def.trust_root, &def.trust_root_file) {
312 (Some(_), Some(_)) => {
313 return Err(bad(
314 "both trust-root and trust-root-file given — pick one".into()
315 ));
316 }
317 (Some(inline), None) => inline.trim().to_string(),
318 (None, Some(file)) => {
319 let key_path = path.parent().unwrap_or(Path::new(".")).join(file);
320 std::fs::read_to_string(&key_path)
321 .map_err(|e| {
322 bad(format!(
323 "cannot read trust-root-file {}: {e}",
324 key_path.display()
325 ))
326 })?
327 .trim()
328 .to_string()
329 }
330 (None, None) => return Err(bad("no trust-root or trust-root-file".into())),
331 };
332 if hex_key.len() != 64 || !hex_key.chars().all(|c| c.is_ascii_hexdigit()) {
333 return Err(bad(
334 "trust root is not a 64-hex-char ed25519 public key".into()
335 ));
336 }
337 let trust_root = (0..hex_key.len())
338 .step_by(2)
339 .map(|i| u8::from_str_radix(&hex_key[i..i + 2], 16).expect("checked hex"))
340 .collect();
341 let mut retired_roots = Vec::with_capacity(def.retired_roots.len());
345 for raw in &def.retired_roots {
346 let hex = raw.key.trim().to_ascii_lowercase();
347 if hex.len() != 64 || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
348 return Err(bad(format!(
349 "retired root {:?} is not a 64-hex-char ed25519 public key",
350 raw.key
351 )));
352 }
353 let key: Vec<u8> = (0..hex.len())
354 .step_by(2)
355 .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("checked hex"))
356 .collect();
357 if key == trust_root {
363 return Err(bad(format!(
364 "the realm's live trust-root {hex} is also listed in retired-roots — \
365 a root cannot be both current and retired; remove it from one"
366 )));
367 }
368 retired_roots.push(RetiredRoot {
369 key,
370 retired: raw.retired.clone(),
371 last_layer: raw.last_layer.clone(),
372 });
373 }
374
375 Ok(Realm {
376 name: name.to_string(),
377 registry: def.registry.clone(),
378 sources: std::iter::once(def.registry.clone())
379 .chain(def.mirrors.iter().cloned())
380 .collect(),
381 trust_root,
382 signed_index: def.signed_index,
383 retired_roots,
384 })
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 fn realms_dir(content: &str) -> tempfile::TempDir {
392 let tmp = tempfile::tempdir().unwrap();
393 std::fs::write(tmp.path().join(REALMS_FILE), content).unwrap();
394 tmp
395 }
396
397 const NEW: &str = "7d3b892e6a33c70043becc708e08042e1cef0d54dd5ae6f23d7d4c68de1da1a0";
398 const OLD: &str = "4e771dc62a08be89e3450f8cd807da58ff70af4a4e124ebf2d2b71684cfd9973";
399
400 fn realm_with_retired(retired: &str) -> String {
401 format!(
402 "[realm.r]\nregistry = \"oci://example/x\"\ntrust-root = \"{NEW}\"\n\
403 retired-roots = [{retired}]\n"
404 )
405 }
406
407 #[test]
411 fn a_realm_can_declare_the_roots_it_has_retired() {
412 let dir = realms_dir(&realm_with_retired(&format!(
413 "{{ key = \"{OLD}\", retired = \"2026-09-07\", last-layer = \"2026.09.1\" }}"
414 )));
415 let realm = resolve_realm(dir.path(), "r").unwrap();
416 assert_eq!(realm.retired_roots.len(), 1);
417 let r = &realm.retired_roots[0];
418 assert_eq!(r.hex(), OLD);
419 assert_eq!(r.retired, "2026-09-07");
420 assert_eq!(r.last_layer.as_deref(), Some("2026.09.1"));
421 assert_ne!(
422 r.key, realm.trust_root,
423 "a retired root is not the live one"
424 );
425 }
426
427 #[test]
433 fn a_retired_root_is_never_a_key_anything_verifies_against() {
434 let dir = realms_dir(&realm_with_retired(&format!(
435 "{{ key = \"{OLD}\", retired = \"2026-09-07\" }}"
436 )));
437 let realm = resolve_realm(dir.path(), "r").unwrap();
438
439 let live: Vec<u8> = (0..64)
441 .step_by(2)
442 .map(|i| u8::from_str_radix(&NEW[i..i + 2], 16).unwrap())
443 .collect();
444 assert_eq!(realm.trust_root, live);
445 let expected = Realm {
448 retired_roots: Vec::new(),
449 ..realm.clone()
450 };
451 assert_eq!(
452 realm.fingerprint(),
453 expected.fingerprint(),
454 "retired roots must not change the store namespace"
455 );
456 }
457
458 #[test]
463 fn declaring_the_live_root_as_retired_is_refused() {
464 let dir = realms_dir(&realm_with_retired(&format!(
465 "{{ key = \"{NEW}\", retired = \"2026-09-07\" }}"
466 )));
467 let err = resolve_realm(dir.path(), "r").unwrap_err();
468 let msg = err.to_string();
469 assert!(
470 msg.contains("retired") && msg.contains("trust-root"),
471 "the error must say the live root is listed as retired, got: {msg}"
472 );
473 }
474
475 #[test]
482 fn a_retired_root_that_is_not_a_key_is_refused() {
483 for (key, why) in [
484 ("not-a-key", "fails both length and alphabet"),
485 (
486 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
487 "right LENGTH, not hex",
488 ),
489 ("abcdef", "hex, wrong LENGTH"),
490 (
491 "4e771dc62a08be89e3450f8cd807da58ff70af4a4e124ebf2d2b71684cfd997",
492 "hex, one char SHORT",
493 ),
494 ] {
495 let dir = realms_dir(&realm_with_retired(&format!(
496 "{{ key = \"{key}\", retired = \"2026-09-07\" }}"
497 )));
498 let err = resolve_realm(dir.path(), "r")
499 .expect_err(&format!("must refuse a retired root that {why}: {key}"))
500 .to_string();
501 assert!(
502 err.contains("64-hex"),
503 "a malformed retired root ({why}) must be refused like a malformed live one, \
504 got: {err}"
505 );
506 }
507 }
508
509 #[test]
516 fn a_retired_roots_fingerprint_is_the_one_its_partition_was_written_under() {
517 use crate::verify::generate_root_keypair;
518 let (_sk, old_pk) = generate_root_keypair();
519 let (_sk2, new_pk) = generate_root_keypair();
520 let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
521
522 let before = realms_dir(&format!(
526 "[realm.r]\nregistry = \"oci://example/x\"\ntrust-root = \"{}\"\n",
527 hex(&old_pk)
528 ));
529 let was_live = resolve_realm(before.path(), "r").unwrap().fingerprint();
530
531 let after = realms_dir(&format!(
533 "[realm.r]\nregistry = \"oci://example/x\"\ntrust-root = \"{}\"\n\
534 retired-roots = [{{ key = \"{}\", retired = \"2026-09-07\" }}]\n",
535 hex(&new_pk),
536 hex(&old_pk)
537 ));
538 let realm = resolve_realm(after.path(), "r").unwrap();
539
540 assert_eq!(
541 realm.retired_roots[0].fingerprint(),
542 was_live,
543 "a retired root must fingerprint to the partition it wrote, or `varve list` \
544 looks for a directory that does not exist"
545 );
546 assert_eq!(was_live.len(), 16, "the store namespace is 16 hex chars");
547 assert!(was_live.chars().all(|c| c.is_ascii_hexdigit()));
548 }
549
550 #[test]
554 fn a_realm_file_without_retired_roots_is_unchanged() {
555 let dir = realms_dir(&format!(
556 "[realm.r]\nregistry = \"oci://example/x\"\ntrust-root = \"{NEW}\"\n"
557 ));
558 let realm = resolve_realm(dir.path(), "r").unwrap();
559 assert!(realm.retired_roots.is_empty());
560 }
561
562 #[test]
569 fn a_partition_left_behind_by_a_retired_root_is_named_as_such() {
570 use crate::verify::generate_root_keypair;
571 let (_old_sk, old_pk) = generate_root_keypair();
572 let (_new_sk, new_pk) = generate_root_keypair();
573 let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
574 let dir = realms_dir(&format!(
575 "[realm.r]\nregistry = \"oci://example/x\"\ntrust-root = \"{}\"\n\
576 retired-roots = [{{ key = \"{}\", retired = \"2026-09-07\" }}]\n",
577 hex(&new_pk),
578 hex(&old_pk)
579 ));
580 let realm = resolve_realm(dir.path(), "r").unwrap();
581
582 assert_eq!(
584 realm.partition_label(&realm.fingerprint()).as_deref(),
585 Some("r")
586 );
587
588 let old_fp = realm.retired_roots[0].fingerprint();
591 assert_ne!(old_fp, realm.fingerprint());
592 let label = realm
593 .partition_label(&old_fp)
594 .expect("a retired root's partition must be recognised");
595 assert!(label.contains('r'), "names the realm: {label}");
596 assert!(label.contains("retired"), "says it is retired: {label}");
597 assert!(label.contains("2026-09-07"), "says when: {label}");
598
599 assert_eq!(realm.partition_label("0123456789abcdef"), None);
602 }
603
604 #[test]
610 fn a_signature_from_a_retired_root_is_attributed_not_merely_rejected() {
611 use crate::verify::generate_root_keypair;
612 let (old_sk, old_pk) = generate_root_keypair();
613 let (_new_sk, new_pk) = generate_root_keypair();
614 let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
615
616 let dir = realms_dir(&format!(
617 "[realm.r]\nregistry = \"oci://example/x\"\ntrust-root = \"{}\"\n\
618 retired-roots = [{{ key = \"{}\", retired = \"2026-09-07\", \
619 last-layer = \"2026.09.1\" }}]\n",
620 hex(&new_pk),
621 hex(&old_pk)
622 ));
623 let realm = resolve_realm(dir.path(), "r").unwrap();
624
625 let envelope = crate::verify::dsse_sign_typed(b"{}", "application/x.test", &old_sk, "k")
627 .expect("sign with the retired root");
628
629 let why = realm
630 .explain_retired_signature(envelope.as_bytes(), "application/x.test")
631 .expect("a signature from a declared retired root must be attributed");
632 assert!(why.contains("2026-09-07"), "must say WHEN: {why}");
633 assert!(
634 why.contains(&hex(&old_pk)),
635 "must name the retired root: {why}"
636 );
637 assert!(
638 why.contains("2026.09.1"),
639 "must say which layers used it: {why}"
640 );
641 assert!(
642 why.to_lowercase().contains("pin"),
643 "must say the fix is to move the pin: {why}"
644 );
645 }
646
647 #[test]
653 fn a_signature_from_an_unknown_key_is_not_blamed_on_a_rotation() {
654 use crate::verify::generate_root_keypair;
655 let (_old_sk, old_pk) = generate_root_keypair();
656 let (_new_sk, new_pk) = generate_root_keypair();
657 let (stranger_sk, _stranger_pk) = generate_root_keypair();
658 let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
659
660 let dir = realms_dir(&format!(
661 "[realm.r]\nregistry = \"oci://example/x\"\ntrust-root = \"{}\"\n\
662 retired-roots = [{{ key = \"{}\", retired = \"2026-09-07\" }}]\n",
663 hex(&new_pk),
664 hex(&old_pk)
665 ));
666 let realm = resolve_realm(dir.path(), "r").unwrap();
667 let envelope =
668 crate::verify::dsse_sign_typed(b"{}", "application/x.test", &stranger_sk, "k")
669 .expect("sign with a stranger key");
670 assert!(
671 realm
672 .explain_retired_signature(envelope.as_bytes(), "application/x.test")
673 .is_none(),
674 "an unknown signer must not be explained away as a rotation"
675 );
676 }
677
678 #[test]
680 fn every_defined_realm_is_named() {
681 let dir = realms_dir(TWO_REALMS);
687 let mut names = realm_names(dir.path()).unwrap();
688 names.sort();
689 assert_eq!(names, ["acme", "pulseengine"], "both realms named");
690
691 let empty = tempfile::tempdir().unwrap();
693 assert!(realm_names(empty.path()).unwrap().is_empty());
694
695 let bad = realms_dir("this is not toml {{{");
698 assert!(realm_names(bad.path()).is_err());
699 }
700
701 const TWO_REALMS: &str = r#"
702[realm.pulseengine]
703registry = "oci://ghcr.io/pulseengine/layers"
704trust-root = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
705
706[realm.acme]
707registry = "oci://ghcr.io/acme/layers"
708trust-root = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
709"#;
710
711 #[test]
713 fn realms_resolve_by_name_with_walk_up_discovery() {
714 let tmp = realms_dir(TWO_REALMS);
715 let deep = tmp.path().join("a/b");
716 std::fs::create_dir_all(&deep).unwrap();
717 let realm = resolve_realm(&deep, "acme").unwrap();
718 assert_eq!(realm.registry, "oci://ghcr.io/acme/layers");
719 assert_eq!(realm.trust_root, vec![0xbb; 32]);
720 }
721
722 #[test]
724 fn different_roots_mean_different_namespaces() {
725 let tmp = realms_dir(TWO_REALMS);
726 let pe = resolve_realm(tmp.path(), "pulseengine").unwrap();
727 let acme = resolve_realm(tmp.path(), "acme").unwrap();
728 assert_ne!(pe.fingerprint(), acme.fingerprint());
729 let root = Path::new("/var/root");
730 assert_ne!(pe.effective_root(root), acme.effective_root(root));
731 assert!(pe.effective_root(root).starts_with("/var/root/realms"));
732 }
733
734 #[test]
736 fn an_undefined_realm_fails_closed_naming_what_exists() {
737 let tmp = realms_dir(TWO_REALMS);
738 let err = resolve_realm(tmp.path(), "evil-corp").unwrap_err();
739 let msg = err.to_string();
740 assert!(msg.contains("evil-corp") && msg.contains("pulseengine") && msg.contains("acme"));
741 }
742
743 #[test]
745 fn a_missing_realms_file_fails_closed_with_guidance() {
746 let tmp = tempfile::tempdir().unwrap();
747 let err = resolve_realm(tmp.path(), "pulseengine").unwrap_err();
748 assert!(err.to_string().contains(REALMS_FILE));
749 }
750
751 #[test]
753 fn trust_root_file_is_read_relative_to_the_realms_file() {
754 let tmp = tempfile::tempdir().unwrap();
755 std::fs::create_dir_all(tmp.path().join("keys")).unwrap();
756 std::fs::write(tmp.path().join("keys/root.pub"), "cc".repeat(32)).unwrap();
757 std::fs::write(
758 tmp.path().join(REALMS_FILE),
759 "[realm.filekey]\nregistry = \"oci://r/x\"\ntrust-root-file = \"keys/root.pub\"\n",
760 )
761 .unwrap();
762 let realm = resolve_realm(tmp.path(), "filekey").unwrap();
763 assert_eq!(realm.trust_root, vec![0xcc; 32]);
764 }
765
766 #[test]
768 fn malformed_definitions_are_refused() {
769 for (name, body) in [
770 ("nokey", "[realm.nokey]\nregistry = \"oci://r/x\"\n"),
771 (
772 "badkey",
773 "[realm.badkey]\nregistry = \"oci://r/x\"\ntrust-root = \"zz\"\n",
774 ),
775 (
778 "shorthex",
779 "[realm.shorthex]\nregistry = \"oci://r/x\"\ntrust-root = \"cccccccccccccccccccccccccccccccc\"\n",
780 ),
781 (
782 "bothkeys",
783 "[realm.bothkeys]\nregistry = \"oci://r/x\"\ntrust-root = \"aa\"\ntrust-root-file = \"f\"\n",
784 ),
785 ] {
786 let tmp = realms_dir(body);
787 assert!(
788 resolve_realm(tmp.path(), name).is_err(),
789 "{name} must refuse"
790 );
791 }
792 }
793
794 #[test]
796 fn a_realm_declares_whether_it_publishes_a_signed_index() {
797 let tmp = realms_dir(
802 r#"
803[realm.declaring]
804registry = "oci://example.test/layers"
805trust-root = "7d3b892e6a33c70043becc708e08042e1cef0d54dd5ae6f23d7d4c68de1da1a0"
806signed-index = true
807
808[realm.silent]
809registry = "oci://example.test/other"
810trust-root = "7d3b892e6a33c70043becc708e08042e1cef0d54dd5ae6f23d7d4c68de1da1a0"
811"#,
812 );
813 assert!(
814 resolve_realm(tmp.path(), "declaring").unwrap().signed_index,
815 "a realm that declares an index must be recorded as declaring it"
816 );
817 assert!(
818 !resolve_realm(tmp.path(), "silent").unwrap().signed_index,
819 "the default must be false, or every existing realm breaks at once"
820 );
821 }
822}
823
824#[cfg(test)]
825mod mirror_tests {
826 use super::*;
827
828 fn parse(text: &str, name: &str) -> Realm {
829 let dir = std::env::temp_dir().join(format!("varve-realm-mirror-{name}"));
830 let _ = std::fs::remove_dir_all(&dir);
831 std::fs::create_dir_all(&dir).expect("scratch");
832 std::fs::write(dir.join(REALMS_FILE), text).expect("write");
833 resolve_realm(&dir, name).expect("parses")
834 }
835
836 #[test]
840 fn a_realm_naming_one_registry_still_works_and_has_one_source() {
841 let r = parse(
842 "[realm.solo]\nregistry = \"oci://ghcr.io/o/r\"\n\
843 trust-root = \"7d3b892e6a33c70043becc708e08042e1cef0d54dd5ae6f23d7d4c68de1da1a0\"\n",
844 "solo",
845 );
846 assert_eq!(r.registry, "oci://ghcr.io/o/r");
847 assert_eq!(r.sources, vec!["oci://ghcr.io/o/r".to_string()]);
848 }
849
850 #[test]
854 fn mirrors_follow_the_primary_in_the_order_they_are_written() {
855 let r = parse(
856 "[realm.many]\nregistry = \"oci://primary\"\n\
857 mirrors = [\"oci://second\", \"oci://third\"]\n\
858 trust-root = \"7d3b892e6a33c70043becc708e08042e1cef0d54dd5ae6f23d7d4c68de1da1a0\"\n",
859 "many",
860 );
861 assert_eq!(
862 r.sources,
863 vec![
864 "oci://primary".to_string(),
865 "oci://second".to_string(),
866 "oci://third".to_string()
867 ]
868 );
869 assert_eq!(r.registry, "oci://primary");
871 }
872
873 #[test]
878 fn mirrors_cannot_carry_a_trust_root_of_their_own() {
879 let dir = std::env::temp_dir().join("varve-realm-mirror-root");
880 let _ = std::fs::remove_dir_all(&dir);
881 std::fs::create_dir_all(&dir).expect("scratch");
882 std::fs::write(
883 dir.join(REALMS_FILE),
884 "[realm.x]\nregistry = \"oci://a\"\n\
885 mirrors = [{ registry = \"oci://b\", trust-root = \"dead\" }]\n\
886 trust-root = \"7d3b892e6a33c70043becc708e08042e1cef0d54dd5ae6f23d7d4c68de1da1a0\"\n",
887 )
888 .expect("write");
889 assert!(
890 resolve_realm(&dir, "x").is_err(),
891 "a mirror must not be able to declare its own trust root"
892 );
893 }
894}
895
896#[cfg(test)]
897mod shipped_realm_agrees_with_shipped_key {
898 use super::*;
899
900 #[test]
918 fn the_committed_realms_file_names_the_committed_key() {
919 let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"))
920 .parent()
921 .and_then(Path::parent)
922 .expect("crates/varve-core is two levels below the repo root")
923 .to_path_buf();
924
925 let key_file = repo_root.join("trust-roots/rolling.pub");
926 let shipped_key = std::fs::read_to_string(&key_file)
927 .expect("trust-roots/rolling.pub is committed")
928 .trim()
929 .to_ascii_lowercase();
930 assert_eq!(
931 shipped_key.len(),
932 64,
933 "{} must hold one 64-hex ed25519 public key",
934 key_file.display()
935 );
936
937 let realm = resolve_realm(&repo_root, "pulseengine")
939 .expect("this repository commits varve-realms.toml with realm 'pulseengine'");
940
941 let named_root = realm
942 .trust_root
943 .iter()
944 .map(|b| format!("{b:02x}"))
945 .collect::<String>();
946
947 assert_eq!(
948 named_root, shipped_key,
949 "varve-realms.toml names a rolling root that is NOT the key in \
950 trust-roots/rolling.pub. A layer signed with the key file will be \
951 REJECTED by every consumer that resolves the realm. Rotate both, \
952 or neither."
953 );
954 }
955}