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}