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