#!/usr/bin/env python3
"""
verify_eve_decision.py — Independently verify an EVE CoreGuard governance
decision evidence record. STANDALONE: this script imports NOTHING from EVE.

A bank's model-risk / audit team can run this with only:
    pip install cryptography
and EVE's published public key (https://eveaicore.com/.well-known/eve-pubkey).

It recomputes the decision hash and verifies the Ed25519 signature against the
PUBLIC key. No shared secret, no EVE service call, no ability to forge. If any
field of the signed record is altered, verification FAILS.

Usage:
    # verify a record against the live published key
    python verify_eve_decision.py record.json

    # verify against a local public-key PEM (fully offline / air-gapped)
    python verify_eve_decision.py record.json --pubkey eve-pubkey.pem

    # read the record from stdin
    cat record.json | python verify_eve_decision.py -

The record may be either a full evidence object (with a "decision_record"
field, as returned by /v1/decisions/evaluate?include_evidence=true) or a bare
decision_record.

Exit code: 0 = VERIFIED, 1 = FAILED/INVALID, 2 = usage/dependency error.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import sys
import urllib.request

DEFAULT_PUBKEY_URL = "https://eveaicore.com/.well-known/eve-pubkey"

# These field names + the canonicalization below MUST match how EVE signs a
# decision (core/coreguard/audit.py: build_audit_record / verify_audit). They
# are reproduced here verbatim so this verifier stays independent of EVE code.
_HASH_FIELDS = ("audit_id", "request_id", "decision", "violations",
                "risk_score", "timestamp")


def _canonical_hash(record: dict) -> str:
    """Recompute the 'sha256-<hex>' content hash over the signed payload."""
    payload = {
        "audit_id":   record.get("audit_id"),
        "request_id": record.get("request_id", ""),
        "decision":   record.get("decision", ""),
        "violations": record.get("violations", 0),
        "risk_score": record.get("risk_score", 0),
        "timestamp":  record.get("timestamp", ""),
    }
    # org_id is part of the signed payload only when present (tenant records).
    if "org_id" in record:
        payload["org_id"] = record["org_id"]
    canonical = json.dumps(payload, sort_keys=True, default=str)
    return "sha256-" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def _load_pubkey(args):
    if args.pubkey:
        with open(args.pubkey, "rb") as fh:
            pem = fh.read()
    else:
        with urllib.request.urlopen(args.pubkey_url, timeout=20) as resp:
            pem = resp.read()
    from cryptography.hazmat.primitives.serialization import load_pem_public_key
    return load_pem_public_key(pem)


def verify(record: dict, args) -> tuple[bool, str]:
    stored_hash = record.get("hash", "")
    stored_sig = record.get("signature", "")
    if not stored_hash or not stored_sig:
        return False, "record is missing 'hash' or 'signature'"

    # 1. Tamper check: recompute the hash over the decision fields. If any field
    #    was altered, the recomputed hash will not match the stored one.
    expected_hash = _canonical_hash(record)
    if expected_hash != stored_hash:
        return False, (
            "HASH MISMATCH — a field in the decision record was altered.\n"
            f"  recomputed: {expected_hash}\n  stored:     {stored_hash}"
        )

    # 2. Signature check. Only Ed25519 is independently verifiable with a public
    #    key. HMAC records require the shared secret (not third-party verifiable).
    if stored_sig.startswith("hmac-"):
        return False, (
            "signature is HMAC (symmetric) — NOT independently verifiable. "
            "Ask EVE to sign with EVE_CERT_SIGN_ALG=ed25519."
        )
    if not stored_sig.startswith("ed25519-"):
        return False, f"unsupported signature algorithm: {stored_sig.split('-')[0]}"

    sig_bytes = bytes.fromhex(stored_sig[len("ed25519-"):])
    try:
        from cryptography.exceptions import InvalidSignature
        pub = _load_pubkey(args)
        # The signature is over the content-hash STRING bytes (not the raw digest).
        pub.verify(sig_bytes, expected_hash.encode("utf-8"))
    except InvalidSignature:
        return False, (
            "SIGNATURE INVALID — the record was not produced by the key behind "
            "the published public key (or was tampered with)."
        )
    except Exception as exc:  # noqa: BLE001
        return False, f"verification error: {type(exc).__name__}: {exc}"

    return True, "VERIFIED — hash recomputed and Ed25519 signature confirmed " \
                 "against the published public key. No EVE involvement required."


def _extract_record(obj: dict) -> dict:
    """Accept a full evidence object or a bare decision_record."""
    if isinstance(obj, dict) and isinstance(obj.get("decision_record"), dict):
        return obj["decision_record"]
    return obj


def main(argv=None) -> int:
    ap = argparse.ArgumentParser(description="Independently verify an EVE CoreGuard decision evidence record.")
    ap.add_argument("record", help="path to the evidence/decision JSON ('-' for stdin)")
    ap.add_argument("--pubkey", help="path to a public-key PEM (offline verification)")
    ap.add_argument("--pubkey-url", default=DEFAULT_PUBKEY_URL,
                    help=f"URL of the published public key (default: {DEFAULT_PUBKEY_URL})")
    args = ap.parse_args(argv)

    try:
        import cryptography  # noqa: F401
    except ImportError:
        print("ERROR: this verifier needs the 'cryptography' package: pip install cryptography",
              file=sys.stderr)
        return 2

    try:
        raw = sys.stdin.read() if args.record == "-" else open(args.record, "r", encoding="utf-8").read()
        obj = json.loads(raw)
    except Exception as exc:  # noqa: BLE001
        print(f"ERROR: could not read/parse record: {exc}", file=sys.stderr)
        return 2

    record = _extract_record(obj)
    ok, message = verify(record, args)
    decision = record.get("decision", "?")
    audit_id = record.get("audit_id", "?")
    print(f"Decision : {decision}")
    print(f"Audit ID : {audit_id}")
    print(f"Result   : {'[VERIFIED] ' if ok else '[FAILED] '}{message}")
    return 0 if ok else 1


if __name__ == "__main__":
    raise SystemExit(main())
