#!/usr/bin/env python3 """Verify CAIN42_FINAL_PUBLIC_CLAIMS.json without trusting any CAIN-42 website. No CAIN imports; needs Python 3.8+ and `cryptography`. python3 verify_claims.py https://clawx.click/evidence/claims/CAIN42_FINAL_PUBLIC_CLAIMS.json \\ --pin # compare with the key published on the other two sites Checks: REGISTRY the registry digest re-hashes and is signed by the pinned evidence-root key (without --pin, the key is fetched from all three sites and must be identical) ARTIFACTS every artifact is fetched from the chosen site (or --base) and re-hashed: VALID / TAMPERED / UNREACHABLE, with what was expected and what was found STATUS a claim's status/level must be consistent (NOT_IMPLEMENTED => level 0 and no artifacts; level >= 3 => at least one artifact) SCOPE (registry v2) every claim states its evidence environment (LIVE / DISPOSABLE_CLUSTER / OFFLINE / SIMULATED). The verifier re-derives it from the cluster_id values inside each fetched artifact (an id outside the registry's live_clusters means the evidence is not live), re-derives the public label from status + environment with its own copy of the rule, and rejects a claim whose limits say "disposable" while its environment does not. FRESHNESS a claim past its revalidate_by time is STALE: it may have been true, it is no longer current. TOPOLOGY with --check-topology, each live cluster's current members are fetched from its public membership endpoint; if they differ from the members the registry was signed against, every claim with evidence from that cluster is STALE until re-proven on the new membership. A claim is VALID only if the registry is valid, all its artifacts match and its scope is consistent. FINAL RESULT is VALID, STALE (valid but at least one claim expired) or INVALID. """ from __future__ import annotations import base64 import hashlib import json import re import sys import time import urllib.request from typing import Any, Dict, List from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey SITES = {"cainstudio.online": "https://cainstudio.online/proof/bundle/", "mcpgate.online": "https://mcpgate.online/proof/bundle/", "clawx.click": "https://clawx.click/evidence/"} def fetch(url: str) -> bytes: if url.startswith("http"): with urllib.request.urlopen(url, timeout=30) as r: return r.read() with open(url, "rb") as f: return f.read() ENVS = ("LIVE", "DISPOSABLE_CLUSTER", "OFFLINE", "SIMULATED") def expected_label(status: str, envs: List[str]) -> str: """Independent copy of the registry's label rule (the registry states it in label_rule).""" if status == "FAILED": return "FAILED" if status in ("UNVERIFIED", "NOT_IMPLEMENTED"): return "NOT ESTABLISHED" if status == "SIMULATED" or envs == ["SIMULATED"]: return "SIMULATED" if status in ("VERIFIED", "REPRODUCIBLE"): names = {"LIVE": "LIVE", "DISPOSABLE_CLUSTER": "DISPOSABLE CLUSTER", "OFFLINE": "OFFLINE"} return " + ".join(names[e] for e in ENVS if e in envs and e in names) + " VERIFIED" return "IMPLEMENTED" def canon(o) -> bytes: return json.dumps(o, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode() def changed_clusters(reg: Dict[str, Any]) -> Dict[str, str]: """cluster_id -> why its live membership no longer matches the one the registry was signed against.""" out = {} for cid, t in (reg.get("live_topology") or {}).items(): want = sorted(f"{m['node_id']}:{m['public_key_b64']}" for m in t["members"]) if hashlib.sha256("\n".join(want).encode()).hexdigest() != t.get("key_fingerprint"): out[cid] = "registry topology entry is internally inconsistent" continue try: live = json.loads(fetch(t["membership_url"]))["membership"]["members"] got = sorted(f"{m['node_id']}:{m['public_key_b64']}" for m in live) if got != want: out[cid] = "live membership differs from the one the claims were proven against" except Exception as e: # noqa: BLE001 out[cid] = f"live membership unreachable ({str(e)[:60]})" return out def verify(reg_src: str, pin: str = None, base: str = None, check_topology: bool = False) -> Dict[str, Any]: reg = json.loads(fetch(reg_src)) out = {"registry": {}, "claims": []} body = {k: v for k, v in reg.items() if k not in ("registry_digest", "signature_b64")} digest = hashlib.sha256(canon(body)).hexdigest() keys = {} if pin is None: for site, root in SITES.items(): try: keys[site] = json.loads(fetch(root + "claims/evidence-root.pub.json"))["public_key_b64"] except Exception as e: # noqa: BLE001 keys[site] = f"UNREACHABLE: {e}" pin = keys.get("clawx.click") reasons = [] if pin is None or (keys and len(set(keys.values())) != 1): reasons.append(f"evidence-root key differs between sites or is unreachable: {keys}") if reg.get("evidence_root_public_key_b64") != pin: reasons.append("registry names a different evidence-root key than the pinned one") if digest != reg.get("registry_digest"): reasons.append("registry content does not match its digest (edited after signing)") try: Ed25519PublicKey.from_public_bytes(base64.b64decode(pin)).verify(base64.b64decode(reg["signature_b64"]), digest.encode()) except Exception: reasons.append("signature does not verify against the pinned evidence-root key") out["registry"] = {"result": "VALID" if not reasons else "INVALID", "why": reasons, "digest": digest, "pinned_key": pin, "keys_seen": keys} base = base or reg_src.rsplit("/claims/", 1)[0] + "/" v2 = str(reg.get("registry", "")).endswith("/v2") live_clusters = set(reg.get("live_clusters") or []) now = time.time() moved = changed_clusters(reg) if (v2 and check_topology) else {} out["topology"] = {"checked": bool(v2 and check_topology), "changed": moved} for c in reg.get("claims", []): rows, bad = [], [] for a in c["artifacts"]: url = base + a["bundle"] + "/" + a["file"] try: raw = fetch(url) got = hashlib.sha256(raw).hexdigest() ok = got == a["sha256"] if ok and v2: seen = set(re.findall(rb'"cluster_id"\s*:\s*"([^"]+)"', raw)) seen = {x.decode() for x in seen} if seen and not seen <= set(a.get("cluster_ids") or []): bad.append(f"{a['file']}: records clusters {sorted(seen)} the registry does not list for it") if seen and a.get("environment") == "LIVE" and not seen <= live_clusters: bad.append(f"{a['file']}: labelled LIVE but produced by {sorted(seen - live_clusters)}, not a live cluster") rows.append({"file": f"{a['bundle']}/{a['file']}", "result": "VALID" if ok else "TAMPERED", "expected": a["sha256"], "found": got}) if not ok: bad.append(f"{a['file']}: expected sha256 {a['sha256'][:16]}..., found {got[:16]}...") except Exception as e: # noqa: BLE001 rows.append({"file": f"{a['bundle']}/{a['file']}", "result": "UNREACHABLE", "error": str(e)[:120]}) bad.append(f"{a['file']}: unreachable") if c["status"] == "NOT_IMPLEMENTED" and (c["evidence_level"] != 0 or c["artifacts"]): bad.append("NOT_IMPLEMENTED claim carries evidence level or artifacts") if c["evidence_level"] >= 3 and not c["artifacts"]: bad.append(f"evidence level {c['evidence_level']} without artifacts") stale = False if v2: envs = c.get("evidence_environment") or [] if not envs or any(e not in ENVS for e in envs): bad.append(f"evidence environment missing or unknown: {envs}") art_envs = [e for e in ENVS if any(a.get("environment") == e for a in c["artifacts"])] if c["artifacts"] and art_envs != envs: bad.append(f"claim environment {envs} differs from its artifacts' {art_envs}") if c.get("public_label") != expected_label(c["status"], envs): bad.append(f"public label {c.get('public_label')!r} should be {expected_label(c['status'], envs)!r}") if "disposable" in c.get("limits", "").lower() and "DISPOSABLE_CLUSTER" not in envs: bad.append("limits say the evidence is from a disposable cluster; the environment does not") try: stale = time.mktime(time.strptime(c["revalidate_by"], "%Y-%m-%dT%H:%M:%SZ")) - time.timezone < now except Exception: # noqa: BLE001 bad.append("revalidate_by missing or malformed") for a in c["artifacts"]: for cid in a.get("cluster_ids") or []: if a.get("environment") == "LIVE" and cid in moved: stale = True note = f"topology: {cid} {moved[cid]}" if note not in bad: c.setdefault("_notes", []).append(note) result = "VALID" if not bad and out["registry"]["result"] == "VALID" else ( "TAMPERED" if any("expected sha256" in b for b in bad) else "INVALID") if result == "VALID" and stale: result = "STALE" bad.extend(sorted(set(c.pop("_notes", [])))) out["claims"].append({"claim_id": c["claim_id"], "status": c["status"], "evidence_level": c["evidence_level"], "public_label": c.get("public_label"), "revalidate_by": c.get("revalidate_by"), "result": result, "why": bad, "artifacts": rows, "limits": c["limits"]}) results = {x["result"] for x in out["claims"]} out["final"] = "INVALID" if out["registry"]["result"] != "VALID" or results - {"VALID", "STALE"} else ( "STALE" if "STALE" in results else "VALID") return out def main(argv: List[str]) -> int: if len(argv) < 2: print(__doc__) return 2 pin = argv[argv.index("--pin") + 1] if "--pin" in argv else None base = argv[argv.index("--base") + 1] if "--base" in argv else None r = verify(argv[1], pin, base, "--check-topology" in argv) if "--json" in argv: print(json.dumps(r, indent=1)) return 0 if r["final"] == "VALID" else 1 print(f"REGISTRY: {r['registry']['result']} (digest {r['registry']['digest'][:16]}...)") if r.get("topology", {}).get("checked"): moved = r["topology"]["changed"] print("TOPOLOGY: " + ("unchanged since signing" if not moved else "; ".join(f"{k}: {v}" for k, v in moved.items()))) for w in r["registry"]["why"]: print(f" - {w}") for c in r["claims"]: label = f" {c['public_label']}" if c.get("public_label") else "" print(f"{c['result']:9s} {c['claim_id']:34s} {c['status']:16s} level {c['evidence_level']}{label}") for w in c["why"]: print(f" - {w}") print(f"\nFINAL RESULT: {r['final']}") return 0 if r["final"] == "VALID" else 1 if __name__ == "__main__": sys.exit(main(sys.argv))