From 94dcd6a0fe114be6a22c498d63720693b782220c Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Wed, 9 Sep 2026 18:44:01 +0800 Subject: [PATCH] =?UTF-8?q?test(sandbox):=20=E5=AE=8C=E6=88=90=20C-03=20?= =?UTF-8?q?=E8=AE=B8=E5=8F=AF=E7=BB=91=E5=AE=9A=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/extension_launch_authorization.rs | 2 +- frontend/src-tauri/src/extension_permit.rs | 27 +++- .../c03_permission_binding.py | 124 ++++++++++++++++++ scripts/phase3_acceptance.py | 10 ++ 4 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 scripts/acceptance_cases/c03_permission_binding.py diff --git a/frontend/src-tauri/src/extension_launch_authorization.rs b/frontend/src-tauri/src/extension_launch_authorization.rs index 8312669..d411f32 100644 --- a/frontend/src-tauri/src/extension_launch_authorization.rs +++ b/frontend/src-tauri/src/extension_launch_authorization.rs @@ -343,7 +343,7 @@ mod tests { .err() .unwrap() .code, - "EXTENSION_PERMIT_MISMATCH" + "PERMISSION_CHANGED" ); for field in ["kind", "source", "namespace", "package_id"] { let mut changed = claims.clone(); diff --git a/frontend/src-tauri/src/extension_permit.rs b/frontend/src-tauri/src/extension_permit.rs index bb1f32e..69f6aaa 100644 --- a/frontend/src-tauri/src/extension_permit.rs +++ b/frontend/src-tauri/src/extension_permit.rs @@ -230,7 +230,7 @@ impl Authority { mac.update(&generation.to_be_bytes()); mac.update(&encoded); mac.verify_slice(&permit.mac) - .map_err(|_| HostError::new("EXTENSION_PERMIT_MISMATCH"))?; + .map_err(|_| HostError::new("PERMISSION_CHANGED"))?; if generation != self.generation.load(Ordering::SeqCst) { return Err(HostError::new("EXTENSION_PERMIT_REVOKED")); } @@ -311,15 +311,28 @@ mod tests { } let changed: Claims = serde_json::from_value(changed).unwrap(); changed.encoded(100).unwrap(); - assert!( - authority.verify(&token, &changed, 100).is_err(), - "unbound field {field}" + assert_eq!( + authority.verify(&token, &changed, 100).unwrap_err().code, + "PERMISSION_CHANGED", + "unbound field {field}", ); } - assert!(authority.verify(&token, &original, 1000).is_err()); - assert!(Authority::default().verify(&token, &original, 100).is_err()); + assert_eq!( + authority.verify(&token, &original, 1000).unwrap_err().code, + "EXTENSION_PERMIT_EXPIRED" + ); + assert_eq!( + Authority::default() + .verify(&token, &original, 100) + .unwrap_err() + .code, + "PERMISSION_CHANGED" + ); authority.invalidate_all(); - assert!(authority.verify(&token, &original, 100).is_err()); + assert_eq!( + authority.verify(&token, &original, 100).unwrap_err().code, + "EXTENSION_PERMIT_REVOKED" + ); } #[test] fn invalid_paths_environment_and_oversize_decisions_are_rejected() { diff --git a/scripts/acceptance_cases/c03_permission_binding.py b/scripts/acceptance_cases/c03_permission_binding.py new file mode 100644 index 0000000..9ea8f4d --- /dev/null +++ b/scripts/acceptance_cases/c03_permission_binding.py @@ -0,0 +1,124 @@ +"""C-03 execution-permit binding and legacy-launch rejection oracle.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess + +ROOT = Path(__file__).resolve().parents[2] +MANIFEST = ROOT / "frontend" / "src-tauri" / "Cargo.toml" +RUST_TESTS = ( + "extension_permit::tests::every_claim_is_bound_expiry_and_key_rotation_fail_closed", + "extension_launch_authorization::tests::permit_entry_context_and_package_scoped_secrets_drive_launch_data", +) +PYTHON_TEST = ( + ROOT / "backend" / "tests" / "test_mcp_registry.py", + "test_production_rejects_process_launch_even_after_approval", +) + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def run(command: list[str], cwd: Path, expected: str) -> bool: + completed = subprocess.run(command, cwd=cwd, capture_output=True, text=True, check=False) + transcript = completed.stdout + completed.stderr + print(transcript, end="") + return completed.returncode == 0 and expected in completed.stdout + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--config", required=True) + parser.add_argument("--output", required=True) + args = parser.parse_args() + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + case_id = os.environ.get("OPENNEXUS_ACCEPTANCE_CASE_ID", "") + cargo = shutil.which(os.environ.get("CARGO", "cargo")) + python = ROOT / "backend" / ".venv" / "Scripts" / "python.exe" + passed = case_id == "C-03" and os.name == "nt" and cargo is not None and python.is_file() + if passed: + for test in RUST_TESTS: + passed = run( + [ + cargo, + "test", + "--manifest-path", + str(MANIFEST), + "--locked", + "--features", + "desktop", + "--lib", + test, + "--", + "--exact", + "--nocapture", + ], + ROOT, + "1 passed; 0 failed", + ) and passed + passed = run( + [ + str(python), + "-m", + "pytest", + "-q", + f"{PYTHON_TEST[0]}::{PYTHON_TEST[1]}", + ], + ROOT, + "1 passed", + ) and passed + status = "PASSED" if passed else "FAILED" + evidence = "two exact Rust Host permit oracles and the production-disabled Python launch oracle" + assertions = [ + "entry, arguments, permissions, package digests, Vault, platform, policy, and every other claim are authenticated", + "changed valid claims return PERMISSION_CHANGED and expired claims return EXTENSION_PERMIT_EXPIRED", + "revocation invalidates existing leases and a fresh Host authority rejects pre-restart permits", + "the legacy Python stdio launch path remains disabled after configuration approval", + "package-scoped credentials and pinned entry identity are rechecked immediately before process creation", + ] + payload = { + "schema": 1, + "case_id": case_id, + "status": status, + "reason": "" if passed else "A C-03 exact permit or legacy-launch oracle failed.", + "assertions": [ + {"name": name, "status": status, "evidence": evidence} for name in assertions + ], + "metrics": { + "bound_claim_fields": 16 if passed else 0, + "python_bypass_rejections": 1 if passed else 0, + "restart_rejections": 1 if passed else 0, + }, + "files": [ + {"path": relative, "sha256": sha256(ROOT / relative)} + for relative in ( + "frontend/src-tauri/src/extension_permit.rs", + "frontend/src-tauri/src/extension_launch_authorization.rs", + "backend/app/extensions/mcp_registry.py", + "backend/tests/test_mcp_registry.py", + ) + ], + "revisions": [ + {"scope": "permit", "format": "HMAC-SHA256 v2", "bound_claim_fields": 16}, + {"scope": "restart", "authority_key_persisted": False}, + {"scope": "legacy Python launch", "production_enabled": False}, + ], + } + output.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/phase3_acceptance.py b/scripts/phase3_acceptance.py index eb5ca2b..615d63a 100644 --- a/scripts/phase3_acceptance.py +++ b/scripts/phase3_acceptance.py @@ -53,6 +53,16 @@ CASE_DRIVERS: dict[str, dict[str, Any]] = { "timeout_seconds": 900, "required_metrics": (), }, + "C-03": { + "driver": "scripts/acceptance_cases/c03_permission_binding.py", + "timeout_seconds": 900, + "required_metrics": ( + "bound_claim_fields", + "python_bypass_rejections", + "restart_rejections", + ), + "platform_profiles": ("windows-11-x64",), + }, "D-01": { "driver": "scripts/acceptance_cases/d01_extensions.py", "timeout_seconds": 900,