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