1use std::collections::BTreeMap;
13use std::path::{Path, PathBuf};
14
15use serde::{Deserialize, Serialize};
16
17use crate::install::VerifyError;
18use crate::layer::{LayerId, Line};
19use crate::verify::{dsse_sign_typed, dsse_verify_typed};
20
21pub const LINE_STATUS_PAYLOAD_TYPE: &str = "application/vnd.pulseengine.varve.line-status.v1+json";
24
25pub const LINE_STATUS_TAG_PREFIX: &str = "line-status-";
33
34pub fn status_tag(line: &str) -> String {
42 format!("{LINE_STATUS_TAG_PREFIX}{line}")
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct KnownProblem {
48 pub id: String,
49 pub title: String,
50 pub severity: String,
51 pub affected: Vec<String>,
53 #[serde(skip_serializing_if = "Option::is_none", default)]
54 pub workaround: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none", default)]
56 pub detection: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none", default)]
58 pub mitigation: Option<String>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(deny_unknown_fields)]
63pub struct LineStatus {
64 pub line: String,
66 pub counter: u64,
69 #[serde(rename = "issued-at")]
71 pub issued_at: String,
72 #[serde(
74 rename = "support-until",
75 skip_serializing_if = "Option::is_none",
76 default
77 )]
78 pub support_until: Option<String>,
79 #[serde(
90 rename = "min-counter",
91 skip_serializing_if = "Option::is_none",
92 default
93 )]
94 pub min_counter: Option<u64>,
95 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
97 pub yanked: BTreeMap<String, String>,
98 #[serde(
99 rename = "known-problems",
100 skip_serializing_if = "Vec::is_empty",
101 default
102 )]
103 pub known_problems: Vec<KnownProblem>,
104}
105
106#[derive(Debug, thiserror::Error)]
107pub enum LineStatusError {
108 #[error("line-status envelope rejected: {0}")]
114 Envelope(String),
115 #[error("cannot sign the line-status document: {0}")]
116 Sign(String),
117 #[error("line-status payload is not valid: {0}")]
118 Payload(String),
119 #[error("line-status covers line {got} but line {expected} was requested")]
120 LineMismatch { expected: String, got: String },
121 #[error(
122 "refusing stale line-status document for {line}: presented counter {presented}, cached {cached}"
123 )]
124 Stale {
125 line: String,
126 presented: u64,
127 cached: u64,
128 },
129 #[error(
134 "{what} names layer '{id}', which is not a layer of line {line} ({reason}) — `varve \
135 status` matches layer ids exactly, so this entry would never fire for any installed \
136 layer; fix the id and re-sign the document"
137 )]
138 DeadReference {
139 what: String,
140 id: String,
141 line: String,
142 reason: String,
143 },
144 #[error(
151 "{what} names layer '{id}', which line {line} does not contain — it exposes: {existing}. \
152 `varve status` matches layer ids EXACTLY, so this entry would fire for nobody: you \
153 would see success, every consumer would see nothing, and the advisory would silently \
154 not exist. Fix the id, or pass --force to pre-sign an advisory for a layer that is not \
155 deposited yet."
156 )]
157 UnknownLayer {
158 what: String,
159 id: String,
160 line: String,
161 existing: String,
162 },
163 #[error(
164 "{layout} is not an OCI image layout (it has no index.json) — point --layout at the \
165 directory `varve deposit --out` produced"
166 )]
167 NotALayout { layout: String },
168 #[error("io error at {path}")]
172 Io {
173 path: String,
174 #[source]
175 source: std::io::Error,
176 },
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
190pub enum KnownLayers {
191 Known {
193 source: String,
195 line: Option<String>,
198 layers: Vec<String>,
199 },
200 Unknown { why: String },
202}
203
204impl KnownLayers {
205 pub fn from_index(index: &crate::lineindex::LineIndex) -> Self {
208 KnownLayers::Known {
209 source: format!(
210 "the signed line-index for {} (counter {})",
211 index.line, index.counter
212 ),
213 line: Some(index.line.clone()),
214 layers: index.layers.iter().map(|e| e.layer.clone()).collect(),
215 }
216 }
217
218 pub fn unknown(why: impl Into<String>) -> Self {
219 KnownLayers::Unknown { why: why.into() }
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct RefCheck {
227 pub existence_checked: bool,
230 pub note: String,
232}
233
234pub fn known_layers_from_index(
241 envelope: &[u8],
242 root_public_key: &[u8],
243) -> Result<KnownLayers, crate::lineindex::IndexError> {
244 let doc = crate::lineindex::LineIndex::verify_and_parse(envelope, root_public_key)?;
245 Ok(KnownLayers::from_index(&doc))
246}
247
248pub fn known_layers_in_layout_dirs(dirs: &[std::path::PathBuf], line: &str) -> KnownLayers {
263 let mut layers: Vec<String> = Vec::new();
264 let mut scanned = 0usize;
265 let visit = |dir: &Path, layers: &mut Vec<String>| {
266 let mut candidates: Vec<Vec<u8>> = Vec::new();
272 if let Ok(index) = std::fs::read(dir.join("index.json"))
273 && let Ok(idx) = serde_json::from_slice::<serde_json::Value>(&index)
274 {
275 for m in idx
276 .get("manifests")
277 .and_then(|m| m.as_array())
278 .into_iter()
279 .flatten()
280 {
281 if let Some(d) = m.get("digest").and_then(|d| d.as_str())
282 && let Some((_, hex)) = d.split_once(':')
283 && let Ok(b) = std::fs::read(dir.join("blobs").join("sha256").join(hex))
284 {
285 candidates.push(b);
286 }
287 }
288 }
289 if let Ok(entries) = std::fs::read_dir(dir.join("manifests")) {
290 for e in entries.filter_map(|e| e.ok()) {
291 if let Ok(b) = std::fs::read(e.path()) {
292 candidates.push(b);
293 }
294 }
295 }
296 if candidates.is_empty() {
297 return false;
298 }
299 for bytes in candidates {
300 let payload = std::str::from_utf8(&bytes)
305 .ok()
306 .and_then(|t| wsc::dsse::DsseEnvelope::from_json(t).ok())
307 .and_then(|env| env.payload_bytes().ok())
308 .unwrap_or_else(|| bytes.clone());
309 if let Ok(m) = crate::manifest::LayerManifest::parse(&payload) {
310 let id = m.layer.to_string();
311 if id.starts_with(&format!("{line}.")) && !layers.contains(&id) {
312 layers.push(id);
313 }
314 }
315 }
316 true
317 };
318 for dir in dirs {
319 if visit(dir, &mut layers) {
320 scanned += 1;
321 continue;
322 }
323 if let Ok(children) = std::fs::read_dir(dir) {
325 for c in children.filter_map(|e| e.ok()) {
326 if c.path().is_dir() && visit(&c.path(), &mut layers) {
327 scanned += 1;
328 }
329 }
330 }
331 }
332 if scanned == 0 {
333 return KnownLayers::unknown(format!(
334 "no oci-layout was found under {} — pass the directory `varve deposit --out` \
335 wrote, or one holding several of them",
336 dirs.iter()
337 .map(|d| d.display().to_string())
338 .collect::<Vec<_>>()
339 .join(", ")
340 ));
341 }
342 layers.sort();
343 KnownLayers::Known {
344 source: format!("{scanned} local layout(s) the producer holds"),
345 line: Some(line.to_string()),
346 layers,
347 }
348}
349
350pub fn known_layers_in_layout(layout: &Path, line: &str) -> KnownLayers {
357 match crate::lineindex::read_from_layout(layout, line) {
358 Ok(Some(envelope)) => match crate::lineindex::parse_unverified(&envelope) {
359 Ok(doc) if doc.line == line => KnownLayers::from_index(&doc),
360 Ok(doc) => KnownLayers::unknown(format!(
361 "the line-index this layout carries is for line {}, not {line}",
362 doc.line
363 )),
364 Err(e) => KnownLayers::unknown(format!(
365 "the line-index this layout carries could not be read ({e})"
366 )),
367 },
368 Ok(None) => KnownLayers::unknown(format!(
369 "this layout carries no signed line-index for {line}, and a layout holds ONE layer \
370 — it is not a listing of the line. Attach the index first (`varve attach-index`), \
371 or sign against it (`varve sign-status --index <envelope>`)"
372 )),
373 Err(e) => KnownLayers::unknown(format!("the layout's index.json could not be read ({e})")),
374 }
375}
376
377impl LineStatus {
378 pub fn verify_and_parse(
380 envelope: &[u8],
381 root_public_key: &[u8],
382 ) -> Result<Self, LineStatusError> {
383 if let Ok(text) = std::str::from_utf8(envelope)
388 && wsc::dsse::DsseEnvelope::from_json(text).is_err()
389 {
390 return Err(not_an_envelope(text));
391 }
392 let payload = dsse_verify_typed(envelope, LINE_STATUS_PAYLOAD_TYPE, root_public_key)
393 .map_err(|VerifyError(msg)| {
394 let hint = if msg.contains("does not verify") {
398 " (is the document signed by THIS realm's root? `varve pubkey <key>` \
399 prints the public half a signature verifies against)"
400 } else {
401 ""
402 };
403 LineStatusError::Envelope(format!("{msg}{hint}"))
404 })?;
405 serde_json::from_slice(&payload).map_err(|e| LineStatusError::Payload(e.to_string()))
406 }
407
408 pub fn sign(&self, secret_key: &[u8], key_id: &str) -> Result<String, LineStatusError> {
413 self.check_layer_refs()?;
414 let payload = serde_json::to_vec_pretty(self).expect("status serializes");
415 dsse_sign_typed(&payload, LINE_STATUS_PAYLOAD_TYPE, secret_key, key_id)
416 .map_err(|VerifyError(msg)| LineStatusError::Sign(msg))
417 }
418
419 pub fn sign_against(
424 &self,
425 known: &KnownLayers,
426 force: bool,
427 secret_key: &[u8],
428 key_id: &str,
429 ) -> Result<(String, RefCheck), LineStatusError> {
430 let check = self.check_layer_refs_against(known, force)?;
431 let payload = serde_json::to_vec_pretty(self).expect("status serializes");
432 let envelope = dsse_sign_typed(&payload, LINE_STATUS_PAYLOAD_TYPE, secret_key, key_id)
433 .map_err(|VerifyError(msg)| LineStatusError::Sign(msg))?;
434 Ok((envelope, check))
435 }
436
437 pub fn check_layer_refs_against(
450 &self,
451 known: &KnownLayers,
452 force: bool,
453 ) -> Result<RefCheck, LineStatusError> {
454 self.check_layer_refs()?;
455 let referenced = self.yanked.len()
464 + self
465 .known_problems
466 .iter()
467 .map(|p| p.affected.len())
468 .sum::<usize>();
469 let (source, layers) = match known {
470 KnownLayers::Unknown { .. } if referenced == 0 => {
471 return Ok(RefCheck {
472 existence_checked: true,
473 note: "this document names no layer — nothing to check against the line"
474 .to_string(),
475 });
476 }
477 KnownLayers::Unknown { why } => {
478 return Ok(RefCheck {
479 existence_checked: false,
480 note: format!(
481 "advisory references were checked for SHAPE only — NOT against the \
482 layers line {} actually has: {why}. An id naming a layer that does not \
483 exist still signs cleanly here and fires for nobody.",
484 self.line
485 ),
486 });
487 }
488 KnownLayers::Known {
489 source,
490 line,
491 layers,
492 } => {
493 if let Some(listing_line) = line
498 && listing_line != &self.line
499 {
500 return Err(LineStatusError::LineMismatch {
501 expected: self.line.clone(),
502 got: listing_line.clone(),
503 });
504 }
505 (source, layers)
506 }
507 };
508 if force {
509 return Ok(RefCheck {
510 existence_checked: false,
511 note: format!(
512 "--force: advisory references were NOT checked against the layers line {} \
513 has. An entry naming a layer that has not been deposited yet fires only \
514 once it is.",
515 self.line
516 ),
517 });
518 }
519 let existing = if layers.is_empty() {
520 "no layers at all — this line has none yet".to_string()
521 } else {
522 layers.join(", ")
523 };
524 let mut refs = 0usize;
525 let mut check = |what: String, id: &str| -> Result<(), LineStatusError> {
526 refs += 1;
527 if layers.iter().any(|l| l == id) {
528 return Ok(());
529 }
530 Err(LineStatusError::UnknownLayer {
531 what,
532 id: id.to_string(),
533 line: self.line.clone(),
534 existing: existing.clone(),
535 })
536 };
537 for id in self.yanked.keys() {
538 check("the yank entry".to_string(), id)?;
539 }
540 for kp in &self.known_problems {
541 for id in &kp.affected {
542 check(format!("known problem '{}'", kp.id), id)?;
543 }
544 }
545 Ok(RefCheck {
546 existence_checked: true,
547 note: format!(
548 "{refs} advisory reference{} checked against the {} layer{} {source} lists for \
549 line {}",
550 if refs == 1 { "" } else { "s" },
551 layers.len(),
552 if layers.len() == 1 { "" } else { "s" },
553 self.line
554 ),
555 })
556 }
557
558 pub fn check_layer_refs(&self) -> Result<(), LineStatusError> {
565 let line: Line = self.line.parse().map_err(|e| {
566 LineStatusError::Payload(format!("'{}' is not a YYYY.MM line: {e}", self.line))
567 })?;
568 let check = |what: String, id: &str| -> Result<(), LineStatusError> {
569 let dead = |reason: String| LineStatusError::DeadReference {
570 what: what.clone(),
571 id: id.to_string(),
572 line: self.line.clone(),
573 reason,
574 };
575 match id.parse::<LayerId>() {
576 Ok(layer) if layer.line() == &line => Ok(()),
577 Ok(layer) => Err(dead(format!("it belongs to line {}", layer.line()))),
578 Err(e) => Err(dead(e.to_string())),
579 }
580 };
581 for id in self.yanked.keys() {
582 check("the yank entry".to_string(), id)?;
583 }
584 for kp in &self.known_problems {
585 for id in &kp.affected {
586 check(format!("known problem '{}'", kp.id), id)?;
587 }
588 }
589 Ok(())
590 }
591
592 pub fn report_for(&self, layer: &LayerId) -> LayerStatusReport {
594 let name = layer.to_string();
595 let problems: Vec<&KnownProblem> = self
596 .known_problems
597 .iter()
598 .filter(|kp| kp.affected.iter().any(|a| a == &name))
599 .collect();
600 LayerStatusReport {
601 yanked_reason: self.yanked.get(&name).cloned(),
602 support_until: self.support_until.clone(),
603 problems_total: problems.len(),
604 problems_with_workaround: problems.iter().filter(|kp| kp.workaround.is_some()).count(),
605 }
606 }
607}
608
609#[derive(Debug, Clone, PartialEq, Eq)]
610pub struct LayerStatusReport {
611 pub yanked_reason: Option<String>,
612 pub support_until: Option<String>,
613 pub problems_total: usize,
614 pub problems_with_workaround: usize,
615}
616
617#[derive(Debug)]
619pub struct StatusCache {
620 dir: PathBuf,
621}
622
623impl StatusCache {
624 pub fn at_root(root: &Path) -> Self {
625 StatusCache {
626 dir: root.join("state").join("line-status"),
627 }
628 }
629
630 pub fn update(
632 &self,
633 line: &Line,
634 envelope: &[u8],
635 parsed: &LineStatus,
636 ) -> Result<(), LineStatusError> {
637 if let Some(cached) = self.load_parsed(line)?
638 && parsed.counter < cached.counter
639 {
640 return Err(LineStatusError::Stale {
641 line: line.to_string(),
642 presented: parsed.counter,
643 cached: cached.counter,
644 });
645 }
646 let io = |path: &Path, source: std::io::Error| LineStatusError::Io {
647 path: path.display().to_string(),
648 source,
649 };
650 std::fs::create_dir_all(&self.dir).map_err(|e| io(&self.dir, e))?;
651 let path = self.envelope_path(line);
652 std::fs::write(&path, envelope).map_err(|e| io(&path, e))?;
653 Ok(())
654 }
655
656 pub fn envelope_bytes(&self, line: &Line) -> Result<Option<Vec<u8>>, LineStatusError> {
663 let path = self.envelope_path(line);
664 match std::fs::read(&path) {
665 Ok(bytes) => Ok(Some(bytes)),
666 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
667 Err(source) => Err(LineStatusError::Io {
668 path: path.display().to_string(),
669 source,
670 }),
671 }
672 }
673
674 pub fn load(
676 &self,
677 line: &Line,
678 root_public_key: &[u8],
679 ) -> Result<Option<LineStatus>, LineStatusError> {
680 let path = self.envelope_path(line);
681 match std::fs::read(&path) {
682 Ok(bytes) => Ok(Some(LineStatus::verify_and_parse(&bytes, root_public_key)?)),
683 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
684 Err(source) => Err(LineStatusError::Io {
685 path: path.display().to_string(),
686 source,
687 }),
688 }
689 }
690
691 fn load_parsed(&self, line: &Line) -> Result<Option<LineStatus>, LineStatusError> {
692 let path = self.envelope_path(line);
693 match std::fs::read(&path) {
694 Ok(bytes) => {
695 let text = std::str::from_utf8(&bytes)
698 .map_err(|_| LineStatusError::Payload("cache is not UTF-8".into()))?;
699 let env = wsc::dsse::DsseEnvelope::from_json(text)
700 .map_err(|e| LineStatusError::Payload(e.to_string()))?;
701 let payload = env
702 .payload_bytes()
703 .map_err(|e| LineStatusError::Payload(e.to_string()))?;
704 Ok(Some(
705 serde_json::from_slice(&payload)
706 .map_err(|e| LineStatusError::Payload(e.to_string()))?,
707 ))
708 }
709 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
710 Err(source) => Err(LineStatusError::Io {
711 path: path.display().to_string(),
712 source,
713 }),
714 }
715 }
716
717 fn envelope_path(&self, line: &Line) -> PathBuf {
718 self.dir.join(format!("{line}.dsse.json"))
719 }
720}
721
722pub const LINE_STATUS_ARTIFACT_TYPE: &str = LINE_STATUS_PAYLOAD_TYPE;
724pub const ANN_LINE: &str = "eu.pulseengine.varve.status-line";
726
727pub fn attach_to_layout(
731 layout: &Path,
732 line: &Line,
733 envelope: &[u8],
734) -> Result<(), LineStatusError> {
735 let io = |path: &Path, source: std::io::Error| LineStatusError::Io {
736 path: path.display().to_string(),
737 source,
738 };
739 let digest = crate::store::manifest_digest(envelope);
740 let hex = digest.strip_prefix("sha256:").expect("digest shape");
741 let blob_dir = layout.join("blobs").join("sha256");
742 std::fs::create_dir_all(&blob_dir).map_err(|e| io(&blob_dir, e))?;
743 let blob_path = blob_dir.join(hex);
744 std::fs::write(&blob_path, envelope).map_err(|e| io(&blob_path, e))?;
745
746 let index_path = layout.join("index.json");
747 let mut index: serde_json::Value =
748 serde_json::from_slice(&std::fs::read(&index_path).map_err(|e| io(&index_path, e))?)
749 .map_err(|e| LineStatusError::Payload(format!("index.json: {e}")))?;
750 let entries = index["manifests"]
751 .as_array_mut()
752 .ok_or_else(|| LineStatusError::Payload("index.json has no manifests array".into()))?;
753 let line_name = line.to_string();
754 entries.retain(|e| {
755 !(e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE
756 && e["annotations"][ANN_LINE] == *line_name)
757 });
758 entries.push(serde_json::json!({
759 "mediaType": "application/json",
760 "artifactType": LINE_STATUS_ARTIFACT_TYPE,
761 "digest": digest,
762 "size": envelope.len(),
763 "annotations": { ANN_LINE: line_name }
764 }));
765 std::fs::write(
766 &index_path,
767 serde_json::to_vec_pretty(&index).expect("index serializes"),
768 )
769 .map_err(|e| io(&index_path, e))?;
770 Ok(())
771}
772
773pub fn cache_baseline_from_source(
782 source: &dyn crate::source::LayerSource,
783 layer: &crate::source::LayerRef,
784 line: &Line,
785 root_pk: &[u8],
786 store_root: &Path,
787) -> Result<Option<u64>, LineStatusError> {
788 let baseline = source
789 .fetch_line_status(layer)
790 .map_err(|e| LineStatusError::Payload(format!("fetching baseline line-status: {e}")))?;
791
792 let published = source
797 .fetch_published_line_status(&line.to_string())
798 .unwrap_or(None);
799
800 let mut best: Option<(u64, Vec<u8>, LineStatus)> = None;
807 let mut first_error: Option<LineStatusError> = None;
808 for envelope in [baseline, published].into_iter().flatten() {
809 let doc = match LineStatus::verify_and_parse(&envelope, root_pk) {
810 Ok(doc) => doc,
811 Err(e) => {
812 first_error.get_or_insert(e);
813 continue;
814 }
815 };
816 if doc.line != line.to_string() {
821 first_error.get_or_insert(LineStatusError::LineMismatch {
822 expected: line.to_string(),
823 got: doc.line.clone(),
824 });
825 continue;
826 }
827 if best.as_ref().is_none_or(|(c, _, _)| doc.counter > *c) {
828 best = Some((doc.counter, envelope, doc));
829 }
830 }
831
832 match best {
833 Some((counter, envelope, doc)) => {
834 StatusCache::at_root(store_root).update(line, &envelope, &doc)?;
835 Ok(Some(counter))
836 }
837 None => match first_error {
840 Some(e) => Err(e),
841 None => Ok(None),
842 },
843 }
844}
845
846pub fn attach_envelope_to_layout(
852 layout: &Path,
853 envelope: &[u8],
854) -> Result<(Line, u64), LineStatusError> {
855 let (line, counter, _) = attach_envelope_to_layout_checked(layout, envelope, false)?;
856 Ok((line, counter))
857}
858
859pub fn attach_envelope_to_layout_checked(
862 layout: &Path,
863 envelope: &[u8],
864 force: bool,
865) -> Result<(Line, u64, RefCheck), LineStatusError> {
866 if !layout.join("index.json").is_file() {
870 return Err(LineStatusError::NotALayout {
871 layout: layout.display().to_string(),
872 });
873 }
874 let text = std::str::from_utf8(envelope)
875 .map_err(|e| LineStatusError::Payload(format!("envelope is not utf-8: {e}")))?;
876 let env = wsc::dsse::DsseEnvelope::from_json(text).map_err(|_| not_an_envelope(text))?;
877 let payload = env
878 .payload_bytes()
879 .map_err(|e| LineStatusError::Payload(format!("envelope payload: {e}")))?;
880 let doc: LineStatus = serde_json::from_slice(&payload)
881 .map_err(|e| LineStatusError::Payload(format!("status document: {e}")))?;
882 let line: Line = doc
883 .line
884 .parse()
885 .map_err(|e| LineStatusError::Payload(format!("status line '{}': {e}", doc.line)))?;
886 let check = doc.check_layer_refs_against(&known_layers_in_layout(layout, &doc.line), force)?;
893 if let Some(existing) = read_any_from_layout(layout)?
900 && let Ok(prev) = parse_unverified(&existing)
901 && prev.line == doc.line
902 && doc.counter < prev.counter
903 {
904 return Err(LineStatusError::Stale {
905 line: doc.line.clone(),
906 presented: doc.counter,
907 cached: prev.counter,
908 });
909 }
910 if let Some(layout_line) = layout_line(layout)
914 && layout_line != line.to_string()
915 {
916 return Err(LineStatusError::LineMismatch {
917 expected: layout_line,
918 got: line.to_string(),
919 });
920 }
921 attach_to_layout(layout, &line, envelope)?;
922 Ok((line, doc.counter, check))
923}
924
925fn not_an_envelope(text: &str) -> LineStatusError {
931 if serde_json::from_str::<LineStatus>(text).is_ok() {
932 LineStatusError::Payload(
933 "this is the UNSIGNED status document, not a signed envelope — sign it first \
934 (`varve sign-status --file <doc> --key <key> --out <envelope>`) and pass the \
935 envelope"
936 .into(),
937 )
938 } else {
939 LineStatusError::Payload(
940 "not a DSSE envelope — expected the signed output of `varve sign-status`".into(),
941 )
942 }
943}
944
945fn parse_unverified(envelope: &[u8]) -> Result<LineStatus, LineStatusError> {
949 let text = std::str::from_utf8(envelope)
950 .map_err(|e| LineStatusError::Payload(format!("envelope is not utf-8: {e}")))?;
951 let env = wsc::dsse::DsseEnvelope::from_json(text).map_err(|_| not_an_envelope(text))?;
952 let payload = env
953 .payload_bytes()
954 .map_err(|e| LineStatusError::Payload(format!("envelope payload: {e}")))?;
955 serde_json::from_slice(&payload)
956 .map_err(|e| LineStatusError::Payload(format!("status document: {e}")))
957}
958
959pub(crate) fn layout_line(layout: &Path) -> Option<String> {
963 let index: serde_json::Value =
964 serde_json::from_slice(&std::fs::read(layout.join("index.json")).ok()?).ok()?;
965 for m in index["manifests"].as_array()? {
966 let digest = m["digest"].as_str()?.replace(':', "-");
967 let blob = layout
968 .join("blobs")
969 .join("sha256")
970 .join(digest.trim_start_matches("sha256-"));
971 let Ok(bytes) = std::fs::read(&blob) else {
972 continue;
973 };
974 let Ok(text) = std::str::from_utf8(&bytes) else {
977 continue;
978 };
979 let Ok(env) = wsc::dsse::DsseEnvelope::from_json(text) else {
980 continue;
981 };
982 let Ok(payload) = env.payload_bytes() else {
983 continue;
984 };
985 let Ok(doc) = serde_json::from_slice::<serde_json::Value>(&payload) else {
986 continue;
987 };
988 if let Some(line) = doc["annotations"]["eu.pulseengine.varve.line"].as_str() {
989 return Some(line.to_string());
990 }
991 }
992 None
993}
994
995pub fn read_any_from_layout(layout: &Path) -> Result<Option<Vec<u8>>, LineStatusError> {
1000 let index_path = layout.join("index.json");
1001 let bytes = match std::fs::read(&index_path) {
1002 Ok(bytes) => bytes,
1003 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1004 Err(source) => {
1005 return Err(LineStatusError::Io {
1006 path: index_path.display().to_string(),
1007 source,
1008 });
1009 }
1010 };
1011 let index: serde_json::Value = serde_json::from_slice(&bytes)
1012 .map_err(|e| LineStatusError::Payload(format!("index.json: {e}")))?;
1013 let Some(entry) = index["manifests"].as_array().and_then(|entries| {
1014 entries
1015 .iter()
1016 .find(|e| e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE)
1017 }) else {
1018 return Ok(None);
1019 };
1020 let digest = entry["digest"]
1021 .as_str()
1022 .ok_or_else(|| LineStatusError::Payload("status entry has no digest".into()))?;
1023 let hex = digest.strip_prefix("sha256:").unwrap_or(digest);
1024 let blob_path = layout.join("blobs").join("sha256").join(hex);
1025 std::fs::read(&blob_path)
1026 .map(Some)
1027 .map_err(|source| LineStatusError::Io {
1028 path: blob_path.display().to_string(),
1029 source,
1030 })
1031}
1032
1033pub fn read_from_layout(layout: &Path, line: &Line) -> Result<Option<Vec<u8>>, LineStatusError> {
1035 let index_path = layout.join("index.json");
1036 let bytes = match std::fs::read(&index_path) {
1037 Ok(bytes) => bytes,
1038 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1039 Err(source) => {
1040 return Err(LineStatusError::Io {
1041 path: index_path.display().to_string(),
1042 source,
1043 });
1044 }
1045 };
1046 let index: serde_json::Value = serde_json::from_slice(&bytes)
1047 .map_err(|e| LineStatusError::Payload(format!("index.json: {e}")))?;
1048 let line_name = line.to_string();
1049 let Some(entry) = index["manifests"].as_array().and_then(|entries| {
1050 entries.iter().find(|e| {
1051 e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE
1052 && e["annotations"][ANN_LINE] == *line_name
1053 })
1054 }) else {
1055 return Ok(None);
1056 };
1057 let digest = entry["digest"]
1058 .as_str()
1059 .ok_or_else(|| LineStatusError::Payload("status entry has no digest".into()))?;
1060 let hex = digest.strip_prefix("sha256:").unwrap_or(digest);
1061 let blob_path = layout.join("blobs").join("sha256").join(hex);
1062 std::fs::read(&blob_path)
1063 .map(Some)
1064 .map_err(|source| LineStatusError::Io {
1065 path: blob_path.display().to_string(),
1066 source,
1067 })
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072 use super::*;
1073 use crate::verify::generate_root_keypair;
1074
1075 fn status(counter: u64) -> LineStatus {
1076 LineStatus {
1077 min_counter: None,
1078 line: "2026.07".into(),
1079 counter,
1080 issued_at: "2026-08-07T00:00:00Z".into(),
1081 support_until: Some("2028-07-31".into()),
1082 yanked: BTreeMap::from([(
1083 "2026.07.0".to_string(),
1084 "CVE-2026-0001 in synth".to_string(),
1085 )]),
1086 known_problems: vec![
1087 KnownProblem {
1088 id: "KP-1".into(),
1089 title: "synth mla fusion regresses flat_flight".into(),
1090 severity: "medium".into(),
1091 affected: vec!["2026.07.0".into()],
1092 workaround: Some("disable mla fusion".into()),
1093 detection: None,
1094 mitigation: None,
1095 },
1096 KnownProblem {
1097 id: "KP-2".into(),
1098 title: "witness truth-table gap on nested variants".into(),
1099 severity: "high".into(),
1100 affected: vec!["2026.07.0".into(), "2026.07.1".into()],
1101 workaround: None,
1102 detection: Some("witness gap rows non-empty".into()),
1103 mitigation: None,
1104 },
1105 ],
1106 }
1107 }
1108
1109 #[test]
1111 fn a_signed_status_document_round_trips() {
1112 let (sk, pk) = generate_root_keypair();
1113 let envelope = status(1).sign(&sk, "varve-root-1").unwrap();
1114 let parsed = LineStatus::verify_and_parse(envelope.as_bytes(), &pk).unwrap();
1115 assert_eq!(parsed, status(1));
1116 }
1117
1118 #[test]
1120 fn a_layer_manifest_envelope_cannot_pose_as_a_status_document() {
1121 let (sk, pk) = generate_root_keypair();
1122 let manifest = crate::manifest::fixtures::manifest(
1124 "2026.07.0",
1125 "qualified",
1126 1,
1127 "2026-08-07T00:00:00Z",
1128 );
1129 let envelope = crate::verify::sign_layer_manifest(&manifest, &sk, "varve-root-1").unwrap();
1130 let err = LineStatus::verify_and_parse(envelope.as_bytes(), &pk).unwrap_err();
1131 assert!(err.to_string().contains("payload type"), "got: {err}");
1132 }
1133
1134 #[test]
1136 fn the_report_names_yank_support_window_and_problem_counts() {
1137 let doc = status(1);
1138 let report = doc.report_for(&"2026.07.0".parse().unwrap());
1139 assert_eq!(
1140 report.yanked_reason.as_deref(),
1141 Some("CVE-2026-0001 in synth")
1142 );
1143 assert_eq!(report.support_until.as_deref(), Some("2028-07-31"));
1144 assert_eq!(report.problems_total, 2);
1145 assert_eq!(report.problems_with_workaround, 1);
1146 let clean = doc.report_for(&"2026.07.2".parse().unwrap());
1147 assert_eq!(clean.yanked_reason, None);
1148 assert_eq!(clean.problems_total, 0);
1149 }
1150
1151 #[test]
1153 fn attaching_status_to_a_layout_leaves_every_layer_blob_untouched() {
1154 use crate::deposit::{DepositSpec, DepositTool, deposit};
1155 let (sk, pk) = generate_root_keypair();
1156 let tmp = tempfile::tempdir().unwrap();
1157 let dest = tmp.path().join("layout");
1158 let spec = DepositSpec {
1159 includes: Vec::new(),
1160 layer: "2026.07.0".parse().unwrap(),
1161 channel: "qualified".into(),
1162 counter: 1,
1163 issued_at: "2026-08-07T00:00:00Z".into(),
1164 tools: vec![DepositTool {
1165 name: "synth".into(),
1166 version: "1".into(),
1167 platform: None,
1168 bytes: b"t".to_vec(),
1169 source: None,
1170 runner: None,
1171 kind: None,
1172 sdk_prefix: None,
1173 }],
1174 };
1175 let outcome = deposit(&spec, &sk, "k", &dest).unwrap();
1176
1177 let blob_dir = dest.join("blobs/sha256");
1179 let before: std::collections::BTreeMap<String, Vec<u8>> = std::fs::read_dir(&blob_dir)
1180 .unwrap()
1181 .map(|e| {
1182 let p = e.unwrap().path();
1183 (
1184 p.file_name().unwrap().to_string_lossy().into_owned(),
1185 std::fs::read(&p).unwrap(),
1186 )
1187 })
1188 .collect();
1189
1190 let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1191 let envelope = status(1).sign(&sk, "k").unwrap();
1192 attach_to_layout(&dest, &line, envelope.as_bytes()).unwrap();
1193
1194 for (name, bytes) in &before {
1197 assert_eq!(&std::fs::read(blob_dir.join(name)).unwrap(), bytes);
1198 }
1199 let carried = read_from_layout(&dest, &line).unwrap().unwrap();
1200 let parsed = LineStatus::verify_and_parse(&carried, &pk).unwrap();
1201 assert_eq!(parsed.counter, 1);
1202 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
1203 assert!(
1204 blob_dir.join(hex).is_file(),
1205 "layer manifest blob still present"
1206 );
1207
1208 let envelope2 = status(2).sign(&sk, "k").unwrap();
1210 attach_to_layout(&dest, &line, envelope2.as_bytes()).unwrap();
1211 let index: serde_json::Value =
1212 serde_json::from_slice(&std::fs::read(dest.join("index.json")).unwrap()).unwrap();
1213 let count = index["manifests"]
1214 .as_array()
1215 .unwrap()
1216 .iter()
1217 .filter(|e| e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE)
1218 .count();
1219 assert_eq!(count, 1);
1220 }
1221
1222 #[test]
1224 fn the_cache_refuses_a_counter_regression() {
1225 let (sk, pk) = generate_root_keypair();
1226 let tmp = tempfile::tempdir().unwrap();
1227 let cache = StatusCache::at_root(tmp.path());
1228 let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1229
1230 let newer = status(2);
1231 let env2 = newer.sign(&sk, "k").unwrap();
1232 cache.update(&line, env2.as_bytes(), &newer).unwrap();
1233
1234 let older = status(1);
1235 let env1 = older.sign(&sk, "k").unwrap();
1236 let err = cache.update(&line, env1.as_bytes(), &older).unwrap_err();
1237 assert!(matches!(
1238 err,
1239 LineStatusError::Stale {
1240 presented: 1,
1241 cached: 2,
1242 ..
1243 }
1244 ));
1245
1246 let loaded = cache.load(&line, &pk).unwrap().unwrap();
1248 assert_eq!(loaded.counter, 2);
1249 }
1250
1251 #[test]
1259 fn a_correction_published_under_its_own_tag_overtakes_an_older_baseline() {
1260 use crate::source::{LayerRef, MemorySource};
1261 let (sk, pk) = generate_root_keypair();
1262 let tmp = tempfile::tempdir().unwrap();
1263 let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1264
1265 let baseline = status(1).sign(&sk, "k").unwrap();
1266 let correction = status(7).sign(&sk, "k").unwrap();
1267 let source = MemorySource::new()
1268 .with_line_status(baseline.as_bytes())
1269 .with_published_line_status(correction.as_bytes());
1270
1271 let cached = cache_baseline_from_source(
1272 &source,
1273 &LayerRef::Name("2026.07.0".parse().unwrap()),
1274 &line,
1275 &pk,
1276 tmp.path(),
1277 )
1278 .unwrap();
1279 assert_eq!(cached, Some(7), "the newer of the two is what is kept");
1280 assert_eq!(
1281 StatusCache::at_root(tmp.path())
1282 .load(&line, &pk)
1283 .unwrap()
1284 .unwrap()
1285 .counter,
1286 7
1287 );
1288 }
1289
1290 #[test]
1296 fn a_stale_tag_document_does_not_walk_a_consumer_back_past_its_baseline() {
1297 use crate::source::{LayerRef, MemorySource};
1298 let (sk, pk) = generate_root_keypair();
1299 let tmp = tempfile::tempdir().unwrap();
1300 let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1301
1302 let source = MemorySource::new()
1303 .with_line_status(status(9).sign(&sk, "k").unwrap().as_bytes())
1304 .with_published_line_status(status(2).sign(&sk, "k").unwrap().as_bytes());
1305
1306 let cached = cache_baseline_from_source(
1307 &source,
1308 &LayerRef::Name("2026.07.0".parse().unwrap()),
1309 &line,
1310 &pk,
1311 tmp.path(),
1312 )
1313 .unwrap();
1314 assert_eq!(cached, Some(9), "the baseline is newer and must win");
1315 }
1316
1317 #[test]
1330 fn an_equal_counter_tag_document_cannot_displace_the_baseline() {
1331 use crate::source::{LayerRef, MemorySource};
1332 let (sk, pk) = generate_root_keypair();
1333 let tmp = tempfile::tempdir().unwrap();
1334 let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1335
1336 let yanking = status(5);
1343 assert!(
1344 yanking.yanked.contains_key("2026.07.0"),
1345 "fixture precondition: the baseline must actually yank"
1346 );
1347 let mut quiet = status(5);
1348 quiet.yanked.clear();
1349
1350 let source = MemorySource::new()
1351 .with_line_status(yanking.sign(&sk, "k").unwrap().as_bytes())
1352 .with_published_line_status(quiet.sign(&sk, "k").unwrap().as_bytes());
1353
1354 cache_baseline_from_source(
1355 &source,
1356 &LayerRef::Name("2026.07.0".parse().unwrap()),
1357 &line,
1358 &pk,
1359 tmp.path(),
1360 )
1361 .unwrap();
1362
1363 let kept = StatusCache::at_root(tmp.path())
1364 .load(&line, &pk)
1365 .unwrap()
1366 .unwrap();
1367 assert!(
1368 kept.yanked.contains_key("2026.07.0"),
1369 "an equal-counter document served under the tag must NOT suppress the \
1370 baseline's yank"
1371 );
1372 }
1373
1374 #[test]
1380 fn an_unverifiable_tag_document_does_not_deny_the_good_baseline() {
1381 use crate::source::{LayerRef, MemorySource};
1382 let (sk, pk) = generate_root_keypair();
1383 let (other_sk, _) = generate_root_keypair();
1384 let tmp = tempfile::tempdir().unwrap();
1385 let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1386
1387 let forged = status(999).sign(&other_sk, "k").unwrap();
1391 let source = MemorySource::new()
1392 .with_line_status(status(3).sign(&sk, "k").unwrap().as_bytes())
1393 .with_published_line_status(forged.as_bytes());
1394
1395 let cached = cache_baseline_from_source(
1396 &source,
1397 &LayerRef::Name("2026.07.0".parse().unwrap()),
1398 &line,
1399 &pk,
1400 tmp.path(),
1401 )
1402 .unwrap();
1403 assert_eq!(
1404 cached,
1405 Some(3),
1406 "the forged document is discarded and the baseline still lands"
1407 );
1408 }
1409
1410 #[test]
1414 fn a_tag_document_for_the_wrong_line_is_not_cached_under_this_one() {
1415 use crate::source::{LayerRef, MemorySource};
1416 let (sk, pk) = generate_root_keypair();
1417 let tmp = tempfile::tempdir().unwrap();
1418 let requested: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1419 let wrong = LineStatus {
1420 min_counter: None,
1421 line: "2026.08".into(),
1422 counter: 500,
1423 issued_at: "2026-08-07T00:00:00Z".into(),
1424 support_until: None,
1425 yanked: BTreeMap::new(),
1426 known_problems: Vec::new(),
1427 };
1428 let source = MemorySource::new()
1429 .with_line_status(status(4).sign(&sk, "k").unwrap().as_bytes())
1430 .with_published_line_status(wrong.sign(&sk, "k").unwrap().as_bytes());
1431
1432 let cached = cache_baseline_from_source(
1433 &source,
1434 &LayerRef::Name("2026.07.0".parse().unwrap()),
1435 &requested,
1436 &pk,
1437 tmp.path(),
1438 )
1439 .unwrap();
1440 assert_eq!(cached, Some(4), "the wrong-line document is not cached");
1441 }
1442
1443 #[test]
1449 fn the_status_tag_cannot_be_parsed_as_a_layer_id() {
1450 let tag = status_tag("2026.07");
1451 assert_eq!(tag, "line-status-2026.07");
1452 assert!(
1453 tag.parse::<LayerId>().is_err(),
1454 "a status tag that parses as a layer id would be reported as a served layer"
1455 );
1456 assert_ne!(
1457 tag,
1458 crate::lineindex::index_tag("2026.07"),
1459 "status and index must not collide on one tag"
1460 );
1461 }
1462
1463 #[test]
1465 fn a_source_baseline_is_verified_and_cached_so_status_works_offline() {
1466 use crate::source::{LayerRef, MemorySource};
1467 let (sk, pk) = generate_root_keypair();
1468 let tmp = tempfile::tempdir().unwrap();
1469 let store_root = tmp.path();
1470 let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1471 let doc = status(5);
1472 let envelope = doc.sign(&sk, "k").unwrap();
1473 let source = MemorySource::new().with_line_status(envelope.as_bytes());
1474 let layer = LayerRef::Name("2026.07.0".parse().unwrap());
1475
1476 let cached = cache_baseline_from_source(&source, &layer, &line, &pk, store_root).unwrap();
1477 assert_eq!(
1478 cached,
1479 Some(5),
1480 "a carried baseline is cached at its counter"
1481 );
1482
1483 let loaded = StatusCache::at_root(store_root)
1485 .load(&line, &pk)
1486 .unwrap()
1487 .unwrap();
1488 assert_eq!(loaded.counter, 5);
1489 }
1490
1491 #[test]
1493 fn a_baseline_for_the_wrong_line_is_refused_not_miscached() {
1494 use crate::source::{LayerRef, MemorySource};
1499 let (sk, pk) = generate_root_keypair();
1500 let tmp = tempfile::tempdir().unwrap();
1501 let requested: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1502 let doc = LineStatus {
1505 min_counter: None,
1506 line: "2026.08".into(),
1507 counter: 5,
1508 issued_at: "2026-08-07T00:00:00Z".into(),
1509 support_until: None,
1510 yanked: BTreeMap::new(),
1511 known_problems: Vec::new(),
1512 };
1513 let envelope = doc.sign(&sk, "k").unwrap();
1514 let source = MemorySource::new().with_line_status(envelope.as_bytes());
1515 let err = cache_baseline_from_source(
1516 &source,
1517 &LayerRef::Name("2026.07.0".parse().unwrap()),
1518 &requested,
1519 &pk,
1520 tmp.path(),
1521 )
1522 .unwrap_err();
1523 assert!(
1524 matches!(err, LineStatusError::LineMismatch { .. }),
1525 "a baseline for the wrong line must be refused: {err}"
1526 );
1527 assert!(
1528 StatusCache::at_root(tmp.path())
1529 .load(&requested, &pk)
1530 .unwrap()
1531 .is_none(),
1532 "nothing is cached under the requested line"
1533 );
1534 }
1535
1536 #[test]
1538 fn a_source_with_no_baseline_caches_nothing_and_does_not_error() {
1539 use crate::source::{LayerRef, MemorySource};
1540 let (_sk, pk) = generate_root_keypair();
1541 let tmp = tempfile::tempdir().unwrap();
1542 let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1543 let source = MemorySource::new();
1544 let cached = cache_baseline_from_source(
1545 &source,
1546 &LayerRef::Name("2026.07.0".parse().unwrap()),
1547 &line,
1548 &pk,
1549 tmp.path(),
1550 )
1551 .unwrap();
1552 assert_eq!(cached, None);
1553 }
1554
1555 #[test]
1557 fn a_baseline_signed_by_an_impostor_is_refused_not_cached() {
1558 use crate::source::{LayerRef, MemorySource};
1559 let (attacker_sk, _) = generate_root_keypair();
1560 let (_real_sk, real_pk) = generate_root_keypair();
1561 let tmp = tempfile::tempdir().unwrap();
1562 let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1563 let envelope = status(5).sign(&attacker_sk, "k").unwrap();
1564 let source = MemorySource::new().with_line_status(envelope.as_bytes());
1565 let err = cache_baseline_from_source(
1566 &source,
1567 &LayerRef::Name("2026.07.0".parse().unwrap()),
1568 &line,
1569 &real_pk,
1570 tmp.path(),
1571 )
1572 .unwrap_err();
1573 assert!(
1575 StatusCache::at_root(tmp.path())
1576 .load(&line, &real_pk)
1577 .unwrap()
1578 .is_none(),
1579 "a baseline that fails verification must not be cached: {err}"
1580 );
1581 }
1582
1583 #[test]
1585 fn attaching_by_envelope_derives_the_line_from_the_document() {
1586 use crate::deposit::{DepositSpec, DepositTool, deposit};
1587 let (sk, pk) = generate_root_keypair();
1588 let tmp = tempfile::tempdir().unwrap();
1589 let dest = tmp.path().join("layout");
1590 deposit(
1591 &DepositSpec {
1592 includes: Vec::new(),
1593 layer: "2026.07.0".parse().unwrap(),
1594 channel: "qualified".into(),
1595 counter: 1,
1596 issued_at: "2026-08-07T00:00:00Z".into(),
1597 tools: vec![DepositTool {
1598 name: "synth".into(),
1599 version: "1".into(),
1600 platform: None,
1601 bytes: b"t".to_vec(),
1602 source: None,
1603 runner: None,
1604 kind: None,
1605 sdk_prefix: None,
1606 }],
1607 },
1608 &sk,
1609 "k",
1610 &dest,
1611 )
1612 .unwrap();
1613
1614 let envelope = status(4).sign(&sk, "k").unwrap();
1615 let (line, counter) = attach_envelope_to_layout(&dest, envelope.as_bytes()).unwrap();
1616 assert_eq!(line.to_string(), "2026.07");
1617 assert_eq!(counter, 4);
1618 let carried = read_any_from_layout(&dest).unwrap().unwrap();
1620 assert_eq!(
1621 LineStatus::verify_and_parse(&carried, &pk).unwrap().counter,
1622 4
1623 );
1624 }
1625
1626 #[test]
1628 fn attaching_a_stale_document_over_a_newer_one_is_refused() {
1629 use crate::deposit::{DepositSpec, DepositTool, deposit};
1637 let (sk, _pk) = generate_root_keypair();
1638 let tmp = tempfile::tempdir().unwrap();
1639 let dest = tmp.path().join("layout");
1640 deposit(
1641 &DepositSpec {
1642 includes: Vec::new(),
1643 layer: "2026.07.0".parse().unwrap(),
1644 channel: "qualified".into(),
1645 counter: 1,
1646 issued_at: "2026-08-07T00:00:00Z".into(),
1647 tools: vec![DepositTool {
1648 name: "synth".into(),
1649 version: "1".into(),
1650 platform: None,
1651 bytes: b"t".to_vec(),
1652 source: None,
1653 runner: None,
1654 kind: None,
1655 sdk_prefix: None,
1656 }],
1657 },
1658 &sk,
1659 "k",
1660 &dest,
1661 )
1662 .unwrap();
1663
1664 attach_envelope_to_layout(&dest, status(7).sign(&sk, "k").unwrap().as_bytes()).unwrap();
1666 let err = attach_envelope_to_layout(&dest, status(3).sign(&sk, "k").unwrap().as_bytes())
1668 .unwrap_err();
1669 assert!(
1670 matches!(
1671 err,
1672 LineStatusError::Stale {
1673 presented: 3,
1674 cached: 7,
1675 ..
1676 }
1677 ),
1678 "a lower counter must be refused, got {err}"
1679 );
1680 let msg = err.to_string();
1681 assert!(msg.contains('3') && msg.contains('7'), "names both: {msg}");
1682 let carried = parse_unverified(&read_any_from_layout(&dest).unwrap().unwrap()).unwrap();
1684 assert_eq!(
1685 carried.counter, 7,
1686 "the newer baseline survives the attempt"
1687 );
1688 attach_envelope_to_layout(&dest, status(7).sign(&sk, "k").unwrap().as_bytes()).unwrap();
1691 }
1692
1693 #[test]
1695 fn an_advisory_that_could_never_fire_is_refused_at_sign_time() {
1696 let (sk, _pk) = generate_root_keypair();
1701 let cases: &[(&str, &str)] = &[
1702 ("2026.7.0", "not a valid YYYY.MM.P id"), ("2026.07", "missing its patch component"), ("2026.08.0", "belongs to another line"), ("2026.07.O", "letter O for zero"),
1706 ];
1707 for (bad, why) in cases {
1708 let mut doc = status(1);
1709 doc.known_problems[0].affected = vec![bad.to_string()];
1710 let err = doc.sign(&sk, "k").unwrap_err();
1711 assert!(
1712 matches!(err, LineStatusError::DeadReference { .. }),
1713 "{why}: affected id {bad:?} must be refused, got: {err}"
1714 );
1715 let msg = err.to_string();
1716 assert!(
1717 msg.contains(bad) && msg.contains("2026.07") && msg.contains("re-sign"),
1718 "the error must name the id, the line, and the fix: {msg}"
1719 );
1720 }
1721 let mut doc = status(1);
1723 doc.yanked = BTreeMap::from([("2026.8.0".to_string(), "CVE".to_string())]);
1724 assert!(matches!(
1725 doc.sign(&sk, "k").unwrap_err(),
1726 LineStatusError::DeadReference { .. }
1727 ));
1728 status(1).sign(&sk, "k").unwrap();
1731 }
1732
1733 #[test]
1735 fn attach_refuses_a_pre_signed_advisory_that_could_never_fire() {
1736 use crate::deposit::{DepositSpec, DepositTool, deposit};
1741 let (sk, _pk) = generate_root_keypair();
1742 let tmp = tempfile::tempdir().unwrap();
1743 let dest = tmp.path().join("layout");
1744 deposit(
1745 &DepositSpec {
1746 includes: Vec::new(),
1747 layer: "2026.07.0".parse().unwrap(),
1748 channel: "qualified".into(),
1749 counter: 1,
1750 issued_at: "2026-08-07T00:00:00Z".into(),
1751 tools: vec![DepositTool {
1752 name: "synth".into(),
1753 version: "1".into(),
1754 platform: None,
1755 bytes: b"t".to_vec(),
1756 source: None,
1757 runner: None,
1758 kind: None,
1759 sdk_prefix: None,
1760 }],
1761 },
1762 &sk,
1763 "k",
1764 &dest,
1765 )
1766 .unwrap();
1767 let mut doc = status(1);
1768 doc.known_problems[0].affected = vec!["2026.7.0".to_string()];
1769 let payload = serde_json::to_vec_pretty(&doc).unwrap();
1770 let envelope = dsse_sign_typed(&payload, LINE_STATUS_PAYLOAD_TYPE, &sk, "k").unwrap();
1771 let err = attach_envelope_to_layout(&dest, envelope.as_bytes()).unwrap_err();
1772 assert!(
1773 matches!(err, LineStatusError::DeadReference { .. }),
1774 "got: {err}"
1775 );
1776 assert!(
1777 read_any_from_layout(&dest).unwrap().is_none(),
1778 "the dead advisory must not land in the layout"
1779 );
1780 }
1781
1782 #[test]
1784 fn attaching_to_a_directory_that_is_not_a_layout_is_refused_before_writing() {
1785 let (sk, _pk) = generate_root_keypair();
1790 let tmp = tempfile::tempdir().unwrap();
1791 let not_a_layout = tmp.path().join("somedir");
1792 std::fs::create_dir_all(¬_a_layout).unwrap();
1793 let envelope = status(1).sign(&sk, "k").unwrap();
1794 let err = attach_envelope_to_layout(¬_a_layout, envelope.as_bytes()).unwrap_err();
1795 assert!(
1796 matches!(err, LineStatusError::NotALayout { .. }),
1797 "got: {err}"
1798 );
1799 assert!(
1800 err.to_string().contains("varve deposit"),
1801 "the error must carry its fix: {err}"
1802 );
1803 assert!(
1804 !not_a_layout.join("blobs").exists(),
1805 "nothing may be written into a directory that is not a layout"
1806 );
1807 }
1808
1809 #[test]
1811 fn the_unsigned_document_mistake_is_named_not_wrapped() {
1812 let raw = serde_json::to_string_pretty(&status(1)).unwrap();
1816 let err = not_an_envelope(&raw);
1817 let msg = err.to_string();
1818 assert!(
1819 msg.contains("UNSIGNED") && msg.contains("varve sign-status"),
1820 "raw document must be diagnosed with its fix: {msg}"
1821 );
1822 let msg = not_an_envelope("garbage").to_string();
1824 assert!(
1825 msg.contains("not a DSSE envelope") && msg.contains("varve sign-status"),
1826 "got: {msg}"
1827 );
1828 let (_sk, pk) = generate_root_keypair();
1830 let err = LineStatus::verify_and_parse(raw.as_bytes(), &pk).unwrap_err();
1831 assert!(err.to_string().contains("UNSIGNED"), "got: {err}");
1832 }
1833
1834 #[test]
1836 fn a_deposit_layouts_baseline_is_readable_without_naming_the_line() {
1837 use crate::deposit::{DepositSpec, DepositTool, deposit};
1841 let (sk, pk) = generate_root_keypair();
1842 let tmp = tempfile::tempdir().unwrap();
1843 let dest = tmp.path().join("layout");
1844 let spec = DepositSpec {
1845 includes: Vec::new(),
1846 layer: "2026.07.0".parse().unwrap(),
1847 channel: "qualified".into(),
1848 counter: 1,
1849 issued_at: "2026-08-07T00:00:00Z".into(),
1850 tools: vec![DepositTool {
1851 name: "synth".into(),
1852 version: "1".into(),
1853 platform: None,
1854 bytes: b"t".to_vec(),
1855 source: None,
1856 runner: None,
1857 kind: None,
1858 sdk_prefix: None,
1859 }],
1860 };
1861 deposit(&spec, &sk, "k", &dest).unwrap();
1862
1863 assert!(read_any_from_layout(&dest).unwrap().is_none());
1865
1866 let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1867 let envelope = status(3).sign(&sk, "k").unwrap();
1868 attach_to_layout(&dest, &line, envelope.as_bytes()).unwrap();
1869
1870 let carried = read_any_from_layout(&dest).unwrap().unwrap();
1871 let parsed = LineStatus::verify_and_parse(&carried, &pk).unwrap();
1872 assert_eq!(parsed.counter, 3);
1873 }
1874
1875 fn listing(layers: &[&str]) -> KnownLayers {
1880 KnownLayers::Known {
1881 source: "the signed line-index for 2026.07 (counter 1)".into(),
1882 line: Some("2026.07".into()),
1883 layers: layers.iter().map(|s| s.to_string()).collect(),
1884 }
1885 }
1886
1887 #[test]
1889 fn an_affected_id_naming_no_existing_layer_is_refused_and_the_verdict_lists_what_does_exist() {
1890 let mut doc = status(1);
1895 doc.yanked.clear();
1896 doc.known_problems = vec![KnownProblem {
1897 id: "KP-1".into(),
1898 title: "t".into(),
1899 severity: "high".into(),
1900 affected: vec!["2026.07.10".into()],
1901 workaround: None,
1902 detection: None,
1903 mitigation: None,
1904 }];
1905 let err = doc
1906 .check_layer_refs_against(&listing(&["2026.07.0", "2026.07.1"]), false)
1907 .expect_err("an advisory that can never fire must be refused");
1908 assert!(
1909 matches!(&err, LineStatusError::UnknownLayer { id, .. } if id == "2026.07.10"),
1910 "got: {err}"
1911 );
1912 let msg = err.to_string();
1913 assert!(msg.contains("KP-1"), "name the entry at fault: {msg}");
1914 assert!(
1918 msg.contains("2026.07.0") && msg.contains("2026.07.1"),
1919 "the refusal must list the ids that exist: {msg}"
1920 );
1921 assert!(msg.contains("--force"), "{msg}");
1922 }
1923
1924 #[test]
1926 fn a_yank_key_is_checked_against_existing_layers_too_not_only_affected() {
1927 let mut doc = status(1);
1931 doc.known_problems.clear();
1932 doc.yanked = BTreeMap::from([("2026.07.9".to_string(), "CVE".to_string())]);
1933 let err = doc
1934 .check_layer_refs_against(&listing(&["2026.07.0"]), false)
1935 .expect_err("a yank naming no layer must be refused");
1936 assert!(
1937 matches!(&err, LineStatusError::UnknownLayer { what, id, .. }
1938 if what.contains("yank") && id == "2026.07.9"),
1939 "got: {err}"
1940 );
1941 }
1942
1943 #[test]
1945 fn a_document_whose_ids_all_exist_passes_and_says_what_was_checked() {
1946 let check = status(1)
1949 .check_layer_refs_against(&listing(&["2026.07.0", "2026.07.1"]), false)
1950 .expect("every id in the fixture exists on the line");
1951 assert!(check.existence_checked);
1952 assert!(
1953 check.note.contains("checked against") && check.note.contains("2 layers"),
1954 "the note must state the check that RAN: {}",
1955 check.note
1956 );
1957 }
1958
1959 #[test]
1961 fn where_the_line_is_not_visible_the_answer_says_which_check_was_not_run() {
1962 let check = status(1)
1967 .check_layer_refs_against(&KnownLayers::unknown("no line-index was supplied"), false)
1968 .unwrap();
1969 assert!(
1970 !check.existence_checked,
1971 "an unchecked document must not report itself as checked"
1972 );
1973 assert!(
1974 check.note.contains("NOT") && check.note.contains("no line-index was supplied"),
1975 "the note must name the check that did NOT run, and why: {}",
1976 check.note
1977 );
1978 }
1979
1980 #[test]
1982 fn force_allows_a_layer_not_deposited_yet_but_never_a_malformed_id() {
1983 let mut doc = status(1);
1988 doc.yanked.clear();
1989 doc.known_problems = vec![KnownProblem {
1990 id: "KP-1".into(),
1991 title: "t".into(),
1992 severity: "high".into(),
1993 affected: vec!["2026.07.9".into()],
1994 workaround: None,
1995 detection: None,
1996 mitigation: None,
1997 }];
1998 let check = doc
1999 .check_layer_refs_against(&listing(&["2026.07.0"]), true)
2000 .expect("--force pre-signs for a layer not deposited yet");
2001 assert!(
2002 !check.existence_checked,
2003 "forcing must not report the check as having run"
2004 );
2005 assert!(check.note.contains("--force"), "{}", check.note);
2006
2007 for id in ["2026.07", "twenty-twenty-six", "2026.08.0"] {
2010 doc.known_problems[0].affected = vec![id.to_string()];
2011 match doc.check_layer_refs_against(&listing(&["2026.07.0"]), true) {
2012 Err(LineStatusError::DeadReference { .. }) => {}
2013 other => panic!("'{id}' must be refused even under --force, got: {other:?}"),
2014 }
2015 }
2016 }
2017
2018 #[test]
2020 fn a_listing_for_another_line_is_refused_rather_than_used() {
2021 let wrong = KnownLayers::Known {
2025 source: "the signed line-index for 2026.08".into(),
2026 line: Some("2026.08".into()),
2027 layers: vec!["2026.08.0".into()],
2028 };
2029 let err = status(1)
2030 .check_layer_refs_against(&wrong, false)
2031 .expect_err("a listing for another line must not be used as this line's");
2032 assert!(
2033 matches!(&err, LineStatusError::LineMismatch { expected, got }
2034 if expected == "2026.07" && got == "2026.08"),
2035 "got: {err}"
2036 );
2037 }
2038
2039 #[test]
2041 fn a_layout_becomes_a_listing_only_once_the_signed_index_is_attached() {
2042 use crate::deposit::{DepositSpec, DepositTool, deposit};
2047 let (sk, _pk) = generate_root_keypair();
2048 let tmp = tempfile::tempdir().unwrap();
2049 let dest = tmp.path().join("layout");
2050 deposit(
2051 &DepositSpec {
2052 includes: Vec::new(),
2053 layer: "2026.07.0".parse().unwrap(),
2054 channel: "qualified".into(),
2055 counter: 1,
2056 issued_at: "2026-08-07T00:00:00Z".into(),
2057 tools: vec![DepositTool {
2058 name: "synth".into(),
2059 version: "1".into(),
2060 platform: None,
2061 bytes: b"t".to_vec(),
2062 source: None,
2063 runner: None,
2064 kind: None,
2065 sdk_prefix: None,
2066 }],
2067 },
2068 &sk,
2069 "k",
2070 &dest,
2071 )
2072 .unwrap();
2073
2074 let known = known_layers_in_layout(&dest, "2026.07");
2076 assert!(
2077 matches!(&known, KnownLayers::Unknown { why } if why.contains("not a listing")),
2078 "got: {known:?}"
2079 );
2080
2081 let index = crate::lineindex::LineIndex {
2082 line: "2026.07".into(),
2083 counter: 1,
2084 issued_at: "2026-08-07T00:00:00Z".into(),
2085 layers: vec![crate::lineindex::IndexedLayer {
2086 layer: "2026.07.0".into(),
2087 digest: "sha256:aa".into(),
2088 channel: "qualified".into(),
2089 counter: 1,
2090 }],
2091 };
2092 crate::lineindex::attach_to_layout(
2093 &dest,
2094 "2026.07",
2095 index.sign(&sk, "k").unwrap().as_bytes(),
2096 )
2097 .unwrap();
2098
2099 let known = known_layers_in_layout(&dest, "2026.07");
2100 assert_eq!(
2101 known,
2102 KnownLayers::Known {
2103 source: "the signed line-index for 2026.07 (counter 1)".into(),
2104 line: Some("2026.07".into()),
2105 layers: vec!["2026.07.0".into()],
2106 }
2107 );
2108
2109 let mut doc = status(2);
2113 doc.yanked.clear();
2114 doc.known_problems = vec![KnownProblem {
2115 id: "KP-1".into(),
2116 title: "t".into(),
2117 severity: "high".into(),
2118 affected: vec!["2026.07.1".into()],
2119 workaround: None,
2120 detection: None,
2121 mitigation: None,
2122 }];
2123 let err = attach_envelope_to_layout(&dest, doc.sign(&sk, "k").unwrap().as_bytes())
2124 .expect_err("2026.07.1 is not on this line's index");
2125 assert!(
2126 matches!(&err, LineStatusError::UnknownLayer { .. }),
2127 "got: {err}"
2128 );
2129
2130 doc.known_problems[0].affected = vec!["2026.07.0".into()];
2131 let (_line, counter, check) =
2132 attach_envelope_to_layout_checked(&dest, doc.sign(&sk, "k").unwrap().as_bytes(), false)
2133 .unwrap();
2134 assert_eq!(counter, 2);
2135 assert!(check.existence_checked, "{}", check.note);
2136 }
2137
2138 #[test]
2140 fn signing_reports_the_check_it_ran_alongside_the_envelope() {
2141 let (sk, pk) = generate_root_keypair();
2145 let (envelope, check) = status(1)
2146 .sign_against(&listing(&["2026.07.0", "2026.07.1"]), false, &sk, "k")
2147 .unwrap();
2148 assert!(check.existence_checked);
2149 assert_eq!(
2150 LineStatus::verify_and_parse(envelope.as_bytes(), &pk).unwrap(),
2151 status(1),
2152 "the checked path must sign the same document the plain path does"
2153 );
2154
2155 let mut doc = status(1);
2158 doc.yanked.clear();
2159 doc.known_problems[0].affected = vec!["2026.07.7".into()];
2160 doc.known_problems[1].affected = vec!["2026.07.0".into()];
2161 assert!(
2162 doc.sign_against(&listing(&["2026.07.0"]), false, &sk, "k")
2163 .is_err()
2164 );
2165 }
2166
2167 #[test]
2169 fn a_producer_can_list_their_own_line_without_a_network_or_an_index() {
2170 let tmp = tempfile::tempdir().unwrap();
2175 let (sk, _pk) = crate::generate_root_keypair();
2176 for (id, counter, dir) in [
2183 ("2026.08.0", 1u64, "out-a"),
2184 ("2026.08.1", 2, "out-b"),
2185 ("2026.09.0", 1, "out-other"),
2187 ] {
2188 let spec = crate::deposit::DepositSpec {
2189 layer: id.parse().unwrap(),
2190 channel: "rolling".into(),
2191 counter,
2192 issued_at: "2026-08-07T00:00:00Z".into(),
2193 tools: vec![crate::DepositTool {
2194 name: "t".into(),
2195 version: "1.0".into(),
2196 platform: None,
2197 bytes: b"x".to_vec(),
2198 source: None,
2199 runner: None,
2200 kind: None,
2201 sdk_prefix: None,
2202 }],
2203 includes: Vec::new(),
2204 };
2205 crate::deposit(&spec, &sk, "k", &tmp.path().join(dir)).unwrap();
2206 }
2207
2208 let known = known_layers_in_layout_dirs(&[tmp.path().to_path_buf()], "2026.08");
2210 match &known {
2211 KnownLayers::Known { layers, line, .. } => {
2212 assert_eq!(layers, &["2026.08.0", "2026.08.1"], "got {layers:?}");
2213 assert_eq!(line.as_deref(), Some("2026.08"));
2214 }
2215 KnownLayers::Unknown { why } => panic!("expected a listing, got: {why}"),
2216 }
2217
2218 let mut doc = status(2);
2220 doc.line = "2026.08".into();
2221 doc.yanked = BTreeMap::from([(
2222 "2026.08.10".to_string(),
2223 "typo — never deposited".to_string(),
2224 )]);
2225 doc.known_problems.clear();
2226 let err = doc
2227 .check_layer_refs_against(&known, false)
2228 .expect_err("a yank naming a layer this line does not have must be refused");
2229 let msg = err.to_string();
2230 assert!(
2231 msg.contains("2026.08.10") && msg.contains("2026.08.0"),
2232 "the refusal must name the bad id AND the ids that exist: {msg}"
2233 );
2234
2235 let empty = tempfile::tempdir().unwrap();
2237 assert!(matches!(
2238 known_layers_in_layout_dirs(&[empty.path().to_path_buf()], "2026.08"),
2239 KnownLayers::Unknown { .. }
2240 ));
2241 }
2242}