#!/usr/bin/env python3
"""verify-academic-receipt.py — Standalone verifier for FeedOracle Academic + Curation Receipts.

v2 (2026-05-10) — multi-key aware. Reads the key_id from the receipt's
signature block, fetches the corresponding public key from the published
manifest at https://feedoracle.io/.well-known/academic-l1-signing-manifest.json,
then verifies sha256 + ECDSA signature. Old v1-signed receipts continue to
verify exactly as before — the manifest history retains every retired key
forever.

USAGE:
    # Verify a local receipt file (auto-resolves key from manifest)
    python3 verify-academic-receipt.py path/to/rec-academic-XXX.json

    # Verify with explicit pubkey file (air-gapped — no manifest fetch)
    python3 verify-academic-receipt.py path/to/receipt.json --pubkey academic-l1-signing.pub.pem

    # Verify a receipt fetched from a URL
    python3 verify-academic-receipt.py https://example.com/receipts/rec-academic-XXX.json

    # Pin a specific fingerprint (audit-grade)
    python3 verify-academic-receipt.py receipt.json --expect-fingerprint SHA256:aab48e93...

DEPENDENCIES:
    pip install cryptography

SECURITY NOTES:
    - This script auto-fetches the public key from feedoracle.io/.well-known/
      over HTTPS. Trust is anchored in the TLS certificate of feedoracle.io
      AND in the manifest's documented fingerprints.
    - For audit-grade verification, pin the expected fingerprint of each
      key version (recorded in the manifest's rotation_policy.history) and
      use --pubkey with a locally-stored copy.
    - This script makes no claims about the SEMANTIC correctness of the
      receipt content — only that the bytes were signed by the documented
      key_id and have not been modified since signing.

EXIT CODES:
    0  — receipt is valid (sha256 OK and signature OK, OR pre-1B-F unsigned)
    1  — receipt sha256 mismatch (content modified)
    2  — signature invalid (forged or content modified)
    3  — fetch error (couldn't load receipt, manifest, or public key)
    4  — schema or format error
"""
import argparse
import hashlib
import json
import sys
import urllib.request

try:
    from cryptography.hazmat.primitives import serialization
    from cryptography.hazmat.primitives.asymmetric import ec
    from cryptography.exceptions import InvalidSignature
    from cryptography.hazmat.primitives import hashes
except ImportError:
    print("ERROR: 'cryptography' package not installed. Run: pip install cryptography", file=sys.stderr)
    sys.exit(4)


MANIFEST_URL = "https://feedoracle.io/.well-known/academic-l1-signing-manifest.json"
LEGACY_PUBKEY_URL = "https://feedoracle.io/.well-known/academic-l1-signing.pub.pem"
LEGACY_FINGERPRINT = "SHA256:b9910d8eb372d5f0a9d785e856b0a83524fef209a62cad13a1cea877b4076e0e"


def canonical_json(obj) -> bytes:
    return json.dumps(obj, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8")


def fetch(url_or_path: str) -> bytes:
    if url_or_path.startswith(("http://", "https://")):
        req = urllib.request.Request(url_or_path, headers={"User-Agent": "verify-academic-receipt/2.0"})
        with urllib.request.urlopen(req, timeout=10) as resp:
            return resp.read()
    with open(url_or_path, "rb") as fh:
        return fh.read()


def load_public_key(pem_bytes: bytes):
    return serialization.load_pem_public_key(pem_bytes)


def fingerprint(pub_key) -> str:
    der = pub_key.public_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    return "SHA256:" + hashlib.sha256(der).hexdigest()


def resolve_key_url_from_manifest(key_id: str, manifest: dict) -> tuple[str | None, str | None]:
    """Returns (url, expected_fingerprint) for the given key_id, or (None, None).

    Searches current_key first, then rotation_policy.history[].
    """
    cur = manifest.get("current_key") or {}
    if cur.get("key_id") == key_id:
        return cur.get("url"), cur.get("fingerprint")
    history = (manifest.get("rotation_policy") or {}).get("history", [])
    for entry in history:
        if entry.get("key_id") == key_id:
            return entry.get("url"), entry.get("fingerprint")
    return None, None


def verify_receipt(receipt: dict, pub_key) -> dict:
    body = dict(receipt)
    sig_block = body.pop("signature", None)
    stored_sha = body.pop("sha256", None)

    computed_sha = hashlib.sha256(canonical_json(body)).hexdigest()
    valid_sha = (stored_sha == computed_sha)

    if sig_block is None:
        return {
            "overall_valid": valid_sha,
            "valid_sha256": valid_sha,
            "valid_signature": None,
            "signature_status": "no_signature_pre_1bf_receipt",
            "stored_sha256": stored_sha,
            "computed_sha256": computed_sha,
            "receipt_key_id": None,
            "receipt_fingerprint": None,
        }

    sig_hex = sig_block.get("signature")
    if not sig_hex:
        return {
            "overall_valid": False,
            "valid_sha256": valid_sha,
            "valid_signature": False,
            "signature_status": "signature_field_empty",
            "stored_sha256": stored_sha,
            "computed_sha256": computed_sha,
            "receipt_key_id": sig_block.get("key_id"),
            "receipt_fingerprint": sig_block.get("fingerprint"),
        }

    # Reconstruct what was signed: the body INCLUDING sha256, EXCLUDING signature
    body_for_sig = dict(receipt)
    body_for_sig.pop("signature", None)
    sig_bytes_msg = canonical_json(body_for_sig)

    try:
        sig_bytes = bytes.fromhex(sig_hex)
        pub_key.verify(sig_bytes, sig_bytes_msg, ec.ECDSA(hashes.SHA256()))
        sig_status = "verified"
        sig_valid = True
    except InvalidSignature:
        sig_status = "InvalidSignature"
        sig_valid = False
    except Exception as e:
        sig_status = f"{type(e).__name__}: {e}"
        sig_valid = False

    return {
        "overall_valid": valid_sha and sig_valid,
        "valid_sha256": valid_sha,
        "valid_signature": sig_valid,
        "signature_status": sig_status,
        "stored_sha256": stored_sha,
        "computed_sha256": computed_sha,
        "receipt_key_id": sig_block.get("key_id"),
        "receipt_fingerprint": sig_block.get("fingerprint"),
    }


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("receipt", help="path or URL of the receipt JSON")
    parser.add_argument("--pubkey", help="explicit pubkey file/URL (skips manifest lookup)")
    parser.add_argument("--manifest", default=MANIFEST_URL, help="manifest URL (default: feedoracle.io)")
    parser.add_argument("--expect-fingerprint", help="pin expected key fingerprint (audit-grade)")
    parser.add_argument("--quiet", action="store_true")
    args = parser.parse_args()

    # Fetch receipt
    try:
        receipt_bytes = fetch(args.receipt)
        receipt = json.loads(receipt_bytes)
    except Exception as e:
        print(f"ERROR: cannot fetch/parse receipt: {e}", file=sys.stderr)
        sys.exit(3)

    sig_block = receipt.get("signature") or {}
    receipt_key_id = sig_block.get("key_id") or "academic_l1_v1"  # legacy default

    # Resolve which public key to use
    pub_source = "unknown"
    expected_fp_from_manifest = None
    if args.pubkey:
        pub_url = args.pubkey
        pub_source = "explicit --pubkey"
    else:
        # Try manifest first
        try:
            manifest_bytes = fetch(args.manifest)
            manifest = json.loads(manifest_bytes)
        except Exception as e:
            if not args.quiet:
                print(f"WARN: cannot fetch manifest ({e}); falling back to legacy v1 URL", file=sys.stderr)
            manifest = None

        if manifest:
            url, fp = resolve_key_url_from_manifest(receipt_key_id, manifest)
            if url:
                pub_url = url
                expected_fp_from_manifest = fp
                pub_source = f"manifest → {receipt_key_id}"
            else:
                pub_url = LEGACY_PUBKEY_URL
                pub_source = f"manifest had no entry for {receipt_key_id}, falling back to legacy v1 URL"
        else:
            pub_url = LEGACY_PUBKEY_URL
            pub_source = "legacy v1 URL (no manifest)"

    # Fetch public key
    try:
        pem_bytes = fetch(pub_url)
        pub_key = load_public_key(pem_bytes)
    except Exception as e:
        print(f"ERROR: cannot fetch/load public key from {pub_url}: {e}", file=sys.stderr)
        sys.exit(3)

    fp = fingerprint(pub_key)

    if not args.quiet:
        print(f"Receipt key_id:       {receipt_key_id}")
        print(f"Public key source:    {pub_source}")
        print(f"Public key URL:       {pub_url}")
        print(f"  Fingerprint:        {fp}")

        # Explicit pin FIRST — strictest check before downstream output
        if args.expect_fingerprint:
            if fp == args.expect_fingerprint:
                print(f"  Pinning:            ✓ matches --expect-fingerprint")
            else:
                print(f"  Pinning:            ✗ MISMATCH (expected {args.expect_fingerprint})", file=sys.stderr)
                sys.exit(2)

        # Manifest cross-check
        if expected_fp_from_manifest:
            if fp == expected_fp_from_manifest:
                print(f"  Manifest match:     ✓ ({expected_fp_from_manifest})")
            else:
                print(f"  Manifest match:     ✗ MISMATCH", file=sys.stderr)
                print(f"    manifest:         {expected_fp_from_manifest}", file=sys.stderr)
                print(f"    actual:           {fp}", file=sys.stderr)
                sys.exit(2)

        # Receipt-internal claim cross-check
        receipt_fp = sig_block.get("fingerprint")
        if receipt_fp:
            if fp == receipt_fp:
                print(f"  Receipt match:      ✓")
            else:
                print(f"  Receipt match:      ✗ MISMATCH", file=sys.stderr)
                print(f"    receipt claims:   {receipt_fp}", file=sys.stderr)
                sys.exit(2)

    # Verify
    result = verify_receipt(receipt, pub_key)

    if args.quiet:
        print(f"valid={result['overall_valid']} sha256={result['valid_sha256']} "
              f"signature={result['valid_signature']} key_id={result['receipt_key_id']}")
    else:
        print()
        print(f"Receipt:        {args.receipt}")
        print(f"  receipt_id:           {receipt.get('receipt_id', '?')}")
        print(f"  schema_version:       {receipt.get('schema_version', '?')}")
        print(f"  receipt_kind:         {receipt.get('receipt_kind', receipt.get('verdict', '?'))}")
        print()
        print(f"Verification result:")
        print(f"  sha256 valid:         {result['valid_sha256']}")
        print(f"    stored:             {result['stored_sha256']}")
        print(f"    computed:           {result['computed_sha256']}")
        print(f"  signature valid:      {result['valid_signature']}")
        print(f"    status:             {result['signature_status']}")
        print(f"    receipt key_id:     {result['receipt_key_id']}")
        if result.get("receipt_fingerprint"):
            print(f"    receipt fingerprint: {result['receipt_fingerprint']}")
            print(f"    actual key fp:      {fp}")
            print(f"    fingerprint match:  {'✓' if result['receipt_fingerprint'] == fp else '✗'}")
        print()
        if result["overall_valid"]:
            print("✅ OVERALL: VALID")
        else:
            print("❌ OVERALL: INVALID")

    if not result["valid_sha256"]:
        sys.exit(1)
    if result["valid_signature"] is False:
        sys.exit(2)
    sys.exit(0)


if __name__ == "__main__":
    main()
