1use crate::install::{ManifestVerifier, VerifyError};
11use crate::manifest::{LayerManifest, ManifestError};
12use crate::store::{InstalledLayer, Store, StoreError, manifest_digest};
13
14pub const ENVELOPE_FILE: &str = "layer.dsse.json";
17
18#[derive(Debug, thiserror::Error)]
19pub enum ReverifyError {
20 #[error(
21 "layer {digest} has no retained signature envelope ({ENVELOPE_FILE}) — cannot re-verify \
22 its signature; reinstall from a signed source"
23 )]
24 NoEnvelope { digest: String },
25 #[error(transparent)]
26 Verify(#[from] VerifyError),
27 #[error(
28 "retained envelope verifies, but its payload does not match layer.json — the core entry \
29 was modified after install"
30 )]
31 PayloadMismatch,
32 #[error(transparent)]
33 Manifest(#[from] ManifestError),
34 #[error("payload '{tool}' is missing from the installed layer")]
35 MissingTool { tool: String },
36 #[error("payload '{tool}' does not match its signed digest {digest} — its bytes were altered")]
37 ToolDigestMismatch { tool: String, digest: String },
38 #[error(transparent)]
39 Store(#[from] StoreError),
40 #[error("io error at {path}: {source}")]
41 Io {
42 path: String,
43 #[source]
44 source: std::io::Error,
45 },
46}
47
48fn named(entry: &crate::manifest::ManifestEntry, name: &str) -> String {
53 match crate::store::entry_version(entry) {
54 Some(version) => format!("{name}@{version}"),
55 None => name.to_string(),
56 }
57}
58
59pub fn verify_installed(
62 store: &Store,
63 layer: &InstalledLayer,
64 verifier: &dyn ManifestVerifier,
65 platform: &str,
66) -> Result<usize, ReverifyError> {
67 let io = |path: &std::path::Path, source: std::io::Error| ReverifyError::Io {
68 path: path.display().to_string(),
69 source,
70 };
71
72 let envelope_path = layer.root.join(ENVELOPE_FILE);
74 let envelope = match std::fs::read(&envelope_path) {
75 Ok(bytes) => bytes,
76 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
77 return Err(ReverifyError::NoEnvelope {
78 digest: layer.digest.clone(),
79 });
80 }
81 Err(e) => return Err(io(&envelope_path, e)),
82 };
83 let payload = verifier.verify(&envelope)?;
84
85 let manifest_path = layer.root.join("layer.json");
87 let stored = std::fs::read(&manifest_path).map_err(|e| io(&manifest_path, e))?;
88 if payload != stored {
89 return Err(ReverifyError::PayloadMismatch);
90 }
91
92 let manifest = LayerManifest::parse(&payload)?;
94 let mut checked = 0;
95 for entry in &manifest.entries {
96 if !crate::platform::entry_matches(
97 entry
98 .annotations
99 .get(crate::platform::ANN_PLATFORM)
100 .map(String::as_str),
101 platform,
102 ) {
103 continue;
104 }
105 if entry.kind() == Ok(crate::kind::PayloadKind::Layer) {
108 continue;
109 }
110 let Some(tool) = entry.annotations.get("eu.pulseengine.tool") else {
111 continue;
112 };
113 let Some(path) = store.entry_path(layer, entry) else {
117 return Err(ReverifyError::MissingTool {
118 tool: named(entry, tool),
119 });
120 };
121 let bytes = std::fs::read(&path).map_err(|e| io(&path, e))?;
122 if manifest_digest(&bytes) != entry.digest {
123 return Err(ReverifyError::ToolDigestMismatch {
124 tool: named(entry, tool),
125 digest: entry.digest.clone(),
126 });
127 }
128 checked += 1;
129 }
130 Ok(checked)
131}
132
133#[cfg(test)]
134mod tests {
135 #[cfg(unix)]
139 fn premise_unavailable() -> bool {
140 use std::os::unix::fs::PermissionsExt;
141 let Ok(dir) = tempfile::tempdir() else {
142 return true;
143 };
144 let probe = dir.path().join("probe");
145 if std::fs::write(&probe, b"x").is_err() {
146 return true;
147 }
148 if std::fs::set_permissions(&probe, std::fs::Permissions::from_mode(0o000)).is_err() {
149 return true;
150 }
151 let readable = std::fs::read(&probe).is_ok();
152 let _ = std::fs::set_permissions(&probe, std::fs::Permissions::from_mode(0o644));
153 readable
154 }
155
156 use super::*;
157 use crate::install::{InstallPolicy, install};
158 use crate::manifest::fixtures::manifest_with_tools;
159 use crate::pin::Pin;
160 use crate::rollback::HighWaterMarks;
161 use crate::source::MemorySource;
162 use crate::verify::{PinnedKeyVerifier, generate_root_keypair, sign_layer_manifest};
163
164 struct Installed {
165 _tmp: tempfile::TempDir,
166 store: Store,
167 layer: InstalledLayer,
168 verifier: PinnedKeyVerifier,
169 }
170
171 fn installed_layer() -> Installed {
172 let (sk, pk) = generate_root_keypair();
173 let synth = b"synth-bytes".to_vec();
174 let blob_digest = manifest_digest(&synth);
175 let payload = manifest_with_tools(
176 "2026.07.0",
177 "qualified",
178 1,
179 "2026-07-31T09:14:00Z",
180 &[("synth", &blob_digest)],
181 );
182 let envelope = sign_layer_manifest(&payload, &sk, "varve-root-1").unwrap();
183 let source = MemorySource::new()
184 .with_manifest(envelope.as_bytes())
185 .with_blob(&blob_digest, &synth);
186 let pin = Pin::parse(
187 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\n",
188 "varve.toml",
189 )
190 .unwrap();
191 let tmp = tempfile::tempdir().unwrap();
192 let root = tmp.path().join("root");
193 let store = Store::at(&root);
194 let mut marks = HighWaterMarks::load(&root).unwrap();
195 let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
196 let policy = InstallPolicy {
197 index: None,
198 now: "2026-08-07T00:00:00Z",
199 staleness_threshold_days: 90,
200 platform: "test-platform",
201 };
202 let outcome = install(&pin, &source, &verifier, &store, &mut marks, &policy).unwrap();
203 let layer = store.get(&outcome.digest).unwrap().unwrap();
204 Installed {
205 _tmp: tmp,
206 store,
207 layer,
208 verifier,
209 }
210 }
211
212 #[test]
214 fn a_freshly_installed_layer_reverifies() {
215 let ctx = installed_layer();
216 let checked =
217 verify_installed(&ctx.store, &ctx.layer, &ctx.verifier, "test-platform").unwrap();
218 assert_eq!(checked, 1);
219 }
220
221 fn installed_two_versions() -> (Installed, Vec<u8>, Vec<u8>) {
223 use crate::manifest::fixtures::manifest_with_payloads;
224 let (sk, pk) = generate_root_keypair();
225 let a = b"serde-1.0.200-crate".to_vec();
226 let b = b"serde-1.0.210-crate".to_vec();
227 let (da, db) = (manifest_digest(&a), manifest_digest(&b));
228 let payload = manifest_with_payloads(
229 "2026.07.0",
230 "qualified",
231 1,
232 "2026-07-31T09:14:00Z",
233 &[
234 ("serde", "1.0.200", "crate", &da),
235 ("serde", "1.0.210", "crate", &db),
236 ],
237 );
238 let envelope = sign_layer_manifest(&payload, &sk, "varve-root-1").unwrap();
239 let source = MemorySource::new()
240 .with_manifest(envelope.as_bytes())
241 .with_blob(&da, &a)
242 .with_blob(&db, &b);
243 let pin = Pin::parse(
244 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\n",
245 "varve.toml",
246 )
247 .unwrap();
248 let tmp = tempfile::tempdir().unwrap();
249 let root = tmp.path().join("root");
250 let store = Store::at(&root);
251 let mut marks = HighWaterMarks::load(&root).unwrap();
252 let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
253 let policy = InstallPolicy {
254 index: None,
255 now: "2026-08-07T00:00:00Z",
256 staleness_threshold_days: 90,
257 platform: "test-platform",
258 };
259 let outcome = install(&pin, &source, &verifier, &store, &mut marks, &policy).unwrap();
260 let layer = store.get(&outcome.digest).unwrap().unwrap();
261 (
262 Installed {
263 _tmp: tmp,
264 store,
265 layer,
266 verifier,
267 },
268 a,
269 b,
270 )
271 }
272
273 #[test]
275 fn each_version_of_one_name_is_checked_against_its_own_signed_digest() {
276 let (ctx, a, b) = installed_two_versions();
283 assert_eq!(
284 verify_installed(&ctx.store, &ctx.layer, &ctx.verifier, "test-platform").unwrap(),
285 2,
286 "both versions must be checked, not one file twice"
287 );
288 assert_eq!(
290 std::fs::read(ctx.layer.root.join("payloads/serde/1.0.200")).unwrap(),
291 a
292 );
293 assert_eq!(
294 std::fs::read(ctx.layer.root.join("payloads/serde/1.0.210")).unwrap(),
295 b
296 );
297
298 for (version, path) in [
299 ("1.0.200", "payloads/serde/1.0.200"),
300 ("1.0.210", "payloads/serde/1.0.210"),
301 ] {
302 let (ctx, ..) = installed_two_versions();
303 std::fs::write(ctx.layer.root.join(path), b"EVIL").unwrap();
304 let err = verify_installed(&ctx.store, &ctx.layer, &ctx.verifier, "test-platform")
305 .unwrap_err();
306 assert!(
307 matches!(&err, ReverifyError::ToolDigestMismatch { tool, .. }
308 if tool == &format!("serde@{version}")),
309 "tampering with {version} must be caught AND named: {err}"
310 );
311 }
312
313 let (ctx, ..) = installed_two_versions();
316 std::fs::remove_file(ctx.layer.root.join("payloads/serde/1.0.210")).unwrap();
317 let err =
318 verify_installed(&ctx.store, &ctx.layer, &ctx.verifier, "test-platform").unwrap_err();
319 assert!(
320 matches!(&err, ReverifyError::MissingTool { tool } if tool == "serde@1.0.210"),
321 "got: {err}"
322 );
323 }
324
325 #[test]
327 fn install_retains_the_envelope_for_offline_reverification() {
328 let ctx = installed_layer();
329 assert!(
330 ctx.layer.root.join(ENVELOPE_FILE).is_file(),
331 "install must retain the signature envelope"
332 );
333 }
334
335 #[test]
337 fn an_altered_tool_binary_is_detected() {
338 let ctx = installed_layer();
339 std::fs::write(ctx.layer.root.join("bin/synth"), b"EVIL").unwrap();
340 let err =
341 verify_installed(&ctx.store, &ctx.layer, &ctx.verifier, "test-platform").unwrap_err();
342 assert!(
343 matches!(err, ReverifyError::ToolDigestMismatch { ref tool, .. } if tool == "synth"),
344 "got: {err}"
345 );
346 }
347
348 #[test]
350 fn an_altered_manifest_payload_is_detected() {
351 let ctx = installed_layer();
352 let path = ctx.layer.root.join("layer.json");
353 let mut bytes = std::fs::read(&path).unwrap();
354 bytes.extend_from_slice(b" ");
355 std::fs::write(&path, bytes).unwrap();
356 let err =
357 verify_installed(&ctx.store, &ctx.layer, &ctx.verifier, "test-platform").unwrap_err();
358 assert!(matches!(err, ReverifyError::PayloadMismatch), "got: {err}");
359 }
360
361 #[cfg(unix)]
363 #[test]
364 fn an_unreadable_envelope_is_an_io_error_not_a_missing_one() {
365 if premise_unavailable() {
376 eprintln!("skipping: this environment does not deny reads on mode 000");
377 return;
378 }
379 use std::os::unix::fs::PermissionsExt;
380 let ctx = installed_layer();
381 let path = ctx.layer.root.join(ENVELOPE_FILE);
382 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap();
383 let err =
384 verify_installed(&ctx.store, &ctx.layer, &ctx.verifier, "test-platform").unwrap_err();
385 let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644));
386 assert!(
387 matches!(err, ReverifyError::Io { .. }),
388 "an unreadable envelope must be an Io error, got: {err}"
389 );
390 }
391
392 #[test]
394 fn a_missing_envelope_is_its_own_loud_verdict() {
395 let ctx = installed_layer();
396 std::fs::remove_file(ctx.layer.root.join(ENVELOPE_FILE)).unwrap();
397 let err =
398 verify_installed(&ctx.store, &ctx.layer, &ctx.verifier, "test-platform").unwrap_err();
399 assert!(
400 matches!(err, ReverifyError::NoEnvelope { .. }),
401 "got: {err}"
402 );
403 }
404}