plecto_control/artifact.rs
1//! Resolving a manifest `source` into the bytes to load, verifying the content-digest pin
2//! (ADR 000007). `ArtifactStore` is the seam (cf. host's `KvBackend`): the offline OCI
3//! image-layout reader (`oci::OciLayoutStore`) and a future remote/wkg fetcher plug in here
4//! without touching the manifest / chain / reload logic. `MemoryStore` is the reference /
5//! test store.
6
7use std::collections::HashMap;
8
9use sha2::{Digest, Sha256};
10
11use crate::error::ControlError;
12
13/// The verified bytes a filter loads from: the component plus its cosign signature, and the
14/// (mandatory) SBOM plus its signature (ADR 000006). Produced after the digest-pin check.
15#[derive(Debug, Clone)]
16pub struct ResolvedArtifact {
17 pub component: Vec<u8>,
18 pub component_signature: Vec<u8>,
19 pub sbom: Vec<u8>,
20 pub sbom_signature: Vec<u8>,
21}
22
23/// Resolves a manifest `source` into a `ResolvedArtifact`, verifying that the artifact's
24/// content digest equals `pinned_digest` (`sha256:...`). Implementations are fail-closed on
25/// any mismatch or malformed artifact.
26pub trait ArtifactStore: Send + Sync {
27 fn resolve(&self, source: &str, pinned_digest: &str) -> Result<ResolvedArtifact, ControlError>;
28}
29
30/// `sha256:<hex>` over `bytes` — the OCI content-digest form.
31pub(crate) fn sha256_pinned(bytes: &[u8]) -> String {
32 format!("sha256:{}", hex::encode(Sha256::digest(bytes)))
33}
34
35/// An in-memory `ArtifactStore` keyed by source name — the reference / test store. Its
36/// content digest is `sha256` over the **component bytes**, so the pin mechanism is exercised
37/// without an on-disk OCI layout.
38///
39/// Note the pin semantics differ from the real store (f000004 #5): `OciLayoutStore` pins the
40/// OCI **image-manifest** digest (the Merkle root over all layers), whereas this store pins
41/// the component digest directly. The `digest` a manifest carries therefore means different
42/// things across stores — a deliberate test simplification, not a portable pin. The offline
43/// `OciLayoutStore` is the real one; production manifests pin image-manifest digests.
44#[derive(Default)]
45pub struct MemoryStore {
46 artifacts: HashMap<String, ResolvedArtifact>,
47}
48
49impl MemoryStore {
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 /// Insert an artifact under `source`; returns the `sha256:...` digest to pin it by in a
55 /// manifest.
56 pub fn insert(&mut self, source: impl Into<String>, artifact: ResolvedArtifact) -> String {
57 let digest = sha256_pinned(&artifact.component);
58 self.artifacts.insert(source.into(), artifact);
59 digest
60 }
61}
62
63impl ArtifactStore for MemoryStore {
64 fn resolve(&self, source: &str, pinned_digest: &str) -> Result<ResolvedArtifact, ControlError> {
65 let artifact = self
66 .artifacts
67 .get(source)
68 .ok_or_else(|| ControlError::Artifact {
69 source_ref: source.to_string(),
70 reason: "not found in store".to_string(),
71 })?;
72 let actual = sha256_pinned(&artifact.component);
73 if actual != pinned_digest {
74 return Err(ControlError::DigestMismatch {
75 source_ref: source.to_string(),
76 expected: pinned_digest.to_string(),
77 actual,
78 });
79 }
80 Ok(artifact.clone())
81 }
82}