jmix_rs/
package_validation.rs1use crate::{assertion::AssertionManager, encryption::DecryptionManager, error::{JmixError, JmixResult}, types::{Audit, Files, Manifest, Metadata}, validation::SchemaValidator};
2use sha2::{Digest, Sha256};
3use std::{fs, path::{Path, PathBuf}};
4
5#[derive(Debug, Clone, Default)]
6pub struct ValidationOptions {
7 pub schema_dir: Option<String>,
8 pub validate_schema: bool,
9 pub verify_assertions: bool,
10 pub recipient_secret_key_path: Option<PathBuf>,
11}
12
13#[derive(Debug, Clone, Default)]
14pub struct ValidationReport {
15 pub schema_ok: Option<bool>,
16 pub payload_hash_ok: Option<bool>,
17 pub assertions_ok: Option<bool>,
18 pub encryption_ok: Option<bool>,
19 pub errors: Vec<String>,
20}
21
22pub fn validate_package(package_dir: &Path, opts: &ValidationOptions) -> JmixResult<ValidationReport> {
23 let mut report = ValidationReport::default();
24
25 let manifest_path = package_dir.join("manifest.json");
27 if !manifest_path.exists() {
28 return Err(JmixError::Other(format!("manifest.json not found at {}", manifest_path.display())));
29 }
30
31 let manifest_str = fs::read_to_string(&manifest_path)?;
32 let manifest: Manifest = serde_json::from_str(&manifest_str).map_err(JmixError::Json)?;
33
34 let audit_path = package_dir.join("audit.json");
36 let audit: Option<Audit> = if audit_path.exists() {
37 Some(serde_json::from_str(&fs::read_to_string(&audit_path)?)
38 .map_err(JmixError::Json)?)
39 } else { None };
40
41 let is_encrypted = manifest.security.encryption.is_some();
43
44 if opts.validate_schema {
46 let validator = if let Some(dir) = &opts.schema_dir { SchemaValidator::new(Some(dir.clone())) } else { SchemaValidator::with_default_config() };
47 let mut schema_ok = true;
48 if let Err(e) = validator.validate_manifest(&manifest) { schema_ok = false; report.errors.push(format!("manifest schema: {}", e)); }
49 if let Some(a) = &audit {
50 if let Err(e) = validator.validate_audit(a) { schema_ok = false; report.errors.push(format!("audit schema: {}", e)); }
51 }
52 if !is_encrypted {
54 let payload_dir = package_dir.join("payload");
55 let metadata_path = payload_dir.join("metadata.json");
56 if metadata_path.exists() {
57 match serde_json::from_str::<Metadata>(&fs::read_to_string(&metadata_path)?) {
58 Ok(metadata) => {
59 if let Err(e) = validator.validate_metadata(&metadata) { schema_ok = false; report.errors.push(format!("metadata schema: {}", e)); }
60 }
61 Err(e) => { schema_ok = false; report.errors.push(format!("metadata parse: {}", e)); }
62 }
63 }
64 let files_path = payload_dir.join("files.json");
65 if files_path.exists() {
66 match serde_json::from_str::<Files>(&fs::read_to_string(&files_path)?) {
67 Ok(files) => {
68 if let Err(e) = validator.validate_files(&files) { schema_ok = false; report.errors.push(format!("files schema: {}", e)); }
69 }
70 Err(e) => { schema_ok = false; report.errors.push(format!("files parse: {}", e)); }
71 }
72 }
73 }
74 report.schema_ok = Some(schema_ok);
75 }
76
77 if is_encrypted {
79 match (&manifest.security.encryption, &opts.recipient_secret_key_path) {
81 (Some(enc_info), Some(secret_path)) => {
82 let payload_enc_path = package_dir.join("payload.enc");
83 if !payload_enc_path.exists() {
84 report.errors.push("payload.enc missing for encrypted package".to_string());
85 report.encryption_ok = Some(false);
86 report.payload_hash_ok = Some(false);
87 } else {
88 let ciphertext = fs::read(&payload_enc_path)?;
89 let dec = DecryptionManager::from_secret_key_file(secret_path)
90 .map_err(|e| JmixError::Other(format!("Failed to create decryption manager: {}", e)))?;
91 match dec.decrypt(&ciphertext, enc_info) {
92 Ok(plaintext_tar) => {
93 report.encryption_ok = Some(true);
94 let mut hasher = Sha256::new();
96 hasher.update(&plaintext_tar);
97 let hash = format!("sha256:{:x}", hasher.finalize());
98 let ok = hash == manifest.security.payload_hash;
99 if !ok { report.errors.push("payload hash mismatch (encrypted)".to_string()); }
100 report.payload_hash_ok = Some(ok);
101 }
102 Err(e) => {
103 report.encryption_ok = Some(false);
104 report.payload_hash_ok = Some(false);
105 report.errors.push(format!("decryption failed: {}", e));
106 }
107 }
108 }
109 }
110 (Some(_), None) => {
111 report.encryption_ok = None;
113 report.payload_hash_ok = None;
114 }
115 _ => {}
116 }
117 } else {
118 let payload_dir = package_dir.join("payload");
120 if payload_dir.exists() {
121 let ok = match compute_payload_hash_for_dir(&payload_dir) {
122 Ok(hash) => {
123 let eq = hash == manifest.security.payload_hash;
124 if !eq { report.errors.push("payload hash mismatch (unencrypted)".to_string()); }
125 eq
126 }
127 Err(e) => { report.errors.push(format!("payload hash compute error: {}", e)); false }
128 };
129 report.payload_hash_ok = Some(ok);
130 } else {
131 report.payload_hash_ok = Some(false);
132 report.errors.push("payload/ directory missing".to_string());
133 }
134 }
135
136 if opts.verify_assertions {
138 let mut ok = true;
139 if let Some(assertion) = &manifest.sender.assertion {
141 match AssertionManager::verify_assertion(assertion, &manifest.sender, &manifest) {
142 Ok(crate::assertion::VerificationResult::Valid { .. }) => {}
143 Ok(_) => { ok = false; report.errors.push("sender assertion invalid".to_string()); }
144 Err(e) => { ok = false; report.errors.push(format!("sender assertion error: {}", e)); }
145 }
146 }
147 if let Some(requester) = &manifest.requester {
149 if let Some(assertion) = &requester.assertion {
150 match AssertionManager::verify_assertion(assertion, requester, &manifest) {
151 Ok(crate::assertion::VerificationResult::Valid { .. }) => {}
152 Ok(_) => { ok = false; report.errors.push("requester assertion invalid".to_string()); }
153 Err(e) => { ok = false; report.errors.push(format!("requester assertion error: {}", e)); }
154 }
155 }
156 }
157 for receiver in &manifest.receiver {
159 if let Some(assertion) = &receiver.assertion {
160 match AssertionManager::verify_assertion(assertion, receiver, &manifest) {
161 Ok(crate::assertion::VerificationResult::Valid { .. }) => {}
162 Ok(_) => { ok = false; report.errors.push("receiver assertion invalid".to_string()); }
163 Err(e) => { ok = false; report.errors.push(format!("receiver assertion error: {}", e)); }
164 }
165 }
166 }
167 report.assertions_ok = Some(ok);
168 }
169
170 Ok(report)
171}
172
173fn compute_payload_hash_for_dir<P: AsRef<Path>>(payload_dir: P) -> JmixResult<String> {
175 use walkdir::WalkDir;
176 let payload_dir = payload_dir.as_ref();
177 let mut paths: Vec<PathBuf> = Vec::new();
178 for entry in WalkDir::new(payload_dir).into_iter().filter_map(|e| e.ok()) {
179 if entry.file_type().is_file() {
180 paths.push(entry.path().to_path_buf());
181 }
182 }
183 paths.sort_by(|a, b| {
184 let ra = a.strip_prefix(payload_dir).unwrap_or(a);
185 let rb = b.strip_prefix(payload_dir).unwrap_or(b);
186 ra.as_os_str().to_string_lossy().cmp(&rb.as_os_str().to_string_lossy())
187 });
188 let mut hasher = Sha256::new();
189 for abs_path in paths {
190 let rel = abs_path.strip_prefix(payload_dir).unwrap_or(&abs_path);
191 let rel_str = rel.as_os_str().to_string_lossy();
192 hasher.update(rel_str.as_bytes());
193 hasher.update(&[b'\n']);
194 let data = fs::read(&abs_path)?;
195 hasher.update(&data);
196 }
197 let hash = hasher.finalize();
198 Ok(format!("sha256:{:x}", hash))
199}