#!/usr/bin/env python3
"""
Beweissiegel - unabhaengiges Pruef-Tool
=============================================

Prueft eine exportierte Beweissiegel-Nachweisdatei (JSON-Beweispaket)
VOLLSTAENDIG UNABHAENGIG vom Beweissiegel-Server. Funktioniert auch dann noch,
wenn Beweissiegel als Dienst gar nicht mehr existiert - genau dafuer ist
es gebaut.

Was geprueft wird:
  1. Hash-Kette: entry_hash wird lokal neu berechnet und mit dem im Beweis
     enthaltenen Wert verglichen. Bestaetigt: Das Dokument gehoert
     unveraendert an genau diese Position in der Kette dieses Kunden.
  2. RFC-3161-Zeitstempel (falls vorhanden): unabhaengige Bestaetigung einer
     externen Zeitstempelstelle (DigiCert), dass genau dieser Hash zu einem
     bestimmten Zeitpunkt existierte.
  3. OpenTimestamps/Bitcoin-Verankerung (falls vorhanden): unabhaengige,
     oeffentliche, dezentrale Bestaetigung ueber die Bitcoin-Blockchain.

Benutzung:
  python3 beweissiegel_verify.py nachweis.json

Voraussetzungen:
  - Python 3.8 oder neuer (Schritt 1 braucht NUR die Standardbibliothek)
  - Fuer Schritt 2 (optional, empfohlen): openssl (unter Linux/Mac fast
    immer schon installiert)
  - Fuer Schritt 3 (optional, empfohlen): das freie 'ots'-Kommandozeilentool
    -> pip install opentimestamps-client

Dieses Skript braucht KEINE Verbindung zu Beweissiegel und KEIN Vertrauen in
Beweissiegel als Firma - es prueft ausschliesslich mit Standard-Kryptografie
und oeffentlich einsehbaren, unabhaengigen Quellen (Bitcoin-Blockchain,
externe Zeitstempelstelle). Wer diesem Skript nicht vertrauen will, kann
jede Zeile selbst lesen (kein Netzwerkzugriff ausser optional zu den unten
genannten oeffentlichen Diensten) oder die Pruefschritte von Hand
nachvollziehen (Formel fuer entry_hash steht unten im Code).
"""
import argparse
import base64
import hashlib
import json
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
BOLD = "\033[1m"
RESET = "\033[0m"


def _color(enabled):
    return sys.stdout.isatty() if enabled is None else enabled


USE_COLOR = _color(None)


def _c(code):
    return code if USE_COLOR else ""


def _ok(msg):
    print(f"{_c(GREEN)}[OK]{_c(RESET)} {msg}")


def _fail(msg):
    print(f"{_c(RED)}[FEHLER]{_c(RESET)} {msg}")


def _warn(msg):
    print(f"{_c(YELLOW)}[HINWEIS]{_c(RESET)} {msg}")


def _section(title):
    print()
    print(f"{_c(BOLD)}=== {title} ==={_c(RESET)}")


def compute_entry_hash(prev_entry_hash, document_hash, timestamp, customer_id):
    """Muss exakt dieselbe Formel sein wie im Beweissiegel-Server
    (app/main.py, Funktion create_seal):

        entry_hash = SHA256(f"{prev_entry_hash}|{document_hash}|{timestamp}|{customer_id}")

    Alle vier Werte als Text aneinandergehaengt (mit '|' getrennt), UTF-8
    kodiert. Bei der ersten Versiegelung eines Kunden ist prev_entry_hash
    64 Nullen ("0000...0").
    """
    material = f"{prev_entry_hash}|{document_hash}|{timestamp}|{customer_id}"
    return hashlib.sha256(material.encode("utf-8")).hexdigest()


def verify_hash_chain(bundle):
    _section("1. Hash-Kette (rein lokal, keine Internetverbindung noetig)")
    required = ["document_hash", "prev_entry_hash", "timestamp", "customer_id", "entry_hash"]
    missing = [k for k in required if bundle.get(k) is None]
    if missing:
        _fail(f"Beweispaket unvollstaendig, es fehlen Felder: {', '.join(missing)}")
        return False

    computed = compute_entry_hash(
        bundle["prev_entry_hash"],
        bundle["document_hash"],
        bundle["timestamp"],
        bundle["customer_id"],
    )
    expected = bundle["entry_hash"]
    if computed.lower() == str(expected).lower():
        _ok(f"entry_hash stimmt ueberein: {expected}")
        _ok("Dieser Eintrag ist unveraendert und gehoert nachweislich an diese Stelle der Kette.")
        return True
    _fail("entry_hash stimmt NICHT ueberein!")
    _fail(f"  im Beweispaket:      {expected}")
    _fail(f"  lokal neu berechnet: {computed}")
    _fail("Das bedeutet: Entweder wurde das Beweispaket veraendert, oder document_hash/")
    _fail("timestamp/customer_id/prev_entry_hash im Paket stimmen nicht mit dem Original ueberein.")
    return False


def _extract_message_digest(openssl_text):
    """Holt den 'Message data'-Hash aus der Textausgabe von
    'openssl ts -reply -text' heraus.

    openssl gibt den Hash NICHT als einzelne durchgehende Hex-Zeile aus,
    sondern als klassischen Hexdump mit Offset-Spalte und ASCII-Spalte, z.B.:

        Message data:
            0000 - a7 bf 1c 23 23 8f 14 4b-ab e0 16 0a fd ac c8 1e   ...##..K........
            0010 - fe 86 c8 9c 7c f0 a2 a2-f1 6a 6c e8 84 69 e0 e6   ....|....jl..i..

    Diese Funktion parst genau dieses Format und haengt die Hex-Bytepaare
    zu einem einzelnen Hex-String zusammen.
    """
    lines = openssl_text.splitlines()
    collecting = False
    hex_chunks = []
    for line in lines:
        if not collecting:
            if line.strip().startswith("Message data:"):
                collecting = True
            continue
        stripped = line.strip()
        if not stripped:
            break
        m = re.match(r"^\s*[0-9a-fA-F]+\s*-\s*(.+?)(?:\s{2,}.*)?$", line)
        if not m:
            break
        hex_part = m.group(1).replace("-", " ")
        tokens = hex_part.split()
        if not tokens or not all(len(t) == 2 and re.fullmatch(r"[0-9a-fA-F]{2}", t) for t in tokens):
            break
        hex_chunks.extend(tokens)
    if hex_chunks:
        return "".join(hex_chunks).lower()

    # Fallback fuer den Fall, dass eine andere openssl-Version den Hash als
    # einzelne durchgehende Hex-Zeile ausgibt statt als Hexdump.
    for line in lines:
        candidate = line.strip().lower()
        if len(candidate) == 64 and re.fullmatch(r"[0-9a-f]{64}", candidate):
            return candidate
    return None


def verify_rfc3161(bundle):
    _section("2. RFC-3161-Zeitstempel (externe Zeitstempelstelle, DigiCert)")
    token_b64 = bundle.get("rfc3161_token_base64")
    if not token_b64:
        _warn("Kein RFC-3161-Token in diesem Beweispaket enthalten - ueberspringe.")
        return None

    openssl = shutil.which("openssl")
    if not openssl:
        _warn("'openssl' wurde auf diesem Rechner nicht gefunden - kann das Token nicht")
        _warn("automatisch pruefen. Unter Linux/Mac meist vorinstalliert, unter Windows")
        _warn("z.B. ueber Git for Windows oder https://slproweb.com/products/Win32OpenSSL.html")
        return None

    try:
        token_bytes = base64.b64decode(token_b64)
    except Exception as e:
        _fail(f"rfc3161_token_base64 ist kein gueltiges Base64: {e}")
        return False

    with tempfile.TemporaryDirectory() as tmpdir:
        token_path = Path(tmpdir) / "token.tsr"
        token_path.write_bytes(token_bytes)

        result = subprocess.run(
            [openssl, "ts", "-reply", "-in", str(token_path), "-text"],
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            _fail("Konnte Token nicht lesen - ist die Datei ein gueltiges RFC-3161-Token?")
            _fail(result.stderr.strip())
            return False

        print(result.stdout.strip())

        entry_hash = str(bundle.get("entry_hash", "")).lower()
        digest_in_token = _extract_message_digest(result.stdout)

        if digest_in_token is None:
            _warn("Konnte den Hash im Token-Text nicht automatisch herausparsen - bitte")
            _warn("obige Ausgabe manuell mit entry_hash vergleichen.")
            match_result = None
        elif digest_in_token == entry_hash:
            _ok("Der im Zeitstempel-Token enthaltene Hash (message imprint) stimmt")
            _ok("mit entry_hash ueberein.")
            match_result = True
        else:
            _fail("Der im Token enthaltene Hash weicht vom entry_hash ab!")
            _fail(f"  im Token:  {digest_in_token}")
            _fail(f"  erwartet:  {entry_hash}")
            return False

        _warn(
            "Fuer die VOLLE Signaturpruefung (dass der Token wirklich kryptografisch echt\n"
            "von DigiCert stammt) wird zusaetzlich DigiCerts oeffentliches TSA-Zertifikat\n"
            "benoetigt (das ist eine oeffentliche Datei, kein Geheimnis):\n"
            "  https://knowledge.digicert.com/general-information/timestamp-server\n"
            f"  openssl ts -verify -in {token_path} -digest {entry_hash} -CAfile digicert_tsa_chain.pem"
        )
        return match_result


def verify_opentimestamps(bundle):
    _section("3. OpenTimestamps / Bitcoin-Blockchain-Verankerung")
    ots_b64 = bundle.get("ots_proof_base64")
    if not ots_b64:
        _warn("Keine OpenTimestamps-Verankerung in diesem Beweispaket enthalten - ueberspringe.")
        return None

    ots_bin = shutil.which("ots")
    if not ots_bin:
        _warn("Das 'ots'-Kommandozeilentool ist auf diesem Rechner nicht installiert.")
        _warn("Installieren mit:  pip install opentimestamps-client")
        _warn("Danach dieses Skript erneut ausfuehren.")
        return None

    entry_hash = str(bundle.get("entry_hash", ""))
    try:
        proof_bytes = base64.b64decode(ots_b64)
    except Exception as e:
        _fail(f"ots_proof_base64 ist kein gueltiges Base64: {e}")
        return False

    with tempfile.TemporaryDirectory() as tmpdir:
        # Wichtig: exakt dieselbe Konstruktion wie beim Erzeugen des Beweises
        # (app/blockchain.py, stamp_entry_hash) - eine Textdatei, deren
        # Inhalt der entry_hash-String ist (kein Zeilenumbruch angehaengt),
        # daneben liegt die .ots-Beweisdatei.
        data_path = Path(tmpdir) / "entry.txt"
        data_path.write_text(entry_hash)
        proof_path = Path(str(data_path) + ".ots")
        proof_path.write_bytes(proof_bytes)

        try:
            result = subprocess.run(
                [ots_bin, "--no-cache", "verify", str(proof_path)],
                capture_output=True,
                text=True,
                timeout=30,
            )
        except subprocess.TimeoutExpired:
            _warn("Zeitueberschreitung beim Kontakt mit dem OpenTimestamps-Kalenderserver.")
            _warn("Bitte spaeter erneut versuchen.")
            return None

        output = (result.stdout + result.stderr).strip()
        print(output)

        low = output.lower()
        if result.returncode == 0 and ("bitcoin" in low or "block" in low) and "pending" not in low:
            _ok("Bitcoin-Blockchain-Verankerung bestaetigt - unabhaengig gegen das oeffentliche,")
            _ok("dezentrale Bitcoin-Netzwerk geprueft, nicht nur gegen Beweissiegels eigene Angabe.")
            return True
        if "pending" in low:
            _warn("Verankerung ist noch ausstehend (Bitcoin-Bestaetigung dauert nach dem")
            _warn("Siegeln in der Regel mehrere Stunden). Bitte spaeter erneut versuchen.")
            return None
        _fail("Verankerung konnte nicht bestaetigt werden.")
        return False


def main():
    parser = argparse.ArgumentParser(
        description="Unabhaengige Pruefung eines Beweissiegel-Nachweispakets.",
    )
    parser.add_argument("beweisdatei", help="Pfad zur exportierten .json-Nachweisdatei")
    parser.add_argument(
        "--no-color", action="store_true", help="Ausgabe ohne Terminalfarben (z.B. fuer Protokolle)"
    )
    args = parser.parse_args()

    global USE_COLOR
    if args.no_color:
        USE_COLOR = False

    path = Path(args.beweisdatei)
    if not path.exists():
        print(f"Datei nicht gefunden: {path}", file=sys.stderr)
        sys.exit(1)

    try:
        bundle = json.loads(path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as e:
        print(f"Datei ist kein gueltiges JSON: {e}", file=sys.stderr)
        sys.exit(1)

    print(f"{_c(BOLD)}Beweissiegel - unabhaengiges Pruef-Tool{_c(RESET)}")
    print(f"Pruefe Datei: {path}")
    if bundle.get("label"):
        print(f"Referenz/Label: {bundle['label']}")

    chain_ok = verify_hash_chain(bundle)
    rfc_ok = verify_rfc3161(bundle)
    ots_ok = verify_opentimestamps(bundle)

    _section("Zusammenfassung")
    (_ok if chain_ok else _fail)(f"Hash-Kette: {'gueltig' if chain_ok else 'UNGUELTIG oder unvollstaendig'}")

    if rfc_ok is True:
        _ok("RFC-3161-Zeitstempel: Hash im Token stimmt ueberein (volle Signaturpruefung siehe Hinweis oben)")
    elif rfc_ok is False:
        _fail("RFC-3161-Zeitstempel: Abweichung oder Fehler gefunden")
    else:
        _warn("RFC-3161-Zeitstempel: nicht geprueft (siehe Hinweise oben)")

    if ots_ok is True:
        _ok("Bitcoin-Blockchain-Verankerung: bestaetigt")
    elif ots_ok is False:
        _fail("Bitcoin-Blockchain-Verankerung: nicht bestaetigt")
    else:
        _warn("Bitcoin-Blockchain-Verankerung: nicht geprueft oder noch ausstehend (siehe Hinweise oben)")

    print()
    hard_failure = (not chain_ok) or (rfc_ok is False) or (ots_ok is False)
    if hard_failure:
        print(f"{_c(RED)}{_c(BOLD)}Gesamtergebnis: Es wurden Unstimmigkeiten gefunden - siehe oben!{_c(RESET)}")
        sys.exit(2)
    else:
        print(f"{_c(GREEN)}{_c(BOLD)}Gesamtergebnis: Keine Unstimmigkeiten gefunden.{_c(RESET)}")
        sys.exit(0)


if __name__ == "__main__":
    main()

