#!/usr/bin/env python3
"""Verify a CAIN-42 Evolution #6 live LEASE bundle without trusting any CAIN website or importing CAIN code.

Needs Python 3.8+, `cryptography`, and verify_pbft_qc_bundle.py (published next to this file). The PBFT
membership is pinned from membership.json; each hypervisor key from LEASE_RUN.json.

  python3 verify_e6_lease.py <bundle-dir-or-url>

For every lease condition (each PASS/FAIL):
  CERTIFICATE  the ZoD authorization certificate re-hashes to its digest and is signed by that run's hypervisor
  CONSENSUS    the certificate digest was ordered by the LIVE cluster: the operation recomputed HERE hashes to the
               proposal digest of a commit QC with >= 3 of the 4 pinned signers
  CHAIN        the evidence chain re-hashes, links and is signed by the hypervisor
  RESULT       the per-condition result row is signed by that run's hypervisor and names this ZoD and chain head
  REFUSAL      the chain shows one tool call succeeding under that authority, then a signed DENIED for the same
               ZoD, and no tool call after it; every summary field in the row (live_authorized_call_succeeded,
               refused_after_trip, tool_ran_after_trip, result) must equal what the chain shows, else FAIL
  Evidence-deletion case: the refusal cannot be logged on a deleted log, so the chain is the pre-deletion one
  and the refusal rests on the hypervisor-SIGNED result row alone. It is reported as SELF-REPORTED, not as
  chain-derived: it proves the hypervisor key attested it, not that an outside observer saw it.
Plus DECISION: the CAIN decision the authorizations rest on is itself quorum-certified.
"""
from __future__ import annotations

import base64
import hashlib
import json
import sys
import urllib.request
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from cryptography.exceptions import InvalidSignature  # noqa: E402
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey  # noqa: E402

D_EVIDENCE, D_ZOD_AUTH, D_APPROVAL, D_ZOD_ORDER = ("CAIN45/EVIDENCE/v1", "CAIN45/ZOD-AUTHORIZATION-CERTIFICATE/v1",
                                                   "CAIN45/HUMAN-APPROVAL/v1", "CAIN45/ZOD-AUTHORIZATION-ORDER/v1")
def canon(o):
    return json.dumps(o, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()


def digest(domain, fields):
    return hashlib.sha256(canon({"domain": domain, **fields})).hexdigest()


def sig_ok(pub, sig, domain, fields):
    try:
        Ed25519PublicKey.from_public_bytes(base64.b64decode(pub)).verify(base64.b64decode(sig), digest(domain, fields).encode())
        return True
    except (InvalidSignature, ValueError, TypeError):
        return False


def load(base, name):
    if base.startswith("http"):
        with urllib.request.urlopen(base.rstrip("/") + "/" + name, timeout=30) as r:
            return json.loads(r.read())
    return json.loads((Path(base) / name).read_text())


D_RESULT = "CAIN42/E6-LEASE-RESULT/v1"


def chain_ok(chain, hv):
    prev, bad = "0" * 64, []
    for i, e in enumerate(chain, 1):
        f = {k: e[k] for k in ("seq", "prev_hash", "at", "zod_id", "event", "data")}
        if e["seq"] != i or e["prev_hash"] != prev or digest(D_EVIDENCE, f) != e["entry_hash"] \
                or not sig_ok(hv, e["signature_b64"], D_EVIDENCE, f):
            bad.append(i)
        prev = e["entry_hash"]
    return not bad, bad


def verify(base):
    import verify_pbft_qc_bundle as Q
    run, chains, decision = load(base, "LEASE_RUN.json"), load(base, "EVIDENCE_CHAINS.json"), load(base, "decision.json")
    qcs, membership = load(base, "qcs.json"), load(base, "membership.json")
    mb = Q.Membership(membership["membership"])
    keys, out = run["hypervisor_public_keys_b64"], []

    dq, c = qcs["cain_decision"], decision["consensus"]
    dop = {"action": "fabric_decision", "resource": f"fabric/{decision['decision_id']}",
           "data": {"decision_id": decision["decision_id"], "commitment_sha256": c["commitment_sha256"],
                    "pre_verdict": c["pre_verdict"]}}
    want = Q.proposal_request_digest({"operation": dop, "request_id": decision["decision_id"], "client_id": "cain-hosted-fabric"})
    ok, why = Q.verify_qc(dq, mb)
    out.append(("DECISION", ok and dq["proposal_digest"] == want and dq["sequence"] == c["sequence"],
                f"{decision['decision_id']} pre-verdict {c['pre_verdict']}, QC seq {dq['sequence']}: {why or 'quorum ok'}"))

    for name, zd in run["zods"].items():
        hv = keys[name]
        cert = zd["consensus_certificate"]
        body = {k: v for k, v in cert.items() if k not in ("consensus", "certificate_digest", "signature_b64", "issuer_public_key_b64")}
        good = digest(D_ZOD_AUTH, body) == cert["certificate_digest"] and sig_ok(hv, cert["signature_b64"], D_ZOD_AUTH, body) \
            and cert["zod_id"] == zd["zod_id"] and cert["decision"]["decision_id"] == decision["decision_id"]
        out.append(("CERTIFICATE", good, f"{name}: {zd['zod_id']}"))
        con = cert["consensus"]
        op = {"action": "zod_authorization", "resource": f"zod/{zd['zod_id']}",
              "data": {"zod_id": zd["zod_id"], "certificate_digest": cert["certificate_digest"], "domain": D_ZOD_ORDER}}
        want = Q.proposal_request_digest({"operation": op, "request_id": con["request_id"], "client_id": con["client_id"]})
        qc = qcs[name]
        ok, why = Q.verify_qc(qc, mb)
        out.append(("CONSENSUS", ok and qc["proposal_digest"] == want and qc["sequence"] == con["sequence"],
                    f"{name}: QC seq {qc['sequence']} signers {', '.join(qc.get('signer_set') or [])}: {why or 'quorum ok, digest matches'}"))

    for res in run["results"]:
        name = res["condition"]
        chain, hv, zid = chains[name], keys[name], run["zods"][name]["zod_id"]
        fields = {k: v for k, v in res.items() if k != "signature_b64"}
        rok = sig_ok(hv, res.get("signature_b64", ""), D_RESULT, fields) and res.get("zod_id") == zid \
            and res.get("chain_head") == (chain[-1]["entry_hash"] if chain else None)
        out.append(("RESULT", rok, f"{name}: result row signed by the hypervisor, bound to {zid} and its chain head"))
        cok, bad = chain_ok(chain, hv)
        out.append(("CHAIN", cok, f"{name}: {len(chain)} entries" + (f"; broken at {bad[:5]}" if bad else "")))
        mine = [e["event"] for e in chain if e["zod_id"] == zid]
        served_before = "TOOL_RESULT" in mine
        if name == "required evidence disappears":
            ok = res.get("source") == "SELF-REPORTED" and served_before and "DENIED" not in mine \
                and res["live_authorized_call_succeeded"] is True and res["refused_after_trip"] is True \
                and res["tool_ran_after_trip"] == 0 and res["result"] == "PASS"
            out.append(("REFUSAL", ok, f"{name}: SELF-REPORTED (hypervisor-signed; the log was deleted): "
                                       f"{res['detail'][:70]}"))
            continue
        first = mine.index("TOOL_RESULT") if served_before else None
        after = mine[first + 1:] if served_before else []
        refused = "DENIED" in after
        ran_after = after[after.index("DENIED"):].count("TOOL_RESULT") if refused else after.count("TOOL_RESULT")
        derived = {"live_authorized_call_succeeded": served_before, "refused_after_trip": refused,
                   "tool_ran_after_trip": ran_after,
                   "result": "PASS" if served_before and refused and ran_after == 0 else "FAIL"}
        mismatch = [k for k, v in derived.items() if res.get(k) != v]
        ok = derived["result"] == "PASS" and not mismatch and res.get("source") == "CHAIN-DERIVED"
        den = [e for e in chain if e["zod_id"] == zid and e["event"] == "DENIED"]
        out.append(("REFUSAL", ok, f"{name}: " + (f"summary disagrees with chain on {mismatch}" if mismatch else
                    (den[-1]["data"].get("reason", "")[:90] if den else "no DENIED entry"))))
    return out


def main(argv):
    if len(argv) != 1:
        print(__doc__)
        return 2
    res = verify(argv[0])
    for name, ok, detail in res:
        print(f"[{'PASS' if ok else 'FAIL'}] {name:12s} {detail}")
    good = all(ok for _, ok, _ in res)
    print(f"\n{sum(ok for _, ok, _ in res)}/{len(res)} checks")
    print("VERIFIED" if good else "NOT VERIFIED")
    return 0 if good else 1


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
