# lane_vm_proof_v26.py - local differential proof harness (toy LFI fixture vs allowlist fix) # # v26 changes from v25: # - normalize_body() also replaces the lab's absolute path with , so the PHP warning # leaked by v6_empty_param no longer makes normalized hashes device-specific # - Ed25519 signature over the normalized Merkle roots + proof.txt hash (proof.sig.json). # The private key lives OUTSIDE the lab dir (default ~/.lane_vm_signing/), or sign # elsewhere with LANE_VM_SIGN=external (the script writes proof.payload.json for you to sign) # - `--verify [--pubkey FILE_OR_B64]` checks signature, roots and proof.txt hash without # needing the Fernet key # - Fernet key is created lazily (not at import), so --verify touches nothing # # Trust notes: # * Fernet key sits next to proof.txt: tamper-EVIDENT against people without it only. # * The signature is only as independent as the private key's custody. If the key stays on # the same phone, anyone with that phone can re-sign. Keep the key elsewhere, or publish # the public key fingerprint and each run's payload hash somewhere you cannot edit. # * Pin the public key when verifying (--pubkey). Using the key embedded in proof.sig.json # only proves internal consistency. import argparse import base64 import concurrent.futures import datetime import hashlib import hmac import json import os import pathlib import re import socket import subprocess import sys import time import urllib.error import urllib.request import uuid try: import fcntl # Unix only except ImportError: fcntl = None from cryptography.exceptions import InvalidSignature from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes as crypto_hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.hazmat.primitives.kdf.hkdf import HKDF VERSION = "v26" HOME = pathlib.Path(os.environ.get("LANE_VM_HOME", os.path.expanduser("~/php-lab"))).resolve() HOME.mkdir(parents=True, exist_ok=True) ORIG_CWD = os.getcwd() os.chdir(HOME) TEST_ORDER = [ "hallucination", "lfi_control", "real_cve_control", "dario_penalty_check", "v6_empty_param", "v6_traversal_control", "v7_double_run", "v7_encoded_control", "v8_null_byte_control", "v8_case_control", ] SIGN_KEY_FILE = pathlib.Path( os.environ.get("LANE_VM_SIGN_KEY_FILE", os.path.expanduser("~/.lane_vm_signing/ed25519_private.pem")) ).expanduser() # ---------- helpers ---------- def atomic_write_text(path, text): tmp = f"{path}.tmp" with open(tmp, "w", encoding="utf-8") as f: f.write(text) f.flush() os.fsync(f.fileno()) os.replace(tmp, path) # ---------- fixtures ---------- def setup_fixtures(): # Always regenerate php.ini. Opcache is disabled and its lock file points at a writable dir # (PHP 8.5 on Termux fails with "Cannot create lock" when /tmp is unwritable). import tempfile tmpdir = os.environ.get("TMPDIR") or tempfile.gettempdir() pathlib.Path(tmpdir).mkdir(parents=True, exist_ok=True) atomic_write_text( "php.ini", "; Default PHP configuration for Lane-VM lab\n[PHP]\nerror_reporting = E_ALL\ndisplay_errors = On\n" "opcache.enable = 0\nopcache.enable_cli = 0\n" f"opcache.lockfile_path = {tmpdir}\n", ) base = pathlib.Path("fixtures") home_php = " Welcome to Lane-VM Home'; ?>" vuln = base / "vuln" (vuln / "tmp").mkdir(parents=True, exist_ok=True) (vuln / "home.php").write_text(home_php, encoding="utf-8") (vuln / "tmp" / "sess_test").write_text("SESS_PAYLOAD_MARKER_DATA", encoding="utf-8") (vuln / "index.php").write_text("""""", encoding="utf-8") fixed = base / "fixed" (fixed / "tmp").mkdir(parents=True, exist_ok=True) (fixed / "home.php").write_text(home_php, encoding="utf-8") (fixed / "tmp" / "sess_test").write_text("SESS_PAYLOAD_MARKER_DATA", encoding="utf-8") (fixed / "index.php").write_text("""""", encoding="utf-8") # ---------- keys / sealing (Fernet + HMAC, local) ---------- FERNET_KEY = None HMAC_KEY = None cipher_suite = None def get_or_create_key(): env_key = os.environ.get("LANE_VM_KEY") if env_key: return env_key.encode() key_path = pathlib.Path(".lane_vm.key") if key_path.exists(): return key_path.read_bytes() try: fd = os.open(key_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) key = Fernet.generate_key() os.write(fd, key) os.close(fd) return key except FileExistsError: return key_path.read_bytes() def init_crypto(): global FERNET_KEY, HMAC_KEY, cipher_suite FERNET_KEY = get_or_create_key() HMAC_KEY = HKDF( algorithm=crypto_hashes.SHA256(), length=32, salt=b"lane-vm-salt-2026", info=b"lane-vm-hmac", ).derive(FERNET_KEY) cipher_suite = Fernet(FERNET_KEY) def verify_manifest(path): manifest_path = pathlib.Path(f"{path}.manifest.json") if not pathlib.Path(path).exists() or not manifest_path.exists(): return False try: enc = pathlib.Path(path).read_text(encoding="utf-8") man = json.loads(manifest_path.read_text(encoding="utf-8")) sha_ok = hashlib.sha256(enc.encode()).hexdigest() == man["sha256"] hmac_ok = hmac.compare_digest( hmac.new(HMAC_KEY, enc.encode(), hashlib.sha256).hexdigest(), man["hmac"] ) return sha_ok and hmac_ok except Exception: return False def atomic_save_proof_transaction(content_dict, dockerfile_content): content = json.dumps(content_dict, indent=2) lock_file = open("proof.lock", "w", encoding="utf-8") try: if fcntl: fcntl.flock(lock_file, fcntl.LOCK_EX) prev_hash = "" if pathlib.Path("proof.json").exists(): try: prev = json.loads(pathlib.Path("proof.json").read_text(encoding="utf-8")) prev_hash = hashlib.sha256(json.dumps(prev, sort_keys=True).encode()).hexdigest() except Exception: prev_hash = "" enc = cipher_suite.encrypt(content.encode()).decode() manifest = { "sha256": hashlib.sha256(enc.encode()).hexdigest(), "hmac": hmac.new(HMAC_KEY, enc.encode(), hashlib.sha256).hexdigest(), "nonce": str(uuid.uuid4()), "prev_proof_hash": prev_hash, "created": datetime.datetime.now().isoformat(), "version": VERSION, } for n in range(4): path = f"proof.txt.redundant.{n}" atomic_write_text(path, cipher_suite.encrypt(content.encode()).decode()) if cipher_suite.decrypt(pathlib.Path(path).read_bytes()).decode() != content: raise ValueError(f"Redundant copy {n} verification failed") atomic_write_text("proof.txt", enc) if cipher_suite.decrypt(pathlib.Path("proof.txt").read_bytes()).decode() != content: raise ValueError("proof.txt verification failed") atomic_write_text("proof.txt.manifest.json", json.dumps(manifest, indent=2)) atomic_write_text("proof.json", json.dumps(content_dict, indent=2)) if json.loads(pathlib.Path("proof.json").read_text(encoding="utf-8")) != content_dict: raise ValueError("proof.json verification failed") atomic_write_text("Dockerfile.lane-vm", dockerfile_content) if not verify_manifest("proof.txt"): raise ValueError("Manifest verification failed post-write") with open("proof_chain.log", "a", encoding="utf-8") as f: f.write(f"{manifest['created']} {manifest['sha256']} prev={prev_hash[:16]}\n") return manifest["sha256"] finally: if fcntl: fcntl.flock(lock_file, fcntl.LOCK_UN) lock_file.close() # ---------- Ed25519 signing of the roots ---------- def pub_raw_bytes(public_key): return public_key.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) def load_or_create_signing_key(): if SIGN_KEY_FILE.exists(): return serialization.load_pem_private_key(SIGN_KEY_FILE.read_bytes(), password=None) SIGN_KEY_FILE.parent.mkdir(parents=True, exist_ok=True, mode=0o700) key = Ed25519PrivateKey.generate() pem = key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ) try: fd = os.open(SIGN_KEY_FILE, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) os.write(fd, pem) os.close(fd) except FileExistsError: return serialization.load_pem_private_key(SIGN_KEY_FILE.read_bytes(), password=None) pub_b64 = base64.b64encode(pub_raw_bytes(key.public_key())).decode() pub_file = SIGN_KEY_FILE.parent / "ed25519_public.b64" pub_file.write_text(pub_b64 + "\n", encoding="utf-8") print(f"[Lane-VM {VERSION}] Created new signing key: {SIGN_KEY_FILE}") print(f"[Lane-VM {VERSION}] Public key (publish this): {pub_b64}") return key def build_signing_payload(proof, manifest): payload = { "scheme": "lane-vm-signed-roots/1", "version": VERSION, "run_id": proof["run_id"], "timestamp": proof["timestamp"], "vuln_merkle_root_normalized": proof["vuln"]["merkle_root"], "fixed_merkle_root_normalized": proof["fixed"]["merkle_root"], "differential_proof": proof["differential_proof"], "proof_txt_sha256": manifest["sha256"], "prev_proof_hash": manifest["prev_proof_hash"], } return json.dumps(payload, sort_keys=True, separators=(",", ":")) def sign_proof(proof): manifest = json.loads(pathlib.Path("proof.txt.manifest.json").read_text(encoding="utf-8")) payload_str = build_signing_payload(proof, manifest) atomic_write_text("proof.payload.json", payload_str) payload_sha = hashlib.sha256(payload_str.encode()).hexdigest() if os.environ.get("LANE_VM_SIGN", "local").lower() == "external": print(f"[Lane-VM {VERSION}] External signing mode: sign proof.payload.json with your own key.") print(f"[Lane-VM {VERSION}] payload sha256: {payload_sha}") return None if HOME in SIGN_KEY_FILE.resolve().parents: print(f"[Lane-VM {VERSION}] WARNING: signing key is inside the lab dir; move it elsewhere.") key = load_or_create_signing_key() pub_raw = pub_raw_bytes(key.public_key()) fingerprint = hashlib.sha256(pub_raw).hexdigest() record = { "algorithm": "ed25519", "payload": payload_str, "signature_b64": base64.b64encode(key.sign(payload_str.encode())).decode(), "public_key_b64": base64.b64encode(pub_raw).decode(), "public_key_sha256": fingerprint, } atomic_write_text("proof.sig.json", json.dumps(record, indent=2)) with open("proof_signatures.log", "a", encoding="utf-8") as f: f.write(f"{proof['timestamp']} key={fingerprint[:16]} payload_sha256={payload_sha}\n") print(f"[Lane-VM {VERSION}] Signed. key fingerprint: {fingerprint[:16]} payload sha256: {payload_sha[:16]}") return fingerprint def _load_pubkey_arg(arg): p = pathlib.Path(arg) text = p.read_text(encoding="utf-8").strip() if p.is_file() else arg.strip() return base64.b64decode(text) def verify_signed_proof(pubkey_arg=None): """Returns 0 if everything checks out, 3 otherwise. Needs no Fernet key.""" ok = True def report(name, passed, detail=""): nonlocal ok ok = ok and passed print(f" [{'PASS' if passed else 'FAIL'}] {name}{(' - ' + detail) if detail else ''}") try: rec = json.loads(pathlib.Path("proof.sig.json").read_text(encoding="utf-8")) proof = json.loads(pathlib.Path("proof.json").read_text(encoding="utf-8")) payload_str = rec["payload"] payload = json.loads(payload_str) except Exception as e: print(f"[Lane-VM {VERSION}] Cannot read proof.sig.json / proof.json: {e}") return 3 print(f"[Lane-VM {VERSION}] Verifying run {payload.get('run_id')} in {HOME}") embedded_raw = base64.b64decode(rec["public_key_b64"]) if pubkey_arg: try: pub_raw = _load_pubkey_arg(pubkey_arg) except Exception as e: print(f" [FAIL] could not read --pubkey: {e}") return 3 report("pinned public key matches embedded key", pub_raw == embedded_raw) else: pub_raw = embedded_raw print(" [WARN] no --pubkey given: using the key embedded in proof.sig.json (internal consistency only)") try: Ed25519PublicKey.from_public_bytes(pub_raw).verify( base64.b64decode(rec["signature_b64"]), payload_str.encode() ) report("Ed25519 signature over payload", True, f"key {hashlib.sha256(pub_raw).hexdigest()[:16]}") except (InvalidSignature, ValueError) as e: report("Ed25519 signature over payload", False, type(e).__name__) for side in ("vuln", "fixed"): try: tests = proof[side]["tests"] recomputed = merkle_root([tests[n]["norm_sha256"] for n in TEST_ORDER]) except Exception as e: report(f"{side} Merkle root recomputed from proof.json", False, str(e)) continue report(f"{side} root: proof.json stored value matches recomputation", proof[side]["merkle_root"] == recomputed) report(f"{side} root: signed payload matches recomputation", payload.get(f"{side}_merkle_root_normalized") == recomputed) try: actual = hashlib.sha256(pathlib.Path("proof.txt").read_text(encoding="utf-8").encode()).hexdigest() report("proof.txt hash matches signed payload", actual == payload.get("proof_txt_sha256")) except Exception as e: report("proof.txt hash matches signed payload", False, str(e)) report("proof.json differential flag matches signed payload", proof.get("differential_proof") == payload.get("differential_proof")) print(f"[Lane-VM {VERSION}] VERIFY {'OK' if ok else 'FAILED'}") return 0 if ok else 3 # ---------- compare two proofs ---------- def _loosen_paths(text): # display-only: collapse any absolute prefix before /fixtures/ so bodies from different # machines diff cleanly (the hashes themselves are never computed from this) return re.sub(r"(?)(?:/[^\s<>'\"()]+?)?/fixtures/", "/fixtures/", text) def _resolve_user_path(arg): arg = os.path.expanduser(arg) return arg if os.path.isabs(arg) else os.path.join(ORIG_CWD, arg) def compare_proofs(other_arg, base_arg=None): """Compare two proof.json files test by test. Exit 0 = identical after normalization, 4 = differences, 3 = could not read a file.""" import difflib base_path = _resolve_user_path(base_arg) if base_arg else str(HOME / "proof.json") other_path = _resolve_user_path(other_arg) try: a = json.loads(pathlib.Path(base_path).read_text(encoding="utf-8")) b = json.loads(pathlib.Path(other_path).read_text(encoding="utf-8")) except Exception as e: print(f"[Lane-VM {VERSION}] Cannot read proofs: {e}") return 3 print(f"[Lane-VM {VERSION}] Comparing proofs") for label, path, d in (("A", base_path, a), ("B", other_path, b)): print(f" {label}: {path}") print(f" run {d.get('run_id')} {d.get('timestamp')} {d.get('lane_vm')}") diffs = raw_only = status_diffs = 0 differing = [] for side in ("vuln", "fixed"): ra, rb = a.get(side, {}).get("merkle_root"), b.get(side, {}).get("merkle_root") if ra != rb: diffs += 1 print(f"\n[{side}] normalized Merkle root: {'EQUAL' if ra == rb else 'DIFFERENT'}") print(f" A {str(ra)[:16]}... B {str(rb)[:16]}...") ta, tb = a.get(side, {}).get("tests", {}), b.get(side, {}).get("tests", {}) for name in TEST_ORDER: x, y = ta.get(name), tb.get(name) if x is None or y is None: print(f" {name:22} MISSING in {'A' if x is None else 'B'}") diffs += 1 continue same_norm = x["norm_sha256"] == y["norm_sha256"] same_raw = x["sha256"] == y["sha256"] if same_norm: verdict = "same" if same_raw else "same after normalization (raw differs)" raw_only += 0 if same_raw else 1 else: verdict = "DIFFERS" diffs += 1 differing.append((side, name, x, y)) if x.get("status") != y.get("status"): status_diffs += 1 verdict += f" [status: {x.get('status')} vs {y.get('status')}]" print(f" {name:22} {verdict}") for side, name, x, y in differing: la = _loosen_paths(x.get("body", "")).splitlines() lb = _loosen_paths(y.get("body", "")).splitlines() print(f"\n--- {side}/{name}: body diff (paths loosened for display; bodies stored up to 3000 chars)") print(f" A {len(x.get('body', ''))} chars B {len(y.get('body', ''))} chars") if la == lb: print(" bodies are identical after path loosening: the difference comes from normalization rules or truncation") continue out = list(difflib.unified_diff(la, lb, "A", "B", lineterm="", n=1)) for line in out[:25]: print(" " + line[:200]) if len(out) > 25: print(f" ... {len(out) - 25} more diff lines") flags = ("differential_proof",) for f in flags: if a.get(f) != b.get(f): diffs += 1 print(f"\n{f}: A={a.get(f)} B={b.get(f)} (DIFFERENT)") print(f"\n[Lane-VM {VERSION}] SUMMARY: {diffs} difference(s) after normalization, " f"{raw_only} raw-only difference(s) absorbed by normalization, " f"{status_diffs} status-string difference(s) (informational)") print(f"[Lane-VM {VERSION}] COMPARE {'IDENTICAL' if diffs == 0 else 'DIFFERENT'}") return 0 if diffs == 0 else 4 # ---------- docker ---------- DOCKER_IMAGE = "lane-vm-cpp-python:latest" def generate_dockerfile(): return f"""FROM python:3.12-slim-bookworm RUN apt-get update && apt-get install -y --no-install-recommends php-cli && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir cryptography WORKDIR /lab ENV LANE_VM_HOME=/lab COPY lane_vm_proof_{VERSION}.py /lab/ CMD ["python", "lane_vm_proof_{VERSION}.py"] # Image: {DOCKER_IMAGE} # Mount a signing key with: -v /path/to/keys:/keys -e LANE_VM_SIGN_KEY_FILE=/keys/ed25519_private.pem """ # ---------- servers ---------- def port_open(host, port): try: with socket.create_connection((host, port), timeout=0.3): return True except OSError: return False def start_server(docroot, base_port, host="127.0.0.1", ready_timeout=6.0): attempt_logs = [] for offset in range(4): port = base_port + offset if port_open(host, port): attempt_logs.append(f"skipped port {port}: already in use before launch") continue log_name = f"php_server_{docroot.replace('/', '_')}_{port}.log" attempt_logs.append(log_name) log_file = open(log_name, "w", encoding="utf-8") log_file.write(f"[Lane-VM {VERSION}] Starting {docroot} on port {port}\n") log_file.flush() proc = subprocess.Popen( ["php", "-S", f"{host}:{port}", "-t", docroot, "-c", "php.ini"], stdout=log_file, stderr=subprocess.STDOUT, text=True, ) deadline = time.monotonic() + ready_timeout ready = False while time.monotonic() < deadline: if proc.poll() is not None: break if port_open(host, port): time.sleep(0.2) if proc.poll() is None: ready = True break time.sleep(0.1) if ready: name = "fixed_port.txt" if "fixed" in docroot else "vuln_port.txt" atomic_write_text(name, str(port)) return proc, port, log_file if proc.poll() is None: proc.terminate() proc.wait() log_file.close() print(f"[Lane-VM {VERSION}] Failed to bind {docroot}. Attempt logs:") for log in attempt_logs: p = pathlib.Path(log) print(f"--- {log} ---") if p.is_file(): print(p.read_text(encoding="utf-8", errors="replace")) raise RuntimeError(f"Failed to bind server for docroot {docroot}") def stop_server(proc, log_file): if proc: proc.terminate() try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() if log_file and not log_file.closed: log_file.close() # ---------- requests / hashing ---------- def fmt_status(version, code, reason): ver = {10: "1.0", 11: "1.1"}.get(version) return f"HTTP/{ver} {code} {reason}" if ver else f"HTTP {code} {reason}" def fetch_url(url, timeout=10): start = time.monotonic() try: req = urllib.request.Request(url, headers={"User-Agent": f"Lane-VM-{VERSION}/1.0"}) with urllib.request.urlopen(req, timeout=timeout) as resp: body = resp.read().decode("utf-8", errors="replace") return fmt_status(resp.version, resp.status, resp.reason), body, time.monotonic() - start except urllib.error.HTTPError as e: try: body = e.read().decode("utf-8", errors="replace") except Exception: body = "" version = getattr(getattr(e, "fp", None), "version", None) return fmt_status(version, e.code, e.reason), body, time.monotonic() - start except Exception as e: return "no-response", f"error: {e}", time.monotonic() - start def merkle_root(hashes): if not hashes: return "" cur = hashes[:] while len(cur) > 1: nxt = [] for i in range(0, len(cur), 2): a = cur[i] b = cur[i + 1] if i + 1 < len(cur) else a nxt.append(hashlib.sha256((a + b).encode()).hexdigest()) cur = nxt return cur[0] def normalize_body(body): # Absolute lab paths (leaked by PHP warnings) -> ; longest variant first variants = {str(HOME), os.path.abspath(str(HOME)), os.path.realpath(str(HOME))} for h in sorted(variants, key=len, reverse=True): body = body.replace(h, "") cleaned = re.sub(r"\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?\b", "", body) cleaned = re.sub(r"sess_[a-f0-9]{16,}", "sess_TOKEN", cleaned) return cleaned def run_test_suite(host, port): tests = [ ("hallucination", f"http://{host}:{port}/index.php?page=tmp/sess_test"), ("lfi_control", f"http://{host}:{port}/index.php?page=home"), ("real_cve_control", f"http://{host}:{port}/index.php?page=nonexistent"), ("dario_penalty_check", f"http://{host}:{port}/index.php?page=dario"), ("v6_empty_param", f"http://{host}:{port}/index.php?page="), ("v6_traversal_control", f"http://{host}:{port}/index.php?page=../index"), ("v7_double_run", f"http://{host}:{port}/index.php?page=home"), ("v7_encoded_control", f"http://{host}:{port}/index.php?page=%2e%2e%2findex"), ("v8_null_byte_control", f"http://{host}:{port}/index.php?page=home%00"), ("v8_case_control", f"http://{host}:{port}/index.php?page=HOME"), ] results = {} with concurrent.futures.ThreadPoolExecutor(max_workers=4) as ex: futs = {ex.submit(fetch_url, url): (name, url) for name, url in tests} for fut in concurrent.futures.as_completed(futs): name, url = futs[fut] status, body, elapsed = fut.result() raw_h = hashlib.sha256(body.encode()).hexdigest() norm_h = hashlib.sha256(normalize_body(body).encode()).hexdigest() results[name] = { "url": url, "sha256": raw_h, "norm_sha256": norm_h, "status": status, "body_len": len(body), "elapsed_ms": round(elapsed * 1000, 2), "body": body[:3000], } print(f"[{name}] {status} raw:{raw_h[:12]} norm:{norm_h[:12]} len:{len(body)} {elapsed*1000:.0f}ms") return results # ---------- main ---------- def run_lab(): init_crypto() setup_fixtures() START = datetime.datetime.now().isoformat() RUN_ID = str(uuid.uuid4())[:8] print(f"[Lane-VM {VERSION}] {START} run:{RUN_ID}") HOST = "127.0.0.1" VULN_BASE = int(os.environ.get("VULN_PORT", "8080")) FIXED_BASE = int(os.environ.get("FIXED_PORT", "8090")) vuln_proc = vuln_log = fixed_proc = fixed_log = None exit_code = 0 try: vuln_proc, vuln_port, vuln_log = start_server("fixtures/vuln", VULN_BASE, HOST) print(f"[Lane-VM {VERSION}] Vulnerable server on port {vuln_port}") fixed_proc, fixed_port, fixed_log = start_server("fixtures/fixed", FIXED_BASE, HOST) print(f"[Lane-VM {VERSION}] Fixed server on port {fixed_port}") print("\n--- Test matrix: vulnerable ---") vuln_results = run_test_suite(HOST, vuln_port) print("\n--- Test matrix: fixed ---") fixed_results = run_test_suite(HOST, fixed_port) def roots(results): return ( merkle_root([results[n]["norm_sha256"] for n in TEST_ORDER]), merkle_root([results[n]["sha256"] for n in TEST_ORDER]), ) vuln_norm_root, vuln_raw_root = roots(vuln_results) fixed_norm_root, fixed_raw_root = roots(fixed_results) vuln_leaks = "SESS_PAYLOAD_MARKER_DATA" in vuln_results["hallucination"]["body"] fixed_contained = ( "404" in fixed_results["hallucination"]["status"] and "SESS_PAYLOAD_MARKER_DATA" not in fixed_results["hallucination"]["body"] ) vuln_home_ok = "200" in vuln_results["lfi_control"]["status"] and "LANE_HOME_MARKER" in vuln_results["lfi_control"]["body"] fixed_home_ok = "200" in fixed_results["lfi_control"]["status"] and "LANE_HOME_MARKER" in fixed_results["lfi_control"]["body"] differential_proof = vuln_leaks and fixed_contained and vuln_home_ok and fixed_home_ok proof_check = { "vuln_leaks_payload": vuln_leaks, "fixed_contains_payload": fixed_contained, "vuln_home_success": vuln_home_ok, "fixed_home_success": fixed_home_ok, "differential_proof_verified": differential_proof, "vuln_merkle_root_normalized": vuln_norm_root, "fixed_merkle_root_normalized": fixed_norm_root, "vuln_merkle_root_raw": vuln_raw_root, "fixed_merkle_root_raw": fixed_raw_root, "run_id": RUN_ID, } exit_code = 0 if differential_proof else 2 print(f"\n[Lane-VM {VERSION}] Differential checks: {json.dumps(proof_check, indent=2)}\nexit:{exit_code}") proof = { "lane_vm": f"{VERSION} local differential proof", "timestamp": START, "run_id": RUN_ID, "trust_note": "Fernet/HMAC layer is tamper-evident only against parties without the local key. The Ed25519 signature is only as independent as the signing key's custody.", "vuln": {"merkle_root": vuln_norm_root, "tests": vuln_results}, "fixed": {"merkle_root": fixed_norm_root, "tests": fixed_results}, "differential_proof": differential_proof, "proof_checks": proof_check, "exit_code": exit_code, } enc_hash = atomic_save_proof_transaction(proof, generate_dockerfile()) print(f"\n[proof.txt sha:{enc_hash[:16]} saved transactionally]") sign_proof(proof) finally: stop_server(vuln_proc, vuln_log) stop_server(fixed_proc, fixed_log) return exit_code if __name__ == "__main__": ap = argparse.ArgumentParser(description="Lane-VM differential proof harness") ap.add_argument("--verify", action="store_true", help="verify proof.sig.json / proof.json / proof.txt in the lab dir") ap.add_argument("--pubkey", help="pinned Ed25519 public key (base64 string or file) for --verify") ap.add_argument("--compare", metavar="OTHER_PROOF_JSON", help="compare this lab's proof.json with another proof.json, test by test") ap.add_argument("--base", metavar="PROOF_JSON", help="with --compare: use this proof.json instead of the lab's own") args = ap.parse_args() if args.compare: sys.exit(compare_proofs(args.compare, args.base)) if args.verify: sys.exit(verify_signed_proof(args.pubkey)) code = run_lab() if code != 0: sys.exit(code)