jmix_rs/
package_validation.rs

1use 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    // Load manifest
38    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    // Load audit
50    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    // Determine encryption
58    let is_encrypted = manifest.security.encryption.is_some();
59
60    // Schema validation (optional)
61    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        // Load and validate payload/metadata.json and payload/files.json if unencrypted
79        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    // Payload hash verification
116    if is_encrypted {
117        // Need to decrypt to recompute payload hash
118        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                            // hash = sha256 of plaintext tar bytes
140                            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                // Cannot verify without key
161                report.encryption_ok = None;
162                report.payload_hash_ok = None;
163            }
164            _ => {}
165        }
166    } else {
167        // Unencrypted: recompute hash over payload directory deterministically
168        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    // Assertions verification (optional)
195    if opts.verify_assertions {
196        let mut ok = true;
197        // sender
198        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        // requester
212        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        // receivers
232        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
255/// Deterministic payload directory hash used for unencrypted packages
256fn 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}