#!/usr/bin/env python3
"""Verify a DriftGuard evidence bundle — without trusting DriftGuard.

Usage:
    python3 verify.py evidence-bundle.zip driftguard-signing.pub

Verification model (all three must hold for VERIFIED):
  1. SIGNATURE — evidence_manifest.json is Ed25519-signed by the key you were
     given. The manifest lists a sha256 for every file in the bundle, so the
     signature covers the entire bundle's content, not just the manifest.
     (requires: pip install cryptography — without it, authenticity CANNOT be
     established and the verdict is NOT VERIFIED.)
  2. FILE DIGESTS — every file in the bundle matches its signed sha256, and
     every signed file is present. Regenerating or replacing any file — the
     ledger included — breaks this, even if its internal structure is valid.
  3. HASH CHAIN — every ledger entry's sha256 over its canonical JSON matches
     its stored hash and links to the previous entry. An empty ledger fails:
     there is nothing to attest.

This script is deliberately short and self-contained — read it before you run
it. Nothing here talks to a network.
"""
import hashlib
import json
import sys
import zipfile

MANIFEST = "evidence_manifest.json"
MANIFEST_SIG = "evidence_manifest.signature"
LEDGER = "ledger/ledger.jsonl"


def canonical(obj) -> str:
    return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)


def check_signature(z, pubkey_hex):
    """Ed25519 over the manifest bytes. Hard requirement — no library, no verdict."""
    try:
        from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
    except ImportError:
        return False, "cryptography not installed — authenticity cannot be established (pip install cryptography)"
    try:
        manifest_bytes = z.read(MANIFEST)
        sig_hex = z.read(MANIFEST_SIG).decode().strip()
    except KeyError as e:
        return False, f"bundle is missing {e} — unsigned bundles cannot be verified"
    try:
        pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex(pubkey_hex))
        pub.verify(bytes.fromhex(sig_hex), manifest_bytes)
        return True, "manifest signature valid for the given key"
    except Exception as e:
        return False, f"signature INVALID: {e}"


def check_file_digests(z):
    """Every file present and matching the signed manifest — ties content to the signature."""
    manifest = json.loads(z.read(MANIFEST))
    files = manifest.get("files", {})
    if LEDGER not in files:
        return False, "signed manifest does not cover the ledger — refusing to verify"
    names = set(z.namelist()) - {MANIFEST, MANIFEST_SIG}
    for name, meta in files.items():
        if name not in names:
            return False, f"{name}: listed in signed manifest but missing from bundle"
        digest = hashlib.sha256(z.read(name)).hexdigest()
        if digest != meta.get("sha256"):
            return False, f"{name}: content does not match its signed sha256 (tampered or replaced)"
    extra = names - set(files)
    if extra:
        return False, f"unsigned files present in bundle: {sorted(extra)}"
    return True, f"{len(files)} files match their signed digests"


def check_chain(z):
    lines = [l for l in z.read(LEDGER).decode().splitlines() if l.strip()]
    if not lines:
        return False, "ledger is empty — nothing to attest"
    prev_hash = "0"
    for i, line in enumerate(lines):
        entry = json.loads(line)
        if entry.get("prev_hash") != prev_hash:
            return False, f"entry {i}: prev_hash does not link to entry {i-1}"
        stored = entry.pop("hash", None)
        computed = hashlib.sha256(canonical(entry).encode()).hexdigest()
        if computed != stored:
            return False, f"entry {i}: content hash mismatch (tampered)"
        prev_hash = stored
    return True, f"{len(lines)} entries, chain intact"


def main():
    if len(sys.argv) != 3:
        print(__doc__)
        sys.exit(2)
    bundle_path, pub_path = sys.argv[1], sys.argv[2]
    pubkey_hex = open(pub_path).read().strip()
    z = zipfile.ZipFile(bundle_path)

    results = []
    for label, fn in (("signature", lambda: check_signature(z, pubkey_hex)),
                      ("file digests", lambda: check_file_digests(z)),
                      ("hash chain", lambda: check_chain(z))):
        try:
            ok, msg = fn()
        except Exception as e:
            ok, msg = False, f"check error: {e}"
        results.append(ok)
        print(("PASS" if ok else "FAIL"), label, "—", msg)

    if all(results):
        print("\nVERIFIED — every file matches its signed digest, the signature matches"
              " the stated key, and the ledger chain is intact.")
        sys.exit(0)
    print("\nNOT VERIFIED — do not rely on this bundle.")
    sys.exit(1)


if __name__ == "__main__":
    main()
