#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
VÉRIFIEUR DE PREUVES CHASER — indépendant, hors-ligne, stdlib pure
==================================================================

Un AUDITEUR EXTERNE vérifie un bundle de preuve Chaser (chaser-proof/v1) SANS
installer Chaser et SANS réseau. Ce fichier est autoportant : il réimplémente
la vérification RFC 6962 (inclusion + cohérence) et, pour la signature Ed25519,
importe le vérifieur pur-Python `_ed25519_verify.py` (lui aussi stdlib seule,
publiable à côté de ce script). Aucune clé privée, aucune dépendance pip.

Ce qu'il prouve, en une passe :
  1. INCLUSION  : la feuille est bien l'entrée `index` de l'arbre `taille`
                  engagé par `racine` (audit path RFC 6962) ;
  2. COHÉRENCE  : (si présente) l'arbre engagé étend un arbre antérieur publié
                  (append-only, aucune réécriture) ;
  3. SIGNATURE  : (si présente) la racine du STH est signée Ed25519 par la clé
                  publique fournie — authenticité en plus de l'intégrité.

Sortie : (ok: bool, details: dict). Un seul octet altéré -> ok=False.

Usage :  python verifier_preuve.py bundle.json
         (ou importer `verifier(bundle_dict)` dans du code tiers)
"""

import hashlib
import json
import os
import sys

# Canonicalisation IDENTIQUE au producteur (mesure_frontiere._canon) : JSON trié,
# séparateurs compacts, non-ASCII conservé. C'est la SEULE convention partagée.
def _canon(data):
    return json.dumps(data, sort_keys=True, ensure_ascii=False,
                      separators=(",", ":")).encode("utf-8")


def _hf(d):                      # hash de feuille RFC 6962 : SHA256(0x00 || d)
    return hashlib.sha256(b"\x00" + d).digest()


def _hn(g, dr):                  # hash de nœud RFC 6962 : SHA256(0x01 || g || d)
    return hashlib.sha256(b"\x01" + g + dr).digest()


def verifier_inclusion(data, index, taille, chemin_hex, racine_hex):
    """RFC 9162 §2.1.3.2 : recalcule la racine depuis la feuille et l'audit
    path, compare à la racine engagée. True SSI la feuille est bien à sa place."""
    try:
        if not 0 <= index < taille:
            return False
        fn, sn = index, taille - 1
        r = _hf(_canon(data))
        for p_hex in chemin_hex:
            p = bytes.fromhex(p_hex)
            if sn == 0:
                return False
            if fn & 1 or fn == sn:
                r = _hn(p, r)
                if not fn & 1:
                    while fn and not fn & 1:
                        fn >>= 1
                        sn >>= 1
            else:
                r = _hn(r, p)
            fn >>= 1
            sn >>= 1
        return sn == 0 and r.hex() == racine_hex
    except (ValueError, TypeError):
        return False


def verifier_coherence(m, n, chemin_hex, racine1_hex, racine2_hex):
    """RFC 9162 §2.1.4.2 : l'arbre `racine2` (taille n) étend-il l'arbre
    `racine1` (taille m) ? True = journal seulement AGRANDI (append-only)."""
    try:
        if m == n:
            return not chemin_hex and racine1_hex == racine2_hex
        if not 0 < m < n or not chemin_hex:
            return False
        noeuds = [bytes.fromhex(h) for h in chemin_hex]
        if m & (m - 1) == 0:                         # m puissance de 2
            noeuds = [bytes.fromhex(racine1_hex)] + noeuds
        fn, sn = m - 1, n - 1
        while fn & 1:
            fn >>= 1
            sn >>= 1
        fr = sr = noeuds[0]
        for c in noeuds[1:]:
            if sn == 0:
                return False
            if fn & 1 or fn == sn:
                fr = _hn(c, fr)
                sr = _hn(c, sr)
                if not fn & 1:
                    while fn and not fn & 1:
                        fn >>= 1
                        sn >>= 1
            else:
                sr = _hn(sr, c)
            fn >>= 1
            sn >>= 1
        return sn == 0 and fr.hex() == racine1_hex and sr.hex() == racine2_hex
    except (ValueError, TypeError):
        return False


def _verifier_signature(sth):
    """Vérifie la signature Ed25519 de la racine du STH, si présente. Renvoie
    True (valide), False (invalide), ou None (pas de signature à vérifier)."""
    if "signature" not in sth:
        return None
    try:
        sys.path.insert(0, os.path.join(os.path.dirname(
            os.path.abspath(__file__)), "..", "moteur"))
        import _ed25519_verify as ed
    except Exception:
        return None                       # vérifieur Ed25519 absent -> non testé
    try:
        msg = sth.get("message")
        if msg is None:
            msg = f"chaser-proof/v1|{sth['taille']}|{sth['racine']}"
        return ed.verify(bytes.fromhex(sth["signature"]), msg.encode("utf-8"),
                         bytes.fromhex(sth["cle_publique"]))
    except Exception:
        return False


def verifier(bundle):
    """Vérifie un bundle chaser-proof/v1. Renvoie (ok, details).
    ok=True SSI l'inclusion est valide ET la cohérence (si présente) valide ET
    la signature (si présente) valide."""
    details = {"inclusion": None, "coherence": None, "signature": None}
    try:
        incl = bundle["inclusion"]
        sth = bundle["sth"]
        feuille = bundle["feuille"]
    except (KeyError, TypeError):
        return False, {"raison": "bundle mal formé (champs manquants)"}

    # 1. la racine d'inclusion doit être celle du STH (sinon on prouverait
    #    l'inclusion dans un AUTRE arbre que celui engagé/signé).
    if incl.get("racine") != sth.get("racine"):
        return False, {"raison": "racine d'inclusion != racine du STH"}

    details["inclusion"] = verifier_inclusion(
        feuille, incl["index"], incl["taille"], incl["chemin"], incl["racine"])
    if not details["inclusion"]:
        return False, {**details, "raison": "preuve d'inclusion invalide"}

    # 2. cohérence append-only (facultative)
    if "coherence" in bundle:
        c = bundle["coherence"]
        details["coherence"] = verifier_coherence(
            c["premiere_taille"], c["seconde_taille"], c["chemin"],
            c["racine_premiere"], c["racine_seconde"])
        if details["coherence"] and c["racine_seconde"] != sth.get("racine"):
            details["coherence"] = False
        if not details["coherence"]:
            return False, {**details, "raison": "preuve de cohérence invalide"}

    # 3. signature (facultative)
    details["signature"] = _verifier_signature(sth)
    if details["signature"] is False:
        return False, {**details, "raison": "signature Ed25519 invalide"}

    return True, details


def _test():
    """Auto-test minimal SANS Chaser : construit un petit arbre RFC 6962 à la
    main, prouve l'inclusion, mute, vérifie le rejet. (Le round-trip complet
    avec le producteur est dans moteur/export_preuve.py::test.)"""
    echecs = []

    def check(cond, nom):
        print(("  OK  " if cond else " FAIL ") + nom)
        if not cond:
            echecs.append(nom)

    print("Auto-test — vérifieur indépendant (RFC 6962, stdlib pure)")

    # arbre de 4 feuilles
    datas = [{"x": i} for i in range(4)]
    fs = [_hf(_canon(d)) for d in datas]
    n01 = _hn(fs[0], fs[1])
    n23 = _hn(fs[2], fs[3])
    racine = _hn(n01, n23).hex()
    # audit path de la feuille 2 : [f3, n01]
    chemin = [fs[3].hex(), n01.hex()]
    check(verifier_inclusion(datas[2], 2, 4, chemin, racine),
          "inclusion feuille 2/4 vérifiée à la main")
    check(not verifier_inclusion({"x": 99}, 2, 4, chemin, racine),
          "feuille mutée -> inclusion rejetée")
    check(not verifier_inclusion(datas[2], 2, 4,
                                 [("0" if chemin[0][0] != "0" else "f")
                                  + chemin[0][1:], chemin[1]], racine),
          "chemin muté -> inclusion rejetée")

    # bundle complet minimal
    bundle = {"feuille": datas[1], "sth": {"taille": 4, "racine": racine},
              "inclusion": {"index": 1, "taille": 4,
                            "chemin": [fs[0].hex(), n23.hex()], "racine": racine}}
    ok, det = verifier(bundle)
    check(ok and det["inclusion"] and det["signature"] is None,
          "bundle non signé : valide, signature non applicable")
    bundle["inclusion"]["racine"] = "00" * 32
    ok2, _ = verifier(bundle)
    check(not ok2, "racine incohérente STH/inclusion -> rejet")

    print(("\nTOUS VERTS ✅" if not echecs
           else f"\n{len(echecs)} ÉCHEC(S) ❌ : " + ", ".join(echecs)))
    return 0 if not echecs else 1


def main():
    try:
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    except (AttributeError, ValueError, OSError):
        pass
    args = sys.argv[1:]
    if not args or args[0] == "test":
        return _test()
    try:
        bundle = json.load(open(args[0], encoding="utf-8"))
    except (OSError, ValueError) as exc:
        print("Bundle illisible :", exc)
        return 2
    ok, details = verifier(bundle)
    print(json.dumps({"valide": ok, "details": details}, ensure_ascii=False,
                     indent=2))
    return 0 if ok else 1


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