#!/usr/bin/env python3 """CASAN Plan-09 — Evidence Pack verifier (MVP). Recomputes the hash of every file in a pack and compares it to the stored artifact-manifest.json. Any change to any packed file flips a hash and fails verification. (The bash wrapper additionally verifies the RSA signature over manifest-head.txt, which stops an attacker who rewrites the manifest too.) Argv: Exit: 0 intact, 1 tamper detected / manifest missing. """ import hashlib import json import os import sys def main(): pack_dir = sys.argv[1] manifest_path = os.path.join(pack_dir, "artifact-manifest.json") if not os.path.isfile(manifest_path): sys.stderr.write("EVIDENCE_PACK_NO_MANIFEST\n") return 1 try: manifest = json.load(open(manifest_path, encoding="utf-8")) except ValueError: sys.stderr.write("EVIDENCE_PACK_MANIFEST_CORRUPT\n") return 1 stored = manifest.get("files", {}) mismatches = [] # Every file recorded in the manifest must still hash to the same value. for fn, want in stored.items(): path = os.path.join(pack_dir, fn) if not os.path.isfile(path): mismatches.append(f"{fn}:missing") continue with open(path, "rb") as f: got = hashlib.sha256(f.read()).hexdigest() if got != want: mismatches.append(f"{fn}:hash_changed") # A new unmanifested file (except sig/head) is also tampering. for fn in os.listdir(pack_dir): if fn in ("artifact-manifest.json", "evidence-pack.sig", "manifest-head.txt"): continue if os.path.isfile(os.path.join(pack_dir, fn)) and fn not in stored: mismatches.append(f"{fn}:unexpected_file") # The stored manifest_head must match the recomputed head of `stored`. canonical = json.dumps(stored, sort_keys=True, separators=(",", ":")) head_now = hashlib.sha256(canonical.encode()).hexdigest() if head_now != manifest.get("manifest_head"): mismatches.append("manifest_head:mismatch") if mismatches: sys.stderr.write("EVIDENCE_PACK_TAMPERED " + " ".join(mismatches) + "\n") return 1 cert = "unknown" rs = os.path.join(pack_dir, "run-summary.json") if os.path.isfile(rs): try: cert = str(json.load(open(rs, encoding="utf-8")).get("certified")) except ValueError: pass print(f"EVIDENCE_PACK_INTACT files={len(stored)} certified={cert} head={head_now}") return 0 if __name__ == "__main__": sys.exit(main())