# VERIFY.md — den Withdraw-Zeugen nachprüfen, ohne uns zu glauben Diese Anleitung ist für Dritte. Sie setzt nichts voraus außer `python3`, `openssl`, `curl` und einer Internetverbindung. Sie braucht **keinen** Zugang zu unseren Servern und **kein** Wohlwollen von uns. Der Report (`report.html` / `report.pdf`) ist nur die Darstellung. Der Beweis ist der Trail: `log.jsonl` plus `checkpoints.jsonl`. Alles hier prüft den Trail. Was am Ende bewiesen ist und was nicht, steht in Abschnitt 6 — bitte lesen, bevor jemand daraus mehr macht, als drinsteht. --- ## 0. Was Sie brauchen Die Trails liegen offen im Netz, ein Verzeichnis je Lauf: https://anycast.dev/witness// Übersicht über alle veröffentlichten Läufe: | Lauf-ID | Was er zeigt | Erwartetes Ergebnis | |---|---|---| | `2026-09-18-fra1` | Experiment Nr. 1, Lauf 1 — Drain fra1-lab (Virtua FRA) | `PASS: 8580 entries, 80 checkpoints` | | `2026-09-18-ewr1` | Experiment Nr. 1, Lauf 2 — Drain ewr1 (Vultr NJ) | `PASS: 7138 entries, 69 checkpoints` | | `2026-09-19-fra1` | Experiment Nr. 1, Lauf 3 — Rückkehr nach Wartung | `PASS: 76623 entries, 438 checkpoints` | | Stück | Woher | |---|---| | Trail (`log.jsonl.gz`, `checkpoints.jsonl`, `origin`) | `https://anycast.dev/witness//` | | Dateiliste mit Größe und SHA-256 | `https://anycast.dev/witness//MANIFEST.txt` | | Public Key | **https://anycast.dev/.well-known/logsiegel-pubkey.txt** — nicht die Kopie im Trail | | TSA-Token | `https://anycast.dev/witness//anchors/*.tsr` | | TSA-Zertifikate | und `…/digicert-chain.pem` (oder Ihr eigener Systemtrust) | Dieselben Dateien liegen zusätzlich im Repo des Betreibers (`data/witness/trails//`); das Repo ist nicht öffentlich, die Adressen oben sind es. Wer das Repo hat, nimmt dort `tools/witness/verify_trails.py`, das alle Läufe in einem Rutsch gegen die veröffentlichte Schlüsseldatei prüft. ```bash pip install logsiegel==0.1.2 ``` ### Einen Lauf holen ```bash RUN=2026-09-18-fra1 # oder 2026-09-18-ewr1, 2026-09-19-fra1 BASE=https://anycast.dev/witness/$RUN mkdir "$RUN" && cd "$RUN" curl -sO "$BASE/MANIFEST.txt" curl -sO "$BASE/log.jsonl.gz" curl -sO "$BASE/checkpoints.jsonl" curl -sO "$BASE/origin" mkdir -p keys anchors curl -s -o keys/signing_key.pub "$BASE/keys/signing_key.pub" for f in $(awk '$3 ~ /^anchors\// {print $3}' MANIFEST.txt); do curl -s -o "$f" "$BASE/$f" done ``` Mit `wget` statt `curl` geht es in einem Rutsch, die Verzeichnisse bleiben erhalten: ```bash awk -v b="$BASE" '$1 ~ /^[0-9a-f]{64}$/ && $3 !~ /^log\.jsonl$/ {print b"/"$3}' \ MANIFEST.txt > urls.txt wget -x -nH --cut-dirs=2 -i urls.txt ``` ### Nachsehen, dass Sie bekommen haben, was dasteht ```bash gunzip -k log.jsonl.gz # das Original bleibt liegen awk '$1 ~ /^[0-9a-f]{64}$/ {print $1" "$3}' MANIFEST.txt | shasum -a 256 -c # GNU-Systeme: … | sha256sum -c ``` Erwartet: `OK` für jede Zeile, einschließlich `log.jsonl` — das Manifest nennt auch die Summe der **entpackten** Datei. Das Entpacken ändert nichts am Beweis. Das Manifest kommt vom selben Server wie die Dateien; es ist eine Transportprüfung, kein Vertrauensanker. Der Vertrauensanker ist der Schlüssel aus `/.well-known/` (Abschnitt 1) und sind die fremden Zeitstempel (Abschnitt 3). ### Den Schlüssel holen — den veröffentlichten **Der wichtige Punkt zum Schlüssel.** Im Trail liegt eine Kopie des Public Keys (`keys/signing_key.pub`). Die prüft gegen sich selbst und beweist damit nichts gegen uns: wer das Verzeichnis kontrolliert, kann Log und Schlüssel gemeinsam neu schreiben. Nehmen Sie den Schlüssel von der veröffentlichten Adresse, und geben Sie ihn überall mit `--pubkey` mit. ```bash curl -s -o pubkeys.txt https://anycast.dev/.well-known/logsiegel-pubkey.txt sed -n "/Lauf-ID: *$RUN\$/,/END PUBLIC KEY/p" pubkeys.txt \ | sed -n '/BEGIN PUBLIC KEY/,/END PUBLIC KEY/p' > pub.pem ``` Die Schlüsseldatei nennt zu jedem Lauf ein Veröffentlichungsdatum. Bei den Läufen von Experiment Nr. 1 liegt es **vor** dem Lauf; wer die Datei damals gespeichert hat, kann das gegen seine eigene Kopie halten. --- ## 1. Hash-Kette, Merkle-Wurzeln, Signaturen ```bash logsiegel verify . --pubkey pub.pem ``` Erwartet: `PASS: entries, checkpoints` — die Zahlen stehen in der Tabelle in Abschnitt 0. Geprüft wird damit: jeder Eintrag trägt den Hash seines Vorgängers, die Reihenfolge stimmt, jeder Eintrag steht in kanonischer Form da (keine unsichtbaren Änderungen), jeder Checkpoint ist mit dem Schlüssel signiert, und jede signierte Merkle-Wurzel passt zu den Einträgen, über die sie behauptet wird. Ein geänderter, eingeschobener, umsortierter oder abgeschnittener Eintrag lässt das scheitern. **Gegenprobe.** Machen Sie den Test kaputt, sonst wissen Sie nicht, ob er misst: ```bash mkdir /tmp/kaputt && cp -r log.jsonl checkpoints.jsonl origin /tmp/kaputt/ python3 - <<'PY' p = "/tmp/kaputt/log.jsonl" lines = open(p, "rb").read().split(b"\n") lines[10] = lines[10].replace(b'"probe.rt_ms"', b'"probe.rt_xs"') open(p, "wb").write(b"\n".join(lines)) PY logsiegel verify /tmp/kaputt --pubkey pub.pem # muss FAIL sagen ``` --- ## 2. Ein einzelnes Receipt (ohne den Trail) Ein Receipt enthält einen einzelnen Eintrag, einen RFC-6962-Inklusionsbeweis und den signierten Checkpoint, der ihn abdeckt. Es prüft offline und ohne den restlichen Trail. Schneiden Sie sich eines selbst — die Eintragsnummern der Kernaussagen nennt der Report, jede andere Nummer geht genauso: ```bash logsiegel receipt . --seq 4 --out receipt-000004.json logsiegel verify-receipt receipt-000004.json --pubkey pub.pem ``` Erwartet: `PASS: entry 4 of origin 'anycast.dev/witness/2026-09-18-fra1'`. Wer lieber im Browser prüft: ist eine einzelne HTML-Datei, die offline läuft — Receipt hineinziehen, Schlüssel einsetzen. Ein Receipt selbst prüfen heißt auch: den Eintragstext lesen. Der Inhalt eines Eintrags steht im Klartext in `attrs`; nur die Rohdaten (`payload`) wären ausgelagert und über `input_hash` gebunden — diese Läufe haben keine. Ein selbst geschnittenes Receipt beweist nichts, was der Trail nicht schon zeigt. Sein Sinn ist der Aufbewahrungsfall: wer heute ein Receipt oder einen Checkpoint wegspeichert, kann uns morgen an dem festhalten, was wir heute signiert haben (Abschnitt 6). --- ## 3. Existenz vor einem Zeitpunkt (RFC 3161) Die Anker beweisen, dass der Trail **in dieser Form** schon existierte, bevor eine fremde Zeitstempelstelle ihn gesehen hat. Jeder Anker stempelt eine Checkpoint-Wurzel. ```bash # Die Wurzel, die gestempelt wurde — aus dem anchor-Eintrag im Trail: python3 - <<'PY' import json for ln in open("log.jsonl"): e = json.loads(ln) if e["event"] == "witness.anchor.tsa": a = e["attrs"] print(a["anchor.checkpoint_size"], a["anchor.root"], a["anchor.token_file"]) PY curl -sO https://anycast.dev/witness/tsa-certs/digicert-root-g4.pem curl -sO https://anycast.dev/witness/tsa-certs/digicert-chain.pem openssl ts -verify \ -digest \ -in \ -CAfile digicert-root-g4.pem \ -untrusted digicert-chain.pem ``` `anchor.token_file` steht schon mit Verzeichnis da (`anchors/cp000525-digicert-20260917T173000Z.tsr`), das ist die ``. `` ist `anchor.root` aus derselben Zeile. Erwartet: `Verification: OK`. Zeit und Seriennummer im Klartext: ```bash openssl ts -reply -in -text | grep -E 'Time stamp|Serial|Nonce|Status' ``` Wenn Sie unseren Zertifikatsdateien nicht trauen — gut so. Nehmen Sie Ihren eigenen Systemtrust, das Ergebnis muss dasselbe sein: ```bash openssl ts -verify -digest -in -CApath /etc/ssl/certs ``` Und die gestempelte Wurzel muss die Wurzel im Trail sein: ```bash python3 - <<'PY' import json size = cps = [json.loads(l) for l in open("checkpoints.jsonl")] print([c["root"] for c in cps if c["size"] == size]) PY ``` --- ## 4. Die fremden Zeitstempel nachziehen Der Trail behauptet Zeiten. Zwei davon sind nicht unsere: ### RIPE Atlas Jedes `witness.probe.result` mit `probe.kind=atlas` trägt `probe.msm_id` und `probe.prb_id`. Die Messungen sind öffentlich: ``` https://atlas.ripe.net/measurements// https://atlas.ripe.net/api/v2/measurements//results/?format=json&start=&stop= ``` Vergleichen Sie `probe.ts`, `probe.rt_ms`, `probe.rcode` und vor allem `probe.nsid` mit den Rohergebnissen bei RIPE. Weicht etwas ab, ist der Report falsch — nicht RIPE. Die `msm_id`s stehen im Trail und im Report unter „Datenebene": ```bash python3 - <<'PY' import json ids = {json.loads(l)["attrs"].get("probe.msm_id") for l in open("log.jsonl")} print(sorted(i for i in ids if i)) PY ``` ### RIS-Live Jedes `witness.ris.event` trägt `ris.collector`, `ris.peer_ip`, `ris.peer_asn`, `ris.ts` und `ris.prefix`. Gegenprobe über RIPEstat: ``` https://stat.ripe.net/data/bgp-updates/data.json?resource=94.249.165.0/24&starttime=&endtime= ``` Erwartet: dieselben Withdraws, von denselben Peers, in derselben Sekunde. RIPEstat ist die Quelle, unser Trail ist die Kopie — bei Abweichung gilt RIPEstat. --- ## 5. Die Zahlen im Report nachrechnen Die zentrale Zahl des Experiments — Sekunden zwischen dem lokalen Withdraw und dem letzten RIS-Peer — steht direkt im Log, ohne unseren Code: ```bash python3 - <<'PY' import json from datetime import datetime def ts(s): return datetime.fromisoformat(s.replace("Z", "+00:00")) entries = [json.loads(l) for l in open("log.jsonl")] post = [e for e in entries if e["event"] == "witness.bgp.local" and e["attrs"]["witness.phase"] == "post_drain"][-1] t0 = ts(post["attrs"]["bgp.action_ts"]) w = sorted(ts(e["attrs"]["ris.ts"]) for e in entries if e["event"] == "witness.ris.event" and e["attrs"]["ris.type"] == "withdraw" and ts(e["attrs"]["ris.ts"]) >= t0) print("erster RIS-Peer:", (w[0] - t0).total_seconds(), "s") print("letzter RIS-Peer:", (w[-1] - t0).total_seconds(), "s") node = [e for e in entries if e["event"] in ("witness.intent", "witness.bgp.local")][-1]["attrs"]["witness.node"] sight = sorted(ts(e["attrs"]["probe.ts"]) for e in entries if e["event"] == "witness.probe.result" and str(e["attrs"].get("probe.nsid", "")).split(".")[0] == node and ts(e["attrs"]["probe.ts"]) >= t0) print("letzte Sichtung des gedrainten Nodes:", (sight[-1] - t0).total_seconds(), "s") PY ``` Wer den Report selbst bauen will: `tools/witness/report.py` ist deterministisch, gleicher Trail, gleiches Dokument. Das Skript liegt im Repo des Betreibers und ist nicht Teil dieser Veröffentlichung; die Zahlen oben hängen nicht daran. Feldbedeutungen: `tools/witness/schema.md`. Definitionen, die der Report benutzt und die Sie kennen müssen, um dieselbe Zahl zu bekommen: * **Bezugspunkt** ist `bgp.action_ts` des `post_drain`-Eintrags (die Uhr des Nodes), nicht der Zeitpunkt des Trail-Eintrags. * **„hat den gedrainten Node gesehen"** heißt: die Antwort trug dessen NSID. Antworten ohne NSID zählen nicht — weder dafür noch dagegen. * **„ohne Antwort"** heißt `probe.rcode == TIMEOUT`. * **Nenner für die RIS-Prozente** ist die Menge der Peers, die entweder vor dem Drain ein Announce zeigten oder danach ein Withdraw — nicht alle RIS-Peers der Welt. * **Drain-Fenster** ist `[bgp.action_ts(post_drain), bgp.action_ts(post_restore))`. --- ## 6. Was damit bewiesen ist — und was nicht **Bewiesen:** * Die aufgezeichneten Einträge sind unverändert, in dieser Reihenfolge, ohne Lücken in der Kette und ohne nachträgliche Einschübe (Abschnitt 1, 2). * Der Trail existierte in dieser Form vor den Token-Zeitpunkten, bezeugt von einer Zeitstempelstelle, die nicht wir sind (Abschnitt 3). * Die Kontroll- und Datenebene-Zeiten stammen von RIPE und lassen sich dort unabhängig nachziehen (Abschnitt 4). **Nicht bewiesen — ausdrücklich:** * **Vollständigkeit.** Wir könnten Ereignisse weggelassen haben, bevor sie in die Kette kamen. Eine Hash-Kette schützt, was drinsteht; sie sagt nichts über das, was nie hineinkam. Das ist keine Schwäche dieser Umsetzung, sondern eine Eigenschaft von append-only Logs überhaupt. * **Unsere Uhr.** Die Einträge tragen die Zeit von tradonly. Belastbar sind ausschließlich die fremden Zeiten (RIPE, TSA). Der Report hält die drei Ebenen getrennt; wer sie vermischt, liest ihn falsch. * **Dass wir den Schlüssel nicht neu benutzt haben.** Wir halten den privaten Schlüssel. Wir könnten den gesamten Trail neu schreiben und neu signieren. Genau das wird sichtbar, wenn jemand anderes einen früheren Checkpoint oder ein früheres Receipt aufbewahrt hat — deshalb hängen die TSA-Token daran. **Wenn Sie diesen Trail ernst nehmen: speichern Sie sich einen Checkpoint weg.** Das ist die Lücke, die ein unabhängiger Witness schließen würde. Der ist hier nicht gebaut. * **„Die Welt".** Die Atlas-Messung hat 100 Probes. Regionen ohne Probe kommen im Report nicht vor. Wer einen Fehler findet: . Ein belegter Widerspruch zu diesen Zahlen ist uns lieber als ein Report, der ungeprüft stehen bleibt.