"""Check a FULCRUM seal with nobody to ask. Standard library only; needs verify.py beside it (the canonical form).

    curl -s -H "Authorization: Bearer $TOKEN" https://paretoalphasystems.com/api/v1/seals/1 > seal.json
    curl -s https://paretoalphasystems.com/.well-known/fulcrum-keys.json > keys.json
    python3 verify_seal.py seal.json keys.json          # every line true, exit 0

What this proves: the statement is the bytes that were signed, by a key ParetoAlpha published, and it says what the
seal says. What it does not do is run the simulation again: asking the same question again does that (the seal's
`request` is the query), and seal_matches_answer() compares the bytes.
"""
import hashlib, json
from verify import _dump, content_sha256

# Ed25519 verification is RFC 8032, section 5.1.7, in plain integers.

_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
    x2 = (y * y - 1) * pow(_D * y * y + 1, _P - 2, _P) % _P
    if x2 == 0:
        return None if sign else 0
    x = pow(x2, (_P + 3) // 8, _P)
    if (x * x - x2) % _P != 0:
        x = x * _I % _P
    if (x * x - x2) % _P != 0:
        return None
    return _P - x if (x & 1) != sign else x


def _decode(b: bytes):
    if len(b) != 32:
        return None
    y = int.from_bytes(b, "little")
    sign, y = 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 _add(a, b):
    A, B = (a[1] - a[0]) * (b[1] - b[0]) % _P, (a[1] + a[0]) * (b[1] + b[0]) % _P
    C, Dd = 2 * a[3] * b[3] * _D % _P, 2 * a[2] * b[2] % _P
    E, F, G, H = B - A, Dd - C, Dd + C, B + A
    return (E * F % _P, G * H % _P, F * G % _P, E * H % _P)


def _mul(s: int, pt):
    q = (0, 1, 1, 0)
    while s > 0:
        if s & 1:
            q = _add(q, pt)
        pt = _add(pt, pt)
        s >>= 1
    return q


def _same(a, b) -> bool:
    return (a[0] * b[2] - b[0] * a[2]) % _P == 0 and (a[1] * b[2] - b[1] * a[2]) % _P == 0


_GY = 4 * pow(5, _P - 2, _P) % _P
_G = (_recover_x(_GY, 0), _GY, 1, _recover_x(_GY, 0) * _GY % _P)


def ed25519_verify(public: bytes, message: bytes, signature: bytes) -> bool:
    if len(public) != 32 or len(signature) != 64:
        return False
    A, R = _decode(public), _decode(signature[:32])
    s = int.from_bytes(signature[32:], "little")
    if A is None or R is None or s >= _L:
        return False
    h = int.from_bytes(hashlib.sha512(signature[:32] + public + message).digest(), "little") % _L
    return _same(_mul(s, _G), _add(R, _mul(h, A)))


def _b64url(s: str) -> bytes:
    import base64
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))


def verify_seal(seal: dict, keys: dict) -> dict:
    """seal: the "seal" object of GET /api/v1/seals/{id}. keys: /.well-known/fulcrum-keys.json. Every value True = good."""
    text = seal["statement"]
    st = json.loads(text)
    key = next((k for k in keys["keys"] if k["kid"] == seal["signingKeyId"]), None)
    return {
        "signature": bool(key) and ed25519_verify(_b64url(key["x"]), text.encode("utf-8"), _b64url(seal["signature"])),
        "statement_hash": hashlib.sha256(text.encode("utf-8")).hexdigest() == seal["statementSha256"],
        "statement_is_canonical": _dump(st) == text,
        "statement_matches_seal": all(st.get(k) == seal.get(k) for k in ("contentSha256", "requestSha256", "engine", "asOf")),
        "executes_nothing": st.get("executes") is False,
    }


def seal_matches_answer(seal: dict, body: dict) -> bool:
    """Ask the same question again (the seal's request, as the query) and pass the response body: same bytes, same engine."""
    return content_sha256(body["data"]) == seal["contentSha256"] and body["meta"].get("engine") == seal["engine"]


if __name__ == "__main__":
    import sys
    if len(sys.argv) != 3:
        sys.exit("usage: python3 verify_seal.py seal.json keys.json")
    s = json.load(open(sys.argv[1])); s = s.get("data", s); s = s.get("seal", s)
    r = verify_seal(s, json.load(open(sys.argv[2])))
    print(json.dumps(r, indent=2)); sys.exit(0 if all(r.values()) else 1)
