scrollcase_consumer/
verify.rs1use std::collections::BTreeSet;
10use std::path::{Path, PathBuf};
11
12use crate::archive::{list_zip_entries, read_zip_entry_text, ArchiveEntry};
13use crate::contract::documents::SignedDocument;
14use crate::contract::links::EntryKind;
15use crate::contract::targets::{assert_python_entry_point, box_target_adapter, BoxTargetAdapter};
16use crate::error::{fail, Error, Result};
17use crate::execution::assert_execution_files;
18use crate::filesystem::sha256_file;
19use crate::release::{BoxManifest, ReleaseManifest};
20use crate::trust::{load_trusted_keys, verify_signed_document, TrustedKey};
21
22#[derive(Debug, Clone)]
24pub struct InspectedRelease {
25 pub release_path: PathBuf,
27 pub signed: SignedDocument,
29 pub release: ReleaseManifest,
31 pub adapter: &'static BoxTargetAdapter,
33}
34
35pub fn inspect_release_document(
47 release_document_path: &Path,
48 public_key_path: &Path,
49) -> Result<InspectedRelease> {
50 let trusted = load_trusted_keys(public_key_path)?;
51 inspect_release_document_with_keys(release_document_path, &trusted)
52}
53
54pub fn inspect_release_document_with_keys(
60 release_document_path: &Path,
61 trusted: &[TrustedKey],
62) -> Result<InspectedRelease> {
63 let release_path = release_document_path
64 .canonicalize()
65 .unwrap_or_else(|_| release_document_path.to_path_buf());
66 let raw = std::fs::read(&release_path).map_err(|error| {
67 Error::new(format!(
68 "Invalid signed release document {}: {error}",
69 release_path.display()
70 ))
71 })?;
72
73 let signed = SignedDocument::parse(&raw)?;
74 let payload = verify_signed_document(&signed, trusted)?;
75
76 if payload.value.get("schemaVersion").and_then(serde_json::Value::as_u64) == Some(1) {
78 fail!("Unsupported schemaVersion 1; rebuild this box with Scrollcase v2.");
79 }
80 let release: ReleaseManifest = serde_json::from_value(payload.value)
81 .map_err(|error| Error::new(format!("Invalid release manifest: {error}.")))?;
82 release.validate()?;
83
84 let adapter = box_target_adapter(&release.target)?;
85 assert_python_entry_point(adapter, &release.python_entry_point)?;
86
87 Ok(InspectedRelease {
88 release_path,
89 signed,
90 release,
91 adapter,
92 })
93}
94
95pub fn assert_box_manifest_agreement(
106 box_manifest: &BoxManifest,
107 release: &ReleaseManifest,
108) -> Result<()> {
109 let mismatch = if box_manifest.schema_version != release.schema_version {
113 Some("schemaVersion")
114 } else if box_manifest.box_id != release.box_id {
115 Some("boxId")
116 } else if box_manifest.model_id != release.model_id {
117 Some("modelId")
118 } else if box_manifest.runtime_id != release.runtime_id {
119 Some("runtimeId")
120 } else if box_manifest.version != release.version {
121 Some("version")
122 } else if box_manifest.target != release.target {
123 Some("target")
124 } else if box_manifest.python_entry_point != release.python_entry_point {
125 Some("pythonEntryPoint")
126 } else if box_manifest.model_cache_subdir != release.model_cache_subdir {
127 Some("modelCacheSubdir")
128 } else if box_manifest.environment != release.environment {
129 Some("environment")
130 } else if box_manifest.self_test != release.self_test {
131 Some("selfTest")
132 } else if box_manifest.execution != release.execution {
133 Some("execution")
134 } else if box_manifest.weights != release.weights {
135 Some("weights")
136 } else if box_manifest.assets != release.assets {
137 Some("assets")
138 } else if box_manifest.provenance != release.provenance {
139 Some("provenance")
140 } else {
141 None
142 };
143 if let Some(field) = mismatch {
144 fail!("box.json mismatch: {field}");
145 }
146 Ok(())
147}
148
149#[derive(Debug, Clone)]
151pub struct InspectedArchive {
152 pub release: InspectedRelease,
154 pub archive_path: PathBuf,
156 pub box_manifest: BoxManifest,
158 pub entries: Vec<ArchiveEntry>,
160}
161
162pub fn inspect_box_archive(
173 release_document_path: &Path,
174 public_key_path: &Path,
175 archive_override: Option<&Path>,
176) -> Result<InspectedArchive> {
177 let release = inspect_release_document(release_document_path, public_key_path)?;
178 inspect_archive_for(release, archive_override)
179}
180
181pub fn inspect_archive_for(
187 inspected: InspectedRelease,
188 archive_override: Option<&Path>,
189) -> Result<InspectedArchive> {
190 let release = &inspected.release;
191 let archive_path = match archive_override {
195 Some(path) => path.to_path_buf(),
196 None => inspected
197 .release_path
198 .parent()
199 .unwrap_or(Path::new("."))
200 .join(format!("{}.zip", release.archive.sha256)),
201 };
202 let metadata = std::fs::metadata(&archive_path)
203 .map_err(|_| Error::new(format!("Archive not found: {}", archive_path.display())))?;
204 if metadata.len() != release.archive.size_bytes {
205 fail!("Archive size mismatch.");
206 }
207 if sha256_file(&archive_path)? != release.archive.sha256 {
208 fail!("Archive SHA-256 mismatch.");
209 }
210
211 let entries = list_zip_entries(&archive_path)?;
212 let files: BTreeSet<String> = entries
216 .iter()
217 .filter(|entry| entry.kind == EntryKind::File)
218 .map(|entry| entry.path.clone())
219 .collect();
220 let resolvable: BTreeSet<String> = entries
221 .iter()
222 .filter(|entry| matches!(entry.kind, EntryKind::File | EntryKind::Link))
223 .map(|entry| entry.path.clone())
224 .collect();
225
226 if !files.contains("box.json") {
227 fail!("Archive is missing box.json.");
228 }
229 let raw = read_zip_entry_text(&archive_path, "box.json")?;
230 let box_manifest: BoxManifest = serde_json::from_str(&raw)
231 .map_err(|error| Error::new(format!("Invalid box.json: {error}.")))?;
232 assert_box_manifest_agreement(&box_manifest, release)?;
233
234 if !resolvable.contains(&release.python_entry_point) {
235 fail!("Archive is missing {}.", release.python_entry_point);
236 }
237 assert_execution_files(
238 release.execution.as_ref(),
239 inspected.adapter,
240 &release.provenance.python_version,
241 &resolvable,
242 )?;
243
244 Ok(InspectedArchive {
245 release: inspected,
246 archive_path,
247 box_manifest,
248 entries,
249 })
250}