Skip to main content

varve_core/
install.rs

1//! The verified install pipeline (REQ-VERIFY-001, REQ-ROLLBACK-001).
2//!
3//! Order is the security argument, so it is fixed here and tested:
4//!
5//! 1. fetch manifest bytes (by pin digest if pinned, else by name)
6//! 2. **verify the manifest signature** against the trust root
7//! 3. parse strictly; cross-check layer name, channel, pinned digest
8//! 4. anti-rollback check against the per-line high-water mark
9//! 5. fetch each blob; **verify each digest** against the signed manifest
10//! 6. lay down in the core
11//! 7. only now advance the high-water mark
12//!
13//! Nothing a source returns reaches the core unverified, and a failed
14//! install leaves both the core and the high-water marks untouched. The
15//! kill-criterion (REQ-VERIFY-001): running the same bytes through two
16//! different sources yields identical verdicts — a source that could
17//! influence acceptance has joined the trusted base, and the design is broken.
18
19use crate::layer::LayerId;
20use crate::manifest::{LayerManifest, ManifestError};
21use crate::pin::Pin;
22use crate::rollback::{HighWaterMarks, RollbackError, RollbackVerdict};
23use crate::source::{LayerRef, LayerSource, SourceError};
24use crate::store::{Store, StoreError, manifest_digest};
25
26/// Signature verification over fetched manifest bytes, against the
27/// PulseEngine trust root. Returns the *authenticated payload* (the layer
28/// manifest itself) — for DSSE-enveloped transport the fetched bytes and the
29/// trusted bytes differ, and everything downstream must use only the latter.
30/// Implementations carry the trust root; callers cannot relax it per-source —
31/// the pipeline takes exactly one verifier for all sources.
32pub trait ManifestVerifier {
33    fn verify(&self, fetched_bytes: &[u8]) -> Result<Vec<u8>, VerifyError>;
34}
35
36#[derive(Debug, thiserror::Error)]
37#[error("manifest signature verification failed: {0}")]
38pub struct VerifyError(pub String);
39
40/// A successful install.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct InstallOutcome {
43    pub digest: String,
44    pub layer: LayerId,
45    pub counter: u64,
46    /// `Some(age_days)` when the accepted layer is older than the staleness
47    /// threshold — surfaced, never fatal.
48    pub staleness_days: Option<i64>,
49    /// The greatest counter the realm's signed index asserts for this line
50    /// (REQ-INDEXAUTH-001 clause 4). Reported, NOT enforced: the high-water
51    /// mark records what this machine has accepted, and raising it to what
52    /// merely EXISTS would refuse a deliberately-pinned older layer the moment
53    /// a newer one is published — an availability failure wearing a security
54    /// control's clothes, in the one tool whose purpose is frozen toolchains.
55    pub index_high_water: Option<u64>,
56    /// How many attestations travelled with the layer and were persisted into
57    /// its root (REQ-ATTEST-002). Unverified at this point, by design — see
58    /// `attestcarry::carry_from_source`; `varve verify` is what checks them.
59    pub attestations_carried: usize,
60    /// Why carriage was incomplete, when it was. REPORTED, never fatal: a
61    /// mirror that dropped an attestation blob must not be able to make a
62    /// correctly-signed layer uninstallable, but the loss must not be silent
63    /// either — silence is the exact bandersnatch/Verdaccio failure this
64    /// requirement exists to surface.
65    pub attestation_note: Option<String>,
66}
67
68#[derive(Debug, thiserror::Error)]
69pub enum InstallError {
70    #[error(transparent)]
71    Index(#[from] crate::lineindex::IndexError),
72    #[error(transparent)]
73    Source(#[from] SourceError),
74    #[error(transparent)]
75    Verify(#[from] VerifyError),
76    #[error(transparent)]
77    Manifest(#[from] ManifestError),
78    #[error(
79        "source returned manifest {got} where the pin demands {pinned} — refusing (the digest is the artifact)"
80    )]
81    DigestMismatch { pinned: String, got: String },
82    #[error("manifest is for layer {got}, the pin names {pinned} — refusing")]
83    LayerMismatch { pinned: String, got: String },
84    #[error("manifest is on channel '{got}', the pin selects '{pinned}' — refusing")]
85    ChannelMismatch { pinned: String, got: String },
86    #[error(
87        "rollback refused: layer presents counter {presented} but the {line} line's high-water mark is {high_water} — a stale, validly-signed layer cannot be passed off as current"
88    )]
89    Rollback {
90        line: String,
91        presented: u64,
92        high_water: u64,
93    },
94    #[error(
95        "layer on line {line} presents counter {presented}, below the floor of {floor} this \
96         realm signs for the line — refusing.\n\n\
97         This machine has never installed from {line}, so it has no history to compare against. \
98         That is the one moment anti-rollback protects nobody, and the moment it is worth \
99         attacking: a fresh checkout, a new CI runner, a new laptop are all first contacts. The \
100         realm states a floor in its signed line-status so a consumer with no history still has \
101         one.\n\n\
102         Either the pin names a layer the realm has withdrawn from first-contact use, or \
103         something served an old signed layer to a new machine. Both are worth knowing before \
104         installing."
105    )]
106    BelowFloor {
107        line: String,
108        presented: u64,
109        floor: u64,
110    },
111    #[error("blob {digest} fetched for tool '{tool}' does not match its signed digest — refusing")]
112    BlobDigestMismatch { tool: String, digest: String },
113    #[error("manifest entry {digest} is missing the eu.pulseengine.tool annotation")]
114    UnnamedEntry { digest: String },
115    #[error(
116        "layer {layer} carries no entry for platform {platform} — refusing to install a \
117         wrong-architecture toolchain; use --platform only if you know why"
118    )]
119    NoPlatformEntry { layer: String, platform: String },
120    #[error(transparent)]
121    Store(#[from] StoreError),
122    #[error(transparent)]
123    State(#[from] RollbackError),
124}
125
126/// Policy inputs the caller supplies; time is data, not something the
127/// pipeline samples.
128pub struct InstallPolicy<'a> {
129    /// RFC 3339 "now" for the staleness verdict.
130    pub now: &'a str,
131    pub staleness_threshold_days: u32,
132    /// Target platform (a triple) entries must match. Entries without a
133    /// platform annotation are platform-independent (REQ-PLATFORM-001).
134    pub platform: &'a str,
135    /// The realm's line-index obligation (REQ-INDEXAUTH-001). `None` where the
136    /// pin names no realm — there is then no root that could have signed an
137    /// index, so there is nothing to check.
138    pub index: Option<crate::lineindex::IndexPolicy<'a>>,
139}
140
141pub fn install(
142    pin: &Pin,
143    source: &dyn LayerSource,
144    verifier: &dyn ManifestVerifier,
145    store: &Store,
146    marks: &mut HighWaterMarks,
147    policy: &InstallPolicy<'_>,
148) -> Result<InstallOutcome, InstallError> {
149    // 1. Fetch.
150    let layer_ref = match &pin.digest {
151        Some(digest) => LayerRef::Digest(digest.clone()),
152        None => LayerRef::Name(pin.layer.clone()),
153    };
154    let fetched = source.fetch_manifest(&layer_ref)?;
155
156    // 2. Signature first: nothing else is read from unverified bytes, and no
157    // blob is fetched on the strength of an unverified manifest. Only the
158    // authenticated payload the verifier returns is used from here on.
159    let bytes = verifier.verify(&fetched)?;
160
161    // 3. Strict parse + cross-checks against the pin.
162    let manifest = LayerManifest::parse(&bytes)?;
163    let digest = manifest_digest(&bytes);
164    if let Some(pinned) = &pin.digest
165        && &digest != pinned
166    {
167        return Err(InstallError::DigestMismatch {
168            pinned: pinned.clone(),
169            got: digest,
170        });
171    }
172    if manifest.layer != pin.layer {
173        return Err(InstallError::LayerMismatch {
174            pinned: pin.layer.to_string(),
175            got: manifest.layer.to_string(),
176        });
177    }
178    let pinned_channel = match pin.channel {
179        crate::pin::Channel::Qualified => "qualified",
180        crate::pin::Channel::Rolling => "rolling",
181    };
182    if manifest.channel != pinned_channel {
183        return Err(InstallError::ChannelMismatch {
184            pinned: pinned_channel.to_string(),
185            got: manifest.channel.clone(),
186        });
187    }
188
189    // 3b. The realm's signed index (REQ-INDEXAUTH-001). Before anti-rollback,
190    // because the index can RAISE the mark: a registry that hides the newest
191    // layer must not lower the bar this install has to clear. Everything here
192    // is verified against the realm's root — the source is the party being
193    // constrained and is never asked whether its own listing is honest.
194    let line = manifest.layer.line();
195    let line_str = line.to_string();
196    let index_cache = crate::lineindex::IndexCache::at_root(store.root());
197    let mut index_high_water: Option<u64> = None;
198    // The verified index and the bytes it came from, held until the install
199    // lands: a refused install must not leave a raised index behind, for the
200    // same reason it must not burn the high-water mark.
201    let mut accepted_index: Option<(crate::lineindex::LineIndex, Vec<u8>)> = None;
202    if let Some(index_policy) = &policy.index {
203        let envelope = source.fetch_line_index(&line_str)?;
204        let served = source.served_layers(&line_str)?;
205        // Clause 2 needs something to compare AGAINST, and it is the cache
206        // that supplies it. Passing `None` here left the whole
207        // presented-older-than-held rule unreachable from the pipeline: it
208        // could only ever fire in a test that called `check` itself.
209        let cached = index_cache.load(&line_str)?;
210        let verified = crate::lineindex::check(
211            &line_str,
212            envelope.as_deref(),
213            served.as_deref(),
214            cached.as_ref(),
215            index_policy,
216        )?;
217        index_high_water = verified.as_ref().and_then(|i| i.high_water());
218        if let (Some(doc), Some(bytes)) = (verified, envelope) {
219            accepted_index = Some((doc, bytes));
220        }
221    }
222
223    // REQ-FIRSTCONTACT-001: the realm's signed floor for this line, for a
224    // consumer that has no mark of its own.
225    //
226    // Read from the line-status document, which is already DSSE-signed by the
227    // realm root and already distributed beside the layer — a floor is worth
228    // nothing if whoever serves the bytes can choose it. Verified here against
229    // the same root the index policy carries; a document that does not verify,
230    // or that covers another line, yields NO floor rather than a trusted zero.
231    //
232    // Best-effort, and the residual is worth stating plainly: a source that
233    // simply omits the line-status removes the floor, exactly as it could
234    // before this existed. That is not a regression, and it is not full
235    // protection either — closing it needs the realm to REQUIRE a status the
236    // way `signed-index` requires an index, which is a realm-policy change and
237    // not this requirement.
238    let mut first_contact_floor: Option<u64> = None;
239    if marks.mark(line).is_none()
240        && let Some(index_policy) = &policy.index
241        && let Some(bytes) = source.fetch_line_status(&layer_ref)?
242        && let Ok(doc) =
243            crate::linestatus::LineStatus::verify_and_parse(&bytes, index_policy.root_public_key)
244        && doc.line == line_str
245    {
246        first_contact_floor = doc.min_counter;
247    }
248
249    // 4. Anti-rollback, before any blob moves.
250    match marks.check_with_floor(&manifest, first_contact_floor) {
251        RollbackVerdict::Rollback {
252            line,
253            presented,
254            high_water,
255        } => {
256            return Err(InstallError::Rollback {
257                line,
258                presented,
259                high_water,
260            });
261        }
262        RollbackVerdict::BelowFloor {
263            line,
264            presented,
265            floor,
266        } => {
267            return Err(InstallError::BelowFloor {
268                line,
269                presented,
270                floor,
271            });
272        }
273        RollbackVerdict::Accept => {}
274    }
275
276    // 5. Fetch blobs; each is accepted only if it matches its signed digest.
277    // Platform filtering happens BEFORE any fetch: a foreign entry's blob
278    // never even leaves the source (REQ-PLATFORM-001).
279    // Name, version and dispatchability travel with the bytes: a payload that
280    // is not dispatched by name is laid down under its version too, so two
281    // versions of one crate cannot overwrite each other (REQ-STORE-002).
282    struct Fetched {
283        name: String,
284        version: Option<String>,
285        dispatchable: bool,
286        bytes: Vec<u8>,
287    }
288    let mut tools: Vec<Fetched> = Vec::new();
289    let mut matched = 0usize;
290    for entry in &manifest.entries {
291        if !crate::platform::entry_matches(
292            entry
293                .annotations
294                .get(crate::platform::ANN_PLATFORM)
295                .map(String::as_str),
296            policy.platform,
297        ) {
298            continue;
299        }
300        // A composed layer is a REFERENCE, not a blob to lay down: its digest
301        // names another layer's manifest, which has its own install. Skip it
302        // here — install rejected it outright before, so a real composed layer
303        // could not be installed at all (REQ-COMPOSE-001).
304        if entry.kind() == Ok(crate::kind::PayloadKind::Layer) {
305            continue;
306        }
307        matched += 1;
308        let tool = entry
309            .annotations
310            .get("eu.pulseengine.tool")
311            .ok_or_else(|| InstallError::UnnamedEntry {
312                digest: entry.digest.clone(),
313            })?
314            .clone();
315        let blob = source.fetch_blob(&entry.digest)?;
316        if manifest_digest(&blob) != entry.digest {
317            return Err(InstallError::BlobDigestMismatch {
318                tool,
319                digest: entry.digest.clone(),
320            });
321        }
322        tools.push(Fetched {
323            name: tool,
324            version: crate::store::entry_version(entry).map(str::to_string),
325            dispatchable: crate::store::entry_is_dispatchable(entry),
326            bytes: blob,
327        });
328    }
329
330    // A fully-stamped layer with nothing for this platform fails closed —
331    // a wrong-architecture toolchain must not land looking installed.
332    if matched == 0 && !manifest.entries.is_empty() {
333        return Err(InstallError::NoPlatformEntry {
334            layer: manifest.layer.to_string(),
335            platform: policy.platform.to_string(),
336        });
337    }
338
339    // 6. Lay down.
340    let payloads: Vec<crate::store::Payload<'_>> = tools
341        .iter()
342        .map(|t| crate::store::Payload {
343            name: t.name.as_str(),
344            version: t.version.as_deref(),
345            dispatchable: t.dispatchable,
346            bytes: t.bytes.as_slice(),
347        })
348        .collect();
349    let stored_digest = store.lay_down_payloads(&bytes, &payloads)?;
350    debug_assert_eq!(stored_digest, digest);
351
352    // Retain the signature envelope beside the payload, so `varve verify`
353    // can repeat the install-time verdict offline, forever. (When transport
354    // was already the bare payload — test doubles — there is nothing to keep.)
355    if fetched != bytes
356        && let Some(entry) = store.get(&digest)?
357    {
358        let path = entry.root.join(crate::reverify::ENVELOPE_FILE);
359        std::fs::write(&path, &fetched).map_err(|source| StoreError::Io {
360            path: path.display().to_string(),
361            source,
362        })?;
363    }
364
365    // 6b. Carry the layer's attestations into its root (REQ-ATTEST-002).
366    // Registries publish this evidence and mirrors drop it: bandersnatch and
367    // Verdaccio carry none, and every Bazel Central Registry attestation URL
368    // points at github.com — so without this an air-gapped consumer receives
369    // the bytes and none of the accountability, with no error saying so.
370    // Persisting here is also what lets `archive` put it back on the wire.
371    let mut attestations_carried = 0usize;
372    let mut attestation_note = None;
373    if let Some(entry) = store.get(&digest)? {
374        match crate::attestcarry::carry_from_source(source, &layer_ref, &entry.root) {
375            Ok(n) => attestations_carried = n,
376            // Never fatal. The layer's own signature and digests decided the
377            // install several steps ago; a third party's evidence being
378            // missing, malformed, or unfetchable is evidence ABOUT the
379            // evidence, and refusing here would hand a mirror the power to
380            // make a perfectly-signed layer uninstallable.
381            Err(e) => attestation_note = Some(e.to_string()),
382        }
383    }
384
385    // 7. Only a fully landed layer advances the high-water mark — or the
386    // cached index it was checked against (REQ-INDEXAUTH-001 clause 2).
387    marks.advance(&manifest)?;
388    if let Some((doc, bytes)) = &accepted_index {
389        index_cache.update(&line_str, bytes, doc)?;
390    }
391
392    let staleness_days = crate::rollback::staleness_warning(
393        &manifest.issued_at,
394        policy.now,
395        policy.staleness_threshold_days,
396    );
397    Ok(InstallOutcome {
398        digest,
399        layer: manifest.layer.clone(),
400        counter: manifest.counter,
401        staleness_days,
402        index_high_water,
403        attestations_carried,
404        attestation_note,
405    })
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411    use crate::manifest::fixtures::manifest_with_tools;
412    use crate::pin::Pin;
413    use crate::rollback::HighWaterMarks;
414    use crate::source::{DirSource, MemorySource};
415
416    struct AcceptAll;
417    impl ManifestVerifier for AcceptAll {
418        fn verify(&self, fetched: &[u8]) -> Result<Vec<u8>, VerifyError> {
419            Ok(fetched.to_vec())
420        }
421    }
422
423    struct RejectAll;
424    impl ManifestVerifier for RejectAll {
425        fn verify(&self, _: &[u8]) -> Result<Vec<u8>, VerifyError> {
426            Err(VerifyError("untrusted signature (test)".into()))
427        }
428    }
429
430    fn pin(layer: &str) -> Pin {
431        Pin::parse(
432            &format!(
433                "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"{layer}\"\n"
434            ),
435            "varve.toml",
436        )
437        .unwrap()
438    }
439
440    fn policy() -> InstallPolicy<'static> {
441        InstallPolicy {
442            index: None,
443            now: "2026-08-07T00:00:00Z",
444            staleness_threshold_days: 90,
445            platform: "test-platform",
446        }
447    }
448
449    /// One layer's worth of test material: manifest bytes + blobs.
450    fn july() -> (Vec<u8>, Vec<(String, Vec<u8>)>) {
451        let synth = b"july-synth".to_vec();
452        let rivet = b"july-rivet".to_vec();
453        let blobs = vec![
454            (manifest_digest(&synth), synth),
455            (manifest_digest(&rivet), rivet),
456        ];
457        let bytes = manifest_with_tools(
458            "2026.07.0",
459            "qualified",
460            1,
461            "2026-07-31T09:14:00Z",
462            &[("synth", &blobs[0].0), ("rivet", &blobs[1].0)],
463        );
464        (bytes, blobs)
465    }
466
467    fn memory_source(manifest: &[u8], blobs: &[(String, Vec<u8>)]) -> MemorySource {
468        let mut source = MemorySource::new().with_manifest(manifest);
469        for (digest, bytes) in blobs {
470            source = source.with_blob(digest, bytes);
471        }
472        source
473    }
474
475    fn setup() -> (tempfile::TempDir, Store, HighWaterMarks) {
476        let tmp = tempfile::tempdir().unwrap();
477        let root = tmp.path().join("varve-root");
478        let store = Store::at(&root);
479        let marks = HighWaterMarks::load(&root).unwrap();
480        (tmp, store, marks)
481    }
482
483    // rivet: verifies REQ-VERIFY-001
484    #[test]
485    fn installs_a_verified_layer_end_to_end() {
486        let (_tmp, store, mut marks) = setup();
487        let (bytes, blobs) = july();
488        let source = memory_source(&bytes, &blobs);
489        let outcome = install(
490            &pin("2026.07.0"),
491            &source,
492            &AcceptAll,
493            &store,
494            &mut marks,
495            &policy(),
496        )
497        .unwrap();
498        assert_eq!(outcome.layer.to_string(), "2026.07.0");
499        assert_eq!(outcome.digest, manifest_digest(&bytes));
500        // The layer is resolvable afterwards — install feeds resolution.
501        let entry = store.get(&outcome.digest).unwrap().unwrap();
502        assert!(store.tool_path(&entry, "synth").is_some());
503        assert!(store.tool_path(&entry, "rivet").is_some());
504    }
505
506    // rivet: verifies REQ-STORE-002
507    #[test]
508    fn installing_two_versions_of_one_crate_lands_both_sets_of_bytes() {
509        // Clause 2 through the function that actually runs it. `install`
510        // collected (name, bytes) pairs and handed them to a store that wrote
511        // `bin/<name>`, so the second serde's bytes landed under the first's
512        // name: ONE file, the WRONG contents, and `verify` then failing on the
513        // other entry with nothing explaining why.
514        use crate::manifest::fixtures::manifest_with_payloads;
515        let (_tmp, store, mut marks) = setup();
516        let (a, b) = (
517            b"serde-1.0.200-crate".to_vec(),
518            b"serde-1.0.210-crate".to_vec(),
519        );
520        let (da, db) = (manifest_digest(&a), manifest_digest(&b));
521        let bytes = manifest_with_payloads(
522            "2026.07.0",
523            "qualified",
524            1,
525            "2026-07-31T09:14:00Z",
526            &[
527                ("serde", "1.0.200", "crate", &da),
528                ("serde", "1.0.210", "crate", &db),
529            ],
530        );
531        let source = memory_source(&bytes, &[(da, a.clone()), (db, b.clone())]);
532        let outcome = install(
533            &pin("2026.07.0"),
534            &source,
535            &AcceptAll,
536            &store,
537            &mut marks,
538            &policy(),
539        )
540        .expect("two versions of one crate is the ordinary shape of a dependency graph");
541
542        let entry = store.get(&outcome.digest).unwrap().unwrap();
543        assert_eq!(
544            std::fs::read(entry.root.join("payloads/serde/1.0.200")).unwrap(),
545            a
546        );
547        assert_eq!(
548            std::fs::read(entry.root.join("payloads/serde/1.0.210")).unwrap(),
549            b,
550            "the second version must not have overwritten the first"
551        );
552        // Nothing landed under the bare name, where one version would have won.
553        assert!(!entry.root.join("bin/serde").exists());
554    }
555
556    // rivet: verifies REQ-STORE-002
557    #[test]
558    fn a_signed_manifest_whose_entries_share_one_identity_is_refused_not_overwritten() {
559        // `deposit` refuses this, but deposit is not the only producer varve
560        // installs from: any realm root can sign a manifest, built by any
561        // software. If two entries ever reach one path, install must fail
562        // LOUDLY — writing one over the other would put the wrong bytes under
563        // the right name and make `verify` fail on the innocent entry.
564        use crate::manifest::fixtures::manifest_with_payloads;
565        let (_tmp, store, mut marks) = setup();
566        let (a, b) = (b"first-bytes".to_vec(), b"second-bytes".to_vec());
567        let (da, db) = (manifest_digest(&a), manifest_digest(&b));
568        let bytes = manifest_with_payloads(
569            "2026.07.0",
570            "qualified",
571            1,
572            "2026-07-31T09:14:00Z",
573            &[
574                ("serde", "1.0.200", "crate", &da),
575                ("serde", "1.0.200", "crate", &db),
576            ],
577        );
578        let source = memory_source(&bytes, &[(da, a), (db, b)]);
579        let err = install(
580            &pin("2026.07.0"),
581            &source,
582            &AcceptAll,
583            &store,
584            &mut marks,
585            &policy(),
586        )
587        .expect_err("one identity, two payloads: the store must refuse");
588        assert!(
589            matches!(err, InstallError::Store(StoreError::Collision { .. })),
590            "got: {err}"
591        );
592        assert!(store.list().unwrap().is_empty(), "nothing may be laid down");
593        assert_eq!(
594            marks.mark(&"2026.07".parse().unwrap()),
595            None,
596            "a refused install must not burn the mark"
597        );
598    }
599
600    // rivet: verifies REQ-VERIFY-001
601    #[test]
602    fn kill_criterion_two_sources_one_verdict() {
603        // The same bytes through two different transports must produce
604        // identical verdicts — accept AND reject cases.
605        let (bytes, blobs) = july();
606        let tmp = tempfile::tempdir().unwrap();
607        let dir = DirSource::at(tmp.path().join("archive"));
608        dir.put(
609            &bytes,
610            &blobs
611                .iter()
612                .map(|(d, b)| (d.as_str(), b.as_slice()))
613                .collect::<Vec<_>>(),
614        )
615        .unwrap();
616        let mem = memory_source(&bytes, &blobs);
617
618        let run = |source: &dyn LayerSource, verifier: &dyn ManifestVerifier| {
619            let (_t, store, mut marks) = setup();
620            install(
621                &pin("2026.07.0"),
622                source,
623                verifier,
624                &store,
625                &mut marks,
626                &policy(),
627            )
628            .map_err(|e| e.to_string())
629        };
630
631        let accept_mem = run(&mem, &AcceptAll).unwrap();
632        let accept_dir = run(&dir, &AcceptAll).unwrap();
633        assert_eq!(accept_mem, accept_dir, "same bytes, same acceptance");
634
635        let reject_mem = run(&mem, &RejectAll).unwrap_err();
636        let reject_dir = run(&dir, &RejectAll).unwrap_err();
637        assert_eq!(reject_mem, reject_dir, "same bytes, same rejection");
638    }
639
640    // rivet: verifies REQ-VERIFY-001
641    #[test]
642    fn an_unverified_manifest_fetches_no_blobs_and_installs_nothing() {
643        struct CountingSource {
644            inner: MemorySource,
645            blob_fetches: std::cell::Cell<usize>,
646        }
647        impl LayerSource for CountingSource {
648            fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError> {
649                self.inner.fetch_manifest(layer)
650            }
651            fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError> {
652                self.blob_fetches.set(self.blob_fetches.get() + 1);
653                self.inner.fetch_blob(digest)
654            }
655        }
656        let (_tmp, store, mut marks) = setup();
657        let (bytes, blobs) = july();
658        let source = CountingSource {
659            inner: memory_source(&bytes, &blobs),
660            blob_fetches: std::cell::Cell::new(0),
661        };
662        let err = install(
663            &pin("2026.07.0"),
664            &source,
665            &RejectAll,
666            &store,
667            &mut marks,
668            &policy(),
669        )
670        .unwrap_err();
671        assert!(matches!(err, InstallError::Verify(_)), "got: {err}");
672        assert_eq!(
673            source.blob_fetches.get(),
674            0,
675            "no blob leaves the source before the signature verdict"
676        );
677        assert!(store.list().unwrap().is_empty(), "nothing laid down");
678    }
679
680    // rivet: verifies REQ-VERIFY-001
681    #[test]
682    fn a_source_that_alters_a_blob_is_caught_by_the_signed_digest() {
683        let (_tmp, store, mut marks) = setup();
684        let (bytes, blobs) = july();
685        // Serve the right manifest but tamper with one blob.
686        let mut source = MemorySource::new().with_manifest(&bytes);
687        source = source.with_blob(&blobs[0].0, b"EVIL");
688        source = source.with_blob(&blobs[1].0, &blobs[1].1);
689        let err = install(
690            &pin("2026.07.0"),
691            &source,
692            &AcceptAll,
693            &store,
694            &mut marks,
695            &policy(),
696        )
697        .unwrap_err();
698        assert!(
699            matches!(err, InstallError::BlobDigestMismatch { .. }),
700            "got: {err}"
701        );
702        assert!(
703            store.list().unwrap().is_empty(),
704            "tampered layer must not land"
705        );
706    }
707
708    // rivet: verifies REQ-VERIFY-001
709    #[test]
710    fn a_source_that_answers_a_digest_request_with_other_bytes_is_caught() {
711        struct LyingSource(Vec<u8>);
712        impl LayerSource for LyingSource {
713            fn fetch_manifest(&self, _: &LayerRef) -> Result<Vec<u8>, SourceError> {
714                Ok(self.0.clone())
715            }
716            fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError> {
717                Err(SourceError::NotFound(digest.into()))
718            }
719        }
720        let (_tmp, store, mut marks) = setup();
721        // Pin a digest that is NOT the digest of what the source returns.
722        let (bytes, _) = july();
723        let other = manifest_with_tools("2026.07.0", "qualified", 1, "2026-07-31T09:14:00Z", &[]);
724        let pinned = manifest_digest(&other);
725        let hex = pinned.strip_prefix("sha256:").unwrap();
726        let p = Pin::parse(
727            &format!(
728                "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ndigest = \"sha256:{hex}\"\n"
729            ),
730            "varve.toml",
731        )
732        .unwrap();
733        let err = install(
734            &p,
735            &LyingSource(bytes),
736            &AcceptAll,
737            &store,
738            &mut marks,
739            &policy(),
740        )
741        .unwrap_err();
742        assert!(
743            matches!(err, InstallError::DigestMismatch { .. }),
744            "got: {err}"
745        );
746    }
747
748    // rivet: verifies REQ-ROLLBACK-001
749    #[test]
750    fn a_rolled_back_layer_is_refused_at_install() {
751        let (_tmp, store, mut marks) = setup();
752        // The line's high-water mark is already at 5.
753        let newer = manifest_with_tools("2026.07.2", "qualified", 5, "2026-08-01T00:00:00Z", &[]);
754        marks
755            .advance(&LayerManifest::parse(&newer).unwrap())
756            .unwrap();
757        let (bytes, blobs) = july(); // counter 1 < 5
758        let source = memory_source(&bytes, &blobs);
759        let err = install(
760            &pin("2026.07.0"),
761            &source,
762            &AcceptAll,
763            &store,
764            &mut marks,
765            &policy(),
766        )
767        .unwrap_err();
768        assert!(
769            matches!(
770                err,
771                InstallError::Rollback {
772                    presented: 1,
773                    high_water: 5,
774                    ..
775                }
776            ),
777            "got: {err}"
778        );
779        assert!(store.list().unwrap().is_empty());
780    }
781
782    // rivet: verifies REQ-ROLLBACK-001
783    #[test]
784    fn a_failed_install_does_not_advance_the_high_water_mark() {
785        let (_tmp, store, mut marks) = setup();
786        let (bytes, blobs) = july();
787        // Source is missing one blob: install fails after the rollback check.
788        let source = MemorySource::new()
789            .with_manifest(&bytes)
790            .with_blob(&blobs[0].0, &blobs[0].1);
791        let err = install(
792            &pin("2026.07.0"),
793            &source,
794            &AcceptAll,
795            &store,
796            &mut marks,
797            &policy(),
798        )
799        .unwrap_err();
800        assert!(
801            matches!(err, InstallError::Source(SourceError::NotFound(_))),
802            "got: {err}"
803        );
804        let m = LayerManifest::parse(&bytes).unwrap();
805        assert_eq!(
806            marks.mark(m.layer.line()),
807            None,
808            "failed install must not burn the mark"
809        );
810    }
811
812    // rivet: verifies REQ-ROLLBACK-001
813    #[test]
814    fn staleness_is_surfaced_on_an_accepted_layer() {
815        let (_tmp, store, mut marks) = setup();
816        let (bytes, blobs) = july(); // issued 2026-07-31
817        let source = memory_source(&bytes, &blobs);
818        let policy = InstallPolicy {
819            index: None,
820            now: "2026-12-01T00:00:00Z",
821            staleness_threshold_days: 90,
822            platform: "test-platform",
823        };
824        let outcome = install(
825            &pin("2026.07.0"),
826            &source,
827            &AcceptAll,
828            &store,
829            &mut marks,
830            &policy,
831        )
832        .unwrap();
833        assert_eq!(outcome.staleness_days, Some(123));
834    }
835
836    // rivet: verifies REQ-PLATFORM-001
837    #[test]
838    fn install_selects_only_entries_for_the_target_platform() {
839        let (_tmp, store, mut marks) = setup();
840        let here = b"here-tool".to_vec();
841        let there = b"there-tool".to_vec();
842        let (d_here, d_there) = (manifest_digest(&here), manifest_digest(&there));
843        let bytes = crate::manifest::fixtures::manifest_with_platform_tools(
844            "2026.07.0",
845            "qualified",
846            1,
847            "2026-07-31T09:14:00Z",
848            &[
849                ("synth", &d_here, Some("test-platform")),
850                ("synth", &d_there, Some("other-platform")),
851            ],
852        );
853        // Only the matching blob is served — a correct install never asks
854        // for the foreign one.
855        let source = MemorySource::new()
856            .with_manifest(&bytes)
857            .with_blob(&d_here, &here);
858        let policy = InstallPolicy {
859            index: None,
860            now: "2026-08-07T00:00:00Z",
861            staleness_threshold_days: 90,
862            platform: "test-platform",
863        };
864        let outcome = install(
865            &pin("2026.07.0"),
866            &source,
867            &AcceptAll,
868            &store,
869            &mut marks,
870            &policy,
871        )
872        .unwrap();
873        let entry = store.get(&outcome.digest).unwrap().unwrap();
874        assert_eq!(
875            std::fs::read(store.tool_path(&entry, "synth").unwrap()).unwrap(),
876            here,
877            "the host-platform binary landed"
878        );
879    }
880
881    // rivet: verifies REQ-PLATFORM-001
882    #[test]
883    fn a_layer_with_nothing_for_the_host_platform_fails_closed() {
884        let (_tmp, store, mut marks) = setup();
885        let there = b"there-tool".to_vec();
886        let d_there = manifest_digest(&there);
887        let bytes = crate::manifest::fixtures::manifest_with_platform_tools(
888            "2026.07.0",
889            "qualified",
890            1,
891            "2026-07-31T09:14:00Z",
892            &[("synth", &d_there, Some("other-platform"))],
893        );
894        let source = MemorySource::new()
895            .with_manifest(&bytes)
896            .with_blob(&d_there, &there);
897        let policy = InstallPolicy {
898            index: None,
899            now: "2026-08-07T00:00:00Z",
900            staleness_threshold_days: 90,
901            platform: "test-platform",
902        };
903        let err = install(
904            &pin("2026.07.0"),
905            &source,
906            &AcceptAll,
907            &store,
908            &mut marks,
909            &policy,
910        )
911        .unwrap_err();
912        assert!(
913            matches!(err, InstallError::NoPlatformEntry { .. }),
914            "got: {err}"
915        );
916        assert!(store.list().unwrap().is_empty(), "no wrong-arch bytes land");
917    }
918
919    // rivet: verifies REQ-PLATFORM-001
920    #[test]
921    fn unstamped_legacy_entries_install_on_any_platform() {
922        let (_tmp, store, mut marks) = setup();
923        let (bytes, blobs) = july(); // fixtures without platform annotations
924        let source = memory_source(&bytes, &blobs);
925        let policy = InstallPolicy {
926            index: None,
927            now: "2026-08-07T00:00:00Z",
928            staleness_threshold_days: 90,
929            platform: "any-platform-at-all",
930        };
931        assert!(
932            install(
933                &pin("2026.07.0"),
934                &source,
935                &AcceptAll,
936                &store,
937                &mut marks,
938                &policy
939            )
940            .is_ok()
941        );
942    }
943
944    // rivet: verifies REQ-PIN-001
945    #[test]
946    fn channel_and_layer_mismatches_are_refused() {
947        let (_tmp, store, mut marks) = setup();
948        // Manifest says rolling; pin says qualified.
949        let synth = b"s".to_vec();
950        let d = manifest_digest(&synth);
951        let bytes = manifest_with_tools(
952            "2026.07.0",
953            "rolling",
954            1,
955            "2026-07-31T09:14:00Z",
956            &[("synth", &d)],
957        );
958        let source = MemorySource::new()
959            .with_manifest(&bytes)
960            .with_blob(&d, &synth);
961        let err = install(
962            &pin("2026.07.0"),
963            &source,
964            &AcceptAll,
965            &store,
966            &mut marks,
967            &policy(),
968        )
969        .unwrap_err();
970        assert!(
971            matches!(err, InstallError::ChannelMismatch { .. }),
972            "got: {err}"
973        );
974    }
975
976    // rivet: verifies REQ-ATTEST-002
977    #[test]
978    fn install_carries_the_attestations_the_source_holds_into_the_installed_layer() {
979        // The transport half. Binding shipped in v0.22.0 and reached nobody:
980        // an attestation that stays in the producer's CI is not evidence a
981        // consumer has. This asserts the evidence lands in the installed layer
982        // root — the only place `archive` and `verify` can find it later.
983        use crate::attest::{AttestationKind, sign, statement};
984        let (sk, pk) = crate::verify::generate_root_keypair();
985        let (_tmp, store, mut marks) = setup();
986        let (bytes, blobs) = july();
987        let layer_digest = manifest_digest(&bytes);
988        let sbom = b"{\"bomFormat\":\"CycloneDX\"}";
989        let st = statement(
990            "2026.07.0",
991            &layer_digest,
992            AttestationKind::Sbom,
993            sbom,
994            "acme-ci",
995        );
996        let envelope = sign(&st, &sk, "root-1").unwrap();
997        let source = memory_source(&bytes, &blobs).with_attestation(envelope.as_bytes(), sbom);
998
999        let outcome = install(
1000            &pin("2026.07.0"),
1001            &source,
1002            &AcceptAll,
1003            &store,
1004            &mut marks,
1005            &policy(),
1006        )
1007        .unwrap();
1008        assert_eq!(
1009            outcome.attestations_carried, 1,
1010            "install must report what travelled"
1011        );
1012        assert_eq!(outcome.attestation_note, None);
1013
1014        // …and it is really on disk, verbatim, and still binds offline.
1015        let entry = store.get(&outcome.digest).unwrap().unwrap();
1016        let carried = crate::attestcarry::read_persisted(&entry.root, "2026.07.0").unwrap();
1017        assert_eq!(carried.len(), 1, "the evidence reached the installed layer");
1018        assert_eq!(
1019            carried[0].bytes, sbom,
1020            "the attested bytes are stored VERBATIM — varve transports another party's \
1021             judgement, it never restates it"
1022        );
1023        let reports = crate::attestcarry::report(&carried, &layer_digest, "2026.07.0", &pk);
1024        assert!(reports[0].binds, "reason: {:?}", reports[0].reason);
1025        assert_eq!(reports[0].producer, "acme-ci");
1026    }
1027
1028    // rivet: verifies REQ-ATTEST-002
1029    #[test]
1030    fn a_source_that_loses_its_attestations_still_installs_but_says_so() {
1031        // Deliberate: REPORTING, not refusal. A mirror that drops the evidence
1032        // must not gain the power to make a correctly-signed layer
1033        // uninstallable — that would make varve's availability a function of
1034        // someone else's mirroring. But the loss must not be SILENT either:
1035        // silence is precisely the bandersnatch/Verdaccio failure this
1036        // requirement exists to surface.
1037        struct LosesAttestations(MemorySource);
1038        impl LayerSource for LosesAttestations {
1039            fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError> {
1040                self.0.fetch_manifest(layer)
1041            }
1042            fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError> {
1043                self.0.fetch_blob(digest)
1044            }
1045            fn fetch_attestations(
1046                &self,
1047                _layer: &LayerRef,
1048            ) -> Result<Vec<crate::attestcarry::CarriedAttestation>, SourceError> {
1049                Err(SourceError::Transport("the mirror dropped them".into()))
1050            }
1051        }
1052        let (_tmp, store, mut marks) = setup();
1053        let (bytes, blobs) = july();
1054        let outcome = install(
1055            &pin("2026.07.0"),
1056            &LosesAttestations(memory_source(&bytes, &blobs)),
1057            &AcceptAll,
1058            &store,
1059            &mut marks,
1060            &policy(),
1061        )
1062        .expect("a layer whose own signature and digests are good is still a good layer");
1063        assert_eq!(outcome.attestations_carried, 0);
1064        let note = outcome
1065            .attestation_note
1066            .expect("the loss must be reported, never swallowed");
1067        assert!(note.contains("dropped them"), "note: {note}");
1068    }
1069
1070    /// REQ-FIRSTCONTACT-001 end to end, through install().
1071    ///
1072    /// A machine with no marks file — a fresh checkout, a new CI runner — used
1073    /// to accept ANY counter on a line it had never seen, because there was
1074    /// nothing to compare against. That is the one moment anti-rollback
1075    /// protects nobody, and it is the moment worth attacking.
1076    // rivet: verifies REQ-FIRSTCONTACT-001
1077    #[test]
1078    fn a_new_machine_refuses_a_layer_below_the_realms_signed_floor() {
1079        use crate::lineindex::{IndexPolicy, IndexedLayer, LineIndex};
1080        let (sk, pk) = crate::verify::generate_root_keypair();
1081        let (_tmp, store, mut marks) = setup();
1082
1083        let (bytes, blobs) = july(); // 2026.07.0, counter 1
1084        let mut source = MemorySource::new().with_manifest(&bytes);
1085        for (d, b) in &blobs {
1086            source = source.with_blob(d, b);
1087        }
1088        let source = source
1089            .with_line_index(
1090                LineIndex {
1091                    line: "2026.07".into(),
1092                    counter: 1,
1093                    issued_at: "2026-08-07T00:00:00Z".into(),
1094                    layers: vec![IndexedLayer {
1095                        layer: "2026.07.0".into(),
1096                        digest: manifest_digest(&bytes),
1097                        channel: "qualified".into(),
1098                        counter: 1,
1099                    }],
1100                }
1101                .sign(&sk, "root-1")
1102                .unwrap()
1103                .as_bytes(),
1104            )
1105            // The realm signs a floor of 5 for this line; the layer is 1.
1106            .with_line_status(
1107                crate::linestatus::LineStatus {
1108                    line: "2026.07".into(),
1109                    counter: 1,
1110                    issued_at: "2026-08-07T00:00:00Z".into(),
1111                    min_counter: Some(5),
1112                    support_until: None,
1113                    yanked: Default::default(),
1114                    known_problems: Vec::new(),
1115                }
1116                .sign(&sk, "root-1")
1117                .unwrap()
1118                .as_bytes(),
1119            );
1120
1121        let policy = InstallPolicy {
1122            index: Some(IndexPolicy {
1123                realm: "acme",
1124                root_public_key: &pk,
1125                required: true,
1126            }),
1127            ..policy()
1128        };
1129        let err = install(
1130            &pin("2026.07.0"),
1131            &source,
1132            &AcceptAll,
1133            &store,
1134            &mut marks,
1135            &policy,
1136        )
1137        .expect_err("a first contact below the signed floor must be refused");
1138        match &err {
1139            InstallError::BelowFloor {
1140                line,
1141                presented,
1142                floor,
1143            } => {
1144                assert_eq!(line, "2026.07");
1145                assert_eq!(*presented, 1);
1146                assert_eq!(*floor, 5);
1147            }
1148            other => panic!("expected BelowFloor, got {other:?}"),
1149        }
1150        // And the refusal explains why a NEW machine is the interesting case.
1151        assert!(err.to_string().contains("never installed"), "{err}");
1152    }
1153
1154    /// The same layer, same realm, no stated floor: unchanged behaviour, so a
1155    /// realm that has not adopted this keeps installing exactly as before.
1156    // rivet: verifies REQ-FIRSTCONTACT-001
1157    #[test]
1158    fn a_line_whose_realm_states_no_floor_installs_as_it_always_did() {
1159        use crate::lineindex::{IndexPolicy, IndexedLayer, LineIndex};
1160        let (sk, pk) = crate::verify::generate_root_keypair();
1161        let (_tmp, store, mut marks) = setup();
1162
1163        let (bytes, blobs) = july();
1164        let mut source = MemorySource::new().with_manifest(&bytes);
1165        for (d, b) in &blobs {
1166            source = source.with_blob(d, b);
1167        }
1168        let source = source
1169            .with_line_index(
1170                LineIndex {
1171                    line: "2026.07".into(),
1172                    counter: 1,
1173                    issued_at: "2026-08-07T00:00:00Z".into(),
1174                    layers: vec![IndexedLayer {
1175                        layer: "2026.07.0".into(),
1176                        digest: manifest_digest(&bytes),
1177                        channel: "qualified".into(),
1178                        counter: 1,
1179                    }],
1180                }
1181                .sign(&sk, "root-1")
1182                .unwrap()
1183                .as_bytes(),
1184            )
1185            .with_line_status(
1186                crate::linestatus::LineStatus {
1187                    line: "2026.07".into(),
1188                    counter: 1,
1189                    issued_at: "2026-08-07T00:00:00Z".into(),
1190                    min_counter: None,
1191                    support_until: None,
1192                    yanked: Default::default(),
1193                    known_problems: Vec::new(),
1194                }
1195                .sign(&sk, "root-1")
1196                .unwrap()
1197                .as_bytes(),
1198            );
1199
1200        let policy = InstallPolicy {
1201            index: Some(IndexPolicy {
1202                realm: "acme",
1203                root_public_key: &pk,
1204                required: true,
1205            }),
1206            ..policy()
1207        };
1208        install(
1209            &pin("2026.07.0"),
1210            &source,
1211            &AcceptAll,
1212            &store,
1213            &mut marks,
1214            &policy,
1215        )
1216        .expect("no stated floor must not change anything");
1217    }
1218
1219    // rivet: verifies REQ-INDEXAUTH-001
1220    #[test]
1221    fn install_raises_the_mark_from_the_index_even_when_the_layer_is_hidden() {
1222        // Clause 4 end to end, through install() — the Uptane property. The
1223        // registry serves ONLY the old layer and its signature is perfect. The
1224        // realm's signed index says a newer one exists. Without this, the
1225        // registry's silence sets the bar, and the hidden layer's counter
1226        // never constrains anything.
1227        use crate::lineindex::{IndexPolicy, IndexedLayer, LineIndex};
1228        let (sk, pk) = crate::verify::generate_root_keypair();
1229        let (_tmp, store, mut marks) = setup();
1230
1231        let (bytes, blobs) = july(); // 2026.07.0, counter 1
1232        let mut source = MemorySource::new().with_manifest(&bytes);
1233        for (d, b) in &blobs {
1234            source = source.with_blob(d, b);
1235        }
1236        let source = source.with_line_index(
1237            LineIndex {
1238                line: "2026.07".into(),
1239                counter: 1,
1240                issued_at: "2026-08-07T00:00:00Z".into(),
1241                layers: vec![
1242                    IndexedLayer {
1243                        layer: "2026.07.0".into(),
1244                        digest: manifest_digest(&bytes),
1245                        channel: "qualified".into(),
1246                        counter: 1,
1247                    },
1248                    // The layer the registry is not serving.
1249                    IndexedLayer {
1250                        layer: "2026.07.9".into(),
1251                        digest: "sha256:hidden".into(),
1252                        channel: "qualified".into(),
1253                        counter: 42,
1254                    },
1255                ],
1256            }
1257            .sign(&sk, "root-1")
1258            .unwrap()
1259            .as_bytes(),
1260        );
1261
1262        let policy = InstallPolicy {
1263            index: Some(IndexPolicy {
1264                realm: "acme",
1265                root_public_key: &pk,
1266                required: true,
1267            }),
1268            ..policy()
1269        };
1270        let outcome = install(
1271            &pin("2026.07.0"),
1272            &source,
1273            &AcceptAll,
1274            &store,
1275            &mut marks,
1276            &policy,
1277        )
1278        .expect("a pinned layer must install even when the line has moved on");
1279
1280        // The consumer LEARNS what the realm says the line contains, even
1281        // though the registry never offered it.
1282        assert_eq!(
1283            outcome.index_high_water,
1284            Some(42),
1285            "the realm's assertion must reach the consumer even when the source \
1286             withheld the layer it refers to"
1287        );
1288        // …and the pinned layer still installed. This assertion is the whole
1289        // correction: an earlier draft raised the ENFORCEMENT mark to 42, and
1290        // this very install then failed with `rollback refused: presented 1,
1291        // high_water 42` — a deliberately-pinned layer made uninstallable by
1292        // someone else publishing. varve exists to freeze toolchains; a
1293        // freshness control that unfreezes them is not a fix.
1294        assert_eq!(outcome.layer.to_string(), "2026.07.0");
1295        assert_eq!(
1296            marks.mark(&"2026.07".parse().unwrap()),
1297            Some(1),
1298            "the mark records what this machine ACCEPTED, not what exists"
1299        );
1300    }
1301
1302    // rivet: verifies REQ-INDEXAUTH-001
1303    #[test]
1304    fn a_replayed_older_index_is_refused_on_the_next_install_not_only_in_theory() {
1305        // Clause 2 through the pipeline. `check` has always refused a
1306        // regression against a `cached` document — and install passed `None`
1307        // for `cached`, so the rule could not fire on any real machine: there
1308        // was nothing that REMEMBERED. This test installs twice, and the
1309        // second source replays a superseded index.
1310        use crate::lineindex::{IndexPolicy, IndexedLayer, LineIndex};
1311        let (sk, pk) = crate::verify::generate_root_keypair();
1312        let (_tmp, store, mut marks) = setup();
1313        let (bytes, blobs) = july();
1314
1315        let signed_index = |counter: u64| {
1316            LineIndex {
1317                line: "2026.07".into(),
1318                counter,
1319                issued_at: "2026-08-07T00:00:00Z".into(),
1320                layers: vec![IndexedLayer {
1321                    layer: "2026.07.0".into(),
1322                    digest: manifest_digest(&bytes),
1323                    channel: "qualified".into(),
1324                    counter: 1,
1325                }],
1326            }
1327            .sign(&sk, "root-1")
1328            .unwrap()
1329        };
1330        // Counting the blob fetches is what makes this test able to tell WHERE
1331        // the refusal happened. Without it the test passed with `check` handed
1332        // `None` for the cached document, because the cache's own write-time
1333        // regression guard raised the identical error — after the layer had
1334        // been fetched, laid down, and the mark advanced. Same message, wholly
1335        // different behaviour, and the version that only compared messages
1336        // could not see the difference.
1337        struct CountingSource {
1338            inner: MemorySource,
1339            blob_fetches: std::cell::Cell<usize>,
1340        }
1341        impl LayerSource for CountingSource {
1342            fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError> {
1343                self.inner.fetch_manifest(layer)
1344            }
1345            fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError> {
1346                self.blob_fetches.set(self.blob_fetches.get() + 1);
1347                self.inner.fetch_blob(digest)
1348            }
1349            fn fetch_line_index(&self, line: &str) -> Result<Option<Vec<u8>>, SourceError> {
1350                self.inner.fetch_line_index(line)
1351            }
1352            fn served_layers(&self, line: &str) -> Result<Option<Vec<String>>, SourceError> {
1353                self.inner.served_layers(line)
1354            }
1355        }
1356        let source_with = |envelope: &str| {
1357            let mut s = MemorySource::new().with_manifest(&bytes);
1358            for (d, b) in &blobs {
1359                s = s.with_blob(d, b);
1360            }
1361            CountingSource {
1362                inner: s.with_line_index(envelope.as_bytes()),
1363                blob_fetches: std::cell::Cell::new(0),
1364            }
1365        };
1366        let policy = InstallPolicy {
1367            index: Some(IndexPolicy {
1368                realm: "acme",
1369                root_public_key: &pk,
1370                required: true,
1371            }),
1372            ..policy()
1373        };
1374
1375        install(
1376            &pin("2026.07.0"),
1377            &source_with(&signed_index(9)),
1378            &AcceptAll,
1379            &store,
1380            &mut marks,
1381            &policy,
1382        )
1383        .expect("the first install accepts index #9");
1384
1385        // A second source — a stale mirror, or an attacker who kept a copy of
1386        // a pre-yank index — offers #4. Everything about it verifies.
1387        let replay = source_with(&signed_index(4));
1388        let err = install(
1389            &pin("2026.07.0"),
1390            &replay,
1391            &AcceptAll,
1392            &store,
1393            &mut marks,
1394            &policy,
1395        )
1396        .expect_err("a superseded index must not be replayable over the held one");
1397        assert_eq!(
1398            replay.blob_fetches.get(),
1399            0,
1400            "the index check runs BEFORE anything is fetched or laid down — a \
1401             refusal discovered only when the cache was written would already \
1402             have installed the layer and advanced the mark"
1403        );
1404        assert!(
1405            matches!(
1406                err,
1407                InstallError::Index(crate::lineindex::IndexError::Stale {
1408                    presented: 4,
1409                    cached: 9,
1410                    ..
1411                })
1412            ),
1413            "got: {err}"
1414        );
1415        assert!(
1416            err.to_string().contains('4') && err.to_string().contains('9'),
1417            "names both counters: {err}"
1418        );
1419
1420        // …and the held document is still #9, so a third attempt at the same
1421        // replay fails the same way rather than sliding down.
1422        assert_eq!(
1423            crate::lineindex::IndexCache::at_root(store.root())
1424                .load("2026.07")
1425                .unwrap()
1426                .unwrap()
1427                .counter,
1428            9
1429        );
1430    }
1431
1432    // rivet: verifies REQ-INDEXAUTH-001
1433    #[test]
1434    fn install_refuses_a_source_that_hides_an_indexed_layer() {
1435        // Clause 3 through install(). Every byte this source serves verifies;
1436        // the defect is what it withholds.
1437        use crate::lineindex::{IndexPolicy, IndexedLayer, LineIndex};
1438        let (sk, pk) = crate::verify::generate_root_keypair();
1439        let (_tmp, store, mut marks) = setup();
1440
1441        let (bytes, blobs) = july();
1442        let mut source = MemorySource::new().with_manifest(&bytes);
1443        for (d, b) in &blobs {
1444            source = source.with_blob(d, b);
1445        }
1446        // It admits to serving only the old layer, while the realm's index
1447        // names another.
1448        let source = source.serving(&["2026.07.0"]).with_line_index(
1449            LineIndex {
1450                line: "2026.07".into(),
1451                counter: 1,
1452                issued_at: "2026-08-07T00:00:00Z".into(),
1453                layers: vec![IndexedLayer {
1454                    layer: "2026.07.5".into(),
1455                    digest: "sha256:withheld".into(),
1456                    channel: "qualified".into(),
1457                    counter: 5,
1458                }],
1459            }
1460            .sign(&sk, "root-1")
1461            .unwrap()
1462            .as_bytes(),
1463        );
1464
1465        let policy = InstallPolicy {
1466            index: Some(IndexPolicy {
1467                realm: "acme",
1468                root_public_key: &pk,
1469                required: true,
1470            }),
1471            ..policy()
1472        };
1473        let err = install(
1474            &pin("2026.07.0"),
1475            &source,
1476            &AcceptAll,
1477            &store,
1478            &mut marks,
1479            &policy,
1480        )
1481        .expect_err("a source hiding an indexed layer must be refused");
1482        let msg = err.to_string();
1483        assert!(msg.contains("2026.07.5"), "names the hidden layer: {msg}");
1484        // Nothing was laid down on the strength of a dishonest listing.
1485        assert!(store.list().unwrap().is_empty(), "no install on a refusal");
1486    }
1487}