use proxywarden_lib::component_catalog::{ parse_bundled_catalog_if_present, parse_catalog, validate_bundle, AssetArch, ComponentId, TargetArch, }; use serde_json::{json, Value}; use sha2::{Digest, Sha256}; use std::fs; use std::path::{Path, PathBuf}; #[cfg(windows)] use std::process::Command; use uuid::Uuid; #[test] fn parses_exact_x64_catalog_and_all_trust_policy_variants() { let catalog = parse_value(&valid_catalog()).expect("valid catalog must parse"); assert_eq!(catalog.target_arch, TargetArch::X64); assert_eq!(catalog.components.len(), 5); assert_eq!( catalog .components .iter() .find(|component| component.id == ComponentId::Winsw) .expect("WinSW entry") .asset_arch, AssetArch::Anycpu ); } #[test] fn rejects_unknown_schema_arch_fields_duplicates_and_incomplete_set() { assert_rejected(mutate(|catalog| catalog["schemaVersion"] = json!(2))); assert_rejected(mutate(|catalog| catalog["targetArch"] = json!("arm64"))); assert_rejected(mutate(|catalog| catalog["unexpected"] = json!(true))); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["unexpected"] = json!(true); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "windows-packet-filter")["sourceUrl"] = json!( "https://github.com/attacker/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi" ); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "vc-runtime")["sourceUrl"] = json!("https://attacker.example/vc_redist.x64.exe"); component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["allowedSourceHosts"] = json!(["attacker.example"]); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["license"]["unexpected"] = json!(true); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["updateTrustPolicy"]["unexpected"] = json!(true); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "windows-packet-filter")["id"] = json!("proxifyre"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "windows-packet-filter")["installRole"] = json!("proxifyre-runtime"); })); assert_rejected(mutate(|catalog| { catalog["components"] .as_array_mut() .expect("components array") .pop(); })); } #[test] fn rejects_wrong_component_role_or_architecture() { assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["installRole"] = json!("packet-filter-driver"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["assetArch"] = json!("anycpu"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "winsw")["assetArch"] = json!("x64"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "winsw")["effectiveTarget"] = json!("anycpu"); })); } #[test] fn rejects_unsafe_paths_hash_size_license_version_and_source() { for invalid_path in [ "../asset.zip", "proxifyre/../asset.zip", "proxifyre\\asset.zip", "/proxifyre/asset.zip", "proxifyre/CON.zip", "other/asset.zip", ] { assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["assetPath"] = json!(invalid_path); })); } assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["license"]["path"] = json!("../LICENSE.txt"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "windows-packet-filter")["license"]["path"] = json!("proxifyre/WPF-LICENSE.txt"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "windows-packet-filter")["license"]["path"] = json!("proxifyre/LICENSE.txt"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["license"]["id"] = json!("GPL 3"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["sha256"] = json!("A".repeat(64)); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["sha256"] = json!("a".repeat(63)); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["size"] = json!(0); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["version"] = json!("2.4.0-beta.1"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "winsw")["productVersion"] = json!("2.12.0-rc.1"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["sourceUrl"] = json!( "http://github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip" ); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["sourceUrl"] = json!( "https://user:secret@github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip" ); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["sourceUrl"] = json!("https://github.com/wiresock/proxifyre/releases/download/v2.4.0/wrong.zip"); })); } #[test] fn rejects_invalid_trust_policies() { for id in ["proxifyre", "windows-packet-filter", "sing-box"] { assert_rejected(mutate(|catalog| { component_mut(catalog, id)["updateTrustPolicy"] = json!({ "type": "bundledOnlyNoIndependentProof", "reason": "Wrong policy for this component." }); })); } assert_rejected(mutate(|catalog| { component_mut(catalog, "vc-runtime")["updateTrustPolicy"] = json!({ "type": "bundledOnlyNoIndependentProof", "reason": "Wrong policy for this component." }); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "winsw")["updateTrustPolicy"] = json!({ "type": "githubReleaseDigest", "repository": "winsw/winsw", "tagPattern": "v*", "assetPattern": "WinSW.NET461.exe", "requireStable": true }); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["updateTrustPolicy"]["requireStable"] = json!(false); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["updateTrustPolicy"]["repository"] = json!("attacker/proxifyre"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["updateTrustPolicy"]["tagPattern"] = json!("v**"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["updateTrustPolicy"]["authenticodePublishers"] = json!([]); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["allowedSourceHosts"] = json!([]); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["publishers"] = json!([" "]); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["assetPattern"] = json!("other.exe"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "winsw")["updateTrustPolicy"]["type"] = json!("unknownPolicy"); })); } #[test] fn rejects_component_policy_allowlist_expansion() { assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["updateTrustPolicy"]["repository"] = json!("Wiresock/proxifyre"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["updateTrustPolicy"]["tagPattern"] = json!("v2.*"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["updateTrustPolicy"]["assetPattern"] = json!("*"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "proxifyre")["updateTrustPolicy"]["authenticodePublishers"] = Value::Null; })); assert_rejected(mutate(|catalog| { component_mut(catalog, "windows-packet-filter")["updateTrustPolicy"]["assetPattern"] = json!("Windows.Packet.Filter.*"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "windows-packet-filter")["updateTrustPolicy"] ["authenticodePublishers"] = json!(["The Anti-Cloud Corporation", "Unexpected Publisher"]); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "sing-box")["updateTrustPolicy"]["repository"] = json!("sagernet/sing-box"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "sing-box")["updateTrustPolicy"]["authenticodePublishers"] = json!(["Unexpected Publisher"]); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["allowedSourceHosts"] = json!(["aka.ms", "attacker.example"]); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["assetPattern"] = json!("*"); })); assert_rejected(mutate(|catalog| { component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["publishers"] = json!(["Microsoft Corporation", "Unexpected Publisher"]); })); } #[test] fn rejects_wrong_component_license_ids() { for (id, wrong_license) in [ ("proxifyre", "MIT"), ("windows-packet-filter", "GPL-3.0-only"), ("vc-runtime", "LicenseRef-Microsoft-VCRedist"), ("sing-box", "GPL-3.0-or-later"), ("winsw", "AGPL-3.0-only"), ] { assert_rejected(mutate(|catalog| { component_mut(catalog, id)["license"]["id"] = json!(wrong_license); })); } } #[test] fn rejects_unpinned_or_wrong_vc_runtime_source() { for source in [ "https://aka.ms/vs/17/release/vc_redist.x64.exe", "https://aka.ms/vs/18/release/vc_redist.x64.exe", "https://aka.ms/vs/18/release/14.50.35719/VC_redist.x64.exe", ] { assert_rejected(mutate(|catalog| { component_mut(catalog, "vc-runtime")["sourceUrl"] = json!(source); })); } assert_rejected(mutate(|catalog| { component_mut(catalog, "vc-runtime")["version"] = json!("14.50.35719.0"); })); } #[test] fn validates_exact_bundle_contents_hashes_sizes_and_licenses() { let bundle = TestBundle::new(); let catalog = validate_bundle(bundle.path()).expect("complete bundle must validate"); assert_eq!(catalog.components.len(), 5); } #[test] fn rejects_missing_extra_or_changed_package_assets() { let missing = TestBundle::new(); fs::remove_file(missing.path().join(asset_path("proxifyre"))).expect("remove fixture asset"); assert!(validate_bundle(missing.path()).is_err()); let extra = TestBundle::new(); fs::write(extra.path().join("unexpected.bin"), b"extra").expect("write extra file"); assert!(validate_bundle(extra.path()).is_err()); let changed = TestBundle::new(); let path = changed.path().join(asset_path("proxifyre")); let original = fs::read(&path).expect("read fixture asset"); fs::write(&path, vec![b'x'; original.len()]).expect("change fixture asset"); assert!(validate_bundle(changed.path()).is_err()); let wrong_size = TestBundle::new(); let mut catalog: Value = serde_json::from_slice( &fs::read(wrong_size.path().join("catalog.json")).expect("read fixture catalog"), ) .expect("parse fixture catalog"); component_mut(&mut catalog, "proxifyre")["size"] = json!(999); write_catalog(wrong_size.path(), &catalog); assert!(validate_bundle(wrong_size.path()).is_err()); } #[test] fn rejects_missing_or_empty_license_copy() { let missing = TestBundle::new(); fs::remove_file(missing.path().join("proxifyre/LICENSE.txt")).expect("remove fixture license"); assert!(validate_bundle(missing.path()).is_err()); let empty = TestBundle::new(); fs::write(empty.path().join("proxifyre/LICENSE.txt"), b"").expect("empty fixture license"); assert!(validate_bundle(empty.path()).is_err()); } #[test] fn optional_bundle_parse_is_none_only_when_catalog_is_absent() { let absent = TempDirectory::new(); assert!(parse_bundled_catalog_if_present(absent.path()) .expect("absent catalog is allowed") .is_none()); let present = TestBundle::new(); assert!(parse_bundled_catalog_if_present(present.path()) .expect("present catalog must validate") .is_some()); } #[test] fn production_bundle_validates_when_catalog_exists() { let root = Path::new(env!("CARGO_MANIFEST_DIR")) .join("bundled") .join("components"); let catalog = validate_bundle(&root).expect("production component bundle must validate"); assert_eq!(catalog.components.len(), 5); } #[cfg(windows)] #[test] fn rejects_reparse_bundle_root_and_nested_directory() { let target = TestBundle::new(); let junctions = TempDirectory::new(); let root_junction = junctions.path().join("bundle-root-junction"); let root_guard = create_junction(&root_junction, target.path()); assert!(validate_bundle(&root_junction).is_err()); drop(root_guard); let nested = TestBundle::new(); let proxifyre_target = junctions.path().join("proxifyre-target"); fs::rename(nested.path().join("proxifyre"), &proxifyre_target) .expect("move fixture component behind a junction"); let nested_guard = create_junction(&nested.path().join("proxifyre"), &proxifyre_target); assert!(validate_bundle(nested.path()).is_err()); drop(nested_guard); } fn valid_catalog() -> Value { json!({ "schemaVersion": 1, "targetArch": "x64", "components": [ component( "proxifyre", "2.4.0", "ProxiFyre-v2.4.0-x64-signed.zip", "x64", "https://github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip", json!({ "type": "githubReleaseDigest", "repository": "wiresock/proxifyre", "tagPattern": "v*", "assetPattern": "ProxiFyre-v*-x64-signed.zip", "requireStable": true, "authenticodePublishers": ["The Anti-Cloud Corporation"] }) ), component( "windows-packet-filter", "3.6.2", "Windows.Packet.Filter.3.6.2.1.x64.msi", "x64", "https://github.com/wiresock/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi", json!({ "type": "githubReleaseDigest", "repository": "wiresock/ndisapi", "tagPattern": "v*", "assetPattern": "Windows.Packet.Filter.*.x64.msi", "requireStable": true, "authenticodePublishers": ["The Anti-Cloud Corporation"] }) ), component( "vc-runtime", "14.51.36247.0", "VC_redist.x64.exe", "x64", "https://aka.ms/vs/18/release/14.51.36247/VC_redist.x64.exe", json!({ "type": "buildTimeOnlyAuthenticode", "allowedSourceHosts": ["aka.ms"], "assetPattern": "VC_redist.x64.exe", "publishers": ["Microsoft Corporation"] }) ), component( "sing-box", "1.13.19", "sing-box-1.13.19-windows-amd64.zip", "x64", "https://github.com/SagerNet/sing-box/releases/download/v1.13.19/sing-box-1.13.19-windows-amd64.zip", json!({ "type": "githubReleaseDigest", "repository": "SagerNet/sing-box", "tagPattern": "v*", "assetPattern": "sing-box-*-windows-amd64.zip", "requireStable": true }) ), component( "winsw", "2.12.0", "WinSW.NET461.exe", "anycpu", "https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe", json!({ "type": "bundledOnlyNoIndependentProof", "reason": "Upstream provides no independent digest or Authenticode proof for this asset." }) ) ] }) } fn component( id: &str, version: &str, asset_name: &str, asset_arch: &str, source_url: &str, update_trust_policy: Value, ) -> Value { let bytes = asset_bytes(id); let (license_id, install_role) = match id { "proxifyre" => ("AGPL-3.0-only", "proxifyre-runtime"), "windows-packet-filter" => ("MIT", "packet-filter-driver"), "vc-runtime" => ( "LicenseRef-Microsoft-Visual-Cpp-v14-Redistributable-2026", "vc-runtime-prerequisite", ), "sing-box" => ("LicenseRef-Sing-Box-Project", "sing-box-runtime"), "winsw" => ("MIT", "sing-box-service-wrapper"), _ => panic!("unknown fixture component"), }; json!({ "id": id, "version": version, "fileVersion": if id == "windows-packet-filter" { "3.6.2.1" } else { version }, "productVersion": match id { "windows-packet-filter" => "3.6.2.1", "winsw" => "2.12.0+eef5c6a", _ => version }, "assetPath": format!("{id}/{asset_name}"), "assetArch": asset_arch, "effectiveTarget": "x64", "sha256": sha256(bytes), "size": bytes.len(), "sourceUrl": source_url, "license": { "id": license_id, "path": format!("{id}/LICENSE.txt") }, "installRole": install_role, "updateTrustPolicy": update_trust_policy }) } fn asset_bytes(id: &str) -> &'static [u8] { match id { "proxifyre" => b"fixture-proxifyre-asset", "windows-packet-filter" => b"fixture-packet-filter-asset", "vc-runtime" => b"fixture-vc-runtime-asset", "sing-box" => b"fixture-sing-box-asset", "winsw" => b"fixture-winsw-asset", _ => panic!("unknown fixture component"), } } fn asset_path(id: &str) -> String { valid_catalog()["components"] .as_array() .expect("components array") .iter() .find(|component| component["id"] == id) .expect("fixture component")["assetPath"] .as_str() .expect("asset path") .to_string() } fn sha256(bytes: &[u8]) -> String { format!("{:x}", Sha256::digest(bytes)) } fn mutate(change: impl FnOnce(&mut Value)) -> Value { let mut catalog = valid_catalog(); change(&mut catalog); catalog } fn component_mut<'a>(catalog: &'a mut Value, id: &str) -> &'a mut Value { catalog["components"] .as_array_mut() .expect("components array") .iter_mut() .find(|component| component["id"] == id) .expect("fixture component") } fn parse_value( value: &Value, ) -> Result< proxywarden_lib::component_catalog::ComponentCatalog, proxywarden_lib::component_catalog::ComponentCatalogError, > { parse_catalog(&serde_json::to_vec(value).expect("serialize fixture catalog")) } fn assert_rejected(value: Value) { assert!( parse_value(&value).is_err(), "catalog unexpectedly passed: {value}" ); } fn write_catalog(root: &Path, catalog: &Value) { fs::write( root.join("catalog.json"), serde_json::to_vec_pretty(catalog).expect("serialize fixture catalog"), ) .expect("write fixture catalog"); } struct TestBundle { directory: TempDirectory, } impl TestBundle { fn new() -> Self { let directory = TempDirectory::new(); let catalog = valid_catalog(); for component in catalog["components"].as_array().expect("components array") { let id = component["id"].as_str().expect("component id"); let asset_path = component["assetPath"].as_str().expect("asset path"); let license_path = component["license"]["path"].as_str().expect("license path"); fs::create_dir_all( directory .path() .join(asset_path) .parent() .expect("asset parent"), ) .expect("create component directory"); fs::write(directory.path().join(asset_path), asset_bytes(id)) .expect("write fixture asset"); fs::write( directory.path().join(license_path), format!("License fixture for {id}\n"), ) .expect("write fixture license"); } write_catalog(directory.path(), &catalog); Self { directory } } fn path(&self) -> &Path { self.directory.path() } } struct TempDirectory { path: PathBuf, } impl TempDirectory { fn new() -> Self { let path = std::env::temp_dir().join(format!( "proxywarden-component-catalog-test-{}", Uuid::new_v4() )); fs::create_dir_all(&path).expect("create temporary test directory"); Self { path } } fn path(&self) -> &Path { &self.path } } impl Drop for TempDirectory { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.path); } } #[cfg(windows)] struct JunctionGuard { path: PathBuf, } #[cfg(windows)] impl Drop for JunctionGuard { fn drop(&mut self) { let _ = fs::remove_dir(&self.path); } } #[cfg(windows)] fn create_junction(path: &Path, target: &Path) -> JunctionGuard { let output = Command::new("cmd") .args(["/d", "/c", "mklink", "/J"]) .arg(path) .arg(target) .output() .expect("run mklink for reparse-point fixture"); assert!( output.status.success(), "mklink failed: {}", String::from_utf8_lossy(&output.stderr) ); JunctionGuard { path: path.to_path_buf(), } }