jmix_rs/
package_validation.rs1use crate::{
2 assertion::AssertionManager,
3 encryption::DecryptionManager,
4 error::{JmixError, JmixResult},
5 types::{Audit, Files, Manifest, Metadata},
6 validation::SchemaValidator,
7};
8use sha2::{Digest, Sha256};
9use std::{
10 fs,
11 path::{Path, PathBuf},
12};
13
14#[derive(Debug, Clone, Default)]
15pub struct ValidationOptions {
16 pub schema_dir: Option<String>,
17 pub validate_schema: bool,
18 pub verify_assertions: bool,
19 pub recipient_secret_key_path: Option<PathBuf>,
20}
21
22#[derive(Debug, Clone, Default)]
23pub struct ValidationReport {
24 pub schema_ok: Option<bool>,
25 pub payload_hash_ok: Option<bool>,
26 pub assertions_ok: Option<bool>,
27 pub encryption_ok: Option<bool>,
28 pub errors: Vec<String>,
29}
30
31pub fn validate_package(
32 package_dir: &Path,
33 opts: &ValidationOptions,
34) -> JmixResult<ValidationReport> {
35 let mut report = ValidationReport::default();
36
37 let manifest_path = package_dir.join("manifest.json");
39 if !manifest_path.exists() {
40 return Err(JmixError::Other(format!(
41 "manifest.json not found at {}",
42 manifest_path.display()
43 )));
44 }
45
46 let manifest_str = fs::read_to_string(&manifest_path)?;
47 let manifest: Manifest = serde_json::from_str(&manifest_str).map_err(JmixError::Json)?;
48
49 let audit_path = package_dir.join("audit.json");
51 let audit: Option<Audit> = if audit_path.exists() {
52 Some(serde_json::from_str(&fs::read_to_string(&audit_path)?).map_err(JmixError::Json)?)
53 } else {
54 None
55 };
56
57 let is_encrypted = manifest.security.encryption.is_some();
59
60 if opts.validate_schema {
62 let validator = if let Some(dir) = &opts.schema_dir {
63 SchemaValidator::new(Some(dir.clone()))
64 } else {
65 SchemaValidator::with_default_config()
66 };
67 let mut schema_ok = true;
68 if let Err(e) = validator.validate_manifest(&manifest) {
69 schema_ok = false;
70 report.errors.push(format!("manifest schema: {}", e));
71 }
72 if let Some(a) = &audit {
73 if let Err(e) = validator.validate_audit(a) {
74 schema_ok = false;
75 report.errors.push(format!("audit schema: {}", e));
76 }
77 }
78 if !is_encrypted {
80 let payload_dir = package_dir.join("payload");
81 let metadata_path = payload_dir.join("metadata.json");
82 if metadata_path.exists() {
83 match serde_json::from_str::<Metadata>(&fs::read_to_string(&metadata_path)?) {
84 Ok(metadata) => {
85 if let Err(e) = validator.validate_metadata(&metadata) {
86 schema_ok = false;
87 report.errors.push(format!("metadata schema: {}", e));
88 }
89 }
90 Err(e) => {
91 schema_ok = false;
92 report.errors.push(format!("metadata parse: {}", e));
93 }
94 }
95 }
96 let files_path = payload_dir.join("files.json");
97 if files_path.exists() {
98 match serde_json::from_str::<Files>(&fs::read_to_string(&files_path)?) {
99 Ok(files) => {
100 if let Err(e) = validator.validate_files(&files) {
101 schema_ok = false;
102 report.errors.push(format!("files schema: {}", e));
103 }
104 }
105 Err(e) => {
106 schema_ok = false;
107 report.errors.push(format!("files parse: {}", e));
108 }
109 }
110 }
111 }
112 report.schema_ok = Some(schema_ok);
113 }
114
115 if is_encrypted {
117 match (
119 &manifest.security.encryption,
120 &opts.recipient_secret_key_path,
121 ) {
122 (Some(enc_info), Some(secret_path)) => {
123 let payload_enc_path = package_dir.join("payload.enc");
124 if !payload_enc_path.exists() {
125 report
126 .errors
127 .push("payload.enc missing for encrypted package".to_string());
128 report.encryption_ok = Some(false);
129 report.payload_hash_ok = Some(false);
130 } else {
131 let ciphertext = fs::read(&payload_enc_path)?;
132 let dec =
133 DecryptionManager::from_secret_key_file(secret_path).map_err(|e| {
134 JmixError::Other(format!("Failed to create decryption manager: {}", e))
135 })?;
136 match dec.decrypt(&ciphertext, enc_info) {
137 Ok(plaintext_tar) => {
138 report.encryption_ok = Some(true);
139 let mut hasher = Sha256::new();
141 hasher.update(&plaintext_tar);
142 let hash = format!("sha256:{:x}", hasher.finalize());
143 let ok = hash == manifest.security.payload_hash;
144 if !ok {
145 report
146 .errors
147 .push("payload hash mismatch (encrypted)".to_string());
148 }
149 report.payload_hash_ok = Some(ok);
150 }
151 Err(e) => {
152 report.encryption_ok = Some(false);
153 report.payload_hash_ok = Some(false);
154 report.errors.push(format!("decryption failed: {}", e));
155 }
156 }
157 }
158 }
159 (Some(_), None) => {
160 report.encryption_ok = None;
162 report.payload_hash_ok = None;
163 }
164 _ => {}
165 }
166 } else {
167 let payload_dir = package_dir.join("payload");
169 if payload_dir.exists() {
170 let ok = match compute_payload_hash_for_dir(&payload_dir) {
171 Ok(hash) => {
172 let eq = hash == manifest.security.payload_hash;
173 if !eq {
174 report
175 .errors
176 .push("payload hash mismatch (unencrypted)".to_string());
177 }
178 eq
179 }
180 Err(e) => {
181 report
182 .errors
183 .push(format!("payload hash compute error: {}", e));
184 false
185 }
186 };
187 report.payload_hash_ok = Some(ok);
188 } else {
189 report.payload_hash_ok = Some(false);
190 report.errors.push("payload/ directory missing".to_string());
191 }
192 }
193
194 if opts.verify_assertions {
196 let mut ok = true;
197 if let Some(assertion) = &manifest.sender.assertion {
199 match AssertionManager::verify_assertion(assertion, &manifest.sender, &manifest) {
200 Ok(crate::assertion::VerificationResult::Valid { .. }) => {}
201 Ok(_) => {
202 ok = false;
203 report.errors.push("sender assertion invalid".to_string());
204 }
205 Err(e) => {
206 ok = false;
207 report.errors.push(format!("sender assertion error: {}", e));
208 }
209 }
210 }
211 if let Some(requester) = &manifest.requester {
213 if let Some(assertion) = &requester.assertion {
214 match AssertionManager::verify_assertion(assertion, requester, &manifest) {
215 Ok(crate::assertion::VerificationResult::Valid { .. }) => {}
216 Ok(_) => {
217 ok = false;
218 report
219 .errors
220 .push("requester assertion invalid".to_string());
221 }
222 Err(e) => {
223 ok = false;
224 report
225 .errors
226 .push(format!("requester assertion error: {}", e));
227 }
228 }
229 }
230 }
231 for receiver in &manifest.receiver {
233 if let Some(assertion) = &receiver.assertion {
234 match AssertionManager::verify_assertion(assertion, receiver, &manifest) {
235 Ok(crate::assertion::VerificationResult::Valid { .. }) => {}
236 Ok(_) => {
237 ok = false;
238 report.errors.push("receiver assertion invalid".to_string());
239 }
240 Err(e) => {
241 ok = false;
242 report
243 .errors
244 .push(format!("receiver assertion error: {}", e));
245 }
246 }
247 }
248 }
249 report.assertions_ok = Some(ok);
250 }
251
252 Ok(report)
253}
254
255fn compute_payload_hash_for_dir<P: AsRef<Path>>(payload_dir: P) -> JmixResult<String> {
257 use walkdir::WalkDir;
258 let payload_dir = payload_dir.as_ref();
259 let mut paths: Vec<PathBuf> = Vec::new();
260 for entry in WalkDir::new(payload_dir).into_iter().filter_map(|e| e.ok()) {
261 if entry.file_type().is_file() {
262 paths.push(entry.path().to_path_buf());
263 }
264 }
265 paths.sort_by(|a, b| {
266 let ra = a.strip_prefix(payload_dir).unwrap_or(a);
267 let rb = b.strip_prefix(payload_dir).unwrap_or(b);
268 ra.as_os_str()
269 .to_string_lossy()
270 .cmp(&rb.as_os_str().to_string_lossy())
271 });
272 let mut hasher = Sha256::new();
273 for abs_path in paths {
274 let rel = abs_path.strip_prefix(payload_dir).unwrap_or(&abs_path);
275 let rel_str = rel.as_os_str().to_string_lossy();
276 hasher.update(rel_str.as_bytes());
277 hasher.update(&[b'\n']);
278 let data = fs::read(&abs_path)?;
279 hasher.update(&data);
280 }
281 let hash = hasher.finalize();
282 Ok(format!("sha256:{:x}", hash))
283}