#!/usr/bin/env python3
"""Standalone Human Seal verifier (reference algorithm, v0.1). Shares no code with the FLOCORE application.

Requires:  pip install cryptography
Verify one receipt:   python3 hs_verify.py receipt.json [--action action.json]
Run every vector:     python3 hs_verify.py --vectors test-vectors.json

The algorithm: drop signature_ed25519 and public_key, serialize the rest canonically (sorted keys, no whitespace,
ASCII-only output), verify the Ed25519 signature over those bytes, and, when the action is supplied, check that its
canonical sha256 equals content_hash.
"""
import base64, hashlib, json, sys
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey


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


def b64u(s):
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))


def content_hash(action):
    return "sha256:" + hashlib.sha256(canonical(action).encode("utf-8")).hexdigest()


def verify(receipt, action=None):
    if not isinstance(receipt, dict):
        return {"valid": False, "reason": "receipt must be a JSON object"}
    sig, pub = receipt.get("signature_ed25519"), receipt.get("public_key")
    if not sig or not pub:
        return {"valid": False, "reason": "receipt is unsigned (no signature or public key)"}
    core = {k: v for k, v in receipt.items() if k not in ("signature_ed25519", "public_key")}
    try:
        Ed25519PublicKey.from_public_bytes(b64u(pub)).verify(b64u(sig), canonical(core).encode("utf-8"))
    except Exception:
        return {"valid": False, "reason": "signature check failed"}
    if action is not None and content_hash(action) != receipt.get("content_hash"):
        return {"valid": False, "reason": "content_hash does not match the supplied action (possible swap)"}
    return {"valid": True, "reason": "signature valid", "approver_ref": receipt.get("approver_ref"),
            "decision": receipt.get("decision")}


def run_vectors(path):
    suite = json.load(open(path, encoding="utf-8"))
    failed = 0
    for v in suite["vectors"]:
        action = json.loads(v["action_json"]) if v.get("action_json") else None
        r = verify(json.loads(v["receipt_json"]), action)
        ok = r["valid"] == v["expect_valid"] and (not v.get("expect_reason_contains") or v["expect_reason_contains"] in r["reason"])
        print(("PASS " if ok else "FAIL ") + v["name"].ljust(24) + ("" if ok else f" got {r}"))
        failed += 0 if ok else 1
    print(f"\n{failed} of {len(suite['vectors'])} vectors FAILED" if failed else f"\nAll {len(suite['vectors'])} vectors passed")
    return 1 if failed else 0


if __name__ == "__main__":
    a = sys.argv[1:]
    if len(a) == 2 and a[0] == "--vectors":
        sys.exit(run_vectors(a[1]))
    if a and not a[0].startswith("--"):
        action = json.load(open(a[a.index("--action") + 1])) if "--action" in a else None
        r = verify(json.load(open(a[0])), action)
        print(json.dumps(r, indent=2))
        sys.exit(0 if r["valid"] else 1)
    print(__doc__)
    sys.exit(2)
