Skip to main content

varve_core/
source.rs

1//! Sources — where bytes come from. Pluggable by design; trusted by nobody
2//! (DD-003).
3//!
4//! A source can *obtain* bytes: a manifest by layer name or digest, a blob by
5//! digest. It has no voice in whether those bytes are *accepted* — signature
6//! and digest verification run against the trust root after every fetch, so
7//! swapping the source can change availability, never a verdict. The install
8//! pipeline (`crate::install`) enforces this by construction: nothing a
9//! `LayerSource` returns reaches the core without passing the same checks.
10
11use crate::layer::LayerId;
12
13/// UNTRUSTED discovery: does `bytes` look like a manifest for `id`, either
14/// raw or wrapped in a DSSE envelope? Sources use this to answer name/digest
15/// lookups; it grants nothing — the install pipeline re-verifies signature
16/// and digest on whatever a source returns.
17fn discovery_matches(bytes: &[u8], layer: &LayerRef) -> bool {
18    use crate::manifest::LayerManifest;
19    let candidate_payloads = || -> Vec<Vec<u8>> {
20        let mut out = vec![bytes.to_vec()];
21        if let Ok(text) = std::str::from_utf8(bytes)
22            && let Ok(env) = wsc::dsse::DsseEnvelope::from_json(text)
23            && let Ok(payload) = env.payload_bytes()
24        {
25            out.push(payload);
26        }
27        out
28    };
29    match layer {
30        LayerRef::Digest(digest) => candidate_payloads()
31            .iter()
32            .any(|p| &crate::store::manifest_digest(p) == digest),
33        LayerRef::Name(id) => candidate_payloads()
34            .iter()
35            .any(|p| LayerManifest::parse(p).is_ok_and(|m| &m.layer == id)),
36    }
37}
38
39/// Reference to a layer a source should produce the manifest for.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum LayerRef {
42    /// By name — discovery; the returned manifest's own annotations and
43    /// digest are then checked against the pin.
44    Name(LayerId),
45    /// By exact manifest digest (`sha256:<hex>`).
46    Digest(String),
47}
48
49/// Failures a source may report. `NotFound` is honest absence; everything
50/// else is transport trouble. There is deliberately no way for a source to
51/// report "trust me" — trust is not its department.
52#[derive(Debug, thiserror::Error)]
53pub enum SourceError {
54    #[error("source has no layer matching {0}")]
55    NotFound(String),
56    /// Absence with a KNOWN cause: the source is an archive of one platform and
57    /// the caller wants another. Distinct from `NotFound` because the operator's
58    /// next move is different — nothing is corrupt, nothing is missing from the
59    /// archive that belongs in it, and no amount of re-copying the media will
60    /// help (varve#80).
61    #[error(
62        "this archive carries no payload for {wanted} — it was archived for {archived_for}, and \
63         `varve archive` exports only the payloads the archiving machine installed, so it holds \
64         {archived_for} payloads and nothing else (blob {digest} is not in it). Install the layer \
65         on a machine running {wanted} and archive it there to carry {wanted} across the gap."
66    )]
67    NoPayloadForPlatform {
68        digest: String,
69        wanted: String,
70        archived_for: String,
71    },
72    #[error("source transport error: {0}")]
73    Transport(String),
74}
75
76/// Where bytes come from. Implementations ship in varve (public registry,
77/// archived core, test doubles); the trait is the seam an entitlement
78/// plug-in would use — and the reason none of them can influence acceptance.
79pub trait LayerSource {
80    /// Fetch the manifest bytes for a layer reference.
81    fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError>;
82    /// Fetch a blob (a tool binary) by its digest (`sha256:<hex>`).
83    fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError>;
84    /// Fetch the baseline line-status DSSE envelope this source carries
85    /// beside the layer, if any (REQ-STATUS-DIST-001). Returns the opaque
86    /// envelope bytes — the source is *not* trusted to have verified them;
87    /// the caller re-verifies against the trust root before caching. A
88    /// source that carries no baseline returns `Ok(None)`, which is not an
89    /// error: line-status is updatable evidence, absent on some layers.
90    fn fetch_line_status(&self, _layer: &LayerRef) -> Result<Option<Vec<u8>>, SourceError> {
91        Ok(None)
92    }
93
94    /// Fetch the realm's signed line-index envelope for this line, if the
95    /// source carries one (REQ-INDEXAUTH-001). Same contract as
96    /// `fetch_line_status`: opaque bytes, re-verified by the caller against
97    /// the trust root. The source is never trusted to have checked it — it is
98    /// precisely the party this document exists to constrain.
99    fn fetch_line_index(&self, _line: &str) -> Result<Option<Vec<u8>>, SourceError> {
100        Ok(None)
101    }
102
103    /// Fetch the attestations this source carries beside the layer as
104    /// referrer artifacts (REQ-ATTEST-002). Same contract as
105    /// `fetch_line_status`: OPAQUE, UNTRUSTED bytes. The source is never
106    /// trusted to have verified a statement — it is the party that would
107    /// benefit from a forged one — so the caller persists them verbatim and
108    /// `varve verify` re-checks each against the trust root.
109    ///
110    /// A source carrying none returns `Ok(vec![])`, which is not an error: most
111    /// layers carry no third-party evidence, and demanding some would make
112    /// varve's availability depend on other people's publishing habits.
113    fn fetch_attestations(
114        &self,
115        _layer: &LayerRef,
116    ) -> Result<Vec<crate::attestcarry::CarriedAttestation>, SourceError> {
117        Ok(Vec::new())
118    }
119
120    /// The layer ids this source is willing to serve for a line. Used to
121    /// detect OMISSION against the signed index. A source that cannot
122    /// enumerate returns `Ok(None)` — distinct from `Ok(Some(vec![]))`, which
123    /// means "I enumerate, and I have nothing", and would flag every indexed
124    /// layer as hidden.
125    fn served_layers(&self, _line: &str) -> Result<Option<Vec<String>>, SourceError> {
126        Ok(None)
127    }
128}
129
130/// In-memory source — the test double, and the reference for how little a
131/// source is trusted to do.
132#[derive(Debug, Default)]
133pub struct MemorySource {
134    manifests: Vec<Vec<u8>>,
135    blobs: std::collections::BTreeMap<String, Vec<u8>>,
136    line_status: Option<Vec<u8>>,
137    line_index: Option<Vec<u8>>,
138    served: Option<Vec<String>>,
139    attestations: Vec<crate::attestcarry::CarriedAttestation>,
140}
141
142impl MemorySource {
143    pub fn new() -> Self {
144        Self::default()
145    }
146
147    pub fn with_manifest(mut self, bytes: &[u8]) -> Self {
148        self.manifests.push(bytes.to_vec());
149        self
150    }
151
152    pub fn with_blob(mut self, digest: &str, bytes: &[u8]) -> Self {
153        self.blobs.insert(digest.to_string(), bytes.to_vec());
154        self
155    }
156
157    /// Attach a baseline line-status envelope the source carries beside the
158    /// layer (REQ-STATUS-DIST-001).
159    /// Carry a signed line index (REQ-INDEXAUTH-001).
160    pub fn with_line_index(mut self, envelope: &[u8]) -> Self {
161        self.line_index = Some(envelope.to_vec());
162        self
163    }
164
165    /// What this source admits to serving. Setting it makes the source
166    /// enumerable, which is what lets omission be detected — a source that
167    /// never sets it cannot be accused of hiding.
168    pub fn serving(mut self, layers: &[&str]) -> Self {
169        self.served = Some(layers.iter().map(|s| s.to_string()).collect());
170        self
171    }
172
173    pub fn with_line_status(mut self, envelope: &[u8]) -> Self {
174        self.line_status = Some(envelope.to_vec());
175        self
176    }
177
178    /// Carry an attestation beside the layer (REQ-ATTEST-002). The statement's
179    /// digest is derived from the bytes handed over, not declared: a source
180    /// that could name its own content addresses would be trusted about
181    /// something, and it is trusted about nothing.
182    pub fn with_attestation(mut self, statement: &[u8], attested_bytes: &[u8]) -> Self {
183        self.attestations
184            .push(crate::attestcarry::CarriedAttestation {
185                statement_digest: crate::store::manifest_digest(statement),
186                statement: statement.to_vec(),
187                bytes: attested_bytes.to_vec(),
188            });
189        self
190    }
191}
192
193/// Directory-shaped source: `<root>/manifests/sha256-<hex>` and
194/// `<root>/blobs/sha256-<hex>`. The reading half of the archived core —
195/// and, in tests, the second transport for the two-sources-same-verdict
196/// kill-criterion.
197#[derive(Debug)]
198pub struct DirSource {
199    root: std::path::PathBuf,
200}
201
202impl DirSource {
203    pub fn at(root: impl Into<std::path::PathBuf>) -> Self {
204        DirSource { root: root.into() }
205    }
206
207    /// Write a manifest + blobs into the directory layout (the producing
208    /// side, used by tests and by `archive` later).
209    pub fn put(&self, manifest_bytes: &[u8], blobs: &[(&str, &[u8])]) -> std::io::Result<()> {
210        let manifests = self.root.join("manifests");
211        let blob_dir = self.root.join("blobs");
212        std::fs::create_dir_all(&manifests)?;
213        std::fs::create_dir_all(&blob_dir)?;
214        let digest = crate::store::manifest_digest(manifest_bytes);
215        std::fs::write(manifests.join(digest.replace(':', "-")), manifest_bytes)?;
216        for (digest, bytes) in blobs {
217            std::fs::write(blob_dir.join(digest.replace(':', "-")), bytes)?;
218        }
219        Ok(())
220    }
221}
222
223impl LayerSource for DirSource {
224    fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError> {
225        let dir = self.root.join("manifests");
226        let entries = std::fs::read_dir(&dir)
227            .map_err(|e| SourceError::Transport(format!("{}: {e}", dir.display())))?;
228        for entry in entries.filter_map(|e| e.ok()) {
229            let bytes =
230                std::fs::read(entry.path()).map_err(|e| SourceError::Transport(e.to_string()))?;
231            if discovery_matches(&bytes, layer) {
232                return Ok(bytes);
233            }
234        }
235        Err(SourceError::NotFound(format!("{layer:?}")))
236    }
237
238    fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError> {
239        let path = self.root.join("blobs").join(digest.replace(':', "-"));
240        match std::fs::read(&path) {
241            Ok(bytes) => Ok(bytes),
242            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
243                Err(SourceError::NotFound(digest.to_string()))
244            }
245            Err(e) => Err(SourceError::Transport(e.to_string())),
246        }
247    }
248
249    // `fetch_line_index` and `served_layers` stay at the trait defaults, and
250    // both defaults are the truthful answer rather than a stub
251    // (REQ-INDEXAUTH-001). This layout is `manifests/` + `blobs/` addressed by
252    // digest: it has nowhere to carry a per-line document, and its manifest
253    // directory is whatever someone copied there — not a listing of the line.
254    // `Ok(None)` for `served_layers` therefore means "cannot enumerate", which
255    // is exactly right; returning `Ok(Some(...))` of the files present would
256    // accuse an honest air-gapped copy of hiding every layer it was not given.
257    // A realm that declares `signed-index = true` consequently cannot be
258    // installed from a bare DirSource at all — it fails closed, naming the
259    // realm, which is the correct outcome for a transport that cannot carry
260    // the evidence the realm promised. Use an oci-layout archive instead.
261}
262
263impl LayerSource for MemorySource {
264    fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError> {
265        self.manifests
266            .iter()
267            .find(|bytes| discovery_matches(bytes, layer))
268            .cloned()
269            .ok_or_else(|| SourceError::NotFound(format!("{layer:?}")))
270    }
271
272    fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError> {
273        self.blobs
274            .get(digest)
275            .cloned()
276            .ok_or_else(|| SourceError::NotFound(digest.to_string()))
277    }
278
279    fn fetch_line_index(&self, _line: &str) -> Result<Option<Vec<u8>>, SourceError> {
280        Ok(self.line_index.clone())
281    }
282
283    fn served_layers(&self, _line: &str) -> Result<Option<Vec<String>>, SourceError> {
284        Ok(self.served.clone())
285    }
286
287    fn fetch_line_status(&self, _layer: &LayerRef) -> Result<Option<Vec<u8>>, SourceError> {
288        Ok(self.line_status.clone())
289    }
290
291    fn fetch_attestations(
292        &self,
293        _layer: &LayerRef,
294    ) -> Result<Vec<crate::attestcarry::CarriedAttestation>, SourceError> {
295        Ok(self.attestations.clone())
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    // rivet: verifies REQ-STATUS-DIST-001
304    #[test]
305    fn a_source_carrying_a_baseline_line_status_yields_it() {
306        let envelope = b"an-opaque-dsse-envelope";
307        let source = MemorySource::new().with_line_status(envelope);
308        let got = source
309            .fetch_line_status(&LayerRef::Name("2026.07.0".parse().unwrap()))
310            .unwrap();
311        assert_eq!(
312            got.as_deref(),
313            Some(envelope.as_slice()),
314            "a source that carries a baseline line-status must hand it back for caching"
315        );
316    }
317
318    // rivet: verifies REQ-ATTEST-002
319    #[test]
320    fn a_source_carrying_attestations_hands_over_both_blobs_and_one_without_is_not_an_error() {
321        let source = MemorySource::new().with_attestation(b"a-statement-envelope", b"the-evidence");
322        let got = source
323            .fetch_attestations(&LayerRef::Name("2026.07.0".parse().unwrap()))
324            .unwrap();
325        assert_eq!(got.len(), 1);
326        assert_eq!(
327            got[0].bytes, b"the-evidence",
328            "the attested bytes must travel beside the statement — a claim with nothing to \
329             check it against is what crossing the air gap must never produce"
330        );
331        assert_eq!(
332            got[0].statement_digest,
333            crate::store::manifest_digest(b"a-statement-envelope"),
334            "the digest is derived from the bytes; a source never declares its own address"
335        );
336
337        // Absence is emptiness, not failure: most layers carry no third-party
338        // evidence, and requiring some would make availability depend on other
339        // people's publishing habits.
340        assert!(
341            MemorySource::new()
342                .fetch_attestations(&LayerRef::Name("2026.07.0".parse().unwrap()))
343                .unwrap()
344                .is_empty()
345        );
346    }
347
348    // rivet: verifies REQ-STATUS-DIST-001
349    #[test]
350    fn a_source_without_a_line_status_is_not_an_error() {
351        let source = MemorySource::new();
352        let got = source
353            .fetch_line_status(&LayerRef::Name("2026.07.0".parse().unwrap()))
354            .unwrap();
355        assert_eq!(
356            got, None,
357            "an absent line-status is Ok(None), never an error"
358        );
359    }
360}