From 773033936db9da1a6bd59f1a3a83c01c0efa9042 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 22 Aug 2026 17:56:36 +0200 Subject: [PATCH] Gegenbuch: Pruefmodus schreibt nicht mehr, Widerspruch aufgeloest (R181) R181-01: Der Pruefmodus sagte zu, nichts zu veraendern und kein Schreibrecht zu brauchen - und pushte trotzdem, weil ankerNachziehen() in jedem Modus lief. Ein read-only Audit mutierte damit still das geteilte Substrat. R181-02: Ein Widerspruch zwischen den eigenen Fixes. R180-01 erhebt die Serversperre auf refs/notary/* zur tragenden Pflicht, R180-02 verlangt dort Schreibrecht zur Selbstheilung. Sobald je ein Anker fehlte, bekam jeder read-only pruefende Auditor dauerhaft einen Fehler auf einer voellig gueltigen Kette, den er nicht beheben konnte. Fix: Reparieren nur im Notar-Schreiblauf, im Pruefmodus wird der fehlende Anker gemeldet. Eigener Rueckgabecode 5: "Anker unvollstaendig" ist nicht "nicht feststellbar" - die Kette ist gueltig, nur das Substrat-Gedaechtnis unvollstaendig, ein benannter reparierbarer Defekt. Dieselbe Trennung wie bei Genesis/Adoption. Empirisch beantwortet: Die Notar-Identitaet laesst sich eng auf das Anlegen von refs/notary/* beschraenken, ohne Loeschen oder Ueberschreiben - serverseitig unterscheidbar an der Null-OID. Mit pre-receive-Hook verifiziert: Backfill greift, Loeschen und Force-Overwrite bleiben abgewiesen. Hook als Beispiel in der README. Verifiziert: read-only --check mit fehlendem Anker -> Code 5, nichts gepusht; Notar-Schreiblauf unter derselben ACL -> Anker nachgetragen, exit 0. Co-Authored-By: Claude Opus 5 --- docs/todo.md | 33 ++++++++++++++++++ tools/audit-notary/README.md | 24 +++++++++++-- tools/audit-notary/notary.mjs | 65 +++++++++++++++++++++++++++++------ 3 files changed, 110 insertions(+), 12 deletions(-) diff --git a/docs/todo.md b/docs/todo.md index 6bc4ff8b..8113f3a6 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,39 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🧮 Gegenbuch: Pruefmodus schreibt nicht mehr, Widerspruch aufgeloest (Pentest R181)** (2026-08-18) + - **R181-01 (LOW→MED)** – Der Pruefmodus sagte zu, nichts zu veraendern und + kein Schreibrecht zu brauchen – und pushte trotzdem: `ankerNachziehen()` + lief in JEDEM Modus. Ein „read-only“ Audit mutierte damit still das + geteilte Substrat. Fix: Nachziehen nur im Schreiblauf. + - **R181-02 (MEDIUM) – ein Widerspruch zwischen meinen EIGENEN Fixes.** + R180-01 erhebt die Serversperre auf `refs/notary/*` zur tragenden Pflicht; + R180-02 verlangt genau dort Schreibrecht zur Selbstheilung. Ergebnis: + Sobald je ein Anker fehlte, bekam jeder read-only pruefende Auditor + dauerhaft einen Fehler auf einer **voellig gueltigen** Kette – und konnte + ihn nicht beheben. + Fix: Reparieren ist Sache des Notar-Laufs; im Pruefmodus wird der fehlende + Anker gemeldet, nicht repariert. + - **Eigener Rueckgabecode 5** (seine Frage b): „Anker unvollstaendig“ ist + NICHT „nicht feststellbar“. Die Kette ist geprueft und gueltig, nur das + Substrat-Gedaechtnis ist unvollstaendig – ein benannter, reparierbarer + Defekt mit klarer Handlungsanweisung. Ihn in Code 3 zu werfen hiesse, eine + Anweisung als Ungewissheit zu melden; dieselbe Trennung wie bei + Genesis/Adoption. + - **Seine Frage (a) empirisch beantwortet:** Die Notar-Identitaet laesst sich + eng auf das ANLEGEN von `refs/notary/*` beschraenken, ohne ihr Loeschen + oder Ueberschreiben zu geben – serverseitig unterscheidbar an der Null-OID + beim Anlegen. Mit `pre-receive`-Hook verifiziert: Backfill greift (exit 0), + Loeschen und Force-Overwrite werden weiterhin abgewiesen. Der Hook steht + als Beispiel in der README. + - Verifiziert: read-only `--check` mit fehlendem Anker → **Code 5, nichts + gepusht** (Anker-Anzahl vorher/nachher identisch); Notar-Schreiblauf unter + derselben ACL → „Fehlende Anker nachgetragen: 1“, exit 0. + - Seine Non-Findings uebernommen: Die Backfill-Zuordnung ist gegen das + PIN-Modell robust – jeder Commit mit echter Ledger-Aenderung ist non-merge + und muss keyA-signiert sein, sig-exempte Merges tragen keinen Inhalt in die + Zuordnungsliste. + - [x] **🛑 Gegenbuch: Anker belegen nichts mehr, Anker-Verlust wird laut (Pentest R180-01/-02)** (2026-08-18) - **R180-01 (MEDIUM)** – Genau die Unsicherheit, die ich selbst benannt hatte, bestaetigt: Der Code nahm den **hoechsten noch vorhandenen** diff --git a/tools/audit-notary/README.md b/tools/audit-notary/README.md index b153bdf4..64e2ba72 100644 --- a/tools/audit-notary/README.md +++ b/tools/audit-notary/README.md @@ -103,6 +103,19 @@ Standardzustand. > einen niedrigeren stehen lässt, senkt damit den Vergleichswert selbst – und > in-repo ist das von „gab es nie" nicht zu unterscheiden. > +> **Die Notar-Identität braucht dort eine eng gefasste Ausnahme:** Sie muss +> `refs/notary/*` **anlegen** dürfen, aber ausdrücklich **nicht löschen oder +> überschreiben**. Sonst hebelt eine kompromittierte Notar-Identität genau den +> Schutz aus, um den es hier geht. Server-seitig ist das unterscheidbar – ein +> `pre-receive`-Hook sieht beim Anlegen die Null-OID als alten Wert: +> +> ```sh +> # refs/notary/*: nur anlegen, niemals ändern oder löschen +> case "$ref" in refs/notary/*) +> [ "$old" = "0000000000000000000000000000000000000000" ] || exit 1 ;; +> esac +> ``` +> > Deshalb begründen Anker **keine** Unversehrtheit. Sie können ein > Zurückspulen *widerlegen* (wenn ein hoher Anker nicht mehr im Kopf steckt), > aber nie belegen, dass nichts fehlt. Ein `--check` aus frischem Klon erkennt @@ -198,6 +211,7 @@ Deshalb gilt jetzt: | 0 | alles in Ordnung, Checkpoint angehängt (bzw. Prüfung bestanden) | | 1 | Betriebsfehler (Konfiguration, Commit oder Push fehlgeschlagen) | | 2 | **Befund** – Widerspruch zwischen CRM und Gegenbuch, oder ungültige Signatur | +| 5 | **Anker unvollständig** – die Kette ist gültig, aber `refs/notary/seq-N` fehlt. Reparierbar durch einen Notar-Schreiblauf | | 4 | **Wächter-Gedächtnis fehlt** – Erstinbetriebnahme unbestätigt, oder Speicher nach der Etablierung verloren | | 3 | beglaubigter Stand nicht abschließend feststellbar – Remote fehlt/unerreichbar, erste Beobachtung, Zurückspulen nicht ausschließbar, **oder** Checkpoint erstellt aber nicht verankert | @@ -210,8 +224,14 @@ Code 3 heißt „ich weiß es nicht" – und das ist ausdrücklich kein Freibrie node notary.mjs --check ``` -Führt alle Kontrollen aus, hängt aber nichts an und braucht kein Schreibrecht. -Geeignet für jemanden, der die Kette unabhängig nachvollziehen will. +Führt alle Kontrollen aus, **verändert nichts** und braucht kein Schreibrecht – +auch dann nicht, wenn dabei ein fehlender Anker auffällt. Der wird gemeldet +(Code 5), nicht repariert; Reparieren ist Sache des Notar-Laufs. + +Das ist kein Detail: Die Serversperre für `refs/notary/*` lehnt Schreibzugriffe +ja gerade ab. Würde der Prüfmodus dort nachtragen wollen, bekäme jeder +read-only prüfende Auditor dauerhaft einen Fehler auf einer völlig gültigen +Kette – und könnte ihn nicht beheben. ## Anhängen ist selbst ein Beglaubigungsakt diff --git a/tools/audit-notary/notary.mjs b/tools/audit-notary/notary.mjs index 640274d9..6b5e2fb2 100755 --- a/tools/audit-notary/notary.mjs +++ b/tools/audit-notary/notary.mjs @@ -497,30 +497,58 @@ const beobachtungMerken = () => { // senkte damit den Substrat-Hoechststand still um eins und riss genau das // Fenster auf, das R180-01 ausnutzt. Deshalb wird vor allem anderen // nachgezogen, was fehlt. +let fehlendeAnker = []; + function ankerNachziehen(bisherLaenge) { - if (NUR_LOKAL || !PUSH) return; const vorhanden = new Set(ankerRefs.map((a) => a.seq)); - const fehlend = []; - for (let n = 1; n <= bisherLaenge; n++) if (!vorhanden.has(n)) fehlend.push(n); - if (!fehlend.length) return; + fehlendeAnker = []; + for (let n = 1; n <= bisherLaenge; n++) if (!vorhanden.has(n)) fehlendeAnker.push(n); + if (!fehlendeAnker.length) return; + + // Im PRUEFMODUS wird NICHT nachgezogen (Pentest R181-01/-02). + // + // Zwei Gruende, und beide sind grundsaetzlich: + // 1. Der Pruefmodus sagt zu, nichts zu veraendern und kein Schreibrecht zu + // brauchen. Ein Audit, das dabei still das Substrat mutiert, bricht + // genau diese Zusage. + // 2. `refs/notary/*` MUSS serverseitig gegen Schreibzugriff gesperrt sein + // (R180-01) – dieselbe Sperre lehnt den Nachtrag ab. Ein read-only + // pruefender Auditor bekaeme dadurch dauerhaft einen Fehler auf einer + // voellig gueltigen Kette, den er gar nicht beheben kann. + // + // Der fehlende Anker wird gemeldet, nicht repariert. Reparieren ist Sache + // des Notar-Schreiblaufs, dessen Identitaet in der Serversperre eine + // Ausnahme fuer das ANLEGEN von `refs/notary/*` braucht (nicht fuers + // Loeschen oder Ueberschreiben – sonst hebelt eine kompromittierte + // Notar-Identitaet den Schutz aus R180-01 selbst aus). + if (NUR_PRUEFEN || NUR_LOKAL || !PUSH) return; // Die n-te Aenderung am Gegenbuch entspricht Checkpoint n. const commits = gitStill('log', '--reverse', '--format=%H', ref, '--', LEDGER_NAME); const liste = commits.ok ? commits.out.split('\n').filter(Boolean) : []; let nachgezogen = 0; - for (const n of fehlend) { + const nachgetragen = new Set(); + for (const n of fehlendeAnker) { const sha = liste[n - 1]; if (!sha) continue; - if (gitStill('push', REMOTE, `${sha}:refs/notary/seq-${n}`).ok) nachgezogen++; + if (gitStill('push', REMOTE, `${sha}:refs/notary/seq-${n}`).ok) { + nachgezogen++; + nachgetragen.add(n); + } } if (nachgezogen) console.log(`Fehlende Anker nachgetragen: ${nachgezogen}`); - if (nachgezogen < fehlend.length) { + fehlendeAnker = fehlendeAnker.filter((n) => !nachgetragen.has(n)); + if (fehlendeAnker.length) { console.error( - `${fehlend.length - nachgezogen} Anker-Ref(s) fehlen und ließen sich nicht nachtragen.\n` + + `${fehlendeAnker.length} Anker-Ref(s) fehlen und ließen sich nicht nachtragen: ` + + `${fehlendeAnker.join(', ')}\n` + 'Ohne sie ist der beglaubigte Höchststand nicht aus dem Server rekonstruierbar –\n' + - 'genau die Eigenschaft, auf der dieser Schutz beruht.', + 'genau die Eigenschaft, auf der dieser Schutz beruht.\n' + + 'Wahrscheinlichste Ursache: Die Serversperre für refs/notary/* lässt auch die\n' + + 'Notar-Identität nicht anlegen. Sie braucht dort eine Ausnahme zum ANLEGEN –\n' + + 'ausdrücklich nicht zum Löschen oder Überschreiben.', ); - process.exit(3); + process.exit(5); } } @@ -666,6 +694,22 @@ if (letzter) { // --------------------------------------------------------------------------- if (NUR_PRUEFEN) { const sicher = abschliessendSicher(bisher.length); + // Eigener Code fuer den fehlenden Anker (Pentest R181-02, Frage b). + // + // „Anker fehlt“ ist NICHT dasselbe wie „nicht feststellbar“: Die Kette ist + // geprueft und gueltig, nur das Substrat-Gedaechtnis ist unvollstaendig – + // ein bekannter, benannter, durch einen Notar-Lauf reparierbarer Defekt. + // Ihn in Code 3 zu werfen hiesse, eine konkrete Handlungsanweisung als + // Ungewissheit zu melden; dieselbe Trennung wie bei Genesis/Adoption. + if (fehlendeAnker.length) { + console.log( + `Anker unvollständig: refs/notary/seq-${fehlendeAnker.join(', seq-')} fehlt/fehlen.\n` + + ' Die Kette selbst ist geprüft und gültig – aber der beglaubigte Höchststand ist\n' + + ' ohne diese Refs nicht vollständig aus dem Server rekonstruierbar.\n' + + ' Behebbar durch einen Notar-Schreiblauf; im Prüfmodus wird bewusst nichts\n' + + ' gepusht (dieser Modus verändert nichts und braucht kein Schreibrecht).', + ); + } console.log( `OK: ${bisher.length} Checkpoint(s) geprüft – Signaturen gültig, Reihe lückenlos, ` + `Arbeitsdatei deckungsgleich mit dem signierten Stand` + @@ -673,6 +717,7 @@ if (NUR_PRUEFEN) { OHNE_ANKER() + REWIND_VORBEHALT(), ); beobachtungMerken(); + if (fehlendeAnker.length) process.exit(5); process.exit(sicher ? 0 : 3); }