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 line-status document published under the line's OWN tag,
104    /// independent of any layer (REQ-POSTDEPOSIT-001 clause 1).
105    ///
106    /// Distinct from `fetch_line_status`, which returns the BASELINE carried
107    /// inside a layer's artifact manifest. Both may exist and disagree; the
108    /// caller verifies each and keeps the newer by counter (clause 2). Same
109    /// contract otherwise: opaque, untrusted bytes. Ranking before verifying
110    /// would let whoever serves the tag pick the winner by writing a large
111    /// counter, so the caller must verify FIRST and compare second.
112    ///
113    /// A source with no published document returns `Ok(None)`, which is not an
114    /// error: most lines have never been corrected, and a source that cannot
115    /// serve one must not be able to make that look like a failure.
116    fn fetch_published_line_status(&self, _line: &str) -> Result<Option<Vec<u8>>, SourceError> {
117        Ok(None)
118    }
119
120    /// Fetch the attestations this source carries beside the layer as
121    /// referrer artifacts (REQ-ATTEST-002). Same contract as
122    /// `fetch_line_status`: OPAQUE, UNTRUSTED bytes. The source is never
123    /// trusted to have verified a statement — it is the party that would
124    /// benefit from a forged one — so the caller persists them verbatim and
125    /// `varve verify` re-checks each against the trust root.
126    ///
127    /// A source carrying none returns `Ok(vec![])`, which is not an error: most
128    /// layers carry no third-party evidence, and demanding some would make
129    /// varve's availability depend on other people's publishing habits.
130    fn fetch_attestations(
131        &self,
132        _layer: &LayerRef,
133    ) -> Result<Vec<crate::attestcarry::CarriedAttestation>, SourceError> {
134        Ok(Vec::new())
135    }
136
137    /// The layer ids this source is willing to serve for a line. Used to
138    /// detect OMISSION against the signed index. A source that cannot
139    /// enumerate returns `Ok(None)` — distinct from `Ok(Some(vec![]))`, which
140    /// means "I enumerate, and I have nothing", and would flag every indexed
141    /// layer as hidden.
142    fn served_layers(&self, _line: &str) -> Result<Option<Vec<String>>, SourceError> {
143        Ok(None)
144    }
145}
146
147/// In-memory source — the test double, and the reference for how little a
148/// source is trusted to do.
149#[derive(Debug, Default)]
150pub struct MemorySource {
151    manifests: Vec<Vec<u8>>,
152    blobs: std::collections::BTreeMap<String, Vec<u8>>,
153    line_status: Option<Vec<u8>>,
154    published_line_status: Option<Vec<u8>>,
155    line_index: Option<Vec<u8>>,
156    served: Option<Vec<String>>,
157    attestations: Vec<crate::attestcarry::CarriedAttestation>,
158}
159
160impl MemorySource {
161    pub fn new() -> Self {
162        Self::default()
163    }
164
165    pub fn with_manifest(mut self, bytes: &[u8]) -> Self {
166        self.manifests.push(bytes.to_vec());
167        self
168    }
169
170    pub fn with_blob(mut self, digest: &str, bytes: &[u8]) -> Self {
171        self.blobs.insert(digest.to_string(), bytes.to_vec());
172        self
173    }
174
175    /// Attach a baseline line-status envelope the source carries beside the
176    /// layer (REQ-STATUS-DIST-001).
177    /// Carry a signed line index (REQ-INDEXAUTH-001).
178    pub fn with_line_index(mut self, envelope: &[u8]) -> Self {
179        self.line_index = Some(envelope.to_vec());
180        self
181    }
182
183    /// What this source admits to serving. Setting it makes the source
184    /// enumerable, which is what lets omission be detected — a source that
185    /// never sets it cannot be accused of hiding.
186    pub fn serving(mut self, layers: &[&str]) -> Self {
187        self.served = Some(layers.iter().map(|s| s.to_string()).collect());
188        self
189    }
190
191    pub fn with_line_status(mut self, envelope: &[u8]) -> Self {
192        self.line_status = Some(envelope.to_vec());
193        self
194    }
195
196    /// Carry a line-status document published under the line's own tag
197    /// (REQ-POSTDEPOSIT-001) — a correction, yank, or advisory issued after
198    /// the layer was deposited.
199    pub fn with_published_line_status(mut self, envelope: &[u8]) -> Self {
200        self.published_line_status = Some(envelope.to_vec());
201        self
202    }
203
204    /// Carry an attestation beside the layer (REQ-ATTEST-002). The statement's
205    /// digest is derived from the bytes handed over, not declared: a source
206    /// that could name its own content addresses would be trusted about
207    /// something, and it is trusted about nothing.
208    pub fn with_attestation(mut self, statement: &[u8], attested_bytes: &[u8]) -> Self {
209        self.attestations
210            .push(crate::attestcarry::CarriedAttestation {
211                statement_digest: crate::store::manifest_digest(statement),
212                statement: statement.to_vec(),
213                bytes: attested_bytes.to_vec(),
214            });
215        self
216    }
217}
218
219/// Directory-shaped source: `<root>/manifests/sha256-<hex>` and
220/// `<root>/blobs/sha256-<hex>`. The reading half of the archived core —
221/// and, in tests, the second transport for the two-sources-same-verdict
222/// kill-criterion.
223#[derive(Debug)]
224pub struct DirSource {
225    root: std::path::PathBuf,
226}
227
228impl DirSource {
229    pub fn at(root: impl Into<std::path::PathBuf>) -> Self {
230        DirSource { root: root.into() }
231    }
232
233    /// Write a manifest + blobs into the directory layout (the producing
234    /// side, used by tests and by `archive` later).
235    pub fn put(&self, manifest_bytes: &[u8], blobs: &[(&str, &[u8])]) -> std::io::Result<()> {
236        let manifests = self.root.join("manifests");
237        let blob_dir = self.root.join("blobs");
238        std::fs::create_dir_all(&manifests)?;
239        std::fs::create_dir_all(&blob_dir)?;
240        let digest = crate::store::manifest_digest(manifest_bytes);
241        std::fs::write(manifests.join(digest.replace(':', "-")), manifest_bytes)?;
242        for (digest, bytes) in blobs {
243            std::fs::write(blob_dir.join(digest.replace(':', "-")), bytes)?;
244        }
245        Ok(())
246    }
247}
248
249impl LayerSource for DirSource {
250    fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError> {
251        let dir = self.root.join("manifests");
252        let entries = std::fs::read_dir(&dir)
253            .map_err(|e| SourceError::Transport(format!("{}: {e}", dir.display())))?;
254        for entry in entries.filter_map(|e| e.ok()) {
255            let bytes =
256                std::fs::read(entry.path()).map_err(|e| SourceError::Transport(e.to_string()))?;
257            if discovery_matches(&bytes, layer) {
258                return Ok(bytes);
259            }
260        }
261        Err(SourceError::NotFound(format!("{layer:?}")))
262    }
263
264    fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError> {
265        let path = self.root.join("blobs").join(digest.replace(':', "-"));
266        match std::fs::read(&path) {
267            Ok(bytes) => Ok(bytes),
268            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
269                Err(SourceError::NotFound(digest.to_string()))
270            }
271            Err(e) => Err(SourceError::Transport(e.to_string())),
272        }
273    }
274
275    // `fetch_line_index` and `served_layers` stay at the trait defaults, and
276    // both defaults are the truthful answer rather than a stub
277    // (REQ-INDEXAUTH-001). This layout is `manifests/` + `blobs/` addressed by
278    // digest: it has nowhere to carry a per-line document, and its manifest
279    // directory is whatever someone copied there — not a listing of the line.
280    // `Ok(None)` for `served_layers` therefore means "cannot enumerate", which
281    // is exactly right; returning `Ok(Some(...))` of the files present would
282    // accuse an honest air-gapped copy of hiding every layer it was not given.
283    // A realm that declares `signed-index = true` consequently cannot be
284    // installed from a bare DirSource at all — it fails closed, naming the
285    // realm, which is the correct outcome for a transport that cannot carry
286    // the evidence the realm promised. Use an oci-layout archive instead.
287}
288
289impl LayerSource for MemorySource {
290    fn fetch_manifest(&self, layer: &LayerRef) -> Result<Vec<u8>, SourceError> {
291        self.manifests
292            .iter()
293            .find(|bytes| discovery_matches(bytes, layer))
294            .cloned()
295            .ok_or_else(|| SourceError::NotFound(format!("{layer:?}")))
296    }
297
298    fn fetch_blob(&self, digest: &str) -> Result<Vec<u8>, SourceError> {
299        self.blobs
300            .get(digest)
301            .cloned()
302            .ok_or_else(|| SourceError::NotFound(digest.to_string()))
303    }
304
305    fn fetch_line_index(&self, _line: &str) -> Result<Option<Vec<u8>>, SourceError> {
306        Ok(self.line_index.clone())
307    }
308
309    fn served_layers(&self, _line: &str) -> Result<Option<Vec<String>>, SourceError> {
310        Ok(self.served.clone())
311    }
312
313    fn fetch_line_status(&self, _layer: &LayerRef) -> Result<Option<Vec<u8>>, SourceError> {
314        Ok(self.line_status.clone())
315    }
316
317    fn fetch_published_line_status(&self, _line: &str) -> Result<Option<Vec<u8>>, SourceError> {
318        Ok(self.published_line_status.clone())
319    }
320
321    fn fetch_attestations(
322        &self,
323        _layer: &LayerRef,
324    ) -> Result<Vec<crate::attestcarry::CarriedAttestation>, SourceError> {
325        Ok(self.attestations.clone())
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    // rivet: verifies REQ-STATUS-DIST-001
334    #[test]
335    fn a_source_carrying_a_baseline_line_status_yields_it() {
336        let envelope = b"an-opaque-dsse-envelope";
337        let source = MemorySource::new().with_line_status(envelope);
338        let got = source
339            .fetch_line_status(&LayerRef::Name("2026.07.0".parse().unwrap()))
340            .unwrap();
341        assert_eq!(
342            got.as_deref(),
343            Some(envelope.as_slice()),
344            "a source that carries a baseline line-status must hand it back for caching"
345        );
346    }
347
348    // rivet: verifies REQ-ATTEST-002
349    #[test]
350    fn a_source_carrying_attestations_hands_over_both_blobs_and_one_without_is_not_an_error() {
351        let source = MemorySource::new().with_attestation(b"a-statement-envelope", b"the-evidence");
352        let got = source
353            .fetch_attestations(&LayerRef::Name("2026.07.0".parse().unwrap()))
354            .unwrap();
355        assert_eq!(got.len(), 1);
356        assert_eq!(
357            got[0].bytes, b"the-evidence",
358            "the attested bytes must travel beside the statement — a claim with nothing to \
359             check it against is what crossing the air gap must never produce"
360        );
361        assert_eq!(
362            got[0].statement_digest,
363            crate::store::manifest_digest(b"a-statement-envelope"),
364            "the digest is derived from the bytes; a source never declares its own address"
365        );
366
367        // Absence is emptiness, not failure: most layers carry no third-party
368        // evidence, and requiring some would make availability depend on other
369        // people's publishing habits.
370        assert!(
371            MemorySource::new()
372                .fetch_attestations(&LayerRef::Name("2026.07.0".parse().unwrap()))
373                .unwrap()
374                .is_empty()
375        );
376    }
377
378    // rivet: verifies REQ-STATUS-DIST-001
379    #[test]
380    fn a_source_without_a_line_status_is_not_an_error() {
381        let source = MemorySource::new();
382        let got = source
383            .fetch_line_status(&LayerRef::Name("2026.07.0".parse().unwrap()))
384            .unwrap();
385        assert_eq!(
386            got, None,
387            "an absent line-status is Ok(None), never an error"
388        );
389    }
390}