1use super::*;
10use std::collections::HashSet;
11use std::path::Path;
12
13use mockforge_plugin_core::{
15 FilesystemPermissions, NetworkPermissions, PluginCapabilities, PluginId, PluginManifest,
16 PluginVersion, ResourceLimits,
17};
18
19use wasmparser::{Parser, Payload};
21
22use ring::signature;
24
25use shellexpand;
27
28use base64::{engine::general_purpose, Engine as _};
30use hex;
31
32use serde_json;
34
35#[derive(Debug, Clone)]
37struct PluginSignature {
38 algorithm: String,
39 signature: Vec<u8>,
40 key_id: String,
41}
42
43pub struct PluginValidator {
45 config: PluginLoaderConfig,
47 security_policies: SecurityPolicies,
49}
50
51impl PluginValidator {
52 pub fn new(config: PluginLoaderConfig) -> Self {
54 Self {
55 config,
56 security_policies: SecurityPolicies::default(),
57 }
58 }
59
60 pub async fn validate_manifest(&self, manifest: &PluginManifest) -> LoaderResult<()> {
62 let mut errors = Vec::new();
63
64 if let Err(validation_error) = manifest.validate() {
66 errors.push(PluginLoaderError::manifest(validation_error));
67 }
68
69 if let Err(e) = self.security_policies.validate_manifest(manifest) {
71 errors.push(e);
72 }
73
74 let mut visited = HashSet::new();
76 visited.insert(manifest.info.id.clone());
77 if let Err(e) = self
78 .validate_dependencies(&manifest.info.id, &manifest.dependencies, &mut visited)
79 .await
80 {
81 errors.push(e);
82 }
83
84 if errors.is_empty() {
85 Ok(())
86 } else {
87 Err(PluginLoaderError::validation(format!(
88 "Manifest validation failed with {} errors: {}",
89 errors.len(),
90 errors.into_iter().map(|e| e.to_string()).collect::<Vec<_>>().join(", ")
91 )))
92 }
93 }
94
95 pub fn validate_capabilities(&self, capability_names: &[String]) -> LoaderResult<()> {
97 let capabilities = PluginCapabilities {
99 network: NetworkPermissions::default(),
100 filesystem: FilesystemPermissions::default(),
101 resources: ResourceLimits::default(),
102 custom: capability_names
103 .iter()
104 .map(|name| (name.clone(), serde_json::Value::Bool(true)))
105 .collect(),
106 };
107 self.security_policies.validate_capabilities(&capabilities)
108 }
109
110 pub async fn validate_wasm_file(&self, wasm_path: &Path) -> LoaderResult<()> {
112 if !wasm_path.exists() {
114 return Err(PluginLoaderError::fs("WASM file does not exist".to_string()));
115 }
116
117 let metadata = tokio::fs::metadata(wasm_path)
118 .await
119 .map_err(|e| PluginLoaderError::fs(format!("Cannot read WASM file metadata: {}", e)))?;
120
121 if !metadata.is_file() {
122 return Err(PluginLoaderError::fs("WASM path is not a file".to_string()));
123 }
124
125 let file_size = metadata.len();
127 if file_size > self.security_policies.max_wasm_file_size {
128 return Err(PluginLoaderError::security(format!(
129 "WASM file too large: {} bytes (max: {} bytes)",
130 file_size, self.security_policies.max_wasm_file_size
131 )));
132 }
133
134 self.validate_wasm_module(wasm_path).await?;
136
137 Ok(())
138 }
139
140 pub async fn validate_plugin_file(&self, plugin_path: &Path) -> LoaderResult<PluginManifest> {
142 if !plugin_path.exists() {
143 return Err(PluginLoaderError::fs("Plugin path does not exist".to_string()));
144 }
145
146 if !plugin_path.is_dir() {
147 return Err(PluginLoaderError::fs("Plugin path must be a directory".to_string()));
148 }
149
150 let manifest_path = plugin_path.join("plugin.yaml");
152 if !manifest_path.exists() {
153 return Err(PluginLoaderError::manifest("plugin.yaml not found".to_string()));
154 }
155
156 let manifest = PluginManifest::from_file(&manifest_path)
158 .map_err(|e| PluginLoaderError::manifest(format!("Failed to load manifest: {}", e)))?;
159
160 self.validate_manifest(&manifest).await?;
162
163 let wasm_files: Vec<_> = std::fs::read_dir(plugin_path)
165 .map_err(|e| PluginLoaderError::fs(format!("Cannot read plugin directory: {}", e)))?
166 .filter_map(|entry| entry.ok())
167 .map(|entry| entry.path())
168 .filter(|path| path.extension().is_some_and(|ext| ext == "wasm"))
169 .collect();
170
171 if wasm_files.is_empty() {
172 return Err(PluginLoaderError::load(
173 "No WebAssembly file found in plugin directory".to_string(),
174 ));
175 }
176
177 if wasm_files.len() > 1 {
178 return Err(PluginLoaderError::load(
179 "Multiple WebAssembly files found in plugin directory".to_string(),
180 ));
181 }
182
183 if !self.config.skip_wasm_validation {
185 self.validate_wasm_file(&wasm_files[0]).await?;
186 }
187
188 Ok(manifest)
189 }
190
191 async fn validate_dependencies(
193 &self,
194 current_plugin_id: &PluginId,
195 dependencies: &std::collections::HashMap<mockforge_plugin_core::PluginId, PluginVersion>,
196 visited: &mut HashSet<PluginId>,
197 ) -> LoaderResult<()> {
198 for (plugin_id, version) in dependencies {
199 if self.would_create_circular_dependency(current_plugin_id, plugin_id, visited) {
201 return Err(PluginLoaderError::ValidationError {
202 message: format!(
203 "Circular dependency detected: '{}' -> '{}'",
204 current_plugin_id.0, plugin_id.0
205 ),
206 });
207 }
208
209 if plugin_id.0.is_empty() {
211 return Err(PluginLoaderError::ValidationError {
212 message: "Dependency plugin ID cannot be empty".to_string(),
213 });
214 }
215
216 if plugin_id.0.len() > 100 {
217 return Err(PluginLoaderError::ValidationError {
218 message: format!(
219 "Dependency plugin ID '{}' is too long (max 100 characters)",
220 plugin_id.0
221 ),
222 });
223 }
224
225 if version.major == 0 && version.minor == 0 && version.patch == 0 {
227 tracing::warn!("Dependency '{}' specifies version 0.0.0 which may indicate development/testing", plugin_id.0);
228 }
229
230 if plugin_id.0.contains("..") || plugin_id.0.contains("/") || plugin_id.0.contains("\\")
232 {
233 return Err(PluginLoaderError::SecurityViolation {
234 violation: format!(
235 "Dependency plugin ID '{}' contains potentially unsafe characters",
236 plugin_id.0
237 ),
238 });
239 }
240
241 }
247
248 Ok(())
249 }
250
251 fn would_create_circular_dependency(
255 &self,
256 current_plugin_id: &PluginId,
257 dependency_id: &PluginId,
258 visited: &mut HashSet<PluginId>,
259 ) -> bool {
260 if dependency_id == current_plugin_id {
262 return true;
263 }
264
265 visited.contains(dependency_id)
271 }
272
273 async fn validate_wasm_module(&self, wasm_path: &Path) -> LoaderResult<()> {
275 let module = wasmtime::Module::from_file(&wasmtime::Engine::default(), wasm_path)
277 .map_err(|e| PluginLoaderError::wasm(format!("Invalid WASM module: {}", e)))?;
278
279 self.security_policies.validate_wasm_module(&module)?;
281
282 Ok(())
283 }
284
285 pub async fn validate_plugin_signature(
287 &self,
288 plugin_path: &Path,
289 manifest: &PluginManifest,
290 ) -> LoaderResult<()> {
291 if self.config.allow_unsigned {
293 return Ok(());
294 }
295
296 let sig_path = plugin_path.with_extension("sig");
298 if !sig_path.exists() {
299 return Err(PluginLoaderError::SecurityViolation {
300 violation: format!(
301 "Plugin '{}' requires a signature but none was found",
302 manifest.info.id.0
303 ),
304 });
305 }
306
307 let signature_data =
309 std::fs::read(&sig_path).map_err(|e| PluginLoaderError::ValidationError {
310 message: format!("Failed to read signature file: {}", e),
311 })?;
312
313 let signature = self.parse_signature(&signature_data)?;
315
316 let plugin_data =
318 std::fs::read(plugin_path).map_err(|e| PluginLoaderError::ValidationError {
319 message: format!("Failed to read plugin file: {}", e),
320 })?;
321
322 self.verify_signature(&plugin_data, &signature, manifest).await?;
324
325 Ok(())
326 }
327
328 fn parse_signature(&self, data: &[u8]) -> Result<PluginSignature, PluginLoaderError> {
330 let sig_json: serde_json::Value =
334 serde_json::from_slice(data).map_err(|e| PluginLoaderError::ValidationError {
335 message: format!("Invalid signature JSON format: {}", e),
336 })?;
337
338 let algorithm = sig_json
339 .get("algorithm")
340 .and_then(|v| v.as_str())
341 .ok_or_else(|| PluginLoaderError::ValidationError {
342 message: "Missing or invalid 'algorithm' field".to_string(),
343 })?
344 .to_string();
345
346 let signature_hex =
347 sig_json.get("signature").and_then(|v| v.as_str()).ok_or_else(|| {
348 PluginLoaderError::ValidationError {
349 message: "Missing or invalid 'signature' field".to_string(),
350 }
351 })?;
352
353 let key_id = sig_json
354 .get("key_id")
355 .and_then(|v| v.as_str())
356 .ok_or_else(|| PluginLoaderError::ValidationError {
357 message: "Missing or invalid 'key_id' field".to_string(),
358 })?
359 .to_string();
360
361 if !["rsa", "ecdsa", "ed25519"].contains(&algorithm.as_str()) {
363 return Err(PluginLoaderError::ValidationError {
364 message: format!("Unsupported signature algorithm: {}", algorithm),
365 });
366 }
367
368 let signature =
370 hex::decode(signature_hex).map_err(|e| PluginLoaderError::ValidationError {
371 message: format!("Invalid signature hex: {}", e),
372 })?;
373
374 Ok(PluginSignature {
375 algorithm,
376 signature,
377 key_id,
378 })
379 }
380
381 async fn verify_signature(
383 &self,
384 data: &[u8],
385 signature: &PluginSignature,
386 manifest: &PluginManifest,
387 ) -> LoaderResult<()> {
388 let public_key = self.get_trusted_key(&signature.key_id)?;
390
391 match signature.algorithm.as_str() {
393 "rsa" => {
394 self.verify_rsa_signature(data, &signature.signature, &public_key)?;
396 }
397 "ecdsa" => {
398 self.verify_ecdsa_signature(data, &signature.signature, &public_key)?;
400 }
401 "ed25519" => {
402 self.verify_ed25519_signature(data, &signature.signature, &public_key)?;
404 }
405 _ => {
406 return Err(PluginLoaderError::ValidationError {
407 message: format!("Unsupported algorithm: {}", signature.algorithm),
408 });
409 }
410 }
411
412 self.validate_key_authorization(&signature.key_id, manifest)?;
414
415 Ok(())
416 }
417
418 fn get_trusted_key(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
420 if !self.config.trusted_keys.contains(&key_id.to_string()) {
422 return Err(PluginLoaderError::SecurityViolation {
423 violation: format!("Key '{}' is not in the trusted keys list", key_id),
424 });
425 }
426
427 self.load_key_from_store(key_id)
430 }
431
432 fn load_key_from_store(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
434 if let Ok(key_data) = self.load_key_from_env(key_id) {
436 tracing::info!("Loaded key '{}' from environment variable", key_id);
437 return Ok(key_data);
438 }
439
440 if let Some(key_data) = self.config.key_data.get(key_id) {
442 tracing::info!("Loaded key '{}' from configuration", key_id);
443 return Ok(key_data.clone());
444 }
445
446 if let Ok(key_data) = self.load_key_from_filesystem(key_id) {
448 tracing::info!("Loaded key '{}' from filesystem", key_id);
449 return Ok(key_data);
450 }
451
452 if let Ok(key_data) = self.load_key_from_database(key_id) {
455 tracing::info!("Loaded key '{}' from database provider", key_id);
456 return Ok(key_data);
457 }
458
459 if let Ok(key_data) = self.load_key_from_kms(key_id) {
461 tracing::info!("Loaded key '{}' from key management service", key_id);
462 return Ok(key_data);
463 }
464
465 Err(PluginLoaderError::SecurityViolation {
466 violation: format!("Could not find key data for trusted key: {}", key_id),
467 })
468 }
469
470 fn load_key_from_env(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
472 self.load_key_material_from_prefixes(key_id, &["MOCKFORGE_KEY"], "environment")
473 }
474
475 fn load_key_material_from_prefixes(
476 &self,
477 key_id: &str,
478 prefixes: &[&str],
479 source_name: &str,
480 ) -> Result<Vec<u8>, PluginLoaderError> {
481 let normalized = key_id.to_uppercase().replace("-", "_");
482
483 for prefix in prefixes {
484 let b64_env_key = format!("{}_{}_B64", prefix, normalized);
486 if let Ok(b64_value) = std::env::var(&b64_env_key) {
487 match general_purpose::STANDARD.decode(&b64_value) {
488 Ok(key_data) => return Ok(key_data),
489 Err(e) => {
490 tracing::warn!(
491 "Failed to decode base64 key from {} ({}): {}",
492 b64_env_key,
493 source_name,
494 e
495 );
496 }
497 }
498 }
499
500 let hex_env_key = format!("{}_{}_HEX", prefix, normalized);
502 if let Ok(hex_value) = std::env::var(&hex_env_key) {
503 match hex::decode(&hex_value) {
504 Ok(key_data) => return Ok(key_data),
505 Err(e) => {
506 tracing::warn!(
507 "Failed to decode hex key from {} ({}): {}",
508 hex_env_key,
509 source_name,
510 e
511 );
512 }
513 }
514 }
515
516 let raw_env_key = format!("{}_{}", prefix, normalized);
518 if let Ok(key_data) = std::env::var(&raw_env_key) {
519 return Ok(key_data.into_bytes());
520 }
521 }
522
523 Err(PluginLoaderError::SecurityViolation {
524 violation: format!("Key not found in {}: {}", source_name, key_id),
525 })
526 }
527
528 fn load_key_from_directory(
529 &self,
530 key_id: &str,
531 dir: &std::path::Path,
532 ) -> Result<Vec<u8>, PluginLoaderError> {
533 let candidates = [
534 dir.join(format!("{}.der", key_id)),
535 dir.join(format!("{}.pem", key_id)),
536 dir.join(format!("{}.key", key_id)),
537 dir.join(format!("{}.bin", key_id)),
538 ];
539
540 for path in candidates {
541 if path.exists() {
542 return std::fs::read(&path).map_err(|e| PluginLoaderError::SecurityViolation {
543 violation: format!("Failed to read key file {}: {}", path.display(), e),
544 });
545 }
546 }
547
548 Err(PluginLoaderError::SecurityViolation {
549 violation: format!("Key '{}' not found in directory {}", key_id, dir.display()),
550 })
551 }
552
553 fn load_key_from_filesystem(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
555 let key_paths = vec![
556 format!("~/.mockforge/keys/{}.der", key_id),
557 format!("~/.mockforge/keys/{}.pem", key_id),
558 format!("/etc/mockforge/keys/{}.der", key_id),
559 format!("/etc/mockforge/keys/{}.pem", key_id),
560 ];
561
562 for key_path in key_paths {
563 let expanded_path = shellexpand::tilde(&key_path);
564 let path = std::path::Path::new(expanded_path.as_ref());
565
566 if path.exists() {
567 match std::fs::read(path) {
568 Ok(key_data) => return Ok(key_data),
569 Err(e) => {
570 tracing::warn!("Failed to read key file {}: {}", path.display(), e);
571 continue;
572 }
573 }
574 }
575 }
576
577 Err(PluginLoaderError::SecurityViolation {
578 violation: format!("Key not found in filesystem: {}", key_id),
579 })
580 }
581
582 #[allow(unused)]
588 fn load_key_from_database(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
589 let db_type = std::env::var("MOCKFORGE_DB_TYPE").map_err(|_| {
590 PluginLoaderError::SecurityViolation {
591 violation: "Database key loading requires MOCKFORGE_DB_TYPE environment variable"
592 .to_string(),
593 }
594 })?;
595
596 let connection_string = std::env::var("MOCKFORGE_DB_CONNECTION").map_err(|_| {
597 PluginLoaderError::SecurityViolation {
598 violation:
599 "Database key loading requires MOCKFORGE_DB_CONNECTION environment variable"
600 .to_string(),
601 }
602 })?;
603
604 let table_name =
605 std::env::var("MOCKFORGE_DB_KEY_TABLE").unwrap_or_else(|_| "plugin_keys".to_string());
606
607 tracing::info!("Database key loading configured: type={}, table={}", db_type, table_name);
608 tracing::debug!("Looking up key '{}' in database-backed key source", key_id);
609
610 if let Ok(key_data) =
611 self.load_key_material_from_prefixes(key_id, &["MOCKFORGE_DB_KEY"], "database env")
612 {
613 return Ok(key_data);
614 }
615
616 if let Ok(key_dir) = std::env::var("MOCKFORGE_DB_KEY_DIR") {
617 let expanded = shellexpand::tilde(&key_dir);
618 let path = std::path::Path::new(expanded.as_ref());
619 if path.exists() {
620 return self.load_key_from_directory(key_id, path);
621 }
622 }
623
624 Err(PluginLoaderError::SecurityViolation {
625 violation: format!(
626 "Database key '{}' not found in configured environment or key directory (connection: {})",
627 key_id, connection_string
628 ),
629 })
630 }
631
632 #[allow(unused)]
639 fn load_key_from_kms(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
640 let kms_provider = std::env::var("MOCKFORGE_KMS_PROVIDER").map_err(|_| {
641 PluginLoaderError::SecurityViolation {
642 violation: "KMS key loading requires MOCKFORGE_KMS_PROVIDER environment variable"
643 .to_string(),
644 }
645 })?;
646
647 match kms_provider.to_lowercase().as_str() {
648 "aws" => self.load_key_from_aws_kms(key_id),
649 "vault" => self.load_key_from_vault(key_id),
650 "azure" => self.load_key_from_azure_kv(key_id),
651 "gcp" => self.load_key_from_gcp_kms(key_id),
652 _ => Err(PluginLoaderError::SecurityViolation {
653 violation: format!("Unsupported KMS provider: {}", kms_provider),
654 }),
655 }
656 }
657
658 fn load_key_from_aws_kms(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
660 let region =
661 std::env::var("MOCKFORGE_KMS_REGION").unwrap_or_else(|_| "us-east-1".to_string());
662
663 tracing::info!("AWS KMS key loading configured: region={}", region);
664 tracing::debug!("Looking up key '{}' in AWS KMS", key_id);
665
666 self.load_key_material_from_prefixes(
667 key_id,
668 &["MOCKFORGE_AWS_KMS_KEY", "MOCKFORGE_KMS_KEY"],
669 "AWS KMS",
670 )
671 }
672
673 fn load_key_from_vault(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
675 let vault_addr = std::env::var("MOCKFORGE_VAULT_ADDR").map_err(|_| {
676 PluginLoaderError::SecurityViolation {
677 violation: "Vault key loading requires MOCKFORGE_VAULT_ADDR environment variable"
678 .to_string(),
679 }
680 })?;
681
682 let _vault_token = std::env::var("MOCKFORGE_VAULT_TOKEN").map_err(|_| {
683 PluginLoaderError::SecurityViolation {
684 violation: "Vault key loading requires MOCKFORGE_VAULT_TOKEN environment variable"
685 .to_string(),
686 }
687 })?;
688
689 tracing::info!("HashCorp Vault key loading configured: addr={}", vault_addr);
690 tracing::debug!("Looking up key '{}' in Vault", key_id);
691
692 self.load_key_material_from_prefixes(
693 key_id,
694 &["MOCKFORGE_VAULT_KEY", "MOCKFORGE_KMS_KEY"],
695 "Vault",
696 )
697 }
698
699 fn load_key_from_azure_kv(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
701 tracing::info!("Azure Key Vault key loading requested");
702 tracing::debug!("Looking up key '{}' in Azure Key Vault", key_id);
703
704 self.load_key_material_from_prefixes(
705 key_id,
706 &["MOCKFORGE_AZURE_KV_KEY", "MOCKFORGE_KMS_KEY"],
707 "Azure Key Vault",
708 )
709 }
710
711 fn load_key_from_gcp_kms(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
713 tracing::info!("Google Cloud KMS key loading requested");
714 tracing::debug!("Looking up key '{}' in GCP KMS", key_id);
715
716 self.load_key_material_from_prefixes(
717 key_id,
718 &["MOCKFORGE_GCP_KMS_KEY", "MOCKFORGE_KMS_KEY"],
719 "Google Cloud KMS",
720 )
721 }
722
723 fn verify_rsa_signature(
725 &self,
726 data: &[u8],
727 signature: &[u8],
728 public_key: &[u8],
729 ) -> LoaderResult<()> {
730 let public_key =
732 signature::UnparsedPublicKey::new(&signature::RSA_PKCS1_2048_8192_SHA256, public_key);
733
734 public_key
736 .verify(data, signature)
737 .map_err(|e| PluginLoaderError::SecurityViolation {
738 violation: format!("RSA signature verification failed: {}", e),
739 })?;
740
741 Ok(())
742 }
743
744 fn verify_ecdsa_signature(
746 &self,
747 data: &[u8],
748 signature: &[u8],
749 public_key: &[u8],
750 ) -> LoaderResult<()> {
751 let public_key =
753 signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_ASN1, public_key);
754
755 public_key
757 .verify(data, signature)
758 .map_err(|e| PluginLoaderError::SecurityViolation {
759 violation: format!("ECDSA signature verification failed: {}", e),
760 })?;
761
762 Ok(())
763 }
764
765 fn verify_ed25519_signature(
767 &self,
768 data: &[u8],
769 signature: &[u8],
770 public_key: &[u8],
771 ) -> LoaderResult<()> {
772 let public_key = signature::UnparsedPublicKey::new(&signature::ED25519, public_key);
774
775 public_key
777 .verify(data, signature)
778 .map_err(|e| PluginLoaderError::SecurityViolation {
779 violation: format!("Ed25519 signature verification failed: {}", e),
780 })?;
781
782 Ok(())
783 }
784
785 fn validate_key_authorization(
787 &self,
788 key_id: &str,
789 manifest: &PluginManifest,
790 ) -> LoaderResult<()> {
791 if self.config.trusted_keys.contains(&key_id.to_string()) {
793 return Ok(());
794 }
795
796 Err(PluginLoaderError::SecurityViolation {
797 violation: format!(
798 "Key '{}' is not authorized to sign plugins from '{}'",
799 key_id, manifest.info.author.name
800 ),
801 })
802 }
803
804 pub async fn get_validation_summary(&self, plugin_path: &Path) -> ValidationSummary {
806 let mut summary = ValidationSummary::default();
807
808 if !plugin_path.exists() {
810 summary.errors.push("Plugin path does not exist".to_string());
811 return summary;
812 }
813
814 let manifest_result = self.validate_plugin_file(plugin_path).await;
816 match manifest_result {
817 Ok(manifest) => {
818 summary.manifest_valid = true;
819 summary.manifest = Some(manifest);
820 }
821 Err(e) => {
822 summary.errors.push(format!("Manifest validation failed: {}", e));
823 }
824 }
825
826 if let Ok(wasm_path) = self.find_wasm_file(plugin_path) {
828 let wasm_result = self.validate_wasm_file(&wasm_path).await;
829 summary.wasm_valid = wasm_result.is_ok();
830 if let Err(e) = wasm_result {
831 summary.errors.push(format!("WASM validation failed: {}", e));
832 }
833 } else {
834 summary.errors.push("No WebAssembly file found".to_string());
835 }
836
837 summary.is_valid =
838 summary.manifest_valid && summary.wasm_valid && summary.errors.is_empty();
839 summary
840 }
841
842 fn find_wasm_file(&self, plugin_path: &Path) -> LoaderResult<PathBuf> {
844 let entries = std::fs::read_dir(plugin_path)
845 .map_err(|e| PluginLoaderError::fs(format!("Cannot read directory: {}", e)))?;
846
847 for entry in entries {
848 let entry =
849 entry.map_err(|e| PluginLoaderError::fs(format!("Cannot read entry: {}", e)))?;
850 let path = entry.path();
851
852 if let Some(extension) = path.extension() {
853 if extension == "wasm" {
854 return Ok(path);
855 }
856 }
857 }
858
859 Err(PluginLoaderError::load("No WebAssembly file found".to_string()))
860 }
861}
862
863#[derive(Debug, Clone)]
865pub struct SecurityPolicies {
866 pub max_wasm_file_size: u64,
868 pub allowed_imports: HashSet<String>,
870 pub forbidden_imports: HashSet<String>,
872 pub max_memory_pages: u32,
874 pub max_functions: u32,
876 pub allow_floats: bool,
878 pub allow_simd: bool,
880 pub allow_network_access: bool,
882 pub allow_filesystem_read: bool,
884 pub allow_filesystem_write: bool,
886}
887
888impl Default for SecurityPolicies {
889 fn default() -> Self {
890 let mut allowed_imports = HashSet::new();
891 allowed_imports.insert("env".to_string());
892 allowed_imports.insert("wasi_snapshot_preview1".to_string());
893
894 let mut forbidden_imports = HashSet::new();
895 forbidden_imports.insert("abort".to_string());
896 forbidden_imports.insert("exit".to_string());
897
898 Self {
899 max_wasm_file_size: 10 * 1024 * 1024, allowed_imports,
901 forbidden_imports,
902 max_memory_pages: 256, max_functions: 1000,
904 allow_floats: true,
905 allow_simd: false,
906 allow_network_access: false,
907 allow_filesystem_read: false,
908 allow_filesystem_write: false,
909 }
910 }
911}
912
913impl SecurityPolicies {
914 pub fn validate_manifest(&self, manifest: &PluginManifest) -> LoaderResult<()> {
916 let _caps = PluginCapabilities::from_strings(&manifest.capabilities);
922
923 Ok(())
927 }
928
929 pub fn validate_capabilities(&self, capabilities: &PluginCapabilities) -> LoaderResult<()> {
931 if capabilities.resources.max_memory_bytes > self.max_memory_bytes() {
933 return Err(PluginLoaderError::security(format!(
934 "Memory limit {} exceeds maximum allowed {}",
935 capabilities.resources.max_memory_bytes,
936 self.max_memory_bytes()
937 )));
938 }
939
940 if capabilities.resources.max_cpu_percent > self.max_cpu_percent() {
941 return Err(PluginLoaderError::security(format!(
942 "CPU limit {:.2}% exceeds maximum allowed {:.2}%",
943 capabilities.resources.max_cpu_percent,
944 self.max_cpu_percent()
945 )));
946 }
947
948 Ok(())
949 }
950
951 pub fn validate_wasm_module(&self, module: &wasmtime::Module) -> LoaderResult<()> {
953 self.validate_imports(module)?;
957
958 self.validate_exports(module)?;
960
961 self.validate_memory_usage(module)?;
963
964 self.check_dangerous_operations(module)?;
966
967 self.validate_function_limits(module)?;
969
970 self.validate_data_segments(module)?;
972
973 Ok(())
974 }
975
976 fn validate_imports(&self, module: &wasmtime::Module) -> LoaderResult<()> {
978 let _module_info = module.resources_required();
980
981 for import in module.imports() {
983 let module_name = import.module();
984 let field_name = import.name();
985
986 let allowed_modules = [
988 "wasi_snapshot_preview1",
989 "wasi:io/streams",
990 "wasi:filesystem/types",
991 "mockforge:plugin/host",
992 ];
993
994 if !allowed_modules.contains(&module_name) {
995 return Err(PluginLoaderError::SecurityViolation {
996 violation: format!("Disallowed import module: {}", module_name),
997 });
998 }
999
1000 match module_name {
1002 "wasi_snapshot_preview1" => {
1003 self.validate_wasi_import(field_name)?;
1004 }
1005 "mockforge:plugin/host" => {
1006 self.validate_host_import(field_name)?;
1007 }
1008 _ => {
1009 }
1011 }
1012 }
1013
1014 Ok(())
1015 }
1016
1017 fn validate_wasi_import(&self, field_name: &str) -> LoaderResult<()> {
1019 let allowed_functions = [
1021 "fd_read",
1023 "fd_write",
1024 "fd_close",
1025 "fd_fdstat_get",
1026 "path_open",
1028 "path_readlink",
1029 "path_filestat_get",
1030 "clock_time_get",
1032 "proc_exit",
1034 "random_get",
1036 ];
1037
1038 if !allowed_functions.contains(&field_name) {
1039 return Err(PluginLoaderError::SecurityViolation {
1040 violation: format!("Disallowed WASI function: {}", field_name),
1041 });
1042 }
1043
1044 Ok(())
1045 }
1046
1047 fn validate_host_import(&self, field_name: &str) -> LoaderResult<()> {
1049 let allowed_functions = [
1051 "log_message",
1052 "get_config_value",
1053 "store_data",
1054 "retrieve_data",
1055 ];
1056
1057 if !allowed_functions.contains(&field_name) {
1058 return Err(PluginLoaderError::SecurityViolation {
1059 violation: format!("Disallowed host function: {}", field_name),
1060 });
1061 }
1062
1063 Ok(())
1064 }
1065
1066 fn validate_exports(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1068 let _module_info = module.resources_required();
1069
1070 let mut has_memory_export = false;
1072 let mut function_exports = 0;
1073
1074 for export in module.exports() {
1075 match export.ty() {
1076 wasmtime::ExternType::Memory(_) => {
1077 has_memory_export = true;
1078 }
1079 wasmtime::ExternType::Func(_) => {
1080 function_exports += 1;
1081
1082 }
1085 _ => {
1086 }
1088 }
1089 }
1090
1091 if !has_memory_export {
1093 return Err(PluginLoaderError::ValidationError {
1094 message: "WASM module must export memory".to_string(),
1095 });
1096 }
1097
1098 if function_exports > 1000 {
1100 return Err(PluginLoaderError::SecurityViolation {
1101 violation: format!("Too many function exports: {} (max: 1000)", function_exports),
1102 });
1103 }
1104
1105 Ok(())
1106 }
1107
1108 fn validate_memory_usage(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1110 let _module_info = module.resources_required();
1111
1112 for import in module.imports() {
1113 if let wasmtime::ExternType::Memory(memory_type) = import.ty() {
1114 if let Some(max) = memory_type.maximum() {
1116 if max > 100 {
1117 return Err(PluginLoaderError::SecurityViolation {
1119 violation: format!("Memory limit too high: {} pages (max: 100)", max),
1120 });
1121 }
1122 }
1123
1124 if memory_type.maximum().is_none() && memory_type.is_shared() {
1126 return Err(PluginLoaderError::SecurityViolation {
1127 violation: "Shared memory without maximum limit not allowed".to_string(),
1128 });
1129 }
1130 }
1131 }
1132
1133 for export in module.exports() {
1135 if let wasmtime::ExternType::Memory(memory_type) = export.ty() {
1136 if let Some(max) = memory_type.maximum() {
1137 if max > 100 {
1138 return Err(PluginLoaderError::SecurityViolation {
1139 violation: format!("Exported memory limit too high: {} pages", max),
1140 });
1141 }
1142 }
1143 }
1144 }
1145
1146 Ok(())
1147 }
1148
1149 fn check_dangerous_operations(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1151 let _module_info = module.resources_required();
1158
1159 self.validate_function_sizes(module)?;
1161
1162 let suspicious_imports = ["env", "wasi_unstable", "wasi_experimental"];
1164 for import in module.imports() {
1165 if suspicious_imports.contains(&import.module()) {
1166 return Err(PluginLoaderError::SecurityViolation {
1167 violation: format!("Suspicious import module: {}", import.module()),
1168 });
1169 }
1170 }
1171
1172 Ok(())
1173 }
1174
1175 fn validate_function_limits(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1177 let _module_info = module.resources_required();
1178
1179 let mut function_count = 0;
1180 for export in module.exports() {
1181 if let wasmtime::ExternType::Func(_) = export.ty() {
1182 function_count += 1;
1183 }
1184 }
1185
1186 for import in module.imports() {
1188 if let wasmtime::ExternType::Func(_) = import.ty() {
1189 function_count += 1;
1190 }
1191 }
1192
1193 if function_count > 10000 {
1195 return Err(PluginLoaderError::SecurityViolation {
1196 violation: format!("Too many functions: {} (max: 10000)", function_count),
1197 });
1198 }
1199
1200 Ok(())
1201 }
1202
1203 fn validate_function_sizes(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1205 for export in module.exports() {
1208 if let wasmtime::ExternType::Func(func_type) = export.ty() {
1209 let param_count = func_type.params().len();
1212 let result_count = func_type.results().len();
1213
1214 if param_count > 20 || result_count > 10 {
1216 return Err(PluginLoaderError::SecurityViolation {
1217 violation: format!(
1218 "Function '{}' has suspiciously complex signature: {} params, {} results",
1219 export.name(), param_count, result_count
1220 ),
1221 });
1222 }
1223
1224 let mut complex_types = 0;
1226 for param in func_type.params() {
1227 match param {
1228 wasmtime::ValType::V128 | wasmtime::ValType::Ref(_) => {
1229 complex_types += 1;
1230 }
1231 _ => {}
1232 }
1233 }
1234
1235 if complex_types > param_count / 2 && param_count > 5 {
1236 return Err(PluginLoaderError::SecurityViolation {
1237 violation: format!(
1238 "Function '{}' has unusually complex parameter types: {} complex types out of {} params",
1239 export.name(), complex_types, param_count
1240 ),
1241 });
1242 }
1243 }
1244 }
1245
1246 let mut total_functions = 0;
1248 for export in module.exports() {
1249 if let wasmtime::ExternType::Func(_) = export.ty() {
1250 total_functions += 1;
1251 }
1252 }
1253 for import in module.imports() {
1254 if let wasmtime::ExternType::Func(_) = import.ty() {
1255 total_functions += 1;
1256 }
1257 }
1258
1259 if total_functions > 5000 {
1262 return Err(PluginLoaderError::SecurityViolation {
1263 violation: format!(
1264 "Too many functions: {} (reasonable limit: 5000)",
1265 total_functions
1266 ),
1267 });
1268 }
1269
1270 Ok(())
1275 }
1276
1277 fn validate_data_segments(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1279 let wasm_bytes = module.serialize().map_err(|e| PluginLoaderError::ValidationError {
1284 message: format!("Failed to serialize WASM module: {}", e),
1285 })?;
1286
1287 let parser = Parser::new(0);
1289 let payloads =
1290 parser.parse_all(&wasm_bytes).collect::<Result<Vec<_>, _>>().map_err(|e| {
1291 PluginLoaderError::ValidationError {
1292 message: format!("Failed to parse WASM module: {}", e),
1293 }
1294 })?;
1295
1296 let suspicious_patterns = [
1298 "http://",
1299 "https://",
1300 "/bin/",
1301 "/usr/bin/",
1302 "eval(",
1303 "exec(",
1304 "system(",
1305 "shell",
1306 "cmd.exe",
1307 "powershell",
1308 "wget",
1309 "curl",
1310 "nc ",
1311 "netcat",
1312 "python -c",
1313 "ruby -e",
1314 "node -e",
1315 "bash -c",
1316 "sh -c",
1317 ];
1318
1319 for payload in payloads {
1321 if let Payload::DataSection(data_section) = payload {
1322 for data_segment_result in data_section {
1323 let data_segment =
1324 data_segment_result.map_err(|e| PluginLoaderError::ValidationError {
1325 message: format!("Failed to read data segment: {}", e),
1326 })?;
1327 let data = data_segment.data;
1328
1329 if let Ok(data_str) = std::str::from_utf8(data) {
1331 for pattern in &suspicious_patterns {
1332 if data_str.contains(pattern) {
1333 return Err(PluginLoaderError::SecurityViolation {
1334 violation: format!(
1335 "Data segment contains suspicious content: '{}'",
1336 pattern
1337 ),
1338 });
1339 }
1340 }
1341 } else {
1342 for pattern in &suspicious_patterns {
1344 if data
1345 .windows(pattern.len())
1346 .any(|window| window == pattern.as_bytes())
1347 {
1348 return Err(PluginLoaderError::SecurityViolation {
1349 violation: format!(
1350 "Data segment contains suspicious content: '{}'",
1351 pattern
1352 ),
1353 });
1354 }
1355 }
1356 }
1357 }
1358 }
1359 }
1360
1361 Ok(())
1362 }
1363
1364 pub fn allow_network_access(&self) -> bool {
1366 self.allow_network_access
1367 }
1368
1369 pub fn allow_filesystem_read(&self) -> bool {
1371 self.allow_filesystem_read
1372 }
1373
1374 pub fn allow_filesystem_write(&self) -> bool {
1376 self.allow_filesystem_write
1377 }
1378
1379 pub fn max_memory_bytes(&self) -> usize {
1381 10 * 1024 * 1024 }
1383
1384 pub fn max_cpu_percent(&self) -> f64 {
1386 0.5 }
1388}
1389
1390#[derive(Debug, Clone)]
1392pub struct ValidationSummary {
1393 pub is_valid: bool,
1395 pub manifest_valid: bool,
1397 pub wasm_valid: bool,
1399 pub manifest: Option<PluginManifest>,
1401 pub errors: Vec<String>,
1403 pub warnings: Vec<String>,
1405}
1406
1407impl Default for ValidationSummary {
1408 fn default() -> Self {
1409 Self {
1410 is_valid: true,
1411 manifest_valid: false,
1412 wasm_valid: false,
1413 manifest: None,
1414 errors: Vec::new(),
1415 warnings: Vec::new(),
1416 }
1417 }
1418}
1419
1420impl ValidationSummary {
1421 pub fn add_error<S: Into<String>>(&mut self, error: S) {
1423 self.errors.push(error.into());
1424 self.is_valid = false;
1425 }
1426
1427 pub fn add_warning<S: Into<String>>(&mut self, warning: S) {
1429 self.warnings.push(warning.into());
1430 }
1431
1432 pub fn has_errors(&self) -> bool {
1434 !self.errors.is_empty()
1435 }
1436
1437 pub fn has_warnings(&self) -> bool {
1439 !self.warnings.is_empty()
1440 }
1441
1442 pub fn error_count(&self) -> usize {
1444 self.errors.len()
1445 }
1446
1447 pub fn warning_count(&self) -> usize {
1449 self.warnings.len()
1450 }
1451}
1452
1453#[cfg(test)]
1454mod tests {
1455 use super::*;
1456
1457 #[tokio::test]
1458 async fn test_security_policies_creation() {
1459 let policies = SecurityPolicies::default();
1460 assert!(!policies.allow_network_access());
1461 assert!(!policies.allow_filesystem_read());
1462 assert!(!policies.allow_filesystem_write());
1463 assert_eq!(policies.max_memory_bytes(), 10 * 1024 * 1024);
1464 assert_eq!(policies.max_cpu_percent(), 0.5);
1465 }
1466
1467 #[tokio::test]
1468 async fn test_validation_summary() {
1469 let mut summary = ValidationSummary::default();
1470 assert!(summary.is_valid);
1471 assert!(!summary.has_errors());
1472 assert!(!summary.has_warnings());
1473
1474 summary.add_error("Test error");
1475 assert!(!summary.is_valid);
1476 assert!(summary.has_errors());
1477 assert_eq!(summary.error_count(), 1);
1478
1479 summary.add_warning("Test warning");
1480 assert!(summary.has_warnings());
1481 assert_eq!(summary.warning_count(), 1);
1482 }
1483
1484 #[tokio::test]
1485 async fn test_plugin_validator_creation() {
1486 let config = PluginLoaderConfig::default();
1487 let _validator = PluginValidator::new(config);
1488 }
1491}