jmix_rs/
package_validation.rs

1use 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    // Load manifest
26    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    // Load audit
35    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    // Determine encryption
42    let is_encrypted = manifest.security.encryption.is_some();
43
44    // Schema validation (optional)
45    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        // Load and validate payload/metadata.json and payload/files.json if unencrypted
53        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    // Payload hash verification
78    if is_encrypted {
79        // Need to decrypt to recompute payload hash
80        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                            // hash = sha256 of plaintext tar bytes
95                            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                // Cannot verify without key
112                report.encryption_ok = None;
113                report.payload_hash_ok = None;
114            }
115            _ => {}
116        }
117    } else {
118        // Unencrypted: recompute hash over payload directory deterministically
119        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    // Assertions verification (optional)
137    if opts.verify_assertions {
138        let mut ok = true;
139        // sender
140        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        // requester
148        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        // receivers
158        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
173/// Deterministic payload directory hash used for unencrypted packages
174fn 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}