#!/usr/bin/env python3
"""
Swapoon — offline Letter of Guarantee verifier.

No network access. No dependencies. That is the point: a proof you can only
check on the accused party's own server is not a proof.

    python3 verify_log.py swapoon-log-XXXX.json

Or by pasting values directly:

    python3 verify_log.py --payload '<signed string>' --signature '<hex>'

The expected public key is hardcoded below. Compare it against the one
published on our clearnet site, our .onion mirror, our X account and our
Telegram channel. A key confirmed by a single source proves nothing: if all
four do not agree, trust none of them.

This file is self-contained. Ed25519 verification is implemented below in pure
Python (RFC 8032) so that it runs on a machine with no packages installed, no
internet connection, and no trust in us. If PyNaCl happens to be installed it
is used instead, purely because it is faster.

Requires Python 3.8+.
"""

import argparse
import hashlib
import json
import sys

# ─────────────────────────────────────────────────────────────────────────────
# PASTE THE PUBLIC KEY HERE (64 hex characters)
PUBLIC_KEY_HEX = "2a2c4540b1ea1d643e2dd2eede4c7f725329395b8f0c9a1c8c06c85aa3dbb205"
# ─────────────────────────────────────────────────────────────────────────────


# ═════════════════════════════════════════════════════════════════════════════
# Ed25519 verification, RFC 8032. Reference implementation, verify path only.
# Read it if you like: it is short enough to audit in one sitting, which is
# rather the point of shipping it instead of a compiled dependency.
# ═════════════════════════════════════════════════════════════════════════════

_P = 2 ** 255 - 19
_L = 2 ** 252 + 27742317777372353535851937790883648493
_D = (-121665 * pow(121666, _P - 2, _P)) % _P
_I = pow(2, (_P - 1) // 4, _P)


def _recover_x(y: int, sign: int):
    if y >= _P:
        return None
    xx = (y * y - 1) * pow(_D * y * y + 1, _P - 2, _P)
    x = pow(xx, (_P + 3) // 8, _P)
    if (x * x - xx) % _P != 0:
        x = (x * _I) % _P
    if (x * x - xx) % _P != 0:
        return None
    if (x % 2) != sign:
        x = _P - x
    return x


# Points are held in extended coordinates (X, Y, Z, T) to keep the arithmetic
# free of modular inversions inside the hot loop.
_G_Y = 4 * pow(5, _P - 2, _P) % _P
_G_X = _recover_x(_G_Y, 0)
_G = (_G_X, _G_Y, 1, _G_X * _G_Y % _P)


def _point_add(p, q):
    a = (p[1] - p[0]) * (q[1] - q[0]) % _P
    b = (p[1] + p[0]) * (q[1] + q[0]) % _P
    c = 2 * p[3] * q[3] * _D % _P
    d = 2 * p[2] * q[2] % _P
    e, f, g, h = b - a, d - c, d + c, b + a
    return (e * f % _P, g * h % _P, f * g % _P, e * h % _P)


def _point_mul(s: int, p):
    q = (0, 1, 1, 0)  # neutral element
    while s > 0:
        if s & 1:
            q = _point_add(q, p)
        p = _point_add(p, p)
        s >>= 1
    return q


def _point_equal(p, q) -> bool:
    if (p[0] * q[2] - q[0] * p[2]) % _P != 0:
        return False
    return (p[1] * q[2] - q[1] * p[2]) % _P == 0


def _decompress(comp: bytes):
    if len(comp) != 32:
        return None
    y = int.from_bytes(comp, "little")
    sign = y >> 255
    y &= (1 << 255) - 1
    x = _recover_x(y, sign)
    return None if x is None else (x, y, 1, x * y % _P)


def _verify_pure(payload: bytes, signature: bytes, pubkey: bytes) -> bool:
    if len(signature) != 64 or len(pubkey) != 32:
        return False

    a = _decompress(pubkey)
    if a is None:
        return False

    r = _decompress(signature[:32])
    if r is None:
        return False

    s = int.from_bytes(signature[32:], "little")
    if s >= _L:
        return False

    h = int.from_bytes(
        hashlib.sha512(signature[:32] + pubkey + payload).digest(), "little"
    ) % _L

    return _point_equal(_point_mul(s, _G), _point_add(r, _point_mul(h, a)))


def verify(payload: str, signature_hex: str, pubkey_hex: str) -> bool:
    """Verify a detached Ed25519 signature over the exact payload bytes."""
    try:
        sig = bytes.fromhex(signature_hex)
        pub = bytes.fromhex(pubkey_hex)
    except ValueError:
        return False

    data = payload.encode("utf-8")

    # PyNaCl if present, for speed only. The pure-Python path below is the
    # authoritative one and produces identical results.
    try:
        from nacl.exceptions import BadSignatureError
        from nacl.signing import VerifyKey

        try:
            VerifyKey(pub).verify(data, sig)
            return True
        except (BadSignatureError, ValueError):
            return False
    except ImportError:
        pass

    return _verify_pure(data, sig, pub)


# ═════════════════════════════════════════════════════════════════════════════
# CLI
# ═════════════════════════════════════════════════════════════════════════════

def _unwrap(text: str):
    """
    Accept either the downloaded .json envelope or the bare signed string.

    Pasting the whole file is the expected mistake, not the exception: the file
    is what we handed the user. Refusing it would print "INVALID SIGNATURE" on
    a perfectly valid Letter of Guarantee, which is the worst possible failure
    mode for a tool whose entire job is establishing trust.

    Unwrapping removes a transport layer. It relaxes nothing: whatever comes
    out is verified byte for byte.
    """
    try:
        obj = json.loads(text)
    except (json.JSONDecodeError, TypeError):
        return text, None

    if isinstance(obj, dict) and isinstance(obj.get("payload"), str):
        sig = obj.get("signature")
        return obj["payload"], sig if isinstance(sig, str) else None

    return text, None


def main() -> int:
    ap = argparse.ArgumentParser(
        description="Verify a Swapoon Letter of Guarantee, offline.",
        epilog="Exit code 0 means valid, 1 means invalid, 2 means bad usage.",
    )
    ap.add_argument("file", nargs="?", help="the downloaded .json file")
    ap.add_argument("--payload", help="the signed string, or the full .json")
    ap.add_argument("--signature", help="signature in hex (128 characters)")
    ap.add_argument("--pubkey", default=PUBLIC_KEY_HEX,
                    help="override the hardcoded public key")
    args = ap.parse_args()

    # ── Gather payload and signature ────────────────────────────────────────
    if args.file:
        try:
            with open(args.file, "r", encoding="utf-8") as fh:
                raw = fh.read()
        except OSError as exc:
            print(f"Cannot read {args.file}: {exc}", file=sys.stderr)
            return 2
        payload, signature = _unwrap(raw)
        if signature is None:
            signature = args.signature
    elif args.payload:
        payload, signature = _unwrap(args.payload)
        if args.signature:
            signature = args.signature
    else:
        ap.error("provide a .json file, or --payload")
        return 2

    if not signature:
        print("No signature found. Pass --signature, or use the .json file.",
              file=sys.stderr)
        return 2

    pubkey = args.pubkey.strip()
    if len(pubkey) != 64 or not all(c in "0123456789abcdefABCDEF" for c in pubkey):
        # Distinct from an invalid signature. Reporting a misconfigured key as
        # a failed verification would be actively misleading.
        print("The public key is missing or malformed (64 hex characters "
              "expected).\nEdit PUBLIC_KEY_HEX at the top of this file, or "
              "pass --pubkey.", file=sys.stderr)
        return 2

    # The payload is verified exactly as signed. Never reformatted, never
    # re-serialised: a single altered byte must fail, and that is the feature.
    ok = verify(payload, signature.strip(), pubkey)

    print()
    if ok:
        print("  VALID SIGNATURE")
        print()
        try:
            fields = json.loads(payload)
            if isinstance(fields, dict):
                for key in sorted(fields):
                    print(f"    {key:<17} {fields[key]}")
            else:
                print("    (signed, but not a JSON object)")
        except json.JSONDecodeError:
            print("    (signed, but not readable as JSON)")
        print()
        print("  Swapoon issued this order, with these addresses, at this")
        print("  time, and cannot alter it after the fact.")
        print()
        print("  This does NOT attest that funds were sent.")
    else:
        print("  INVALID SIGNATURE")
        print()
        print("  The data was altered, the signature does not match it, or")
        print("  it was not issued by the holder of this public key.")
        print()
        print("  Before drawing conclusions: check that you pasted the data")
        print("  unmodified, and that the public key above is the one")
        print("  published by Swapoon.")
    print()

    return 0 if ok else 1


if __name__ == "__main__":
    sys.exit(main())
