1use std::path::Path;
29
30use crate::attest::{AttestError, AttestationStatement};
31
32pub const STATEMENT_ARTIFACT_TYPE: &str = crate::attest::PAYLOAD_TYPE;
34
35pub const ATTESTATION_ARTIFACT_TYPE: &str =
37 "application/vnd.pulseengine.varve.attestation-bytes.v1";
38
39pub 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#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct CarriedAttestation {
77 pub statement_digest: String,
79 pub statement: Vec<u8>,
81 pub bytes: Vec<u8>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct AttestationReport {
88 pub kind: String,
89 pub producer: String,
90 pub binds: bool,
93 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
111fn 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
125pub 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 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
187pub 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 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 out.sort_by(|a, b| a.statement_digest.cmp(&b.statement_digest));
264 Ok(out)
265}
266
267pub 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
309pub const STORE_DIR: &str = "attestations";
311
312pub 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 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
339pub 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 out.sort_by(|a, b| a.statement_digest.cmp(&b.statement_digest));
375 Ok(out)
376}
377
378pub 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
400pub 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 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 #[test]
442 fn an_attestation_travels_with_the_layer_and_still_binds() {
443 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 #[test]
475 fn a_statement_whose_evidence_did_not_travel_is_an_error_not_a_skip() {
476 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 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 #[test]
502 fn an_attestation_for_another_layer_is_reported_as_not_binding() {
503 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 #[test]
538 fn an_attestation_signed_by_someone_else_does_not_bind() {
539 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 #[test]
571 fn many_attestations_travel_together_and_reattaching_is_idempotent() {
572 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 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 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 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 #[test]
644 fn evidence_survives_the_store_and_can_be_re_emitted() {
645 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 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 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 #[test]
696 fn verify_reports_each_carried_attestation_and_whether_it_still_binds() {
697 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 #[test]
749 fn a_layer_carrying_nothing_reports_nothing_rather_than_failing() {
750 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 #[test]
763 fn install_time_carriage_takes_what_the_source_holds_and_trusts_none_of_it() {
764 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 #[test]
799 fn a_layout_naming_a_blob_outside_itself_is_refused_before_anything_is_read() {
800 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 #[test]
842 fn persist_names_files_after_the_bytes_not_after_what_the_source_declared() {
843 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 assert_eq!(read_persisted(installed.path(), LAYER).unwrap().len(), 1);
881 }
882
883 #[test]
885 fn an_absent_layout_carries_nothing_but_an_unreadable_one_is_not_silently_empty() {
886 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 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 #[test]
911 fn an_unreadable_attestation_store_is_an_error_not_an_empty_answer() {
912 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 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 #[test]
933 fn a_store_that_lost_the_evidence_is_an_error_on_read_back_too() {
934 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 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}