1use std::path::Path;
12
13use crate::install::VerifyError;
14use crate::layer::LayerId;
15use crate::store::manifest_digest;
16use crate::verify::sign_layer_manifest;
17
18fn is_rfc3339(s: &str) -> bool {
23 let Some((date, time)) = s.split_once('T') else {
24 return false;
25 };
26 if crate::rollback::epoch_days(date).is_none() {
27 return false;
28 }
29 let b = time.as_bytes();
31 if b.len() < 9 || b[2] != b':' || b[5] != b':' {
32 return false;
33 }
34 if !b[..8].iter().enumerate().all(|(i, c)| {
35 if i == 2 || i == 5 {
36 *c == b':'
37 } else {
38 c.is_ascii_digit()
39 }
40 }) {
41 return false;
42 }
43 let rest = &time[8..];
44 rest == "Z" || rest.ends_with('Z') || rest.contains('+') || rest.matches('-').count() == 1
45}
46
47#[derive(Debug, Clone)]
49pub struct DepositSpec {
50 pub layer: LayerId,
51 pub channel: String,
53 pub counter: u64,
56 pub issued_at: String,
59 pub tools: Vec<DepositTool>,
61 pub includes: Vec<DepositInclude>,
63}
64
65#[derive(Debug, Clone, Default)]
67pub struct DepositInclude {
68 pub digest: String,
69 pub realm: Option<String>,
70 pub layer: Option<String>,
71}
72
73#[derive(Debug, Clone)]
74pub struct DepositTool {
75 pub name: String,
76 pub version: String,
77 pub platform: Option<String>,
80 pub bytes: Vec<u8>,
81 pub source: Option<ToolSource>,
85 pub runner: Option<RunnerSpec>,
89 pub kind: Option<crate::kind::PayloadKind>,
92 pub sdk_prefix: Option<String>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
104#[serde(deny_unknown_fields)]
105pub struct RunnerSpec {
106 pub tool: String,
107 #[serde(default)]
108 pub args: Vec<String>,
109 #[serde(rename = "arg-prefix", default)]
110 pub arg_prefix: Option<String>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
115#[serde(deny_unknown_fields)]
116pub struct ToolSource {
117 pub repo: String,
119 pub release: String,
121 pub asset: String,
123 pub sha256: String,
126 #[serde(default)]
130 pub proof: Option<crate::ingest::IngestProof>,
131 #[serde(rename = "proof-signer", default)]
134 pub proof_signer: Option<String>,
135 #[serde(rename = "proof-asserts", default)]
139 pub proof_asserts: Option<String>,
140}
141
142#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
144pub struct DepositOptions {
145 pub force: bool,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct DepositOutcome {
154 pub digest: String,
156 pub layer: LayerId,
157 pub counter: u64,
158}
159
160#[derive(Debug, serde::Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct DepositFileSpec {
165 pub layer: String,
166 pub channel: String,
167 pub counter: u64,
168 #[serde(default, rename = "tool")]
169 pub tools: Vec<SpecTool>,
170 #[serde(default, rename = "include")]
173 pub includes: Vec<SpecInclude>,
174}
175
176#[derive(Debug, serde::Deserialize)]
179#[serde(deny_unknown_fields)]
180pub struct SpecInclude {
181 pub digest: String,
183 #[serde(default)]
185 pub realm: Option<String>,
186 #[serde(default)]
189 pub layer: Option<String>,
190}
191
192#[derive(Debug, serde::Deserialize)]
193#[serde(deny_unknown_fields)]
194pub struct SpecTool {
195 pub name: String,
196 pub version: String,
197 #[serde(default)]
198 pub platform: Option<String>,
199 pub path: String,
201 #[serde(default)]
202 pub source: Option<ToolSource>,
203 #[serde(default)]
204 pub runner: Option<RunnerSpec>,
205 #[serde(default)]
208 pub kind: Option<String>,
209 #[serde(rename = "sdk-prefix", default)]
213 pub sdk_prefix: Option<String>,
214}
215
216pub fn parse_deposit_spec(toml_text: &str) -> Result<DepositFileSpec, DepositError> {
217 toml::from_str(toml_text).map_err(|e| DepositError::Spec(e.to_string()))
218}
219
220impl From<crate::archive::LayoutWriteError> for DepositError {
221 fn from(e: crate::archive::LayoutWriteError) -> Self {
222 match e {
223 crate::archive::LayoutWriteError::Io { path, source } => {
224 DepositError::Io { path, source }
225 }
226 crate::archive::LayoutWriteError::WouldDestroy(d) => {
227 DepositError::WouldDestroySignedWork(d)
228 }
229 }
230 }
231}
232
233#[derive(Debug, thiserror::Error)]
234pub enum DepositError {
235 #[error(
236 "the envelope this deposit just signed does not verify against the key that signed \
237 it — refusing to publish an artifact no consumer could accept"
238 )]
239 SelfVerifyFailed,
240 #[error(
241 "channel {channel:?} is not one a pin can name — use `qualified` or `rolling`. \
242 Signing it would produce a layer no varve.toml could ever select."
243 )]
244 BadChannel { channel: String },
245 #[error(
246 "issued-at {issued_at:?} is not an RFC 3339 timestamp (e.g. 2026-08-01T00:00:00Z). \
247 It is signed into the manifest and drives staleness and the SBOM timestamp, so it \
248 cannot be corrected afterwards."
249 )]
250 BadIssuedAt { issued_at: String },
251 #[error("deposit spec is not valid: {0}")]
252 Spec(String),
253 #[error(transparent)]
258 WouldDestroySignedWork(#[from] crate::referrers::WouldDestroy),
259 #[error("deposit has no tools — an empty layer is not a toolchain")]
260 NoTools,
261 #[error(
262 "tool '{name}' is deposited twice for platform {platform} (versions {first} and \
263 {second}) — a tool is DISPATCHED BY NAME (`varve run {name}`, `varve which {name}`, the \
264 argv[0] shims), so one name must resolve to exactly one binary per platform. Deposit the \
265 other version as a separate layer, or give it a distinct name."
266 )]
267 DuplicateTool {
268 name: String,
269 platform: String,
270 first: String,
271 second: String,
272 },
273 #[error(
274 "payload '{name}' version {version} is deposited twice for platform {platform} — a layer \
275 may hold several VERSIONS of one name, but not one version twice: the two would be the \
276 same payload, and only one set of bytes could land."
277 )]
278 DuplicatePayload {
279 name: String,
280 version: String,
281 platform: String,
282 },
283 #[error(
284 "payload '{name}' is kind {kind} and carries `sdk-prefix` — only a tree payload (sdk) is \
285 relocated, so the prefix would be signed into the manifest, ignored by every adapter, \
286 and believed. Drop it, or deposit this payload as kind = \"sdk\"."
287 )]
288 SdkPrefixOnNonTree { name: String, kind: String },
289 #[error(
290 "sdk '{name}' version {version} declares no `sdk-prefix` — the absolute path it was \
291 BUILT for. Without it `varve export-sdk` has no relocation budget and no path to \
292 patch, so the layer would install and verify and could never be exported. Add \
293 `sdk-prefix = \"/opt/poky/4.0\"` (the path the SDK was built for) to the [[tool]] table."
294 )]
295 SdkPrefixMissing { name: String, version: String },
296 #[error(
297 "sdk '{name}' declares sdk-prefix {prefix:?}, which is not absolute — the prefix is the \
298 path PATCHED INTO the SDK's binaries, and a relative one there would resolve against \
299 whatever directory a compiler happens to run in"
300 )]
301 SdkPrefixNotAbsolute { name: String, prefix: String },
302 #[error(
303 "payload '{name}' from {repo} declares proof = \"unverified\" and records no reason — \
304 \"we could not verify this\" must never be the silent path. Nothing vouched for these \
305 bytes, so the only thing that can travel with them is WHY you shipped them anyway: set \
306 `proof-asserts` on [tool.source] to the operator's justification, which is signed into \
307 the layer where `varve inspect` and every consumer will see it."
308 )]
309 UnverifiedWithoutReason { name: String, repo: String },
310 #[error(
311 "payload '{name}' from {repo} declares proof = \"unverified\" and also names \
312 proof-signer {signer:?} — nothing vouched for these bytes, so naming an identity that \
313 did would be signed, attributable and false. Drop the signer, or declare the mechanism \
314 that actually established it."
315 )]
316 UnverifiedNamesASigner {
317 name: String,
318 repo: String,
319 signer: String,
320 },
321 #[error(
322 "payload '{name}' from {repo} carries ingestion-proof detail ({detail}) but declares no \
323 `proof` mechanism — the detail would be signed into the layer with nothing saying HOW \
324 it was established, which makes it attributable and believed rather than checkable. \
325 Declare `proof = \"cosign-sums\" | \"build-provenance\" | \"unverified\"`, or drop the \
326 detail."
327 )]
328 ProofDetailWithoutMechanism {
329 name: String,
330 repo: String,
331 detail: &'static str,
332 },
333 #[error(transparent)]
334 Sign(#[from] VerifyError),
335 #[error("io error at {path}: {source}")]
336 Io {
337 path: String,
338 #[source]
339 source: std::io::Error,
340 },
341}
342
343fn platform_label(platform: Option<&String>) -> String {
345 platform
346 .cloned()
347 .unwrap_or_else(|| "any (unstamped)".to_string())
348}
349
350fn check_identities(tools: &[&DepositTool]) -> Result<(), DepositError> {
361 let mut dispatched: std::collections::BTreeMap<(&str, Option<&String>), &str> =
363 std::collections::BTreeMap::new();
364 let mut held: std::collections::BTreeSet<(&str, &str, Option<&String>)> =
366 std::collections::BTreeSet::new();
367 for tool in tools {
368 let dispatchable = tool.kind.unwrap_or_default().is_dispatchable();
369 let platform = tool.platform.as_ref();
370 if dispatchable {
371 if let Some(first) = dispatched.insert((&tool.name, platform), &tool.version) {
372 return Err(DepositError::DuplicateTool {
373 name: tool.name.clone(),
374 platform: platform_label(platform),
375 first: first.to_string(),
376 second: tool.version.clone(),
377 });
378 }
379 } else if !held.insert((&tool.name, &tool.version, platform)) {
380 return Err(DepositError::DuplicatePayload {
381 name: tool.name.clone(),
382 version: tool.version.clone(),
383 platform: platform_label(platform),
384 });
385 }
386 }
387 Ok(())
388}
389
390fn check_sdk_prefixes(tools: &[&DepositTool]) -> Result<(), DepositError> {
399 for tool in tools {
400 let is_tree = tool.kind == Some(crate::kind::PayloadKind::Sdk);
401 match (&tool.sdk_prefix, is_tree) {
402 (Some(_), false) => {
403 return Err(DepositError::SdkPrefixOnNonTree {
404 name: tool.name.clone(),
405 kind: tool.kind.unwrap_or_default().as_str().to_string(),
406 });
407 }
408 (None, true) => {
409 return Err(DepositError::SdkPrefixMissing {
410 name: tool.name.clone(),
411 version: tool.version.clone(),
412 });
413 }
414 (Some(prefix), true) if !prefix.starts_with('/') => {
415 return Err(DepositError::SdkPrefixNotAbsolute {
416 name: tool.name.clone(),
417 prefix: prefix.clone(),
418 });
419 }
420 _ => {}
421 }
422 }
423 Ok(())
424}
425
426fn check_ingest_proofs(tools: &[&DepositTool]) -> Result<(), DepositError> {
446 for tool in tools {
447 let Some(source) = &tool.source else { continue };
448 match source.proof {
449 Some(crate::ingest::IngestProof::Unverified) => {
450 if source
451 .proof_asserts
452 .as_ref()
453 .is_none_or(|r| r.trim().is_empty())
454 {
455 return Err(DepositError::UnverifiedWithoutReason {
456 name: tool.name.clone(),
457 repo: source.repo.clone(),
458 });
459 }
460 if let Some(signer) = &source.proof_signer {
461 return Err(DepositError::UnverifiedNamesASigner {
462 name: tool.name.clone(),
463 repo: source.repo.clone(),
464 signer: signer.clone(),
465 });
466 }
467 }
468 Some(_) => {}
469 None => {
470 let detail = match (&source.proof_signer, &source.proof_asserts) {
471 (Some(_), Some(_)) => Some("proof-signer and proof-asserts"),
472 (Some(_), None) => Some("proof-signer"),
473 (None, Some(_)) => Some("proof-asserts"),
474 (None, None) => None,
475 };
476 if let Some(detail) = detail {
477 return Err(DepositError::ProofDetailWithoutMechanism {
478 name: tool.name.clone(),
479 repo: source.repo.clone(),
480 detail,
481 });
482 }
483 }
484 }
485 }
486 Ok(())
487}
488
489pub fn deposit(
492 spec: &DepositSpec,
493 signing_key: &[u8],
494 key_id: &str,
495 dest: &Path,
496) -> Result<DepositOutcome, DepositError> {
497 deposit_with_options(spec, signing_key, key_id, dest, &DepositOptions::default())
498}
499
500pub fn deposit_with_options(
502 spec: &DepositSpec,
503 signing_key: &[u8],
504 key_id: &str,
505 dest: &Path,
506 options: &DepositOptions,
507) -> Result<DepositOutcome, DepositError> {
508 crate::referrers::guard(dest, options.force)?;
514 if spec.tools.is_empty() {
515 return Err(DepositError::NoTools);
516 }
517 let mut tools: Vec<&DepositTool> = spec.tools.iter().collect();
518 tools.sort_by(|a, b| {
522 (&a.name, &a.version, &a.platform).cmp(&(&b.name, &b.version, &b.platform))
523 });
524 check_identities(&tools)?;
525 check_sdk_prefixes(&tools)?;
526 check_ingest_proofs(&tools)?;
527
528 let entries: Vec<serde_json::Value> = tools
533 .iter()
534 .map(|tool| {
535 let mut annotations = serde_json::Map::new();
536 annotations.insert("eu.pulseengine.tool".into(), tool.name.clone().into());
537 annotations.insert(
538 "eu.pulseengine.tool.version".into(),
539 tool.version.clone().into(),
540 );
541 if let Some(platform) = &tool.platform {
542 annotations.insert(
543 crate::platform::ANN_PLATFORM.into(),
544 platform.clone().into(),
545 );
546 }
547 if let Some(source) = &tool.source {
548 annotations.insert(
549 crate::bazel::ANN_SRC_REPO.into(),
550 source.repo.clone().into(),
551 );
552 annotations.insert(
553 crate::bazel::ANN_SRC_RELEASE.into(),
554 source.release.clone().into(),
555 );
556 annotations.insert(
557 crate::bazel::ANN_SRC_ASSET.into(),
558 source.asset.clone().into(),
559 );
560 annotations.insert(
561 crate::bazel::ANN_SRC_SHA256.into(),
562 source.sha256.clone().into(),
563 );
564 if let Some(proof) = source.proof {
577 annotations.insert(crate::ingest::ANN_PROOF.into(), proof.as_str().into());
578 }
579 if let Some(signer) = &source.proof_signer {
580 annotations.insert(
581 crate::ingest::ANN_PROOF_SIGNER.into(),
582 signer.clone().into(),
583 );
584 }
585 if let Some(asserts) = &source.proof_asserts {
586 annotations.insert(
587 crate::ingest::ANN_PROOF_ASSERTS.into(),
588 asserts.clone().into(),
589 );
590 }
591 }
592 if let Some(kind) = tool.kind
596 && kind != crate::kind::PayloadKind::Tool
597 {
598 annotations.insert(crate::kind::ANN_KIND.into(), kind.as_str().into());
599 }
600 if let Some(prefix) = &tool.sdk_prefix {
605 annotations.insert(
606 crate::sdkexport::ANN_SDK_PREFIX.into(),
607 prefix.clone().into(),
608 );
609 }
610 if let Some(runner) = &tool.runner {
611 annotations.insert(crate::bazel::ANN_RUNNER.into(), runner.tool.clone().into());
612 if !runner.args.is_empty() {
613 annotations.insert(
614 crate::bazel::ANN_RUNNER_ARGS.into(),
615 runner.args.join(" ").into(),
616 );
617 }
618 if let Some(prefix) = &runner.arg_prefix {
619 annotations.insert(
620 crate::bazel::ANN_RUNNER_ARG_PREFIX.into(),
621 prefix.clone().into(),
622 );
623 }
624 }
625 serde_json::json!({
626 "mediaType": "application/vnd.oci.image.manifest.v1+json",
627 "digest": manifest_digest(&tool.bytes),
628 "size": tool.bytes.len(),
629 "annotations": annotations,
630 })
631 })
632 .collect();
633 let mut entries = entries;
638 for inc in &spec.includes {
639 let mut annotations = serde_json::Map::new();
640 annotations.insert(
641 crate::kind::ANN_KIND.into(),
642 crate::kind::PayloadKind::Layer.as_str().into(),
643 );
644 if let Some(realm) = &inc.realm {
645 annotations.insert(
646 crate::compose::ANN_INCLUDE_REALM.into(),
647 realm.clone().into(),
648 );
649 }
650 if let Some(layer) = &inc.layer {
651 annotations.insert(
652 crate::compose::ANN_INCLUDE_LAYER.into(),
653 layer.clone().into(),
654 );
655 }
656 entries.push(serde_json::json!({
657 "mediaType": "application/vnd.oci.image.index.v1+json",
658 "digest": inc.digest,
659 "size": 0,
660 "annotations": annotations,
661 }));
662 }
663 if spec.channel.parse::<crate::pin::Channel>().is_err() {
667 return Err(DepositError::BadChannel {
668 channel: spec.channel.clone(),
669 });
670 }
671 if !is_rfc3339(&spec.issued_at) {
672 return Err(DepositError::BadIssuedAt {
673 issued_at: spec.issued_at.clone(),
674 });
675 }
676 let payload_json = serde_json::json!({
677 "schemaVersion": 2,
678 "mediaType": "application/vnd.oci.image.index.v1+json",
679 "artifactType": "application/vnd.pulseengine.varve.layer.v1+json",
680 "annotations": {
681 "eu.pulseengine.varve.layer": spec.layer.to_string(),
682 "eu.pulseengine.varve.line": spec.layer.line().to_string(),
683 "eu.pulseengine.varve.channel": spec.channel,
684 "eu.pulseengine.varve.counter": spec.counter.to_string(),
685 "org.opencontainers.image.created": spec.issued_at,
686 },
687 "manifests": entries,
688 });
689 let payload = serde_json::to_vec_pretty(&payload_json).expect("payload serializes");
690 let envelope = sign_layer_manifest(&payload, signing_key, key_id)?;
691 let public = &signing_key[32..];
695 match crate::verify::dsse_verify_typed(
696 envelope.as_bytes(),
697 crate::verify::LAYER_PAYLOAD_TYPE,
698 public,
699 ) {
700 Ok(back) if back == payload => {}
701 _ => return Err(DepositError::SelfVerifyFailed),
702 }
703
704 let blobs: Vec<(String, Vec<u8>)> = tools
705 .iter()
706 .map(|tool| (manifest_digest(&tool.bytes), tool.bytes.clone()))
707 .collect();
708 crate::archive::write_oci_layout(
709 &payload,
710 envelope.as_bytes(),
711 &blobs,
712 &spec.layer.to_string(),
713 &spec.channel,
714 None,
717 dest,
718 options.force,
719 )
720 .map_err(DepositError::from)?;
721
722 Ok(DepositOutcome {
723 digest: manifest_digest(&payload),
724 layer: spec.layer.clone(),
725 counter: spec.counter,
726 })
727}
728
729#[cfg(test)]
730mod producer_tests {
731 use super::*;
732
733 #[test]
735 fn a_channel_no_pin_could_name_is_refused_before_signing() {
736 assert!("qualified".parse::<crate::pin::Channel>().is_ok());
740 assert!("rolling".parse::<crate::pin::Channel>().is_ok());
741 for bad in ["stable", "Qualified", "", "beta"] {
742 assert!(
743 bad.parse::<crate::pin::Channel>().is_err(),
744 "{bad} must be refused"
745 );
746 }
747 }
748
749 #[test]
751 fn a_deposit_that_would_drop_a_referrer_is_refused_before_a_byte_is_written() {
752 use crate::verify::generate_root_keypair;
753 let (sk, _) = generate_root_keypair();
754 let tmp = tempfile::tempdir().unwrap();
755 let dest = tmp.path().join("layout");
756 let spec = super::tests::spec();
757 deposit(&spec, &sk, "k", &dest).unwrap();
758 let status = crate::linestatus::LineStatus {
761 min_counter: None,
762 line: "2026.08".into(),
763 counter: 1,
764 issued_at: "2026-08-07T00:00:00Z".into(),
765 support_until: None,
766 yanked: Default::default(),
767 known_problems: Vec::new(),
768 };
769 crate::linestatus::attach_to_layout(
770 &dest,
771 &"2026.08".parse().unwrap(),
772 status.sign(&sk, "k").unwrap().as_bytes(),
773 )
774 .unwrap();
775
776 let index_before = std::fs::read(dest.join("index.json")).unwrap();
777 let err = deposit(&spec, &sk, "k", &dest).expect_err("must refuse");
778 assert!(
779 matches!(&err, DepositError::WouldDestroySignedWork { .. }),
780 "got: {err}"
781 );
782 let msg = err.to_string();
783 assert!(
784 msg.contains("line-status") && msg.contains("2026.08"),
785 "{msg}"
786 );
787 assert!(msg.contains("varve attach-status"), "{msg}");
788 assert_eq!(
789 index_before,
790 std::fs::read(dest.join("index.json")).unwrap(),
791 "a refused deposit must not have touched the layout"
792 );
793
794 deposit_with_options(&spec, &sk, "k", &dest, &DepositOptions { force: true })
797 .expect("--force overrides the guard");
798 assert!(
799 crate::linestatus::read_any_from_layout(&dest)
800 .unwrap()
801 .is_none(),
802 "--force is destructive on purpose — the referrer is gone"
803 );
804 }
805
806 #[test]
808 fn depositing_into_a_fresh_or_previously_clean_directory_still_works() {
809 use crate::verify::generate_root_keypair;
814 let (sk, _) = generate_root_keypair();
815 let tmp = tempfile::tempdir().unwrap();
816 let dest = tmp.path().join("layout");
817 let spec = super::tests::spec();
818 let first = deposit(&spec, &sk, "k", &dest).unwrap();
819 let second = deposit(&spec, &sk, "k", &dest).expect("a clean layout may be re-deposited");
820 assert_eq!(first.digest, second.digest);
821 }
822
823 #[test]
825 fn issued_at_must_be_a_timestamp_not_merely_a_date() {
826 assert!(is_rfc3339("2026-08-01T00:00:00Z"));
830 assert!(is_rfc3339("2026-08-01T12:34:56.789Z"));
831 assert!(is_rfc3339("2026-08-01T12:34:56+02:00"));
832 for bad in [
833 "2026-10-01",
834 "not-a-date",
835 "",
836 "2026-08-01T",
837 "2026-13-01T00:00:00Z",
838 ] {
839 assert!(!is_rfc3339(bad), "{bad} must be refused");
840 }
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847 use crate::archive::OciLayoutSource;
848 use crate::install::{InstallPolicy, install};
849 use crate::pin::Pin;
850 use crate::rollback::HighWaterMarks;
851 use crate::store::Store;
852 use crate::verify::{PinnedKeyVerifier, generate_root_keypair};
853
854 pub(super) fn spec() -> DepositSpec {
855 DepositSpec {
856 includes: Vec::new(),
857 layer: "2026.08.0".parse().unwrap(),
858 channel: "qualified".into(),
859 counter: 1,
860 issued_at: "2026-08-07T00:00:00Z".into(),
861 tools: vec![
862 DepositTool {
863 name: "synth".into(),
864 version: "0.45.0".into(),
865 platform: None,
866 bytes: b"synth-bytes".to_vec(),
867 source: None,
868 runner: None,
869 kind: None,
870 sdk_prefix: None,
871 },
872 DepositTool {
873 name: "rivet".into(),
874 version: "0.32.0".into(),
875 platform: None,
876 bytes: b"rivet-bytes".to_vec(),
877 source: None,
878 runner: None,
879 kind: None,
880 sdk_prefix: None,
881 },
882 ],
883 }
884 }
885
886 #[test]
888 fn a_deposit_round_trips_through_the_standard_install_pipeline() {
889 let (sk, pk) = generate_root_keypair();
890 let tmp = tempfile::tempdir().unwrap();
891 let dest = tmp.path().join("deposit");
892 let outcome = deposit(&spec(), &sk, "varve-root-1", &dest).unwrap();
893 assert_eq!(outcome.layer.to_string(), "2026.08.0");
894
895 assert!(dest.join("oci-layout").is_file());
897 assert!(dest.join("index.json").is_file());
898
899 let pin = Pin::parse(
901 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.08.0\"\n",
902 "varve.toml",
903 )
904 .unwrap();
905 let root = tmp.path().join("fresh");
906 let store = Store::at(&root);
907 let mut marks = HighWaterMarks::load(&root).unwrap();
908 let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
909 let policy = InstallPolicy {
910 index: None,
911 now: "2026-08-07T00:00:00Z",
912 staleness_threshold_days: 90,
913 platform: "test-platform",
914 };
915 let installed = install(
916 &pin,
917 &OciLayoutSource::at(&dest),
918 &verifier,
919 &store,
920 &mut marks,
921 &policy,
922 )
923 .unwrap();
924 assert_eq!(installed.digest, outcome.digest);
925 assert_eq!(installed.counter, 1);
926 let entry = store.get(&installed.digest).unwrap().unwrap();
927 let checked =
928 crate::reverify::verify_installed(&store, &entry, &verifier, "test-platform").unwrap();
929 assert_eq!(checked, 2);
930 }
931
932 #[test]
934 fn the_payload_records_layer_line_channel_counter_and_tool_versions() {
935 let (sk, _) = generate_root_keypair();
936 let tmp = tempfile::tempdir().unwrap();
937 let dest = tmp.path().join("deposit");
938 let outcome = deposit(&spec(), &sk, "varve-root-1", &dest).unwrap();
939 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
940 let payload = std::fs::read(dest.join("blobs/sha256").join(hex)).unwrap();
941 let json: serde_json::Value = serde_json::from_slice(&payload).unwrap();
942 let ann = &json["annotations"];
943 assert_eq!(ann["eu.pulseengine.varve.layer"], "2026.08.0");
944 assert_eq!(ann["eu.pulseengine.varve.line"], "2026.08");
945 assert_eq!(ann["eu.pulseengine.varve.channel"], "qualified");
946 assert_eq!(ann["eu.pulseengine.varve.counter"], "1");
947 assert_eq!(
948 ann["org.opencontainers.image.created"],
949 "2026-08-07T00:00:00Z"
950 );
951 let entries = json["manifests"].as_array().unwrap();
952 assert_eq!(entries.len(), 2);
953 assert!(entries.iter().any(|e| {
954 e["annotations"]["eu.pulseengine.tool"] == "synth"
955 && e["annotations"]["eu.pulseengine.tool.version"] == "0.45.0"
956 }));
957 }
958
959 #[test]
961 fn identical_specs_deposit_identical_digests() {
962 let (sk, _) = generate_root_keypair();
963 let tmp = tempfile::tempdir().unwrap();
964 let a = deposit(&spec(), &sk, "varve-root-1", &tmp.path().join("a")).unwrap();
965 let b = deposit(&spec(), &sk, "varve-root-1", &tmp.path().join("b")).unwrap();
966 assert_eq!(
967 a.digest, b.digest,
968 "the payload is deterministic — the digest IS the identity"
969 );
970 }
971
972 #[test]
974 fn empty_and_duplicate_tool_lists_are_refused() {
975 let (sk, _) = generate_root_keypair();
976 let tmp = tempfile::tempdir().unwrap();
977 let mut empty = spec();
978 empty.tools.clear();
979 assert!(matches!(
980 deposit(&empty, &sk, "k", &tmp.path().join("x")).unwrap_err(),
981 DepositError::NoTools
982 ));
983 let mut dup = spec();
984 dup.tools[1].name = "synth".into();
985 let err = deposit(&dup, &sk, "k", &tmp.path().join("y")).unwrap_err();
986 assert!(
987 matches!(&err, DepositError::DuplicateTool { name, .. } if name == "synth"),
988 "got: {err}"
989 );
990 }
991
992 fn ingested(name: &str, repo: &str, proof: crate::ingest::IngestProof) -> DepositTool {
995 let mut tool = payload(name, "1.0.0", None, Some("x86_64-unknown-linux-gnu"));
996 tool.source = Some(ToolSource {
997 repo: repo.into(),
998 release: "v1.0.0".into(),
999 asset: format!("{name}-v1.0.0-x86_64-unknown-linux-gnu.tar.gz"),
1000 sha256: "a".repeat(64),
1001 proof: Some(proof),
1002 proof_signer: match proof {
1003 crate::ingest::IngestProof::Unverified => None,
1004 _ => Some(format!(
1005 "https://github.com/{repo}/.github/workflows/release.yml@refs/tags/v1.0.0"
1006 )),
1007 },
1008 proof_asserts: Some(match proof {
1009 crate::ingest::IngestProof::CosignSums => {
1010 "SHA256SUMS.txt signed for this repo".to_string()
1011 }
1012 crate::ingest::IngestProof::BuildProvenance => {
1013 "built from source commit deadbeef".to_string()
1014 }
1015 crate::ingest::IngestProof::Unverified => {
1016 "NOTHING — operator opt-in: needed for the 2026.09 bring-up".to_string()
1017 }
1018 }),
1019 });
1020 tool
1021 }
1022
1023 #[test]
1025 fn the_mechanism_that_vouched_for_each_payload_is_inside_the_signed_layer() {
1026 use crate::ingest::{ANN_PROOF, ANN_PROOF_ASSERTS, ANN_PROOF_SIGNER, IngestProof};
1032 let (sk, _) = generate_root_keypair();
1033 let tmp = tempfile::tempdir().unwrap();
1034 let mut spec = spec();
1035 spec.tools = vec![
1036 ingested("rivet", "pulseengine/rivet", IngestProof::CosignSums),
1037 ingested(
1038 "wasm-tools",
1039 "bytecodealliance/wasm-tools",
1040 IngestProof::BuildProvenance,
1041 ),
1042 ingested(
1043 "wit-bindgen",
1044 "bytecodealliance/wit-bindgen",
1045 IngestProof::Unverified,
1046 ),
1047 ];
1048 let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
1049 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
1050 let bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
1051 let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
1052 let entries = json["manifests"].as_array().unwrap();
1053 let by_name = |n: &str| {
1054 entries
1055 .iter()
1056 .find(|e| e["annotations"]["eu.pulseengine.tool"] == n)
1057 .unwrap_or_else(|| panic!("{n} is not in the payload"))
1058 .clone()
1059 };
1060
1061 let rivet = by_name("rivet");
1062 assert_eq!(rivet["annotations"][ANN_PROOF], "cosign-sums");
1063 assert_eq!(
1064 rivet["annotations"][ANN_PROOF_SIGNER],
1065 "https://github.com/pulseengine/rivet/.github/workflows/release.yml@refs/tags/v1.0.0"
1066 );
1067
1068 let wasm_tools = by_name("wasm-tools");
1069 assert_eq!(
1070 wasm_tools["annotations"][ANN_PROOF], "build-provenance",
1071 "an attested tool must not read as a cosign-signed one"
1072 );
1073 assert_eq!(
1074 wasm_tools["annotations"][ANN_PROOF_ASSERTS],
1075 "built from source commit deadbeef"
1076 );
1077
1078 let wit_bindgen = by_name("wit-bindgen");
1079 assert_eq!(wit_bindgen["annotations"][ANN_PROOF], "unverified");
1080 assert!(
1081 wit_bindgen["annotations"][ANN_PROOF_SIGNER].is_null(),
1082 "nothing vouched for it, so no signer may be named"
1083 );
1084 assert!(
1085 wit_bindgen["annotations"][ANN_PROOF_ASSERTS]
1086 .as_str()
1087 .unwrap()
1088 .contains("2026.09 bring-up"),
1089 "the recorded opt-in reason travels with the payload"
1090 );
1091 }
1092
1093 #[test]
1095 fn an_unproven_payload_may_not_be_signed_in_silently() {
1096 use crate::ingest::IngestProof;
1101 let (sk, _) = generate_root_keypair();
1102 let tmp = tempfile::tempdir().unwrap();
1103 let mut spec = spec();
1104 let mut tool = ingested(
1105 "wit-bindgen",
1106 "bytecodealliance/wit-bindgen",
1107 IngestProof::Unverified,
1108 );
1109 tool.source.as_mut().unwrap().proof_asserts = None;
1110 spec.tools = vec![tool];
1111 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
1112 assert!(
1113 matches!(&err, DepositError::UnverifiedWithoutReason { name, .. } if name == "wit-bindgen"),
1114 "got: {err}"
1115 );
1116
1117 let mut spec = super::tests::spec();
1120 let mut tool = ingested(
1121 "wit-bindgen",
1122 "bytecodealliance/wit-bindgen",
1123 IngestProof::Unverified,
1124 );
1125 tool.source.as_mut().unwrap().proof_signer = Some("https://github.com/someone/".into());
1126 spec.tools = vec![tool];
1127 let err = deposit(&spec, &sk, "k", &tmp.path().join("e")).unwrap_err();
1128 assert!(
1129 matches!(&err, DepositError::UnverifiedNamesASigner { name, .. } if name == "wit-bindgen"),
1130 "got: {err}"
1131 );
1132 }
1133
1134 #[test]
1136 fn proof_detail_without_a_mechanism_is_refused_rather_than_signed() {
1137 let (sk, _) = generate_root_keypair();
1142 let tmp = tempfile::tempdir().unwrap();
1143 let mut spec = spec();
1144 let mut tool = ingested(
1145 "wasm-tools",
1146 "bytecodealliance/wasm-tools",
1147 crate::ingest::IngestProof::BuildProvenance,
1148 );
1149 tool.source.as_mut().unwrap().proof = None;
1150 spec.tools = vec![tool];
1151 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
1152 assert!(
1153 matches!(&err, DepositError::ProofDetailWithoutMechanism { name, .. } if name == "wasm-tools"),
1154 "got: {err}"
1155 );
1156 }
1157
1158 #[test]
1160 fn a_layer_deposited_before_this_requirement_reads_as_unrecorded_not_as_verified() {
1161 use crate::ingest::IngestProof;
1166 use crate::manifest::ManifestEntry;
1167 let entry = ManifestEntry {
1168 digest: "sha256:abc".into(),
1169 annotations: Default::default(),
1170 };
1171 assert_eq!(entry.ingest_proof(), Ok(None));
1172 assert_eq!(IngestProof::label(None), "unrecorded");
1173
1174 let mut entry = entry;
1175 entry
1176 .annotations
1177 .insert(crate::ingest::ANN_PROOF.into(), "build-provenance".into());
1178 assert_eq!(entry.ingest_proof(), Ok(Some(IngestProof::BuildProvenance)));
1179 entry
1183 .annotations
1184 .insert(crate::ingest::ANN_PROOF.into(), "notary-v2".into());
1185 assert_eq!(
1186 entry.ingest_proof(),
1187 Err(crate::ingest::UnknownProof("notary-v2".into()))
1188 );
1189 }
1190
1191 fn payload(
1193 name: &str,
1194 version: &str,
1195 kind: Option<crate::kind::PayloadKind>,
1196 platform: Option<&str>,
1197 ) -> DepositTool {
1198 DepositTool {
1199 name: name.into(),
1200 version: version.into(),
1201 platform: platform.map(str::to_string),
1202 bytes: format!("{name}-{version}-bytes").into_bytes(),
1203 source: None,
1204 runner: None,
1205 kind,
1206 sdk_prefix: None,
1207 }
1208 }
1209
1210 #[test]
1212 fn two_versions_of_one_crate_are_a_layer_not_a_duplicate() {
1213 use crate::kind::PayloadKind;
1219 let (sk, _) = generate_root_keypair();
1220 let tmp = tempfile::tempdir().unwrap();
1221 let mut spec = spec();
1222 spec.tools = vec![
1223 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
1224 payload("serde", "1.0.210", Some(PayloadKind::Crate), None),
1225 ];
1226 let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
1227
1228 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
1230 let payload_bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
1231 let json: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap();
1232 let entries = json["manifests"].as_array().unwrap();
1233 assert_eq!(entries.len(), 2);
1234 let versions: Vec<&str> = entries
1235 .iter()
1236 .map(|e| {
1237 e["annotations"]["eu.pulseengine.tool.version"]
1238 .as_str()
1239 .unwrap()
1240 })
1241 .collect();
1242 assert_eq!(versions, vec!["1.0.200", "1.0.210"]);
1243 assert_ne!(
1244 entries[0]["digest"], entries[1]["digest"],
1245 "two versions are two artifacts"
1246 );
1247 }
1248
1249 #[test]
1251 fn extensions_deposit_as_vsix_entries_at_several_versions() {
1252 use crate::kind::PayloadKind;
1260 let (sk, _) = generate_root_keypair();
1261 let tmp = tempfile::tempdir().unwrap();
1262 let mut spec = spec();
1263 spec.tools = vec![
1264 payload(
1265 "rust-lang.rust-analyzer",
1266 "0.3.2260",
1267 Some(PayloadKind::Vsix),
1268 None,
1269 ),
1270 payload(
1271 "rust-lang.rust-analyzer",
1272 "0.3.2300",
1273 Some(PayloadKind::Vsix),
1274 None,
1275 ),
1276 payload(
1277 "vadimcn.vscode-lldb",
1278 "1.11.4",
1279 Some(PayloadKind::Vsix),
1280 None,
1281 ),
1282 ];
1283 let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
1284
1285 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
1286 let payload_bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
1287 let json: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap();
1288 let entries = json["manifests"].as_array().unwrap();
1289 assert_eq!(entries.len(), 3, "all three extensions must be signed in");
1290 for e in entries {
1291 assert_eq!(
1292 e["annotations"][crate::kind::ANN_KIND],
1293 "vsix",
1294 "every entry must carry the vsix kind in the SIGNED payload"
1295 );
1296 }
1297 let ids: Vec<(&str, &str)> = entries
1298 .iter()
1299 .map(|e| {
1300 (
1301 e["annotations"]["eu.pulseengine.tool"].as_str().unwrap(),
1302 e["annotations"]["eu.pulseengine.tool.version"]
1303 .as_str()
1304 .unwrap(),
1305 )
1306 })
1307 .collect();
1308 assert_eq!(
1309 ids,
1310 vec![
1311 ("rust-lang.rust-analyzer", "0.3.2260"),
1312 ("rust-lang.rust-analyzer", "0.3.2300"),
1313 ("vadimcn.vscode-lldb", "1.11.4"),
1314 ]
1315 );
1316 assert_ne!(
1317 entries[0]["digest"], entries[1]["digest"],
1318 "two versions of one extension are two artifacts"
1319 );
1320
1321 let mut dup = spec;
1323 dup.tools = vec![
1324 payload("pub.ext", "1.0.0", Some(PayloadKind::Vsix), None),
1325 payload("pub.ext", "1.0.0", Some(PayloadKind::Vsix), None),
1326 ];
1327 let err = deposit(&dup, &sk, "k", &tmp.path().join("e")).unwrap_err();
1328 assert!(
1329 matches!(&err, DepositError::DuplicatePayload { name, version, .. }
1330 if name == "pub.ext" && version == "1.0.0"),
1331 "got: {err}"
1332 );
1333 }
1334
1335 #[test]
1337 fn a_tool_may_not_appear_twice_under_one_name_however_its_versions_differ() {
1338 use crate::kind::PayloadKind;
1344 let (sk, _) = generate_root_keypair();
1345 let tmp = tempfile::tempdir().unwrap();
1346 let mut spec = spec();
1347 spec.tools = vec![
1348 payload("synth", "0.45.0", Some(PayloadKind::Tool), None),
1349 payload("synth", "0.46.0", None, None), ];
1351 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
1352 let msg = err.to_string();
1353 assert!(
1354 matches!(&err, DepositError::DuplicateTool { name, .. } if name == "synth"),
1355 "got: {err}"
1356 );
1357 assert!(msg.contains("0.45.0") && msg.contains("0.46.0"), "{msg}");
1358 assert!(
1362 msg.contains("any"),
1363 "an unstamped entry is any-platform: {msg}"
1364 );
1365 let mut stamped = spec;
1366 for tool in &mut stamped.tools {
1367 tool.platform = Some("x86_64-unknown-linux-gnu".into());
1368 }
1369 let msg = deposit(&stamped, &sk, "k", &tmp.path().join("e"))
1370 .unwrap_err()
1371 .to_string();
1372 assert!(msg.contains("x86_64-unknown-linux-gnu"), "{msg}");
1373 }
1374
1375 #[test]
1377 fn one_version_deposited_twice_is_still_refused_and_the_error_names_it() {
1378 use crate::kind::PayloadKind;
1383 let (sk, _) = generate_root_keypair();
1384 let tmp = tempfile::tempdir().unwrap();
1385 let mut spec = spec();
1386 spec.tools = vec![
1387 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
1388 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
1389 ];
1390 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
1391 let msg = err.to_string();
1392 assert!(
1393 matches!(&err, DepositError::DuplicatePayload { name, version, .. }
1394 if name == "serde" && version == "1.0.200"),
1395 "got: {err}"
1396 );
1397 assert!(msg.contains("serde") && msg.contains("1.0.200"), "{msg}");
1398 }
1399
1400 #[test]
1402 fn platform_still_separates_identities_for_both_rules() {
1403 use crate::kind::PayloadKind;
1408 let (sk, _) = generate_root_keypair();
1409 let tmp = tempfile::tempdir().unwrap();
1410 let mut spec = spec();
1411 spec.tools = vec![
1412 payload("synth", "0.45.0", None, Some("aarch64-apple-darwin")),
1413 payload("synth", "0.45.0", None, Some("x86_64-unknown-linux-gnu")),
1414 payload(
1415 "serde",
1416 "1.0.200",
1417 Some(PayloadKind::Crate),
1418 Some("aarch64-apple-darwin"),
1419 ),
1420 payload(
1421 "serde",
1422 "1.0.200",
1423 Some(PayloadKind::Crate),
1424 Some("x86_64-unknown-linux-gnu"),
1425 ),
1426 ];
1427 deposit(&spec, &sk, "k", &tmp.path().join("d")).expect(
1428 "distinct platforms, distinct
1429 identities",
1430 );
1431 }
1432
1433 #[test]
1435 fn the_signed_digest_does_not_depend_on_the_order_versions_are_listed_in() {
1436 use crate::kind::PayloadKind;
1442 let (sk, _) = generate_root_keypair();
1443 let tmp = tempfile::tempdir().unwrap();
1444 let a_first = vec![
1445 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
1446 payload("serde", "1.0.210", Some(PayloadKind::Crate), None),
1447 ];
1448 let b_first: Vec<DepositTool> = a_first.iter().rev().cloned().collect();
1449 let mut s1 = spec();
1450 s1.tools = a_first;
1451 let mut s2 = spec();
1452 s2.tools = b_first;
1453 assert_eq!(
1454 deposit(&s1, &sk, "k", &tmp.path().join("a"))
1455 .unwrap()
1456 .digest,
1457 deposit(&s2, &sk, "k", &tmp.path().join("b"))
1458 .unwrap()
1459 .digest,
1460 );
1461 }
1462}