#!/usr/bin/env python3
"""Verificador independente do registro público da Stickybit.

Reimplementação em Python, sem código da Stickybit, do que o GIRDER (Go) grava.
Confere, só com a chave pública:
  1. cada entrada: hash = SHA-256 do núcleo canônico, encadeada na anterior, assinada (Ed25519);
  2. cada manifesto diário publicado: o SHA-256 do arquivo bate com payload_sha256 da entrada;
  3. o checkpoint: raiz da árvore de Merkle (RFC 9162) sobre as entradas e assinatura do autor (C2SP signed-note).

  5. opcional (--testemunha VKEY, --quorum K): cossignaturas de testemunhas (c2sp.org/tlog-cosignature,
     Ed25519) sobre o checkpoint, o que tira o razão da classe G1.
  4. opcional (--manifesto): que um arquivo qualquer, por exemplo o manifest de um pacote TUBE,
     tem o SHA-256 selado em alguma entrada do razão. É isso que impede trocar o manifest junto
     com o pacote.

Uso:  pip install cryptography
      python3 verifica.py PASTA [--chave HEX] [--manifesto ARQUIVO ...]
PASTA tem ledger.jsonl, girder.pub, checkpoint.note e, se houver, dias/.
--chave fixa a chave pública esperada. Sem ela, a chave vem da própria pasta, e quem
troca a pasta inteira troca a chave junto. Pegue a chave por outro canal.
Saída 0 = íntegro; 1 = reprovado (a primeira falha é dita).
"""
import argparse, base64, hashlib, json, os, struct, sys
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature

ORDEM = ["seq", "prev", "kind", "ref", "payload_sha256", "summary", "t_plumb", "parents", "branch"]
OMITE_VAZIO = {"payload_sha256", "summary", "t_plumb", "parents", "branch"}
CRU = {"summary", "t_plumb"}  # json.RawMessage no Go: entram exatamente como estão na linha

def falha(msg):
    print("✗ REPROVADO —", msg); sys.exit(1)

def trechos_crus(linha):
    """Devolve o texto JSON exato de cada campo de topo da linha, na forma em que foi gravado."""
    dec = json.JSONDecoder(); out = {}; i = linha.index("{") + 1
    while True:
        while linha[i] in " ,\n": i += 1
        if linha[i] == "}": return out
        chave, i = dec.raw_decode(linha, i)
        while linha[i] in " :": i += 1
        _, j = dec.raw_decode(linha, i)
        out[chave] = linha[i:j]; i = j

def go_str(v):
    # json.Marshal do Go: escapa <, >, & e U+2028/2029, mantém o resto UTF-8
    s = json.dumps(v, ensure_ascii=False, separators=(",", ":"))
    return s.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026").replace("\u2028", "\\u2028").replace("\u2029", "\\u2029")

def nucleo(linha):
    cru = trechos_crus(linha); obj = json.loads(linha); partes = []
    for k in ORDEM:
        if k not in obj: continue
        v = obj[k]
        if k in OMITE_VAZIO and (v in ("", None, [], {}) and k not in CRU): continue
        if k in CRU:
            if cru[k] in ("null",) and k not in obj: continue
            partes.append(f'"{k}":{cru[k]}')
        else:
            partes.append(f'"{k}":{go_str(v)}')
    return ("{" + ",".join(partes) + "}").encode()

def folha(h): return hashlib.sha256(b"\x00" + h).digest()
def no(a, b): return hashlib.sha256(b"\x01" + a + b).digest()
def mth(fs):
    if not fs: return hashlib.sha256(b"").digest()
    if len(fs) == 1: return fs[0]
    k = 1
    while k << 1 < len(fs): k <<= 1
    return no(mth(fs[:k]), mth(fs[k:]))

def main():
    ap = argparse.ArgumentParser(description="Verificador independente do razão GIRDER")
    ap.add_argument("pasta", nargs="?", default=".")
    ap.add_argument("--chave", help="chave pública Ed25519 esperada, em hex")
    ap.add_argument("--manifesto", nargs="*", default=[], help="arquivos cujo SHA-256 deve estar selado no razão")
    ap.add_argument("--origem", default="stickybit.com.br/registro", help="origem de rede do log (além da antiga girder.local/…)")
    ap.add_argument("--testemunha", action="append", default=[], help="vkey de testemunha (tipo 0x04) cuja cossignatura conferir")
    ap.add_argument("--quorum", type=int, default=0, help="mínimo de testemunhas que precisam ter cossinado")
    a = ap.parse_args(); d = a.pasta
    pub_hex = open(os.path.join(d, "girder.pub")).read().strip()
    if a.chave and a.chave.lower() != pub_hex.lower():
        falha(f"girder.pub da pasta ({pub_hex[:16]}…) não é a chave esperada ({a.chave[:16]}…)")
    pub = bytes.fromhex(pub_hex)
    chave = Ed25519PublicKey.from_public_bytes(pub)
    linhas = [l.rstrip("\n") for l in open(os.path.join(d, "ledger.jsonl"), encoding="utf-8") if l.strip()]
    anterior = "0" * 64; hashes = []; manifestos = 0; selados = {}
    for n, l in enumerate(linhas):
        e = json.loads(l)
        if e["seq"] != n: falha(f"entrada {n}: numeração pula (seq={e['seq']}) — algo foi apagado ou reordenado")
        if e["prev"] != anterior: falha(f"entrada {n}: não encadeia na anterior — a linha foi trocada")
        h = hashlib.sha256(nucleo(l)).digest()
        if h.hex() != e["hash"]: falha(f"entrada {n}: hash não bate com o conteúdo — a entrada foi editada")
        try: chave.verify(bytes.fromhex(e["sig"]), h)
        except InvalidSignature: falha(f"entrada {n}: assinatura não confere com a chave pública guardada")
        if e.get("kind") == "diario":
            p = os.path.join(d, "dias", e["ref"] + ".json")
            if os.path.exists(p):
                if hashlib.sha256(open(p, "rb").read()).hexdigest() != e.get("payload_sha256"):
                    falha(f"entrada {n}: o manifesto publicado de {e['ref']} não é o que foi selado")
                manifestos += 1
        anterior = e["hash"]; hashes.append(bytes.fromhex(e["hash"]))
        selados.setdefault(e.get("payload_sha256"), n)
    # checkpoint
    nota = open(os.path.join(d, "checkpoint.note"), encoding="utf-8").read()
    corpo, _, assin = nota.partition("\n\n")
    origem, tam, raiz_b64 = corpo.split("\n")[:3]
    tam = int(tam)
    if tam > len(hashes): falha(f"checkpoint diz {tam} entradas, o razão tem {len(hashes)}")
    if base64.b64decode(raiz_b64) != mth([folha(h) for h in hashes[:tam]]):
        falha("a raiz do checkpoint não é a raiz destas entradas — histórico diferente do assinado")
    if origem not in ("girder.local/" + pub_hex[:16], a.origem): falha(f"origem do checkpoint desconhecida: {origem}")
    kh = hashlib.sha256(origem.encode() + b"\n\x01" + pub).digest()[:4]
    ok = False
    for ln in assin.splitlines():
        if not ln.startswith("— " + origem + " "): continue
        b = base64.b64decode(ln.split()[-1])
        if b[:4] != kh: continue
        try: chave.verify(b[4:], (corpo + "\n").encode()); ok = True
        except InvalidSignature: falha("assinatura do checkpoint não confere")
    if not ok: falha("checkpoint sem assinatura do autor")
    # testemunhas: cossignatura Ed25519 cosignature/v1 (c2sp.org/tlog-cosignature)
    testemunhas = []
    for vk in a.testemunha:
        nome, khex, b64 = vk.split("+", 2)
        raw = base64.b64decode(b64)
        if raw[0] != 0x04 or len(raw) != 33: falha(f"{nome}: só cossignatura Ed25519 (tipo 0x04) é conferida aqui")
        wpub = raw[1:]
        wkh = hashlib.sha256(nome.encode() + b"\n\x04" + wpub).digest()[:4]
        if wkh.hex() != khex: falha(f"vkey de {nome} com keyhash errado")
        wk = Ed25519PublicKey.from_public_bytes(wpub)
        for ln in assin.splitlines():
            if not ln.startswith("— " + nome + " "): continue
            b = base64.b64decode(ln.split()[-1])
            if b[:4] != wkh or len(b) != 4 + 8 + 64: continue
            t = struct.unpack(">Q", b[4:12])[0]
            try:
                wk.verify(b[12:], f"cosignature/v1\ntime {t}\n".encode() + corpo.encode() + b"\n")
                testemunhas.append((nome, t)); break
            except InvalidSignature: falha(f"cossignatura de {nome} não confere")
    if len(testemunhas) < a.quorum:
        falha(f"quórum de testemunhas não atingido: {len(testemunhas)} de {a.quorum}")
    for arq in a.manifesto:
        sh = hashlib.sha256(open(arq, "rb").read()).hexdigest()
        if sh not in selados: falha(f"{arq} não está selado neste razão (sha256 {sh[:16]}…)")
        n = selados[sh]
        if n >= tam: falha(f"{arq} está na entrada {n}, depois do checkpoint assinado ({tam} entradas)")
        print(f"✓ {arq} selado na entrada {n} ({json.loads(linhas[n]).get('kind')} {json.loads(linhas[n]).get('ref')})")
    fix = "fixada pela linha de comando" if a.chave else "lida da própria pasta; use --chave para fixá-la"
    for nome, t in testemunhas:
        print(f"✓ cossinado por {nome} em {t} (POSIX): a testemunha viu esta cabeça consistente com tudo o que já tinha visto")
    classe = "G2, cossinado por testemunha" if testemunhas else "G1, só o autor assinou a cabeça"
    print(f"✓ ÍNTEGRO — {len(linhas)} entradas encadeadas e assinadas, {manifestos} manifestos diários conferidos, "
          f"checkpoint de {tam} entradas com raiz e assinatura corretas (chave {pub_hex[:16]}…, {fix}); classe {classe}")

if __name__ == "__main__":
    main()
