Skip to main content

varve_core/
attestcarry.rs

1//! Carrying attestations with the layer (REQ-ATTEST-002).
2//!
3//! v0.22.0 shipped BINDING — `sign-attestation` / `check-attestation` — and a
4//! review found REQ-ATTEST-001 marked verified with this half unimplemented.
5//! Binding says "this attestation belongs to that layer". Carriage is what
6//! makes the claim reach anyone: attestations attached to a deposit layout as
7//! referrer artifacts, surviving `archive` and an offline install, and
8//! reported by `varve verify`.
9//!
10//! Why it matters more than it sounds: registries DO publish this evidence and
11//! mirroring tools DROP it. bandersnatch and Verdaccio carry no attestations,
12//! and every Bazel Central Registry attestation URL points at github.com — so
13//! the moment a consumer crosses an air gap they receive the bytes without the
14//! evidence about them, and no error says so. A layer that travels without its
15//! attestations is not less trustworthy; it is less ACCOUNTABLE, and the
16//! consumer cannot tell the difference from the inside.
17//!
18//! Two blobs travel per attestation: the signed STATEMENT (varve's assertion
19//! about the attestation) and the attestation BYTES themselves, verbatim. varve
20//! never re-signs another party's judgement — re-signing would launder it under
21//! this root and an assessor could no longer tell who asserted what (DD-021).
22//!
23//! The referrer machinery is line-status's (REQ-STATUS-DIST-001) rather than a
24//! second shape, with one deliberate difference: a line has exactly one status,
25//! so attaching REPLACES; a layer has many attestations, so attaching ADDS and
26//! is keyed by the statement's own digest.
27
28use std::path::Path;
29
30use crate::attest::{AttestError, AttestationStatement};
31
32/// Artifact type for a carried attestation statement.
33pub const STATEMENT_ARTIFACT_TYPE: &str = crate::attest::PAYLOAD_TYPE;
34
35/// Artifact type for the attested bytes travelling verbatim beside it.
36pub const ATTESTATION_ARTIFACT_TYPE: &str =
37    "application/vnd.pulseengine.varve.attestation-bytes.v1";
38
39/// Annotation linking carried bytes back to the statement about them.
40pub const ANN_STATEMENT: &str = "eu.pulseengine.varve.attests";
41
42#[derive(Debug, thiserror::Error)]
43pub enum CarryError {
44    #[error(transparent)]
45    Attest(#[from] AttestError),
46    #[error("io error at {path}")]
47    Io {
48        path: String,
49        #[source]
50        source: std::io::Error,
51    },
52    #[error("{path}: {reason}")]
53    Layout { path: String, reason: String },
54    #[error(
55        "layer {layer} carries an attestation statement whose attested bytes are not present in \
56         the layout (statement {statement}, expected blob {digest}). The statement travelled and \
57         the evidence did not — which is exactly the mirror-boundary failure carriage exists to \
58         prevent."
59    )]
60    OrphanStatement {
61        layer: String,
62        statement: String,
63        digest: String,
64    },
65    #[error(
66        "layer {layer}: the layout index names an attestation blob '{digest}', which is not a \
67         sha256 content address — refusing to resolve it as a path"
68    )]
69    MalformedDigest { layer: String, digest: String },
70    #[error(transparent)]
71    Source(#[from] crate::source::SourceError),
72}
73
74/// One attestation as it sits in a layout.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct CarriedAttestation {
77    /// Digest of the signed statement envelope.
78    pub statement_digest: String,
79    /// The statement envelope bytes.
80    pub statement: Vec<u8>,
81    /// The attested bytes, verbatim as the producer emitted them.
82    pub bytes: Vec<u8>,
83}
84
85/// What `verify` reports about one carried attestation.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct AttestationReport {
88    pub kind: String,
89    pub producer: String,
90    /// Whether the statement verifies against the trust root AND still binds
91    /// to this layer and these bytes.
92    pub binds: bool,
93    /// Why not, when it does not.
94    pub reason: Option<String>,
95}
96
97fn io(path: &Path, source: std::io::Error) -> CarryError {
98    CarryError::Io {
99        path: path.display().to_string(),
100        source,
101    }
102}
103
104fn blob_path(layout: &Path, digest: &str) -> std::path::PathBuf {
105    layout
106        .join("blobs")
107        .join("sha256")
108        .join(digest.strip_prefix("sha256:").unwrap_or(digest))
109}
110
111/// Is `digest` a well-formed `sha256:<64 hex>` content address?
112///
113/// UNTRUSTED-INPUT GUARD. Every digest in a layout's `index.json` is a string
114/// a mirror wrote, and `blob_path` turns it into a PATH COMPONENT: an entry
115/// reading `sha256:../../../../etc/whatever` would escape the layout, and the
116/// caller then WRITES a file named after it into the installed layer root. A
117/// content address is precisely the value that must never be taken on faith by
118/// the code that resolves it.
119fn is_content_address(digest: &str) -> bool {
120    digest
121        .strip_prefix("sha256:")
122        .is_some_and(|hex| hex.len() == 64 && hex.bytes().all(|b| b.is_ascii_hexdigit()))
123}
124
125/// Attach an attestation to a deposit layout: both blobs, plus two referrer
126/// entries. Idempotent — re-attaching the same statement replaces its entries
127/// rather than duplicating them, so a re-run CI step stays clean.
128pub fn attach(
129    layout: &Path,
130    statement_envelope: &[u8],
131    attested_bytes: &[u8],
132) -> Result<String, CarryError> {
133    let st_digest = crate::store::manifest_digest(statement_envelope);
134    let bytes_digest = crate::store::manifest_digest(attested_bytes);
135
136    let dir = layout.join("blobs").join("sha256");
137    std::fs::create_dir_all(&dir).map_err(|e| io(&dir, e))?;
138    for (digest, content) in [
139        (&st_digest, statement_envelope),
140        (&bytes_digest, attested_bytes),
141    ] {
142        let p = blob_path(layout, digest);
143        std::fs::write(&p, content).map_err(|e| io(&p, e))?;
144    }
145
146    let index_path = layout.join("index.json");
147    let mut index: serde_json::Value =
148        serde_json::from_slice(&std::fs::read(&index_path).map_err(|e| io(&index_path, e))?)
149            .map_err(|e| CarryError::Layout {
150                path: index_path.display().to_string(),
151                reason: format!("index.json: {e}"),
152            })?;
153    let entries = index["manifests"]
154        .as_array_mut()
155        .ok_or_else(|| CarryError::Layout {
156            path: index_path.display().to_string(),
157            reason: "index.json has no manifests array".into(),
158        })?;
159    // Replace THIS statement's entries only. A layer has many attestations, so
160    // unlike line-status this must not clear the others.
161    entries.retain(|e| {
162        !(e["digest"] == *st_digest
163            || (e["artifactType"] == ATTESTATION_ARTIFACT_TYPE
164                && e["annotations"][ANN_STATEMENT] == *st_digest))
165    });
166    entries.push(serde_json::json!({
167        "mediaType": "application/json",
168        "artifactType": STATEMENT_ARTIFACT_TYPE,
169        "digest": st_digest,
170        "size": statement_envelope.len(),
171    }));
172    entries.push(serde_json::json!({
173        "mediaType": "application/octet-stream",
174        "artifactType": ATTESTATION_ARTIFACT_TYPE,
175        "digest": bytes_digest,
176        "size": attested_bytes.len(),
177        "annotations": { ANN_STATEMENT: st_digest }
178    }));
179    std::fs::write(
180        &index_path,
181        serde_json::to_vec_pretty(&index).expect("index serializes"),
182    )
183    .map_err(|e| io(&index_path, e))?;
184    Ok(st_digest)
185}
186
187/// Every attestation a layout carries. A statement whose attested bytes are
188/// absent is an ERROR, not a skipped entry: that is precisely the
189/// mirror-boundary failure this requirement exists to catch, and dropping it
190/// silently would reproduce the bug in the code meant to detect it.
191pub fn read_all(layout: &Path, layer: &str) -> Result<Vec<CarriedAttestation>, CarryError> {
192    let index_path = layout.join("index.json");
193    let bytes = match std::fs::read(&index_path) {
194        Ok(b) => b,
195        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
196        Err(source) => return Err(io(&index_path, source)),
197    };
198    let index: serde_json::Value =
199        serde_json::from_slice(&bytes).map_err(|e| CarryError::Layout {
200            path: index_path.display().to_string(),
201            reason: format!("index.json: {e}"),
202        })?;
203    let Some(entries) = index["manifests"].as_array() else {
204        return Ok(Vec::new());
205    };
206
207    let mut out = Vec::new();
208    for entry in entries {
209        if entry["artifactType"] != STATEMENT_ARTIFACT_TYPE {
210            continue;
211        }
212        let Some(st_digest) = entry["digest"].as_str() else {
213            continue;
214        };
215        if !is_content_address(st_digest) {
216            return Err(CarryError::MalformedDigest {
217                layer: layer.to_string(),
218                digest: st_digest.to_string(),
219            });
220        }
221        let st_path = blob_path(layout, st_digest);
222        let statement = std::fs::read(&st_path).map_err(|e| io(&st_path, e))?;
223
224        // Find the bytes this statement is about.
225        let bytes_digest = entries
226            .iter()
227            .find(|e| {
228                e["artifactType"] == ATTESTATION_ARTIFACT_TYPE
229                    && e["annotations"][ANN_STATEMENT] == *st_digest
230            })
231            .and_then(|e| e["digest"].as_str());
232        let Some(bytes_digest) = bytes_digest else {
233            return Err(CarryError::OrphanStatement {
234                layer: layer.to_string(),
235                statement: st_digest.to_string(),
236                digest: "<no referrer entry>".into(),
237            });
238        };
239        if !is_content_address(bytes_digest) {
240            return Err(CarryError::MalformedDigest {
241                layer: layer.to_string(),
242                digest: bytes_digest.to_string(),
243            });
244        }
245        let b_path = blob_path(layout, bytes_digest);
246        let attested = std::fs::read(&b_path).map_err(|_| CarryError::OrphanStatement {
247            layer: layer.to_string(),
248            statement: st_digest.to_string(),
249            digest: bytes_digest.to_string(),
250        })?;
251        out.push(CarriedAttestation {
252            statement_digest: st_digest.to_string(),
253            statement,
254            bytes: attested,
255        });
256    }
257    // Same deterministic order as `read_persisted`. These two readers agreed
258    // by luck until a full-suite run disagreed: index order here, `read_dir`
259    // order there, and `read_dir` order is filesystem-dependent. Evidence that
260    // reshuffles between runs is a diff generator for anyone recording verify
261    // output, and an order-dependent test is a flake waiting to be explained
262    // away.
263    out.sort_by(|a, b| a.statement_digest.cmp(&b.statement_digest));
264    Ok(out)
265}
266
267/// Report on every carried attestation: does it verify against the root, and
268/// does it still bind to THIS layer and THESE bytes?
269///
270/// Reporting, not refusing. An attestation that no longer binds is evidence
271/// about the evidence — a consumer must see it, but a layer whose own
272/// signature and digests are good is still a good layer. Failing the install
273/// on a third party's stale audit would make varve's verdict depend on someone
274/// else's release cadence.
275pub fn report(
276    carried: &[CarriedAttestation],
277    layer_manifest_digest: &str,
278    layer_name: &str,
279    root_pk: &[u8],
280) -> Vec<AttestationReport> {
281    carried
282        .iter()
283        .map(|c| {
284            let st: AttestationStatement =
285                match crate::attest::verify_statement(&c.statement, root_pk) {
286                    Ok(st) => st,
287                    Err(e) => {
288                        return AttestationReport {
289                            kind: "<unverified>".into(),
290                            producer: "<unverified>".into(),
291                            binds: false,
292                            reason: Some(e.to_string()),
293                        };
294                    }
295                };
296            let reason = crate::attest::check(&st, &c.bytes, layer_manifest_digest, layer_name)
297                .err()
298                .map(|e| e.to_string());
299            AttestationReport {
300                kind: st.kind.to_string(),
301                producer: st.producer.clone(),
302                binds: reason.is_none(),
303                reason,
304            }
305        })
306        .collect()
307}
308
309/// Where an installed layer keeps the attestations that travelled with it.
310pub const STORE_DIR: &str = "attestations";
311
312/// Persist carried attestations into an installed layer's root, so they
313/// survive into `archive` and an offline re-export. Without this the evidence
314/// reaches the machine and then dies there — the same mirror-boundary loss,
315/// one hop later.
316pub fn persist(layer_root: &Path, carried: &[CarriedAttestation]) -> Result<(), CarryError> {
317    if carried.is_empty() {
318        return Ok(());
319    }
320    let dir = layer_root.join(STORE_DIR);
321    std::fs::create_dir_all(&dir).map_err(|e| io(&dir, e))?;
322    for c in carried {
323        // The file name is derived from the bytes, never from the declared
324        // `statement_digest`. These records arrive from a SOURCE, and a source
325        // that could choose a file name inside the installed layer root could
326        // overwrite `layer.json` or the retained envelope — the two files the
327        // whole trust chain rests on. Re-deriving costs one hash and removes
328        // the source from the naming decision entirely.
329        let digest = crate::store::manifest_digest(&c.statement);
330        let stem = digest.strip_prefix("sha256:").unwrap_or(&digest);
331        let st = dir.join(format!("{stem}.statement.json"));
332        std::fs::write(&st, &c.statement).map_err(|e| io(&st, e))?;
333        let by = dir.join(format!("{stem}.bytes"));
334        std::fs::write(&by, &c.bytes).map_err(|e| io(&by, e))?;
335    }
336    Ok(())
337}
338
339/// Read back what `persist` wrote. A statement without its bytes is an error
340/// here too — the loss is the thing being detected, wherever it happens.
341pub fn read_persisted(
342    layer_root: &Path,
343    layer: &str,
344) -> Result<Vec<CarriedAttestation>, CarryError> {
345    let dir = layer_root.join(STORE_DIR);
346    let entries = match std::fs::read_dir(&dir) {
347        Ok(e) => e,
348        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
349        Err(source) => return Err(io(&dir, source)),
350    };
351    let mut out = Vec::new();
352    for entry in entries {
353        let path = entry.map_err(|e| io(&dir, e))?.path();
354        let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
355            continue;
356        };
357        let Some(stem) = name.strip_suffix(".statement.json") else {
358            continue;
359        };
360        let statement = std::fs::read(&path).map_err(|e| io(&path, e))?;
361        let bytes_path = dir.join(format!("{stem}.bytes"));
362        let bytes = std::fs::read(&bytes_path).map_err(|_| CarryError::OrphanStatement {
363            layer: layer.to_string(),
364            statement: format!("sha256:{stem}"),
365            digest: bytes_path.display().to_string(),
366        })?;
367        out.push(CarriedAttestation {
368            statement_digest: format!("sha256:{stem}"),
369            statement,
370            bytes,
371        });
372    }
373    // Deterministic order — see the note in `read_all`.
374    out.sort_by(|a, b| a.statement_digest.cmp(&b.statement_digest));
375    Ok(out)
376}
377
378/// Install-time carriage: take whatever attestations the source carries beside
379/// the layer and persist them into the installed layer root. Returns how many
380/// travelled.
381///
382/// Nothing here is verified, deliberately. The bytes are a third party's
383/// evidence arriving over an untrusted transport; verifying at fetch time would
384/// mean a stale or foreign-signed attestation could fail an install whose own
385/// signature and digests are perfect, making varve's verdict a function of
386/// someone else's release cadence. The check belongs at `verify`, where it is
387/// REPORTED (`report_installed`). What carriage owes the consumer is that the
388/// evidence is not silently dropped — the failure mode bandersnatch and
389/// Verdaccio actually exhibit.
390pub fn carry_from_source(
391    source: &dyn crate::source::LayerSource,
392    layer: &crate::source::LayerRef,
393    layer_root: &Path,
394) -> Result<usize, CarryError> {
395    let carried = source.fetch_attestations(layer)?;
396    persist(layer_root, &carried)?;
397    Ok(carried.len())
398}
399
400/// What `varve verify` says about an installed layer's carried attestations:
401/// for each, its kind, its producer, and whether it STILL binds to this layer
402/// and these bytes under the trust root.
403///
404/// `layer_manifest_digest` and `layer_name` must be the values the caller has
405/// already RE-VERIFIED (`reverify::verify_installed`), not the store's
406/// directory name and `layer.json` read raw — those are local labels, and
407/// REQ-ATTEST-001 already shipped a defect where `check-attestation` joined on
408/// them and reported OK over a tampered layer.
409pub fn report_installed(
410    layer_root: &Path,
411    layer_name: &str,
412    layer_manifest_digest: &str,
413    root_pk: &[u8],
414) -> Result<Vec<AttestationReport>, CarryError> {
415    let carried = read_persisted(layer_root, layer_name)?;
416    Ok(report(&carried, layer_manifest_digest, layer_name, root_pk))
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use crate::attest::{AttestationKind, sign, statement};
423    use crate::verify::generate_root_keypair;
424
425    /// A minimal layout with an index.json, as `deposit` writes.
426    fn layout() -> tempfile::TempDir {
427        let tmp = tempfile::tempdir().unwrap();
428        std::fs::write(
429            tmp.path().join("index.json"),
430            br#"{"schemaVersion":2,"manifests":[]}"#,
431        )
432        .unwrap();
433        tmp
434    }
435
436    const LAYER: &str = "2026.08.0";
437    const LAYER_DIGEST: &str =
438        "sha256:1111111111111111111111111111111111111111111111111111111111111111";
439
440    // rivet: verifies REQ-ATTEST-002
441    #[test]
442    fn an_attestation_travels_with_the_layer_and_still_binds() {
443        // The whole point: the statement AND the bytes cross the boundary, and
444        // the claim is still checkable on the far side with no network.
445        let (sk, pk) = generate_root_keypair();
446        let tmp = layout();
447        let bytes = br#"{"_type":"https://in-toto.io/Statement/v1","subject":[]}"#;
448        let st = statement(
449            LAYER,
450            LAYER_DIGEST,
451            AttestationKind::Provenance,
452            bytes,
453            "acme-ci",
454        );
455        let envelope = sign(&st, &sk, "root-1").unwrap();
456
457        attach(tmp.path(), envelope.as_bytes(), bytes).unwrap();
458
459        let carried = read_all(tmp.path(), LAYER).unwrap();
460        assert_eq!(carried.len(), 1, "one attestation carried");
461        assert_eq!(
462            carried[0].bytes, bytes,
463            "the attested bytes travel VERBATIM — varve carries another party's \
464             judgement, it does not restate it"
465        );
466
467        let reports = report(&carried, LAYER_DIGEST, LAYER, &pk);
468        assert_eq!(reports.len(), 1);
469        assert!(reports[0].binds, "reason: {:?}", reports[0].reason);
470        assert_eq!(reports[0].producer, "acme-ci");
471    }
472
473    // rivet: verifies REQ-ATTEST-002
474    #[test]
475    fn a_statement_whose_evidence_did_not_travel_is_an_error_not_a_skip() {
476        // THE mirror-boundary failure. bandersnatch and Verdaccio drop
477        // attestations; a consumer must not receive a layout that looks
478        // attested while the evidence is gone. Silently skipping the entry
479        // would reproduce, inside the detector, the exact bug it exists to
480        // detect.
481        let (sk, _pk) = generate_root_keypair();
482        let tmp = layout();
483        let bytes = b"the-evidence";
484        let st = statement(LAYER, LAYER_DIGEST, AttestationKind::Sbom, bytes, "acme-ci");
485        let envelope = sign(&st, &sk, "root-1").unwrap();
486        attach(tmp.path(), envelope.as_bytes(), bytes).unwrap();
487
488        // A mirror drops the evidence blob but keeps the index entry.
489        std::fs::remove_file(blob_path(tmp.path(), &crate::store::manifest_digest(bytes))).unwrap();
490
491        let err = read_all(tmp.path(), LAYER).expect_err("an orphaned statement must be an error");
492        let msg = err.to_string();
493        assert!(msg.contains(LAYER), "names the layer: {msg}");
494        assert!(
495            msg.contains("did not"),
496            "says the evidence failed to travel, not merely that a file is absent: {msg}"
497        );
498    }
499
500    // rivet: verifies REQ-ATTEST-002
501    #[test]
502    fn an_attestation_for_another_layer_is_reported_as_not_binding() {
503        // Carriage must not launder a mis-issued statement into looking
504        // attached just because it physically travelled beside the layer.
505        let (sk, pk) = generate_root_keypair();
506        let tmp = layout();
507        let bytes = b"evidence-for-someone-else";
508        let other = "sha256:2222222222222222222222222222222222222222222222222222222222222222";
509        let st = statement(
510            "2026.01.0",
511            other,
512            AttestationKind::Provenance,
513            bytes,
514            "acme-ci",
515        );
516        attach(
517            tmp.path(),
518            sign(&st, &sk, "root-1").unwrap().as_bytes(),
519            bytes,
520        )
521        .unwrap();
522
523        let carried = read_all(tmp.path(), LAYER).unwrap();
524        let reports = report(&carried, LAYER_DIGEST, LAYER, &pk);
525        assert!(
526            !reports[0].binds,
527            "a statement for another layer must not bind"
528        );
529        assert!(
530            reports[0].reason.as_ref().unwrap().contains("refusing"),
531            "the reason must say what was refused: {:?}",
532            reports[0].reason
533        );
534    }
535
536    // rivet: verifies REQ-ATTEST-002
537    #[test]
538    fn an_attestation_signed_by_someone_else_does_not_bind() {
539        // The carried statement is varve's own assertion, so it must verify
540        // under the realm's root. Physical proximity in a layout is not
541        // evidence of anything.
542        let (impostor_sk, _) = generate_root_keypair();
543        let (_realm_sk, realm_pk) = generate_root_keypair();
544        let tmp = layout();
545        let bytes = b"evidence";
546        let st = statement(
547            LAYER,
548            LAYER_DIGEST,
549            AttestationKind::Provenance,
550            bytes,
551            "acme-ci",
552        );
553        attach(
554            tmp.path(),
555            sign(&st, &impostor_sk, "not-the-realm").unwrap().as_bytes(),
556            bytes,
557        )
558        .unwrap();
559
560        let carried = read_all(tmp.path(), LAYER).unwrap();
561        let reports = report(&carried, LAYER_DIGEST, LAYER, &realm_pk);
562        assert!(!reports[0].binds);
563        assert_eq!(
564            reports[0].kind, "<unverified>",
565            "an unverified statement's own claims must not be echoed as fact"
566        );
567    }
568
569    // rivet: verifies REQ-ATTEST-002
570    #[test]
571    fn many_attestations_travel_together_and_reattaching_is_idempotent() {
572        // A line-status is one per line and attaching REPLACES. A layer has an
573        // SBOM and a provenance and an audit; attaching must ADD. Getting this
574        // wrong would silently keep only the last one attached.
575        let (sk, pk) = generate_root_keypair();
576        let tmp = layout();
577        let sbom = b"sbom-bytes";
578        let slsa = b"slsa-bytes";
579        for (kind, bytes) in [
580            (AttestationKind::Sbom, sbom.as_slice()),
581            (AttestationKind::Provenance, slsa.as_slice()),
582        ] {
583            let st = statement(LAYER, LAYER_DIGEST, kind, bytes, "acme-ci");
584            attach(
585                tmp.path(),
586                sign(&st, &sk, "root-1").unwrap().as_bytes(),
587                bytes,
588            )
589            .unwrap();
590        }
591        assert_eq!(
592            read_all(tmp.path(), LAYER).unwrap().len(),
593            2,
594            "both carried"
595        );
596
597        // Re-attaching one (a CI re-run) must not duplicate it.
598        let st = statement(LAYER, LAYER_DIGEST, AttestationKind::Sbom, sbom, "acme-ci");
599        attach(
600            tmp.path(),
601            sign(&st, &sk, "root-1").unwrap().as_bytes(),
602            sbom,
603        )
604        .unwrap();
605        let carried = read_all(tmp.path(), LAYER).unwrap();
606        assert_eq!(carried.len(), 2, "re-attaching replaces, never duplicates");
607        // The INDEX itself must not grow either. Two attestations are four
608        // referrer entries, before and after the re-run — a reader that
609        // deduplicates by taking the first match would hide an index quietly
610        // accumulating a stale entry per CI re-run, and the layout is what
611        // `oras cp` publishes, byte for byte.
612        let index: serde_json::Value =
613            serde_json::from_slice(&std::fs::read(tmp.path().join("index.json")).unwrap()).unwrap();
614        assert_eq!(
615            index["manifests"].as_array().unwrap().len(),
616            4,
617            "two attestations are two statements and two payloads — no more, however \
618             many times CI re-runs: {index:#}"
619        );
620        // …and it replaces the RIGHT one. Counting is not enough: a retain
621        // predicate that evicts the other attestations and re-adds this one
622        // twice also counts two, and would silently turn an SBOM+provenance
623        // pair into two copies of the SBOM.
624        let mut kinds: Vec<String> = report(&carried, LAYER_DIGEST, LAYER, &pk)
625            .into_iter()
626            .map(|r| r.kind)
627            .collect();
628        kinds.sort();
629        assert_eq!(
630            kinds,
631            vec!["provenance".to_string(), "sbom".to_string()],
632            "re-attaching one attestation must not evict the others — that is the one \
633             deliberate difference from line-status, where attaching REPLACES"
634        );
635        assert!(
636            report(&carried, LAYER_DIGEST, LAYER, &pk)
637                .iter()
638                .all(|r| r.binds)
639        );
640    }
641
642    // rivet: verifies REQ-ATTEST-002
643    #[test]
644    fn evidence_survives_the_store_and_can_be_re_emitted() {
645        // The requirement's actual subject: the evidence must cross the air
646        // gap, not merely reach the machine. bandersnatch and Verdaccio drop
647        // attestations and every BCR attestation URL points at github.com, so
648        // a consumer downstream of a mirror gets bytes with no accountability
649        // and no error saying so. Persisting into the installed layer is what
650        // lets `archive` put it back on the wire.
651        let (sk, pk) = generate_root_keypair();
652        let src = layout();
653        let sbom = b"sbom-bytes";
654        let prov = b"provenance-bytes";
655        for (kind, bytes) in [
656            (AttestationKind::Sbom, sbom.as_slice()),
657            (AttestationKind::Provenance, prov.as_slice()),
658        ] {
659            let st = statement(LAYER, LAYER_DIGEST, kind, bytes, "acme-ci");
660            attach(
661                src.path(),
662                sign(&st, &sk, "root-1").unwrap().as_bytes(),
663                bytes,
664            )
665            .unwrap();
666        }
667        let carried = read_all(src.path(), LAYER).unwrap();
668
669        // Install persists it into the layer root…
670        let installed = tempfile::tempdir().unwrap();
671        persist(installed.path(), &carried).unwrap();
672        let back = read_persisted(installed.path(), LAYER).unwrap();
673        assert_eq!(back.len(), 2, "both attestations survive the store");
674
675        // …and it re-emits into a fresh layout, byte-identical, still binding
676        // with NO network and NO trust in the transport.
677        let dest = layout();
678        for c in &back {
679            attach(dest.path(), &c.statement, &c.bytes).unwrap();
680        }
681        let round_tripped = read_all(dest.path(), LAYER).unwrap();
682        assert_eq!(
683            round_tripped, carried,
684            "the evidence that crossed the gap is the evidence that was signed"
685        );
686        assert!(
687            report(&round_tripped, LAYER_DIGEST, LAYER, &pk)
688                .iter()
689                .all(|r| r.binds),
690            "every attestation still binds on the far side"
691        );
692    }
693
694    // rivet: verifies REQ-ATTEST-002
695    #[test]
696    fn verify_reports_each_carried_attestation_and_whether_it_still_binds() {
697        // What `varve verify` actually calls. Two attestations under the same
698        // root: one issued for THIS layer, one for another. Both are reported —
699        // reporting, not refusal, is the decision (a layer whose own signature
700        // and digests are good is still a good layer) — but the one that no
701        // longer binds must say so, with its reason, or carriage would launder
702        // a mis-issued statement into looking attached just because it
703        // physically travelled.
704        let (sk, pk) = generate_root_keypair();
705        let installed = tempfile::tempdir().unwrap();
706        let mine = b"sbom-for-this-layer";
707        let theirs = b"audit-for-another-layer";
708        let good = statement(LAYER, LAYER_DIGEST, AttestationKind::Sbom, mine, "acme-ci");
709        let other = statement(
710            "2026.01.0",
711            "sha256:2222222222222222222222222222222222222222222222222222222222222222",
712            AttestationKind::Audit,
713            theirs,
714            "someone-else",
715        );
716        let carried: Vec<CarriedAttestation> =
717            [(&good, mine.as_slice()), (&other, theirs.as_slice())]
718                .into_iter()
719                .map(|(st, bytes)| {
720                    let envelope = sign(st, &sk, "root-1").unwrap();
721                    CarriedAttestation {
722                        statement_digest: crate::store::manifest_digest(envelope.as_bytes()),
723                        statement: envelope.into_bytes(),
724                        bytes: bytes.to_vec(),
725                    }
726                })
727                .collect();
728        persist(installed.path(), &carried).unwrap();
729
730        let reports = report_installed(installed.path(), LAYER, LAYER_DIGEST, &pk).unwrap();
731        assert_eq!(reports.len(), 2, "verify must report EVERY attestation");
732        let sbom = reports.iter().find(|r| r.kind == "sbom").expect("sbom");
733        assert!(sbom.binds, "reason: {:?}", sbom.reason);
734        assert_eq!(sbom.producer, "acme-ci");
735        let audit = reports.iter().find(|r| r.kind == "audit").expect("audit");
736        assert!(
737            !audit.binds,
738            "a statement issued for another layer must not be reported as binding"
739        );
740        assert!(
741            audit.reason.as_ref().unwrap().contains("refusing"),
742            "the reason must say what was refused: {:?}",
743            audit.reason
744        );
745    }
746
747    // rivet: verifies REQ-ATTEST-002
748    #[test]
749    fn a_layer_carrying_nothing_reports_nothing_rather_than_failing() {
750        // Most layers carry no third-party evidence. Demanding some would make
751        // varve's verdict depend on other people's publishing habits.
752        let (_sk, pk) = generate_root_keypair();
753        let installed = tempfile::tempdir().unwrap();
754        assert!(
755            report_installed(installed.path(), LAYER, LAYER_DIGEST, &pk)
756                .unwrap()
757                .is_empty()
758        );
759    }
760
761    // rivet: verifies REQ-ATTEST-002
762    #[test]
763    fn install_time_carriage_takes_what_the_source_holds_and_trusts_none_of_it() {
764        // The seam install() runs through. The source is the party that would
765        // benefit from a forged statement, so carriage stores bytes and asks
766        // no questions — and the verdict is still correct afterwards because
767        // `report` re-checks against the root, not against the source.
768        let (impostor, _) = generate_root_keypair();
769        let (_realm_sk, realm_pk) = generate_root_keypair();
770        let bytes = b"evidence";
771        let st = statement(LAYER, LAYER_DIGEST, AttestationKind::Vex, bytes, "acme-ci");
772        let source = crate::source::MemorySource::new().with_attestation(
773            sign(&st, &impostor, "not-the-realm").unwrap().as_bytes(),
774            bytes,
775        );
776        let installed = tempfile::tempdir().unwrap();
777
778        let n = carry_from_source(
779            &source,
780            &crate::source::LayerRef::Name(LAYER.parse().unwrap()),
781            installed.path(),
782        )
783        .unwrap();
784        assert_eq!(n, 1, "the evidence is carried, not judged, at fetch time");
785
786        let reports = report_installed(installed.path(), LAYER, LAYER_DIGEST, &realm_pk).unwrap();
787        assert!(
788            !reports[0].binds,
789            "a statement signed by anyone but the realm's root must not bind"
790        );
791        assert_eq!(
792            reports[0].kind, "<unverified>",
793            "an unverified statement's own claims must not be echoed as fact"
794        );
795    }
796
797    // rivet: verifies REQ-ATTEST-002
798    #[test]
799    fn a_layout_naming_a_blob_outside_itself_is_refused_before_anything_is_read() {
800        // The digest in a layout index is a string a MIRROR wrote, and it is
801        // used as a path component. Left unchecked, `sha256:../../..` reads
802        // outside the layout and — one step later, through `persist` — names a
803        // file inside the installed layer root, where `layer.json` and the
804        // retained envelope live. A content address is exactly the value that
805        // must not be taken on faith by the code resolving it.
806        // BOTH halves of the shape matter and are checked here. The traversal
807        // is the attack; the short-but-hexadecimal case is the reason the rule
808        // is a conjunction — accepting anything that merely *looks* hex-ish
809        // lets a crafted name through on length alone, and a rule that fires
810        // for only one of its two conditions is not the rule.
811        for bad in [
812            "sha256:../../../../../../etc/passwd",
813            "sha256:abc",
814            "sha256:",
815            "not-a-digest-at-all",
816        ] {
817            let tmp = layout();
818            std::fs::write(
819                tmp.path().join("index.json"),
820                serde_json::to_vec_pretty(&serde_json::json!({
821                    "schemaVersion": 2,
822                    "manifests": [{
823                        "mediaType": "application/json",
824                        "artifactType": STATEMENT_ARTIFACT_TYPE,
825                        "digest": bad,
826                        "size": 1,
827                    }]
828                }))
829                .unwrap(),
830            )
831            .unwrap();
832            let err = read_all(tmp.path(), LAYER).expect_err("'{bad}' is not a content address");
833            assert!(
834                matches!(err, CarryError::MalformedDigest { .. }),
835                "'{bad}' must be refused as a malformed digest, not resolved as a path: {err}"
836            );
837        }
838    }
839
840    // rivet: verifies REQ-ATTEST-002
841    #[test]
842    fn persist_names_files_after_the_bytes_not_after_what_the_source_declared() {
843        // Same attack, one layer deeper. A source hands over a record whose
844        // DECLARED digest is a path. persist must derive the name from the
845        // bytes it actually holds, so the source never gets a say in where a
846        // file lands inside the installed layer.
847        let (sk, _pk) = generate_root_keypair();
848        let installed = tempfile::tempdir().unwrap();
849        let bytes = b"evidence";
850        let st = statement(LAYER, LAYER_DIGEST, AttestationKind::Sbom, bytes, "acme");
851        let envelope = sign(&st, &sk, "root-1").unwrap();
852        persist(
853            installed.path(),
854            &[CarriedAttestation {
855                statement_digest: "sha256:../../../../pwned".into(),
856                statement: envelope.clone().into_bytes(),
857                bytes: bytes.to_vec(),
858            }],
859        )
860        .unwrap();
861
862        let hex = crate::store::manifest_digest(envelope.as_bytes())
863            .strip_prefix("sha256:")
864            .unwrap()
865            .to_string();
866        assert!(
867            installed
868                .path()
869                .join(STORE_DIR)
870                .join(format!("{hex}.statement.json"))
871                .is_file(),
872            "the file must be named after the bytes' own content address"
873        );
874        assert!(
875            !installed.path().parent().unwrap().join("pwned").exists()
876                && !installed.path().join("pwned").exists(),
877            "no file may land outside the attestation store"
878        );
879        // …and it reads back intact, so the guard costs nothing.
880        assert_eq!(read_persisted(installed.path(), LAYER).unwrap().len(), 1);
881    }
882
883    // rivet: verifies REQ-ATTEST-002
884    #[test]
885    fn an_absent_layout_carries_nothing_but_an_unreadable_one_is_not_silently_empty() {
886        // The two failures must not look alike. A layout with no index.json
887        // carries no attestations — ordinary and fine. A layout whose index
888        // cannot be READ is a broken artifact, and answering "carries none"
889        // would report an absence that was never established: the same
890        // silent-drop shape this whole requirement exists to make impossible.
891        let empty = tempfile::tempdir().unwrap();
892        assert!(
893            read_all(empty.path(), LAYER).unwrap().is_empty(),
894            "an absent index.json is an empty layout, not an error"
895        );
896
897        #[cfg(unix)]
898        {
899            let broken = tempfile::tempdir().unwrap();
900            // An index.json that is a directory: present, and unreadable.
901            std::fs::create_dir(broken.path().join("index.json")).unwrap();
902            assert!(
903                read_all(broken.path(), LAYER).is_err(),
904                "an unreadable index must be an error, never an empty answer"
905            );
906        }
907    }
908
909    // rivet: verifies REQ-ATTEST-002
910    #[test]
911    fn an_unreadable_attestation_store_is_an_error_not_an_empty_answer() {
912        // Same rule one hop later, on the installed side. A layer root with no
913        // `attestations/` carries none; a layer root where that name exists and
914        // cannot be enumerated is broken, and must say so rather than report
915        // the layer as carrying nothing.
916        let none = tempfile::tempdir().unwrap();
917        assert!(read_persisted(none.path(), LAYER).unwrap().is_empty());
918
919        #[cfg(unix)]
920        {
921            let broken = tempfile::tempdir().unwrap();
922            // `attestations` exists as a FILE: read_dir cannot enumerate it.
923            std::fs::write(broken.path().join(STORE_DIR), b"not a directory").unwrap();
924            assert!(
925                read_persisted(broken.path(), LAYER).is_err(),
926                "a store that cannot be read must not read back as 'this layer carries none'"
927            );
928        }
929    }
930
931    // rivet: verifies REQ-ATTEST-002
932    #[test]
933    fn a_store_that_lost_the_evidence_is_an_error_on_read_back_too() {
934        // The loss is the thing being detected, wherever it happens — a
935        // half-copied store must not read back as "this layer has fewer
936        // attestations than it does".
937        let (sk, _pk) = generate_root_keypair();
938        let installed = tempfile::tempdir().unwrap();
939        let bytes = b"evidence";
940        let st = statement(LAYER, LAYER_DIGEST, AttestationKind::Audit, bytes, "acme");
941        persist(
942            installed.path(),
943            &[CarriedAttestation {
944                statement_digest: crate::store::manifest_digest(
945                    sign(&st, &sk, "k").unwrap().as_bytes(),
946                ),
947                statement: sign(&st, &sk, "k").unwrap().into_bytes(),
948                bytes: bytes.to_vec(),
949            }],
950        )
951        .unwrap();
952
953        // Something copies the statements and not the payloads.
954        for e in std::fs::read_dir(installed.path().join(STORE_DIR)).unwrap() {
955            let p = e.unwrap().path();
956            if p.extension().is_some_and(|x| x == "bytes") {
957                std::fs::remove_file(p).unwrap();
958            }
959        }
960        assert!(
961            matches!(
962                read_persisted(installed.path(), LAYER),
963                Err(CarryError::OrphanStatement { .. })
964            ),
965            "a statement whose evidence is gone must not read back as absent"
966        );
967    }
968}