#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Verificatore indipendente a riga di comando · formato passaporto-prova/1.
Solo libreria standard di Python 3.8+: nessuna dipendenza, nessun collegamento alla piattaforma.

    python3 verifica.py evento-xxxxxxxxxxxx.json                 verifica il pacchetto (impronta, firma, prova, registro delle chiavi)
    python3 verifica.py evento.json --rete https://…             in piu' chiede alla rete pubblica se la radice e' registrata
    python3 verifica.py evento.json --allegato certificato.pdf   in piu' controlla l'impronta dei file allegati (ripetibile)

Uscita: 0 = verifica riuscita · 1 = verifica NON riuscita · 2 = non ancora verificabile fino in fondo (o rete non interrogata)."""
import argparse
import base64
import hashlib
import json
import math
import struct
import sys
import urllib.request

# ── Keccak-256 (quello delle catene EVM, non SHA3-256) ──
_RC = (0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 0x8000000080008000, 0x000000000000808B, 0x0000000080000001,
       0x8000000080008081, 0x8000000000008009, 0x000000000000008A, 0x0000000000000088, 0x0000000080008009, 0x000000008000000A,
       0x000000008000808B, 0x800000000000008B, 0x8000000000008089, 0x8000000000008003, 0x8000000000008002, 0x8000000000000080,
       0x000000000000800A, 0x800000008000000A, 0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008)
_ROT = ((0, 36, 3, 41, 18), (1, 44, 10, 45, 2), (62, 6, 43, 15, 61), (28, 55, 25, 21, 56), (27, 20, 39, 8, 14))
_M = (1 << 64) - 1


def keccak256(dati):
    p = bytearray(dati)
    p.append(0x01)
    p.extend(b"\x00" * ((-len(p)) % 136))
    p[-1] |= 0x80
    a = [0] * 25
    for i in range(0, len(p), 136):
        for j, w in enumerate(struct.unpack("<17Q", bytes(p[i:i + 136]))):
            a[j] ^= w
        for rc in _RC:
            c = [a[x] ^ a[x + 5] ^ a[x + 10] ^ a[x + 15] ^ a[x + 20] for x in range(5)]
            for x in range(5):
                d = c[(x - 1) % 5] ^ (((c[(x + 1) % 5] << 1) | (c[(x + 1) % 5] >> 63)) & _M)
                for y in range(0, 25, 5):
                    a[x + y] ^= d
            b = [0] * 25
            for x in range(5):
                for y in range(5):
                    v, r = a[x + 5 * y], _ROT[x][y]
                    b[y + 5 * ((2 * x + 3 * y) % 5)] = ((v << r) | (v >> (64 - r))) & _M if r else v
            for y in range(0, 25, 5):
                for x in range(5):
                    a[x + y] = b[x + y] ^ ((~b[(x + 1) % 5 + y]) & b[(x + 2) % 5 + y])
            a[0] ^= rc
    return struct.pack("<4Q", *a[:4])


def esa(b):
    return "0x" + bytes(b).hex()


def da_esa(s):
    s = str(s or "")
    return bytes.fromhex(s[2:] if s[:2] == "0x" else s)


# ── forma canonica RFC 8785 ──
def _numero(f):
    if math.isnan(f) or math.isinf(f):
        raise ValueError("numero non finito")
    if f == 0:
        return "0"
    segno, r = ("-" if f < 0 else ""), repr(abs(f))
    mant, _, esp = r.partition("e")
    e10 = int(esp) if esp else 0
    intero, _, fraz = mant.partition(".")
    cifre = (intero + fraz).lstrip("0")
    n = len(intero.lstrip("0")) + e10 if intero.strip("0") else e10 - (len(fraz) - len(fraz.lstrip("0")))
    cifre = cifre.rstrip("0") or "0"
    k = len(cifre)
    if k <= n <= 21:
        return segno + cifre + "0" * (n - k)
    if 0 < n <= 21:
        return segno + cifre[:n] + "." + cifre[n:]
    if -6 < n <= 0:
        return segno + "0." + "0" * (-n) + cifre
    e = n - 1
    return segno + cifre[0] + ("." + cifre[1:] if k > 1 else "") + "e" + ("+" if e >= 0 else "-") + str(abs(e))


def canonico(o):
    if o is None:
        return "null"
    if o is True:
        return "true"
    if o is False:
        return "false"
    if isinstance(o, str):
        return json.dumps(o, ensure_ascii=False)
    if isinstance(o, int):
        return str(o)
    if isinstance(o, float):
        return _numero(o)
    if isinstance(o, list):
        return "[" + ",".join(canonico(v) for v in o) + "]"
    return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + canonico(o[k]) for k in sorted(o, key=lambda s: s.encode("utf-16-be"))) + "}"


def e_canonico(t):
    try:
        return canonico(json.loads(t)) == t
    except Exception:
        return False


# ── albero di Merkle a coppie ordinate ──
def coppia(a, b):
    return keccak256(a + b if a <= b else b + a)


def foglia_standard(a, b):
    return keccak256(keccak256(a + b))


def radice_da_prova(foglia, prova):
    h = foglia
    for fratello in prova:
        h = coppia(h, da_esa(fratello))
    return h


ETICHETTA_CHIAVI = keccak256(b"passaporto-chiavi/1")

# ── firma ECDSA P-256 / SHA-256 (verifica, in Python puro) ──
_P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
_N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
_B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
_G = (0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296, 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5)


def _inv(x, m):
    return pow(x, m - 2, m)


def _somma(a, b):
    if a is None:
        return b
    if b is None:
        return a
    if a[0] == b[0]:
        if (a[1] + b[1]) % _P == 0:
            return None
        m = (3 * a[0] * a[0] - 3) * _inv(2 * a[1], _P) % _P
    else:
        m = (b[1] - a[1]) * _inv((b[0] - a[0]) % _P, _P) % _P
    x = (m * m - a[0] - b[0]) % _P
    return x, (m * (a[0] - x) - a[1]) % _P


def _per(k, pt):
    r = None
    while k:
        if k & 1:
            r = _somma(r, pt)
        pt = _somma(pt, pt)
        k >>= 1
    return r


def verifica_firma(pubblica_b64, impronta, firma):
    try:
        der = base64.b64decode(pubblica_b64)
        if len(der) != 91 or der[26] != 4 or len(firma) != 64 or len(impronta) != 32:
            return False
        q = (int.from_bytes(der[27:59], "big"), int.from_bytes(der[59:91], "big"))
        if (q[1] * q[1] - (q[0] ** 3 - 3 * q[0] + _B)) % _P:
            return False
        r, s = int.from_bytes(firma[:32], "big"), int.from_bytes(firma[32:], "big")
        if not (0 < r < _N and 0 < s < _N):
            return False
        e = int.from_bytes(hashlib.sha256(impronta).digest(), "big")
        w = _inv(s, _N)
        pt = _somma(_per(e * w % _N, _G), _per(r * w % _N, q))
        return pt is not None and pt[0] % _N == r
    except Exception:
        return False


# ── la verifica ──
def verifica(p, allegati=None):
    passi = []

    def passo(nome, esito, nota=""):
        passi.append((nome, esito, nota))
        return esito

    ev, doc = p.get("evento") or {}, None
    try:
        impronta = da_esa(ev.get("impronta"))
    except ValueError:
        impronta = b""
    if ev.get("oscurato"):
        passo("impronta", None, "contenuto oscurato")
    else:
        passo("impronta", len(impronta) == 32 and keccak256(str(ev.get("canonico") or "").encode("utf-8")) == impronta)
        if passo("forma_canonica", e_canonico(str(ev.get("canonico") or ""))):
            doc = json.loads(ev["canonico"])
            if doc.get("formato") != "passaporto-evento/1":
                passo("formato", False, str(doc.get("formato")))
    if doc is not None and allegati:
        for x in doc.get("allegati") or []:
            passo("allegato", (x["sha256"] in allegati) or None, x.get("nome", ""))
        for sha in allegati:
            if sha not in [x["sha256"] for x in doc.get("allegati") or []]:
                passo("allegato", False, "file non corrispondente a nessun allegato registrato")
    t, firma, rk = p.get("attestazione"), b"", p.get("registro_chiavi")
    try:
        reg = json.loads(rk["documento"]) if rk else None
    except Exception:
        reg = None
    if not t or not t.get("pubblica"):
        passo("firma", False, "attestazione assente")
    else:
        try:
            firma = da_esa(t.get("firma"))
        except ValueError:
            firma = b""
        passo("firma", t.get("tipo_firma") == "ecdsa-p256-sha256" and verifica_firma(t["pubblica"], impronta, firma))
        passo("chiave", esa(keccak256(base64.b64decode(t["pubblica"]))) == t.get("chiave"))
        if doc is not None and reg:
            voce = next((k for k in reg.get("chiavi") or [] if k.get("id") == t.get("chiave")), None)
            if voce:
                passo("chiave_dell_emittente", voce.get("azienda") == (doc.get("emittente") or {}).get("id"))
    pr = p.get("prova")
    if not pr:
        passo("inclusione", None, "evento non ancora incluso in un lotto d'ancoraggio")
        return (False if any(e is False for _, e, _ in passi) else None), passi, None
    try:
        foglia = foglia_standard(impronta, keccak256(firma))
        passo("foglia", esa(foglia) == pr.get("foglia"))
        passo("inclusione", esa(radice_da_prova(foglia, pr.get("fratelli") or [])) == pr.get("radice"))
    except ValueError:
        passo("inclusione", False)
    if rk:
        try:
            ok = esa(keccak256(rk["documento"].encode("utf-8"))) == rk["impronta"] and e_canonico(rk["documento"])
            ok = ok and esa(radice_da_prova(foglia_standard(ETICHETTA_CHIAVI, da_esa(rk["impronta"])), rk.get("fratelli") or [])) == pr.get("radice")
            passo("registro_chiavi", ok)
            if ok and t:
                passo("chiave_nel_registro", any(k.get("id") == t.get("chiave") and k.get("pubblica") == t.get("pubblica") for k in reg.get("chiavi") or []))
        except Exception:
            passo("registro_chiavi", False)
    neg = any(e is False for _, e, _ in passi)
    inc = any(e is None for n, e, _ in passi if n != "allegato")
    return (False if neg else (None if inc else True)), passi, pr.get("radice")


def radice_in_catena(punto, contratto, mittente, radice):
    """ancorataIl(mittente, radice) letto dal contratto di registro: istante della registrazione, 0 = non registrata."""
    dati = "0x" + keccak256(b"ancorataIl(address,bytes32)")[:4].hex() + "00" * 12 + mittente[2:].lower() + radice[2:]
    corpo = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "eth_call", "params": [{"to": contratto, "data": dati}, "latest"]}).encode()
    req = urllib.request.Request(punto, data=corpo, headers={"content-type": "application/json", "user-agent": "verificatore/1"})
    with urllib.request.urlopen(req, timeout=20) as r:
        j = json.loads(r.read().decode())
    if j.get("error") or not j.get("result"):
        raise RuntimeError("risposta della rete non valida")
    return int(j["result"], 16)


def main(argv=None):
    ap = argparse.ArgumentParser(description="Verificatore indipendente (formato passaporto-prova/1)")
    ap.add_argument("pacchetto")
    ap.add_argument("--rete", help="punto di accesso JSON-RPC alla rete pubblica")
    ap.add_argument("--allegato", action="append", default=[])
    a = ap.parse_args(argv)
    try:
        p = json.load(open(a.pacchetto, encoding="utf-8"))
    except Exception as e:
        print("pacchetto illeggibile:", e)
        return 1
    if p.get("formato") != "passaporto-prova/1":
        print("formato non riconosciuto:", p.get("formato"))
        return 1
    allegati = {hashlib.sha256(open(f, "rb").read()).hexdigest(): f for f in a.allegato}
    esito, passi, radice = verifica(p, allegati or None)
    for nome, e, nota in passi:
        print("  [%s] %s%s" % ("OK" if e is True else ("NO" if e is False else ".."), nome, (" · " + nota) if nota else ""))
    if radice:
        print("  radice del lotto:", radice)
    anc = p.get("ancoraggio") or {}
    if esito is True and a.rete:
        try:
            quando = radice_in_catena(a.rete, anc["contratto"], anc["mittente"], radice)
        except Exception as e:
            print("  [..] catena · la rete non risponde:", e)
            esito = None
        else:
            print("  [%s] catena · %s" % ("OK" if quando else "NO", ("radice registrata all'istante %d dal mittente %s" % (quando, anc["mittente"])) if quando
                                         else "la radice NON risulta registrata"))
            esito = True if quando else False
    elif esito is True:
        print("  [..] catena · non interrogata: aggiungere --rete <punto di accesso> (contratto %s, mittente %s, transazione %s)"
              % (anc.get("contratto"), anc.get("mittente"), anc.get("transazione")))
        esito = None
    print("ESITO:", "VERIFICA RIUSCITA" if esito is True else ("VERIFICA NON RIUSCITA" if esito is False else "NON ANCORA VERIFICABILE FINO IN FONDO"))
    return 0 if esito is True else (1 if esito is False else 2)


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