Skip to main content

varve_core/
archive.rs

1//! The archived core (REQ-OFFLINE-001) — the artifact of record.
2//!
3//! A registry is a cache; retention policies forget. A qualified line must
4//! remain reconstructible after every registry has forgotten it, so `varve
5//! archive` exports an installed, verified layer as a **directory-shaped OCI
6//! image layout** — `oci-layout`, `index.json`, `blobs/sha256/<hex>` — the
7//! standard interchange shape (`oras`/`skopeo`-inspectable), with the DSSE
8//! signature envelope carried as a blob so verification travels with the
9//! evidence. Install from an archive runs the *same* pipeline with the same
10//! trust root: where the bytes come from changes, whether they are accepted
11//! does not.
12
13use std::path::Path;
14
15use crate::install::VerifyError;
16use crate::manifest::LayerManifest;
17use crate::reverify::ENVELOPE_FILE;
18use crate::source::{LayerRef, LayerSource, SourceError};
19use crate::store::{InstalledLayer, Store, StoreError, manifest_digest};
20
21/// artifactType marking the envelope blob in the archive index.
22pub const SIGNATURE_ARTIFACT_TYPE: &str = "application/vnd.pulseengine.varve.signature.v1+json";
23/// Annotation on the signature entry naming the manifest digest it signs.
24pub const ANN_SIGNS: &str = "eu.pulseengine.varve.signs";
25
26/// The standard OCI tag annotation. Every OCI client reads it to resolve
27/// `<layout>:<tag>`; without it a layout can only be addressed by digest, and
28/// `oras cp --from-oci-layout ./layout:2026.08.0` — the publish one-liner the
29/// deploy docs offer — cannot resolve at all.
30pub const REF_NAME: &str = "org.opencontainers.image.ref.name";
31
32/// Annotation on the layer descriptor naming the platform whose payloads a
33/// layout carries. `archive` stamps it because an archived core is
34/// single-platform by construction (see `export`); `deposit` does not, because
35/// a deposit is built from the producer's bytes for every platform at once.
36/// Without the stamp a consumer on another platform can only see that a blob is
37/// missing, and cannot be told why (varve#80).
38pub const ANN_ARCHIVED_FOR: &str = "eu.pulseengine.varve.archived-for";
39
40/// What can go wrong writing a layout: an io failure, or a refusal to
41/// overwrite signed work (REQ-NODESTROY-001).
42#[derive(Debug, thiserror::Error)]
43pub(crate) enum LayoutWriteError {
44    #[error("io error at {path}")]
45    Io {
46        path: String,
47        #[source]
48        source: std::io::Error,
49    },
50    #[error(transparent)]
51    WouldDestroy(#[from] crate::referrers::WouldDestroy),
52}
53
54impl From<LayoutWriteError> for ArchiveError {
55    fn from(e: LayoutWriteError) -> Self {
56        match e {
57            LayoutWriteError::Io { path, source } => ArchiveError::Io { path, source },
58            LayoutWriteError::WouldDestroy(d) => ArchiveError::WouldDestroy(d),
59        }
60    }
61}
62
63#[derive(Debug, thiserror::Error)]
64pub enum ArchiveError {
65    #[error("the layer's baseline line-status could not be carried into the archive: {0}")]
66    LineStatus(String),
67    /// REQ-NODESTROY-001 clause 4: `archive` writes a layout in place too, so
68    /// it is guarded by the same rule as `deposit` — through the same code.
69    #[error(transparent)]
70    WouldDestroy(#[from] crate::referrers::WouldDestroy),
71    #[error("io error at {path}")]
72    Io {
73        path: String,
74        #[source]
75        source: std::io::Error,
76    },
77    #[error(
78        "layer {digest} has no retained signature envelope — an archive without its signature \
79         cannot serve as the artifact of record; reinstall from a signed source first"
80    )]
81    NoEnvelope { digest: String },
82    #[error(
83        "payload '{payload}' ({digest}) is named by the layer manifest for platform {platform} but \
84         is not present in the installed layer — an archive missing a payload is not the artifact \
85         of record; reinstall the layer first"
86    )]
87    MissingPayload {
88        payload: String,
89        digest: String,
90        platform: String,
91    },
92    #[error(
93        "payload '{payload}' at {path} hashes to {found}, but the signed manifest names {signed} \
94         for it — refusing to write a blob under a digest its bytes do not have. Either this core \
95         was altered since install, or it was installed for a platform other than {platform} \
96         (`varve install --platform`), in which case archive it with the matching \
97         `varve archive --platform`."
98    )]
99    PayloadDigestMismatch {
100        payload: String,
101        path: String,
102        signed: String,
103        found: String,
104        platform: String,
105    },
106    #[error(
107        "layer {layer} has no payload for platform {platform} in this core — all {omitted} of its \
108         payload entries name other platforms ({platforms}). An empty archive is not the artifact \
109         of record and would only fail on the far side of the gap: install the layer for \
110         {platform}, or archive with the platform this core was installed for."
111    )]
112    NoPayloadForPlatform {
113        layer: String,
114        platform: String,
115        omitted: usize,
116        platforms: String,
117    },
118    #[error("layer.json in the core is not a valid manifest: {0}")]
119    Manifest(#[from] crate::manifest::ManifestError),
120    #[error(transparent)]
121    Carry(#[from] crate::attestcarry::CarryError),
122    #[error(transparent)]
123    Store(#[from] StoreError),
124    #[error(transparent)]
125    Verify(#[from] VerifyError),
126}
127
128/// What an export carried across the gap — and what it could not.
129///
130/// An archive is SINGLE-PLATFORM by construction: `archive` exports an
131/// *installed* layer, and `install` fetches only the payloads of the platform
132/// it installed for, so the bytes for any other platform are simply not on this
133/// machine. That is not something an operator may discover on the far side of
134/// an air gap, so the omission is counted here and printed by the CLI.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct ExportSummary {
137    /// True when this core cached no baseline line-status for the line, so the
138    /// archive carries none and the consumer's `varve status` will fail for as
139    /// long as they hold it (REQ-NOSILENT-001 clause 1).
140    pub baseline_missing: bool,
141    /// The platform whose payloads this archive carries — the only one.
142    pub platform: String,
143    /// Payload blobs written (the manifest and its envelope are not counted).
144    pub archived: usize,
145    /// Manifest entries left out because they name another platform, counted
146    /// per platform — what an operator carrying this media to a mixed site
147    /// needs to know BEFORE they travel.
148    pub omitted: std::collections::BTreeMap<String, usize>,
149}
150
151/// How to name a payload in a verdict: a layer may hold several versions of one
152/// name, so the bare name does not identify WHICH payload is at fault.
153fn named(entry: &crate::manifest::ManifestEntry, name: &str) -> String {
154    match crate::store::entry_version(entry) {
155        Some(version) => format!("{name}@{version}"),
156        None => name.to_string(),
157    }
158}
159
160/// Export one installed layer as a directory-shaped OCI image layout, carrying
161/// the payloads for `platform` — the platform this core was installed for.
162/// Refuses when the layer has no retained envelope: an unsigned archive is
163/// not an artifact of record.
164pub fn export(
165    store: &Store,
166    layer: &InstalledLayer,
167    dest: &Path,
168    platform: &str,
169) -> Result<ExportSummary, ArchiveError> {
170    export_with_options(store, layer, dest, platform, &ArchiveOptions::default())
171}
172
173/// How an archive treats a destination that already carries signed work
174/// (REQ-NODESTROY-001 clauses 2 and 4).
175#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
176pub struct ArchiveOptions {
177    /// Overwrite a layout that already carries referrers, destroying them.
178    pub force: bool,
179    /// Produce an archive even though the installing core cached no baseline
180    /// line-status for this line (REQ-NOSILENT-001 clause 1).
181    pub allow_no_status: bool,
182}
183
184/// `export`, with the destructive case available to callers that ask for it.
185pub fn export_with_options(
186    store: &Store,
187    layer: &InstalledLayer,
188    dest: &Path,
189    platform: &str,
190    options: &ArchiveOptions,
191) -> Result<ExportSummary, ArchiveError> {
192    let io = |path: &Path, source: std::io::Error| ArchiveError::Io {
193        path: path.display().to_string(),
194        source,
195    };
196
197    // Gather everything first; write nothing until the layer proves whole.
198    let manifest_path = layer.root.join("layer.json");
199    let payload = std::fs::read(&manifest_path).map_err(|e| io(&manifest_path, e))?;
200    let envelope_path = layer.root.join(ENVELOPE_FILE);
201    let envelope = match std::fs::read(&envelope_path) {
202        Ok(bytes) => bytes,
203        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
204            return Err(ArchiveError::NoEnvelope {
205                digest: layer.digest.clone(),
206            });
207        }
208        Err(e) => return Err(io(&envelope_path, e)),
209    };
210    let manifest = LayerManifest::parse(&payload)?;
211    let mut blobs: Vec<(String, Vec<u8>)> = Vec::new();
212    let mut omitted: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
213    for entry in &manifest.entries {
214        // A composed layer is a reference to another layer's manifest, not a
215        // blob this layer holds.
216        if entry.kind() == Ok(crate::kind::PayloadKind::Layer) {
217            continue;
218        }
219        let Some(name) = entry.annotations.get("eu.pulseengine.tool") else {
220            continue;
221        };
222        // Platform-filter exactly as `install` and `verify` do (varve#80). A
223        // tool NAME repeats across triples — `kilnd` has one entry per platform
224        // — while install lays down only the host's, so archiving every entry
225        // read ONE host file once per platform and wrote it under four
226        // different digests. Twenty-six of thirty-seven blobs in an archive of
227        // varve's own published layer were the host binary filed under some
228        // other platform's digest, and `archive` exited 0.
229        let entry_platform = entry
230            .annotations
231            .get(crate::platform::ANN_PLATFORM)
232            .map(String::as_str);
233        if let Some(other) =
234            entry_platform.filter(|p| !crate::platform::entry_matches(Some(p), platform))
235        {
236            *omitted.entry(other.to_string()).or_default() += 1;
237            continue;
238        }
239        // By ENTRY, not by name: a layer may hold several versions of one name,
240        // and each must reach the archive under its own signed digest. Reading
241        // `bin/<name>` for both would have put ONE payload's bytes into the
242        // archive twice — a layer that cannot be archived whole cannot cross an
243        // air gap, which is the whole point (REQ-STORE-002 clause 4).
244        let path = store
245            .entry_path(layer, entry)
246            .ok_or_else(|| ArchiveError::MissingPayload {
247                payload: named(entry, name),
248                digest: entry.digest.clone(),
249                platform: platform.to_string(),
250            })?;
251        let bytes = std::fs::read(&path).map_err(|e| io(&path, e))?;
252        // The invariant that makes this an artifact of RECORD, checked before
253        // any of it is written: in a content-addressed layout a blob's NAME is
254        // the hash of its bytes, so writing bytes that hash to something else
255        // produces a file whose name lies. Platform filtering above is what
256        // makes the set right; this is what makes each member of it right, and
257        // it is the check whose absence let varve#80 ship — a blob COUNT was
258        // correct the whole time.
259        let found = manifest_digest(&bytes);
260        if found != entry.digest {
261            return Err(ArchiveError::PayloadDigestMismatch {
262                payload: named(entry, name),
263                path: path.display().to_string(),
264                signed: entry.digest.clone(),
265                found,
266                platform: platform.to_string(),
267            });
268        }
269        blobs.push((entry.digest.clone(), bytes));
270    }
271    // Fail closed on an archive that would carry nothing, the way `install`
272    // refuses a layer with no entry for the target platform. The two shapes
273    // that reach here are a mistyped `--platform` and a core installed for a
274    // different triple, and both otherwise produce a well-formed, signed,
275    // completely empty artifact that only fails on the far side of the gap —
276    // after the media has been carried there.
277    if blobs.is_empty() && !omitted.is_empty() {
278        return Err(ArchiveError::NoPayloadForPlatform {
279            layer: manifest.layer.to_string(),
280            platform: platform.to_string(),
281            omitted: omitted.values().sum(),
282            platforms: omitted.keys().cloned().collect::<Vec<_>>().join(", "),
283        });
284    }
285    // The attestations that travelled into this layer at install time must
286    // travel back out (REQ-ATTEST-002) — otherwise the evidence reaches one
287    // machine and dies there, which is the same mirror-boundary loss one hop
288    // later. Read BEFORE writing anything: the gather-then-write rule above is
289    // what keeps a broken store from producing a half-written artifact of
290    // record. A statement whose bytes are gone is an error here, unlike at
291    // install: this is our own store, not somebody else's mirror, and an
292    // archive is the artifact of record.
293    let carried = crate::attestcarry::read_persisted(&layer.root, &manifest.layer.to_string())?;
294
295    write_oci_layout(
296        &payload,
297        &envelope,
298        &blobs,
299        &manifest.layer.to_string(),
300        &manifest.channel,
301        Some(platform),
302        dest,
303        options.force,
304    )
305    .map_err(ArchiveError::from)?;
306    for c in &carried {
307        crate::attestcarry::attach(dest, &c.statement, &c.bytes)?;
308    }
309    // Carry the line's baseline advisory across the gap (varve#77). Without
310    // this, `archive` dropped it: the deposit layout held three manifests and
311    // the archive held two, so the AIR-GAPPED consumer — the one the baseline
312    // exists for, and the one who cannot ask a registry instead — got a
313    // permanently broken `varve status`. The one transport that most needs a
314    // yank to arrive was the one discarding it. The attestation carriage added
315    // alongside this already did it correctly; line-status was simply never
316    // given the same treatment.
317    //
318    // The bytes are re-attached VERBATIM from the cache and re-verified by the
319    // far side against its own trust root. Archiving is a transport, not a
320    // place where a signed document is re-shaped.
321    // `store.root()`, not `varve_root()`: install writes the cache at the
322    // store's own root and `varve status` reads it there, so a realm
323    // partition keeps its baseline under `realms/<fingerprint>/state`.
324    // Reading from the varve root instead would have found nothing for every
325    // realm install — the same drop this fix exists to close, one layer down.
326    let mut baseline_missing = false;
327    let cache = crate::linestatus::StatusCache::at_root(store.root());
328    match cache
329        .envelope_bytes(layer.layer.line())
330        .map_err(|e| ArchiveError::LineStatus(e.to_string()))?
331    {
332        Some(envelope) => {
333            crate::linestatus::attach_to_layout(dest, layer.layer.line(), &envelope)
334                .map_err(|e| ArchiveError::LineStatus(e.to_string()))?;
335        }
336        // REQ-NOSILENT-001 clause 1: this used to be a silent no-op. `archive`
337        // was already verbose about omitted PLATFORM payloads and mute about
338        // the advisory — the inconsistency that hid it — and the result was an
339        // air-gap artifact whose `varve status` is permanently broken for
340        // every consumer of it. Refuse, unless the operator says otherwise.
341        // REQ-NOSILENT-001 clause 1. This was a silent no-op: `archive` was
342        // verbose about omitted PLATFORM payloads and mute about the advisory,
343        // and the result was an air-gap artifact whose `varve status` is
344        // permanently broken for every consumer of it.
345        //
346        // It WARNS rather than refusing. Refusing was the first cut and it
347        // broke four legitimate flows, which is the useful signal: `archive`
348        // is most often run by the CONSUMER exporting their own core for
349        // transport, and they cannot retroactively add a baseline the producer
350        // never published. A gate that fires on a correct setup nobody can fix
351        // is one people learn to pass `--allow-no-status` to reflexively, and
352        // then it protects nothing.
353        None => baseline_missing = !options.allow_no_status,
354    }
355    Ok(ExportSummary {
356        baseline_missing,
357        platform: platform.to_string(),
358        archived: blobs.len(),
359        omitted,
360    })
361}
362
363/// Write the canonical directory-shaped OCI image layout shared by `archive`
364/// (exporting an installed layer) and `deposit` (creating one): `oci-layout`,
365/// `blobs/sha256/<hex>` for payload + envelope + tools, and an `index.json`
366/// referencing the manifest and its signature blob.
367///
368/// `platform` is `Some` only for an ARCHIVE, which carries one platform's
369/// payloads because that is all the archiving machine installed; a `deposit`
370/// carries every platform the producer built and passes `None`.
371///
372/// This is the ONLY code in varve that writes a layout, which is why
373/// REQ-NODESTROY-001's guard lives here rather than in each caller: a command
374/// that starts writing layouts later inherits it and cannot forget it. That is
375/// also why it takes one argument over clippy's threshold — the alternative is
376/// a guard each caller can omit, which is the requirement's whole subject.
377#[allow(clippy::too_many_arguments)]
378pub(crate) fn write_oci_layout(
379    payload: &[u8],
380    envelope: &[u8],
381    blobs: &[(String, Vec<u8>)],
382    layer_name: &str,
383    channel: &str,
384    platform: Option<&str>,
385    dest: &Path,
386    force: bool,
387) -> Result<(), LayoutWriteError> {
388    // BEFORE the first mkdir. A refused layout must be left byte-identical:
389    // a half-written one is neither the old artifact nor the new one, and no
390    // message describes it (REQ-NODESTROY-001 clause 5).
391    crate::referrers::guard(dest, force)?;
392    let io = |path: &Path, source: std::io::Error| LayoutWriteError::Io {
393        path: path.display().to_string(),
394        source,
395    };
396    let payload_digest = manifest_digest(payload);
397    let envelope_digest = manifest_digest(envelope);
398
399    let blob_dir = dest.join("blobs").join("sha256");
400    std::fs::create_dir_all(&blob_dir).map_err(|e| io(&blob_dir, e))?;
401    let write_blob = |digest: &str, bytes: &[u8]| -> Result<(), LayoutWriteError> {
402        let hex = digest.strip_prefix("sha256:").unwrap_or(digest);
403        let path = blob_dir.join(hex);
404        std::fs::write(&path, bytes).map_err(|e| io(&path, e))
405    };
406    write_blob(&payload_digest, payload)?;
407    write_blob(&envelope_digest, envelope)?;
408    for (digest, bytes) in blobs {
409        write_blob(digest, bytes)?;
410    }
411
412    let marker_path = dest.join("oci-layout");
413    std::fs::write(&marker_path, br#"{"imageLayoutVersion":"1.0.0"}"#)
414        .map_err(|e| io(&marker_path, e))?;
415
416    let mut index = serde_json::json!({
417        "schemaVersion": 2,
418        "mediaType": "application/vnd.oci.image.index.v1+json",
419        "manifests": [
420            {
421                "mediaType": "application/vnd.oci.image.index.v1+json",
422                "digest": payload_digest,
423                "size": payload.len(),
424                "annotations": {
425                    // The standard OCI tag, so `oras cp --from-oci-layout
426                    // ./layout:<layer>` and every other client can address
427                    // this layout by name rather than by digest alone.
428                    REF_NAME: layer_name,
429                    "eu.pulseengine.varve.layer": layer_name,
430                    "eu.pulseengine.varve.channel": channel,
431                }
432            },
433            {
434                "mediaType": "application/json",
435                "artifactType": SIGNATURE_ARTIFACT_TYPE,
436                "digest": envelope_digest,
437                "size": envelope.len(),
438                "annotations": { ANN_SIGNS: payload_digest }
439            }
440        ]
441    });
442    // Say which platform's payloads are in here, so the far side can be TOLD
443    // why a blob is absent rather than left to infer tampering (varve#80).
444    if let Some(platform) = platform {
445        index["manifests"][0]["annotations"][ANN_ARCHIVED_FOR] =
446            serde_json::Value::String(platform.to_string());
447    }
448    let index_path = dest.join("index.json");
449    std::fs::write(
450        &index_path,
451        serde_json::to_vec_pretty(&index).expect("index serializes"),
452    )
453    .map_err(|e| io(&index_path, e))?;
454    Ok(())
455}
456
457/// A `LayerSource` over a directory-shaped OCI image layout — the reading
458/// half of REQ-OFFLINE-001. No registry, no network, verification unchanged.
459#[derive(Debug)]
460pub struct OciLayoutSource {
461    root: std::path::PathBuf,
462    /// The platform this consumer installs for. Used ONLY to explain an absent
463    /// blob; a source has no voice in whether bytes are accepted (DD-003).
464    platform: Option<String>,
465}
466
467impl OciLayoutSource {
468    pub fn at(root: impl Into<std::path::PathBuf>) -> Self {
469        OciLayoutSource {
470            root: root.into(),
471            platform: None,
472        }
473    }
474
475    /// Name the platform this consumer installs for, so a missing payload can
476    /// say WHOSE payload is missing and not merely which digest.
477    pub fn for_platform(mut self, platform: impl Into<String>) -> Self {
478        self.platform = Some(platform.into());
479        self
480    }
481
482    fn blob_path(&self, digest: &str) -> std::path::PathBuf {
483        let hex = digest.strip_prefix("sha256:").unwrap_or(digest);
484        self.root.join("blobs").join("sha256").join(hex)
485    }
486
487    /// The platform this layout was archived for, if it says so. Untrusted
488    /// discovery like everything else a source reads: it shapes a message, not
489    /// a verdict.
490    fn archived_for(&self) -> Option<String> {
491        let index: serde_json::Value =
492            serde_json::from_slice(&std::fs::read(self.root.join("index.json")).ok()?).ok()?;
493        index["manifests"]
494            .as_array()?
495            .iter()
496            .find_map(|e| e["annotations"][ANN_ARCHIVED_FOR].as_str())
497            .map(str::to_string)
498    }
499
500    /// Why a blob is absent. An archive of one platform, asked for another
501    /// platform's payload, is not damaged and not tampered with — and before
502    /// varve#80 the far side got `BlobDigestMismatch`, which reads like an
503    /// attack when the truth was that our own tool wrote the wrong bytes.
504    fn absent(&self, digest: &str) -> SourceError {
505        let wanted = self
506            .platform
507            .clone()
508            .unwrap_or_else(crate::platform::host_platform);
509        match self.archived_for() {
510            Some(archived_for) if archived_for != wanted => SourceError::NoPayloadForPlatform {
511                digest: digest.to_string(),
512                wanted,
513                archived_for,
514            },
515            _ => SourceError::NotFound(digest.to_string()),
516        }
517    }
518}
519
520impl LayerSource for OciLayoutSource {
521    fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError> {
522        // The archive index tells us which blob is the signed envelope for
523        // which manifest digest — untrusted discovery, as always: the
524        // returned envelope still has to verify and match the pin.
525        let index_path = self.root.join("index.json");
526        let index: serde_json::Value = serde_json::from_slice(
527            &std::fs::read(&index_path)
528                .map_err(|e| SourceError::Transport(format!("{}: {e}", index_path.display())))?,
529        )
530        .map_err(|e| SourceError::Transport(format!("index.json: {e}")))?;
531        let entries = index["manifests"].as_array().cloned().unwrap_or_default();
532
533        // Find candidate manifest digests in the index that match the ref.
534        let wanted: Vec<String> = entries
535            .iter()
536            .filter(|e| e["artifactType"] != SIGNATURE_ARTIFACT_TYPE)
537            .filter_map(|e| {
538                let digest = e["digest"].as_str()?.to_string();
539                match layer {
540                    LayerRef::Digest(d) => (&digest == d).then_some(digest),
541                    LayerRef::Name(id) => {
542                        let name = e["annotations"]["eu.pulseengine.varve.layer"].as_str()?;
543                        (name == id.to_string()).then_some(digest)
544                    }
545                }
546            })
547            .collect();
548
549        for digest in wanted {
550            // Prefer the signed envelope blob; fall back to the bare
551            // manifest blob (the pipeline's verifier decides acceptability).
552            let envelope = entries.iter().find(|e| {
553                e["artifactType"] == SIGNATURE_ARTIFACT_TYPE
554                    && e["annotations"][ANN_SIGNS] == *digest
555            });
556            let blob_digest = envelope
557                .and_then(|e| e["digest"].as_str())
558                .map(str::to_string)
559                .unwrap_or_else(|| digest.clone());
560            match std::fs::read(self.blob_path(&blob_digest)) {
561                Ok(bytes) => return Ok(bytes),
562                Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
563                Err(e) => return Err(SourceError::Transport(e.to_string())),
564            }
565        }
566        Err(SourceError::NotFound(format!("{layer:?}")))
567    }
568
569    fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError> {
570        match std::fs::read(self.blob_path(digest)) {
571            Ok(bytes) => Ok(bytes),
572            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(self.absent(digest)),
573            Err(e) => Err(SourceError::Transport(e.to_string())),
574        }
575    }
576
577    fn fetch_line_status(&self, _layer: &LayerRef) -> Result<Option<Vec<u8>>, SourceError> {
578        // The archived layout carries its baseline as a line-status referrer
579        // (REQ-STATUS-DIST-001). Untrusted bytes — the caller re-verifies.
580        crate::linestatus::read_any_from_layout(&self.root)
581            .map_err(|e| SourceError::Transport(e.to_string()))
582    }
583
584    fn fetch_line_index(&self, line: &str) -> Result<Option<Vec<u8>>, SourceError> {
585        // The realm's signed index rides in the layout as its own referrer
586        // (REQ-INDEXAUTH-001), attached by `varve attach-index` beside the
587        // baseline status. Untrusted bytes — the caller re-verifies against
588        // the realm's root.
589        crate::lineindex::read_from_layout(&self.root, line)
590            .map_err(|e| SourceError::Transport(e.to_string()))
591    }
592
593    // `served_layers` is deliberately left at the trait default (`Ok(None)` —
594    // "cannot enumerate"). A layout CAN list its own contents, but its contents
595    // are not a listing OF THE LINE: it is a hand-carried subset, usually one
596    // layer, exported precisely because someone chose it. Answering
597    // `Some(["2026.08.0"])` would make every air-gapped install of a realm with
598    // a multi-layer index fail with `Omitted` — a false accusation of tampering
599    // against the transport varve exists to serve. Omission is a claim only a
600    // party that PURPORTS to list the line can be caught making.
601
602    fn fetch_attestations(
603        &self,
604        layer: &LayerRef,
605    ) -> Result<Vec<crate::attestcarry::CarriedAttestation>, SourceError> {
606        // The same referrer machinery line-status uses (REQ-STATUS-DIST-001),
607        // as REQ-ATTEST-002 requires — one shape, not two. Untrusted bytes:
608        // the layout is a mirror's output and `verify` re-checks every
609        // statement against the trust root.
610        let name = match layer {
611            LayerRef::Name(id) => id.to_string(),
612            LayerRef::Digest(d) => d.clone(),
613        };
614        crate::attestcarry::read_all(&self.root, &name)
615            .map_err(|e| SourceError::Transport(e.to_string()))
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622    use crate::install::{InstallPolicy, install};
623    use crate::manifest::fixtures::manifest_with_tools;
624    use crate::pin::Pin;
625    use crate::rollback::HighWaterMarks;
626    use crate::source::MemorySource;
627    use crate::verify::{PinnedKeyVerifier, generate_root_keypair, sign_layer_manifest};
628
629    fn pin(layer: &str) -> Pin {
630        Pin::parse(
631            &format!(
632                "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"{layer}\"\n"
633            ),
634            "varve.toml",
635        )
636        .unwrap()
637    }
638
639    fn policy() -> InstallPolicy<'static> {
640        InstallPolicy {
641            index: None,
642            now: "2026-08-07T00:00:00Z",
643            staleness_threshold_days: 90,
644            platform: "test-platform",
645        }
646    }
647
648    /// Install a signed layer into a fresh store; return everything needed
649    /// downstream.
650    fn installed() -> (
651        tempfile::TempDir,
652        Store,
653        InstalledLayer,
654        PinnedKeyVerifier,
655        Vec<u8>,
656    ) {
657        let (sk, pk) = generate_root_keypair();
658        let tool = b"synth-bytes".to_vec();
659        let blob_digest = manifest_digest(&tool);
660        let payload = manifest_with_tools(
661            "2026.07.0",
662            "qualified",
663            1,
664            "2026-07-31T09:14:00Z",
665            &[("synth", &blob_digest)],
666        );
667        let envelope = sign_layer_manifest(&payload, &sk, "varve-root-1").unwrap();
668        let source = MemorySource::new()
669            .with_manifest(envelope.as_bytes())
670            .with_blob(&blob_digest, &tool);
671        let tmp = tempfile::tempdir().unwrap();
672        let root = tmp.path().join("root");
673        let store = Store::at(&root);
674        let mut marks = HighWaterMarks::load(&root).unwrap();
675        let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
676        let outcome = install(
677            &pin("2026.07.0"),
678            &source,
679            &verifier,
680            &store,
681            &mut marks,
682            &policy(),
683        )
684        .unwrap();
685        let layer = store.get(&outcome.digest).unwrap().unwrap();
686        (tmp, store, layer, verifier, payload)
687    }
688
689    /// Every blob in a content-addressed layout must hash to the name it is
690    /// filed under. Returns the offenders as (filename, actual digest) — a
691    /// blob whose name lies is exactly the corruption of varve#80, and a blob
692    /// COUNT stayed correct the whole time it shipped.
693    fn blobs_whose_name_lies(dest: &Path) -> Vec<(String, String)> {
694        let mut bad = Vec::new();
695        for e in std::fs::read_dir(dest.join("blobs/sha256"))
696            .unwrap()
697            .filter_map(|e| e.ok())
698        {
699            let name = e.file_name().to_string_lossy().to_string();
700            let actual = manifest_digest(&std::fs::read(e.path()).unwrap());
701            if actual != format!("sha256:{name}") {
702                bad.push((name, actual));
703            }
704        }
705        bad
706    }
707
708    /// A signed layer whose ONE tool name repeats across two platforms — the
709    /// real shape of the pulseengine layer, where `kilnd` has one entry per
710    /// triple — plus a platform-independent payload. Installed for
711    /// `platform-a`, so only `platform-a`'s bytes are on this machine.
712    #[allow(clippy::type_complexity)]
713    fn installed_multi_platform() -> (
714        tempfile::TempDir,
715        Store,
716        InstalledLayer,
717        PinnedKeyVerifier,
718        Vec<u8>,
719        Vec<u8>,
720        Vec<u8>,
721    ) {
722        use crate::manifest::fixtures::manifest_with_platform_tools;
723        let (sk, pk) = generate_root_keypair();
724        let kilnd_a = b"kilnd-built-for-platform-a".to_vec();
725        let kilnd_b = b"kilnd-built-for-platform-b".to_vec();
726        let anyplat = b"a-payload-with-no-platform-claim".to_vec();
727        let (da, db, dn) = (
728            manifest_digest(&kilnd_a),
729            manifest_digest(&kilnd_b),
730            manifest_digest(&anyplat),
731        );
732        let payload = manifest_with_platform_tools(
733            "2026.07.0",
734            "qualified",
735            1,
736            "2026-07-31T09:14:00Z",
737            &[
738                ("kilnd", &da, Some("platform-a")),
739                ("kilnd", &db, Some("platform-b")),
740                ("charter", &dn, None),
741            ],
742        );
743        let envelope = sign_layer_manifest(&payload, &sk, "varve-root-1").unwrap();
744        // The SOURCE has every platform's bytes, as a real registry does.
745        let source = MemorySource::new()
746            .with_manifest(envelope.as_bytes())
747            .with_blob(&da, &kilnd_a)
748            .with_blob(&db, &kilnd_b)
749            .with_blob(&dn, &anyplat);
750        let tmp = tempfile::tempdir().unwrap();
751        let root = tmp.path().join("root");
752        let store = Store::at(&root);
753        let mut marks = HighWaterMarks::load(&root).unwrap();
754        let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
755        let policy = InstallPolicy {
756            platform: "platform-a",
757            ..policy()
758        };
759        let outcome = install(
760            &pin("2026.07.0"),
761            &source,
762            &verifier,
763            &store,
764            &mut marks,
765            &policy,
766        )
767        .unwrap();
768        let layer = store.get(&outcome.digest).unwrap().unwrap();
769        // The premise: install laid down ONE kilnd, at bin/kilnd — the file
770        // both signed entries resolve to by name.
771        assert_eq!(
772            std::fs::read(layer.root.join("bin/kilnd")).unwrap(),
773            kilnd_a
774        );
775        (tmp, store, layer, verifier, kilnd_a, kilnd_b, anyplat)
776    }
777
778    // rivet: verifies REQ-NODESTROY-001
779    #[test]
780    fn archiving_over_a_layout_that_carries_evidence_is_refused_by_the_same_guard() {
781        // Clause 4: `deposit` is not the only command that writes a layout in
782        // place. `archive` writes one too, and an operator refreshing archive
783        // media into the directory they used last time would have dropped the
784        // baseline line-status and every attestation on it — the same loss,
785        // reached from the consumer side, on the transport that most needs the
786        // advisory to arrive. The guard is not repeated here: `write_oci_layout`
787        // is the single writer and asks once, so this test is what proves the
788        // structural claim rather than a second copy of the rule.
789        let (tmp, store, layer, _v, _payload) = installed();
790        let dest = tmp.path().join("archive");
791        export(&store, &layer, &dest, "test-platform").unwrap();
792
793        // Something is attached to the archive media afterwards.
794        let (sk, _pk) = generate_root_keypair();
795        let status = crate::linestatus::LineStatus {
796            line: "2026.07".into(),
797            counter: 1,
798            issued_at: "2026-08-07T00:00:00Z".into(),
799            support_until: None,
800            yanked: Default::default(),
801            known_problems: Vec::new(),
802        };
803        crate::linestatus::attach_to_layout(
804            &dest,
805            &"2026.07".parse().unwrap(),
806            status.sign(&sk, "k").unwrap().as_bytes(),
807        )
808        .unwrap();
809        let before = std::fs::read(dest.join("index.json")).unwrap();
810
811        let err = export(&store, &layer, &dest, "test-platform")
812            .expect_err("re-archiving over attached evidence must be refused");
813        assert!(
814            matches!(&err, ArchiveError::WouldDestroy(_)),
815            "got: {err:?}"
816        );
817        assert!(err.to_string().contains("line-status"), "{err}");
818        assert_eq!(
819            before,
820            std::fs::read(dest.join("index.json")).unwrap(),
821            "a refused archive must leave the layout byte-identical"
822        );
823
824        // …and --force is the deliberate way through, here as for deposit.
825        export_with_options(
826            &store,
827            &layer,
828            &dest,
829            "test-platform",
830            &ArchiveOptions {
831                force: true,
832                allow_no_status: true,
833            },
834        )
835        .expect("--force overrides the guard");
836    }
837
838    // rivet: verifies REQ-OFFLINE-001
839    #[test]
840    fn a_multi_platform_layer_archives_only_this_platforms_payloads() {
841        // varve#80. Every archive fixture before this one had ONE platform, and
842        // with one platform a tool name does not repeat, so `bin/<name>` was
843        // always the right file and every blob was correct. With TWO platforms
844        // the same host file was read once per triple and written under each
845        // triple's digest: an archive of varve's own published layer held 26
846        // blobs whose bytes were not what their names said, and `archive`
847        // exited 0 calling it the artifact of record.
848        let (tmp, store, layer, _v, kilnd_a, kilnd_b, anyplat) = installed_multi_platform();
849        let dest = tmp.path().join("archive");
850        let summary = export(&store, &layer, &dest, "platform-a").unwrap();
851
852        // CONTENT against the digest filename, not a count. A count was right
853        // while the bytes were wrong — that is how this stayed hidden.
854        assert_eq!(
855            blobs_whose_name_lies(&dest),
856            Vec::<(String, String)>::new(),
857            "every blob must hold the bytes its digest names"
858        );
859        let blob = |digest: &str| dest.join("blobs/sha256").join(&digest[7..]);
860        assert_eq!(
861            std::fs::read(blob(&manifest_digest(&kilnd_a))).unwrap(),
862            kilnd_a
863        );
864        assert_eq!(
865            std::fs::read(blob(&manifest_digest(&anyplat))).unwrap(),
866            anyplat,
867            "a payload claiming no platform belongs to every archive"
868        );
869        // platform-b's payload is ABSENT, not present holding platform-a's
870        // bytes. Absence is honest; the wrong bytes under the right name are
871        // not, and fail as tampering on the far side of the gap.
872        assert!(
873            !blob(&manifest_digest(&kilnd_b)).exists(),
874            "the archive must not invent a payload it does not hold"
875        );
876
877        // And the omission is REPORTED, not silent: an operator carrying this
878        // media to a mixed site has to learn it before they travel.
879        assert_eq!(summary.platform, "platform-a");
880        assert_eq!(summary.archived, 2);
881        assert_eq!(
882            summary.omitted,
883            std::collections::BTreeMap::from([("platform-b".to_string(), 1)])
884        );
885
886        // The layout says which platform it carries, so the far side can be
887        // told why rather than left to infer tampering.
888        let index: serde_json::Value =
889            serde_json::from_slice(&std::fs::read(dest.join("index.json")).unwrap()).unwrap();
890        assert_eq!(
891            index["manifests"][0]["annotations"][ANN_ARCHIVED_FOR],
892            "platform-a"
893        );
894    }
895
896    // rivet: verifies REQ-OFFLINE-001
897    #[test]
898    fn a_multi_platform_archive_reinstalls_and_reverifies_on_its_own_platform() {
899        // The round trip that matters: the archive is still a usable artifact
900        // of record for the platform it was made on, and every payload in it
901        // re-verifies against the signed manifest on the far side.
902        let (tmp, store, layer, verifier, kilnd_a, _kb, _kn) = installed_multi_platform();
903        let dest = tmp.path().join("archive");
904        export(&store, &layer, &dest, "platform-a").unwrap();
905
906        let fresh_root = tmp.path().join("fresh");
907        let fresh = Store::at(&fresh_root);
908        let mut marks = HighWaterMarks::load(&fresh_root).unwrap();
909        let policy = InstallPolicy {
910            platform: "platform-a",
911            ..policy()
912        };
913        let outcome = install(
914            &pin("2026.07.0"),
915            &OciLayoutSource::at(&dest).for_platform("platform-a"),
916            &verifier,
917            &fresh,
918            &mut marks,
919            &policy,
920        )
921        .unwrap();
922        let entry = fresh.get(&outcome.digest).unwrap().unwrap();
923        assert_eq!(
924            std::fs::read(entry.root.join("bin/kilnd")).unwrap(),
925            kilnd_a,
926            "the far side must get platform-a's kilnd, not platform-b's entry's bytes"
927        );
928        assert_eq!(
929            crate::reverify::verify_installed(&fresh, &entry, &verifier, "platform-a").unwrap(),
930            2
931        );
932    }
933
934    // rivet: verifies REQ-OFFLINE-001
935    #[test]
936    fn a_consumer_on_another_platform_is_told_the_archive_carries_none_for_it() {
937        // Before varve#80 this consumer got BlobDigestMismatch — it failed
938        // closed, which was right, but the message read like tampering when the
939        // truth was that our own tool had written the wrong bytes. Nothing is
940        // corrupt here and no amount of re-copying the media helps, so the
941        // error has to say what actually happened and what to do instead.
942        let (tmp, store, layer, verifier, ..) = installed_multi_platform();
943        let dest = tmp.path().join("archive");
944        export(&store, &layer, &dest, "platform-a").unwrap();
945
946        let fresh_root = tmp.path().join("fresh-b");
947        let fresh = Store::at(&fresh_root);
948        let mut marks = HighWaterMarks::load(&fresh_root).unwrap();
949        let policy = InstallPolicy {
950            platform: "platform-b",
951            ..policy()
952        };
953        let err = install(
954            &pin("2026.07.0"),
955            &OciLayoutSource::at(&dest).for_platform("platform-b"),
956            &verifier,
957            &fresh,
958            &mut marks,
959            &policy,
960        )
961        .unwrap_err();
962        assert!(
963            matches!(
964                &err,
965                crate::install::InstallError::Source(SourceError::NoPayloadForPlatform {
966                    wanted,
967                    archived_for,
968                    ..
969                }) if wanted == "platform-b" && archived_for == "platform-a"
970            ),
971            "a different-platform consumer must be told which platform this \
972             archive carries, not accused of tampering: {err}"
973        );
974        let text = err.to_string();
975        assert!(
976            text.contains("platform-b") && text.contains("platform-a"),
977            "the message must name both the platform asked for and the one \
978             carried: {text}"
979        );
980        assert!(
981            fresh.list().unwrap().is_empty(),
982            "and nothing lands from a failed install"
983        );
984    }
985
986    // rivet: verifies REQ-PROOF-001
987    #[cfg(unix)]
988    #[test]
989    fn a_blob_that_exists_but_cannot_be_read_is_a_transport_fault_not_an_absent_payload() {
990        // "Absent" now tells a story — which platform this archive carries and
991        // what to do instead — so a blob that exists and cannot be read must
992        // not borrow it. A permissions fault reported as "this archive carries
993        // no payload for X" sends the operator to re-archive on a machine that
994        // was never the problem. (cargo-mutants: the NotFound guard survives
995        // being replaced with `true`.)
996        use std::os::unix::fs::PermissionsExt;
997        let (tmp, store, layer, verifier, kilnd_a, ..) = installed_multi_platform();
998        let dest = tmp.path().join("archive");
999        export(&store, &layer, &dest, "platform-a").unwrap();
1000        let hex = manifest_digest(&kilnd_a)
1001            .strip_prefix("sha256:")
1002            .unwrap()
1003            .to_string();
1004        let blob = dest.join("blobs/sha256").join(&hex);
1005        std::fs::set_permissions(&blob, std::fs::Permissions::from_mode(0o000)).unwrap();
1006        // chmod(000) does not stop root and some filesystems ignore modes, so
1007        // test the PREMISE rather than guessing at uid: a test that cannot hold
1008        // its premise should say so, not go red for the wrong reason.
1009        if std::fs::read(&blob).is_ok() {
1010            eprintln!("skipping: this environment does not deny reads on mode 000");
1011            return;
1012        }
1013        let fresh_root = tmp.path().join("fresh");
1014        let fresh = Store::at(&fresh_root);
1015        let mut marks = HighWaterMarks::load(&fresh_root).unwrap();
1016        let err = install(
1017            &pin("2026.07.0"),
1018            &OciLayoutSource::at(&dest).for_platform("platform-a"),
1019            &verifier,
1020            &fresh,
1021            &mut marks,
1022            &InstallPolicy {
1023                platform: "platform-a",
1024                ..policy()
1025            },
1026        )
1027        .unwrap_err();
1028        let _ = std::fs::set_permissions(&blob, std::fs::Permissions::from_mode(0o644));
1029        assert!(
1030            matches!(
1031                &err,
1032                crate::install::InstallError::Source(SourceError::Transport(_))
1033            ),
1034            "an unreadable blob must be a transport fault, not an absent one: {err}"
1035        );
1036    }
1037
1038    // rivet: verifies REQ-OFFLINE-001
1039    #[test]
1040    fn a_blob_missing_from_an_archive_of_this_platform_is_not_blamed_on_the_platform() {
1041        // The other half of the honest-absence rule. A truncated or damaged
1042        // archive of THIS platform is a real fault the operator must chase —
1043        // telling them "this archive carries no payload for X, it was archived
1044        // for X" would be nonsense, and would send them to re-archive on a
1045        // machine that is already right. (cargo-mutants: the `archived_for !=
1046        // wanted` guard survived being replaced with `true`.)
1047        let (tmp, store, layer, verifier, kilnd_a, ..) = installed_multi_platform();
1048        let dest = tmp.path().join("archive");
1049        export(&store, &layer, &dest, "platform-a").unwrap();
1050        let hex = manifest_digest(&kilnd_a)
1051            .strip_prefix("sha256:")
1052            .unwrap()
1053            .to_string();
1054        std::fs::remove_file(dest.join("blobs/sha256").join(&hex)).unwrap();
1055
1056        let fresh_root = tmp.path().join("fresh");
1057        let fresh = Store::at(&fresh_root);
1058        let mut marks = HighWaterMarks::load(&fresh_root).unwrap();
1059        let err = install(
1060            &pin("2026.07.0"),
1061            &OciLayoutSource::at(&dest).for_platform("platform-a"),
1062            &verifier,
1063            &fresh,
1064            &mut marks,
1065            &InstallPolicy {
1066                platform: "platform-a",
1067                ..policy()
1068            },
1069        )
1070        .unwrap_err();
1071        assert!(
1072            matches!(
1073                &err,
1074                crate::install::InstallError::Source(SourceError::NotFound(_))
1075            ),
1076            "a damaged archive of this platform is not a platform mismatch: {err}"
1077        );
1078    }
1079
1080    // rivet: verifies REQ-OFFLINE-001
1081    #[test]
1082    fn an_archive_that_would_carry_no_payload_at_all_is_refused() {
1083        // A mistyped `--platform`, or a core installed for another triple,
1084        // otherwise produces a well-formed, signed, completely EMPTY layout
1085        // that fails only on the far side of the gap — after the media has been
1086        // carried there. `install` already refuses a layer with no entry for
1087        // the target platform; the export side must refuse the mirror image.
1088        use crate::manifest::fixtures::manifest_with_platform_tools;
1089        let (sk, pk) = generate_root_keypair();
1090        let (a, b) = (b"kilnd-a".to_vec(), b"kilnd-b".to_vec());
1091        let (da, db) = (manifest_digest(&a), manifest_digest(&b));
1092        let payload = manifest_with_platform_tools(
1093            "2026.07.0",
1094            "qualified",
1095            1,
1096            "2026-07-31T09:14:00Z",
1097            &[
1098                ("kilnd", &da, Some("platform-a")),
1099                ("kilnd", &db, Some("platform-b")),
1100            ],
1101        );
1102        let envelope = sign_layer_manifest(&payload, &sk, "varve-root-1").unwrap();
1103        let source = MemorySource::new()
1104            .with_manifest(envelope.as_bytes())
1105            .with_blob(&da, &a)
1106            .with_blob(&db, &b);
1107        let tmp = tempfile::tempdir().unwrap();
1108        let root = tmp.path().join("root");
1109        let store = Store::at(&root);
1110        let mut marks = HighWaterMarks::load(&root).unwrap();
1111        let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
1112        let outcome = install(
1113            &pin("2026.07.0"),
1114            &source,
1115            &verifier,
1116            &store,
1117            &mut marks,
1118            &InstallPolicy {
1119                platform: "platform-a",
1120                ..policy()
1121            },
1122        )
1123        .unwrap();
1124        let layer = store.get(&outcome.digest).unwrap().unwrap();
1125        let dest = tmp.path().join("archive");
1126        let err = export(&store, &layer, &dest, "platfrom-a").unwrap_err();
1127        assert!(
1128            matches!(&err, ArchiveError::NoPayloadForPlatform { platform, omitted, .. }
1129                if platform == "platfrom-a" && *omitted == 2),
1130            "got: {err}"
1131        );
1132        assert!(
1133            !dest.join("index.json").exists(),
1134            "and nothing is written: a layout that exists is a layout somebody will carry"
1135        );
1136    }
1137
1138    // rivet: verifies REQ-OFFLINE-001
1139    #[test]
1140    fn a_payload_whose_bytes_do_not_match_its_signed_digest_is_never_archived() {
1141        // The invariant, standing on its own: `archive` writes a blob only when
1142        // its bytes hash to the digest the signed manifest names for it. This
1143        // is the check whose absence let varve#80 exit 0 on a corrupt artifact,
1144        // and it holds whatever put the wrong bytes there — a foreign-platform
1145        // payload, bit-rot, or an edit after install.
1146        let (tmp, store, layer, ..) = installed_multi_platform();
1147        std::fs::write(layer.root.join("bin/kilnd"), b"NOT-WHAT-WAS-SIGNED").unwrap();
1148        let err = export(&store, &layer, &tmp.path().join("archive"), "platform-a").unwrap_err();
1149        assert!(
1150            matches!(&err, ArchiveError::PayloadDigestMismatch { payload, .. } if payload == "kilnd"),
1151            "got: {err}"
1152        );
1153    }
1154
1155    // rivet: verifies REQ-OFFLINE-001
1156    #[test]
1157    fn export_writes_a_standard_oci_image_layout() {
1158        // rivet: verifies REQ-LAYOUT-001
1159        let (tmp, store, layer, _verifier, payload) = installed();
1160        let dest = tmp.path().join("archive");
1161        export(&store, &layer, &dest, "test-platform").unwrap();
1162
1163        // oci-layout marker file, per the OCI image-layout spec.
1164        let marker: serde_json::Value =
1165            serde_json::from_slice(&std::fs::read(dest.join("oci-layout")).unwrap()).unwrap();
1166        assert_eq!(marker["imageLayoutVersion"], "1.0.0");
1167
1168        // Every blob is stored under its own digest, content-addressed.
1169        let payload_digest = manifest_digest(&payload);
1170        let hex = payload_digest.strip_prefix("sha256:").unwrap();
1171        let manifest_blob = dest.join("blobs/sha256").join(hex);
1172        assert_eq!(std::fs::read(&manifest_blob).unwrap(), payload);
1173
1174        // The layout is addressable by tag. A ten-persona audit graded the
1175        // platform engineer's whole job BLOCKED here: `oras cp
1176        // --from-oci-layout ./layout:2026.08.0` — the one-line publish the
1177        // docs offered — cannot resolve a layout whose index carries no
1178        // org.opencontainers.image.ref.name, and every OCI client uses that
1179        // annotation as the tag.
1180        let index: serde_json::Value =
1181            serde_json::from_slice(&std::fs::read(dest.join("index.json")).unwrap()).unwrap();
1182        let tagged = index["manifests"]
1183            .as_array()
1184            .unwrap()
1185            .iter()
1186            .find(|e| e["annotations"][REF_NAME] == "2026.07.0")
1187            .expect("the layer manifest must be tagged with the layer id");
1188        assert_eq!(
1189            tagged["digest"],
1190            manifest_digest(&payload).as_str(),
1191            "the tag must point at the layer manifest, not the envelope"
1192        );
1193
1194        // index.json references the manifest and the signature blob.
1195        let index: serde_json::Value =
1196            serde_json::from_slice(&std::fs::read(dest.join("index.json")).unwrap()).unwrap();
1197        let entries = index["manifests"].as_array().unwrap();
1198        assert!(
1199            entries
1200                .iter()
1201                .any(|e| e["digest"] == payload_digest.as_str()),
1202            "index must reference the layer manifest"
1203        );
1204        let signature = entries
1205            .iter()
1206            .find(|e| e["artifactType"] == SIGNATURE_ARTIFACT_TYPE)
1207            .expect("index must reference the signature envelope");
1208        assert_eq!(signature["annotations"][ANN_SIGNS], payload_digest.as_str());
1209
1210        // The tool blob is present under its digest.
1211        let manifest = LayerManifest::parse(&payload).unwrap();
1212        for entry in &manifest.entries {
1213            let hex = entry.digest.strip_prefix("sha256:").unwrap();
1214            assert!(
1215                dest.join("blobs/sha256").join(hex).is_file(),
1216                "blob {}",
1217                entry.digest
1218            );
1219        }
1220    }
1221
1222    // rivet: verifies REQ-OFFLINE-001
1223    #[test]
1224    fn an_archived_layer_installs_into_a_fresh_core_with_verification_unchanged() {
1225        let (tmp, store, layer, verifier, payload) = installed();
1226        let dest = tmp.path().join("archive");
1227        export(&store, &layer, &dest, "test-platform").unwrap();
1228
1229        // Fresh machine: new store, no registry, no network — same verifier.
1230        let fresh_root = tmp.path().join("fresh");
1231        let fresh_store = Store::at(&fresh_root);
1232        let mut fresh_marks = HighWaterMarks::load(&fresh_root).unwrap();
1233        let source = OciLayoutSource::at(&dest);
1234        let outcome = install(
1235            &pin("2026.07.0"),
1236            &source,
1237            &verifier,
1238            &fresh_store,
1239            &mut fresh_marks,
1240            &policy(),
1241        )
1242        .unwrap();
1243        assert_eq!(outcome.digest, manifest_digest(&payload));
1244        // And the reinstalled layer re-verifies offline, envelope retained.
1245        let entry = fresh_store.get(&outcome.digest).unwrap().unwrap();
1246        let checked =
1247            crate::reverify::verify_installed(&fresh_store, &entry, &verifier, "test-platform")
1248                .unwrap();
1249        assert_eq!(checked, 1);
1250    }
1251
1252    /// The same signed layer, plus a signed attestation statement carried
1253    /// beside it — the producer's side of REQ-ATTEST-002.
1254    #[allow(clippy::type_complexity)]
1255    fn installed_with_attestation() -> (
1256        tempfile::TempDir,
1257        Store,
1258        InstalledLayer,
1259        PinnedKeyVerifier,
1260        Vec<u8>,
1261        Vec<u8>,
1262        Vec<u8>,
1263    ) {
1264        use crate::attest::{AttestationKind, sign, statement};
1265        let (sk, pk) = generate_root_keypair();
1266        let tool = b"synth-bytes".to_vec();
1267        let blob_digest = manifest_digest(&tool);
1268        let payload = manifest_with_tools(
1269            "2026.07.0",
1270            "qualified",
1271            1,
1272            "2026-07-31T09:14:00Z",
1273            &[("synth", &blob_digest)],
1274        );
1275        let envelope = sign_layer_manifest(&payload, &sk, "varve-root-1").unwrap();
1276        let sbom = b"{\"bomFormat\":\"CycloneDX\",\"components\":[]}".to_vec();
1277        let st = statement(
1278            "2026.07.0",
1279            &manifest_digest(&payload),
1280            AttestationKind::Sbom,
1281            &sbom,
1282            "acme-ci",
1283        );
1284        let statement_envelope = sign(&st, &sk, "varve-root-1").unwrap().into_bytes();
1285        let source = MemorySource::new()
1286            .with_manifest(envelope.as_bytes())
1287            .with_blob(&blob_digest, &tool)
1288            .with_attestation(&statement_envelope, &sbom);
1289        let tmp = tempfile::tempdir().unwrap();
1290        let root = tmp.path().join("root");
1291        let store = Store::at(&root);
1292        let mut marks = HighWaterMarks::load(&root).unwrap();
1293        let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
1294        let outcome = install(
1295            &pin("2026.07.0"),
1296            &source,
1297            &verifier,
1298            &store,
1299            &mut marks,
1300            &policy(),
1301        )
1302        .unwrap();
1303        let layer = store.get(&outcome.digest).unwrap().unwrap();
1304        (tmp, store, layer, verifier, payload, pk.to_vec(), sbom)
1305    }
1306
1307    // rivet: verifies REQ-ATTEST-002
1308    #[test]
1309    fn an_attestation_survives_archive_and_an_offline_install_into_a_fresh_core() {
1310        // THE requirement, end to end in one process: evidence enters at
1311        // install, is re-emitted by `archive` as referrer artifacts, crosses to
1312        // a machine that shares nothing with the first but the pinned root, and
1313        // is still checkable there with no network. Registries publish this
1314        // evidence and mirrors drop it — bandersnatch and Verdaccio carry none,
1315        // and every BCR attestation URL points at github.com — so without this
1316        // an air-gapped consumer gets the bytes and none of the accountability.
1317        let (tmp, store, layer, verifier, payload, pk, sbom) = installed_with_attestation();
1318        let dest = tmp.path().join("archive");
1319        export(&store, &layer, &dest, "test-platform").unwrap();
1320
1321        // The archive carries it as referrer entries, in the layout index.
1322        let carried = crate::attestcarry::read_all(&dest, "2026.07.0").unwrap();
1323        assert_eq!(carried.len(), 1, "archive must re-emit the evidence");
1324        assert_eq!(carried[0].bytes, sbom, "carried verbatim");
1325
1326        // A fresh core, installing from that archive alone.
1327        let fresh_root = tmp.path().join("fresh");
1328        let fresh_store = Store::at(&fresh_root);
1329        let mut fresh_marks = HighWaterMarks::load(&fresh_root).unwrap();
1330        let outcome = install(
1331            &pin("2026.07.0"),
1332            &OciLayoutSource::at(&dest),
1333            &verifier,
1334            &fresh_store,
1335            &mut fresh_marks,
1336            &policy(),
1337        )
1338        .unwrap();
1339        assert_eq!(outcome.attestations_carried, 1, "it crossed the gap");
1340
1341        // …and on the far side it STILL BINDS, offline, against the pinned
1342        // root — which is what `varve verify` reports.
1343        let entry = fresh_store.get(&outcome.digest).unwrap().unwrap();
1344        let reports = crate::attestcarry::report_installed(
1345            &entry.root,
1346            "2026.07.0",
1347            &manifest_digest(&payload),
1348            &pk,
1349        )
1350        .unwrap();
1351        assert_eq!(reports.len(), 1);
1352        assert!(reports[0].binds, "reason: {:?}", reports[0].reason);
1353        assert_eq!(reports[0].kind, "sbom");
1354        assert_eq!(reports[0].producer, "acme-ci");
1355    }
1356
1357    // rivet: verifies REQ-STATUS-DIST-001
1358    #[test]
1359    fn a_yank_survives_archive_and_an_offline_install_into_a_fresh_core() {
1360        // varve#77. `archive` dropped the line-status: the deposit layout held
1361        // three manifests and the archive held two, so the AIR-GAPPED consumer
1362        // — the one who cannot ask a registry instead — got a permanently
1363        // broken `varve status`. The one transport that most needs a yank to
1364        // arrive was the one discarding it.
1365        //
1366        // The assertion is the YANK, deliberately, not a manifest count: a
1367        // count passes when the WRONG blob travels, and a yank that does not
1368        // reach the far side is the whole defect.
1369        use crate::linestatus::{LineStatus, StatusCache};
1370        let (sk, pk) = generate_root_keypair();
1371        let tool = b"synth-bytes".to_vec();
1372        let blob_digest = manifest_digest(&tool);
1373        let payload = manifest_with_tools(
1374            "2026.07.0",
1375            "qualified",
1376            1,
1377            "2026-07-31T09:14:00Z",
1378            &[("synth", &blob_digest)],
1379        );
1380        let envelope = sign_layer_manifest(&payload, &sk, "varve-root-1").unwrap();
1381        let doc = LineStatus {
1382            line: "2026.07".into(),
1383            counter: 3,
1384            issued_at: "2026-08-07T00:00:00Z".into(),
1385            support_until: Some("2028-07-31".into()),
1386            yanked: std::collections::BTreeMap::from([(
1387                "2026.07.0".to_string(),
1388                "CVE-2026-0001 in synth".to_string(),
1389            )]),
1390            known_problems: Vec::new(),
1391        };
1392        let status_envelope = doc.sign(&sk, "varve-root-1").unwrap().into_bytes();
1393        let source = MemorySource::new()
1394            .with_manifest(envelope.as_bytes())
1395            .with_blob(&blob_digest, &tool)
1396            .with_line_status(&status_envelope);
1397
1398        let tmp = tempfile::tempdir().unwrap();
1399        let root = tmp.path().join("root");
1400        let store = Store::at(&root);
1401        let mut marks = HighWaterMarks::load(&root).unwrap();
1402        let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
1403        let outcome = install(
1404            &pin("2026.07.0"),
1405            &source,
1406            &verifier,
1407            &store,
1408            &mut marks,
1409            &policy(),
1410        )
1411        .unwrap();
1412        let line: crate::layer::Line = "2026.07".parse().unwrap();
1413        // What `varve install` does next: verify the carried baseline against
1414        // the trust root and cache it so `status` answers offline.
1415        let cached = crate::linestatus::cache_baseline_from_source(
1416            &source,
1417            &LayerRef::Name("2026.07.0".parse().unwrap()),
1418            &line,
1419            &pk,
1420            store.root(),
1421        )
1422        .unwrap();
1423        assert_eq!(cached, Some(3), "the near side cached the advisory");
1424        let layer = store.get(&outcome.digest).unwrap().unwrap();
1425
1426        let dest = tmp.path().join("archive");
1427        export(&store, &layer, &dest, "test-platform").unwrap();
1428
1429        // A fresh core on the far side of the gap: nothing in common with the
1430        // first but the pinned root and this directory.
1431        let fresh_root = tmp.path().join("fresh");
1432        let fresh = Store::at(&fresh_root);
1433        let mut fresh_marks = HighWaterMarks::load(&fresh_root).unwrap();
1434        let far = OciLayoutSource::at(&dest);
1435        install(
1436            &pin("2026.07.0"),
1437            &far,
1438            &verifier,
1439            &fresh,
1440            &mut fresh_marks,
1441            &policy(),
1442        )
1443        .unwrap();
1444        let carried = crate::linestatus::cache_baseline_from_source(
1445            &far,
1446            &LayerRef::Name("2026.07.0".parse().unwrap()),
1447            &line,
1448            &pk,
1449            fresh.root(),
1450        )
1451        .unwrap();
1452        assert_eq!(carried, Some(3), "the advisory crossed the gap");
1453
1454        // …and on the far side it is READ BACK as a yank, re-verified against
1455        // that machine's own trust root — which is what `varve status` prints.
1456        let there = StatusCache::at_root(fresh.root())
1457            .load(&line, &pk)
1458            .unwrap()
1459            .expect("the far side has a cached status document");
1460        let report = there.report_for(&"2026.07.0".parse().unwrap());
1461        assert_eq!(
1462            report.yanked_reason.as_deref(),
1463            Some("CVE-2026-0001 in synth"),
1464            "the YANK is what had to arrive, not merely some blob"
1465        );
1466    }
1467
1468    // rivet: verifies REQ-STATUS-DIST-001
1469    #[test]
1470    fn exporting_a_layer_whose_line_has_no_cached_status_is_not_an_error() {
1471        // Carrying the baseline must not turn `archive` into a command that
1472        // demands one. Most lines have no advisory, and an archive of a clean
1473        // line is still the artifact of record.
1474        let (tmp, store, layer, _verifier, _payload) = installed();
1475        let dest = tmp.path().join("archive");
1476        export(&store, &layer, &dest, "test-platform").unwrap();
1477        assert!(
1478            crate::linestatus::read_any_from_layout(&dest)
1479                .unwrap()
1480                .is_none()
1481        );
1482    }
1483
1484    // rivet: verifies REQ-STORE-002
1485    #[test]
1486    fn a_layer_holding_two_versions_of_one_name_crosses_an_air_gap_intact() {
1487        // Clause 4. `export` read `bin/<tool>` for every entry, so with two
1488        // versions of one name it would have read ONE file twice and written it
1489        // under the OTHER version's digest — an archive that no longer matches
1490        // its own signed manifest, discovered only on the far side of the air
1491        // gap. A payload that cannot be archived cannot cross, which is the
1492        // whole point of the artifact of record.
1493        use crate::manifest::fixtures::manifest_with_payloads;
1494        let (sk, pk) = generate_root_keypair();
1495        let a = b"serde-1.0.200-crate".to_vec();
1496        let b = b"serde-1.0.210-crate".to_vec();
1497        let (da, db) = (manifest_digest(&a), manifest_digest(&b));
1498        let payload = manifest_with_payloads(
1499            "2026.07.0",
1500            "qualified",
1501            1,
1502            "2026-07-31T09:14:00Z",
1503            &[
1504                ("serde", "1.0.200", "crate", &da),
1505                ("serde", "1.0.210", "crate", &db),
1506            ],
1507        );
1508        let envelope = sign_layer_manifest(&payload, &sk, "varve-root-1").unwrap();
1509        let source = MemorySource::new()
1510            .with_manifest(envelope.as_bytes())
1511            .with_blob(&da, &a)
1512            .with_blob(&db, &b);
1513        let tmp = tempfile::tempdir().unwrap();
1514        let root = tmp.path().join("root");
1515        let store = Store::at(&root);
1516        let mut marks = HighWaterMarks::load(&root).unwrap();
1517        let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
1518        let outcome = install(
1519            &pin("2026.07.0"),
1520            &source,
1521            &verifier,
1522            &store,
1523            &mut marks,
1524            &policy(),
1525        )
1526        .unwrap();
1527        let layer = store.get(&outcome.digest).unwrap().unwrap();
1528
1529        let dest = tmp.path().join("archive");
1530        export(&store, &layer, &dest, "test-platform").unwrap();
1531        // Both blobs are in the archive, each holding ITS OWN bytes — content
1532        // addressing makes this check exact: a blob written under the wrong
1533        // digest is a blob whose name lies.
1534        for (digest, expected) in [(&da, &a), (&db, &b)] {
1535            let hex = digest.strip_prefix("sha256:").unwrap();
1536            assert_eq!(
1537                &std::fs::read(dest.join("blobs/sha256").join(hex)).unwrap(),
1538                expected,
1539                "blob {digest} must hold the bytes it is named for"
1540            );
1541        }
1542
1543        // Reinstall on a machine that shares nothing but the pinned root, and
1544        // re-verify there: both versions, unaltered.
1545        let fresh_root = tmp.path().join("fresh");
1546        let fresh = Store::at(&fresh_root);
1547        let mut fresh_marks = HighWaterMarks::load(&fresh_root).unwrap();
1548        let outcome = install(
1549            &pin("2026.07.0"),
1550            &OciLayoutSource::at(&dest),
1551            &verifier,
1552            &fresh,
1553            &mut fresh_marks,
1554            &policy(),
1555        )
1556        .unwrap();
1557        let entry = fresh.get(&outcome.digest).unwrap().unwrap();
1558        assert_eq!(
1559            crate::reverify::verify_installed(&fresh, &entry, &verifier, "test-platform").unwrap(),
1560            2,
1561            "both versions survive the round trip and re-verify"
1562        );
1563        assert_eq!(
1564            std::fs::read(entry.root.join("payloads/serde/1.0.200")).unwrap(),
1565            a
1566        );
1567        assert_eq!(
1568            std::fs::read(entry.root.join("payloads/serde/1.0.210")).unwrap(),
1569            b
1570        );
1571    }
1572
1573    // rivet: verifies REQ-VERIFY-001
1574    #[test]
1575    fn the_archive_source_cannot_relax_acceptance() {
1576        // Same bytes through the archive path and the memory path: identical
1577        // verdicts, including rejection by a different trust root.
1578        let (tmp, store, layer, _verifier, _payload) = installed();
1579        let dest = tmp.path().join("archive");
1580        export(&store, &layer, &dest, "test-platform").unwrap();
1581
1582        let (_, other_pk) = generate_root_keypair();
1583        let wrong = PinnedKeyVerifier::from_public_key_bytes(&other_pk).unwrap();
1584        let fresh_root = tmp.path().join("fresh2");
1585        let fresh_store = Store::at(&fresh_root);
1586        let mut fresh_marks = HighWaterMarks::load(&fresh_root).unwrap();
1587        let err = install(
1588            &pin("2026.07.0"),
1589            &OciLayoutSource::at(&dest),
1590            &wrong,
1591            &fresh_store,
1592            &mut fresh_marks,
1593            &policy(),
1594        )
1595        .unwrap_err();
1596        assert!(
1597            matches!(err, crate::install::InstallError::Verify(_)),
1598            "archive path must reject exactly like any other: {err}"
1599        );
1600        assert!(fresh_store.list().unwrap().is_empty());
1601    }
1602
1603    // rivet: verifies REQ-OFFLINE-001
1604    #[test]
1605    fn export_refuses_a_layer_without_its_envelope() {
1606        let (tmp, store, layer, _verifier, _payload) = installed();
1607        std::fs::remove_file(layer.root.join(ENVELOPE_FILE)).unwrap();
1608        let err = export(&store, &layer, &tmp.path().join("archive"), "test-platform").unwrap_err();
1609        assert!(matches!(err, ArchiveError::NoEnvelope { .. }), "got: {err}");
1610    }
1611
1612    // rivet: verifies REQ-SCOPE-001
1613    #[test]
1614    fn export_does_not_mutate_the_core() {
1615        let (tmp, store, layer, _verifier, _payload) = installed();
1616        let snapshot = |root: &Path| -> Vec<(String, Vec<u8>)> {
1617            let mut out = Vec::new();
1618            let mut stack = vec![root.to_path_buf()];
1619            while let Some(dir) = stack.pop() {
1620                for e in std::fs::read_dir(&dir).unwrap().filter_map(|e| e.ok()) {
1621                    let p = e.path();
1622                    if p.is_dir() {
1623                        stack.push(p);
1624                    } else {
1625                        out.push((p.display().to_string(), std::fs::read(&p).unwrap()));
1626                    }
1627                }
1628            }
1629            out.sort();
1630            out
1631        };
1632        let before = snapshot(store.root());
1633        export(&store, &layer, &tmp.path().join("archive"), "test-platform").unwrap();
1634        assert_eq!(before, snapshot(store.root()));
1635    }
1636}