From 8d1ffc0df8f391b5ec36c3bcfe8c5658c471f42b Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 21 Aug 2026 22:10:36 +0200 Subject: [PATCH] Gegenbuch: Unsicherheit erreicht jetzt den Rueckgabecode (Pentest R179) R179-b: Der Rewind-Vorbehalt stand in der Ausgabe, der Exit blieb 0. Die eigene README sagt "fuer Cron gilt: jeder Code ausser 0 gehoert gemeldet" - der Zustand "ich bin an dieser Stelle blind" erreichte die Ueberwachung also nie. Dieselbe Klasse wie R174, eine Ebene hoeher. Fix: Code 0 nur bei belegter Gewissheit (Rewind-Schutz zugesichert oder Mindesthoehe erfuellt), sonst Code 3 - bewusst nicht mit dem Manipulationsalarm 2 verschmolzen. R179-01: NOTARY_MIN_SEQ ist eine Untergrenze, kein Ist-Stand. Ein veralteter Wert liess einen Teil-Rewind darueber lautlos durch, und die blosse Praesenz einer Zahl blendete den Vorbehalt aus - MIN_SEQ=0 war ein Freibrief, ein Tippfehler wurde still verschluckt. Ein veralteter Anker erzeugte damit ein selbstbewussteres Ergebnis als gar keiner. Fix: Der Vorbehalt haengt allein an NOTARY_REWIND_PROTECTED=true und benennt bei gesetztem MIN_SEQ dessen Grenze; MIN_SEQ <= 0 oder unparsbar fuehrt zu exit 1 statt stiller Annahme. Verifiziert gegen den Pentest-Aufbau (10 Checkpoints, Rewind auf 7, DB passend gekuerzt, frischer Klon): nicht gesetzt -> exit 3 (vorher 0); 10 und 8 -> Alarm 2; 7 veraltet -> 0 mit Vorbehalt (vorher ohne); 0 und xyz -> exit 1 (vorher stilles 0); Rewind-Schutz zugesichert -> 0 ohne Vorbehalt. Co-Authored-By: Claude Opus 5 --- docs/todo.md | 31 ++++++++++++++++++ tools/audit-notary/README.md | 22 ++++++++++--- tools/audit-notary/notary.mjs | 62 +++++++++++++++++++++++++++++++---- 3 files changed, 104 insertions(+), 11 deletions(-) diff --git a/docs/todo.md b/docs/todo.md index f3698cf5..96c467ff 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,37 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🚦 Gegenbuch: Unsicherheit erreicht jetzt den Rueckgabecode (Pentest R179)** (2026-08-18) + - **R179-b – der wichtigere Punkt:** Mein Rewind-Vorbehalt stand in der + Ausgabe, der Exit blieb **0**. Meine eigene README sagt „fuer Cron gilt: + jeder Code ausser 0 gehoert gemeldet“ – der Zustand „ich bin an dieser + Stelle blind“ erreichte die Ueberwachung also **nie**. Dieselbe Klasse wie + R174 (vergrabener Vorbehalt), eine Ebene hoeher. + Fix: Code **0 nur bei belegter Gewissheit** – entweder ist serverseitiger + Rewind-Schutz zugesichert oder eine mitgegebene Mindesthoehe wurde + erfuellt. Sonst **Code 3**, dieselbe Bedeutung wie beim unerreichbaren + Remote und bewusst NICHT mit dem Manipulationsalarm (2) verschmolzen. + - **R179-01 (MEDIUM)** – `NOTARY_MIN_SEQ` ist eine **Untergrenze**, kein + Ist-Stand. Ein veralteter Wert liess einen Teil-Rewind darueber lautlos + durch; schlimmer: die blosse **Praesenz** einer Zahl blendete den Vorbehalt + aus, ein `MIN_SEQ=0` war damit ein Freibrief und ein Tippfehler (`NaN`) + wurde still verschluckt. Ein veralteter Anker erzeugte so ein + **selbstbewussteres Ergebnis als gar keiner**. + Fix: Der Vorbehalt haengt jetzt allein an der ausdruecklichen Zusicherung + `NOTARY_REWIND_PROTECTED=true`; bei gesetztem MIN_SEQ benennt er zusaetzlich + die Grenze („deckt nur ab, was UNTERHALB dieser Nummer fehlt“). + `MIN_SEQ` ≤ 0 oder unparsbar → **exit 1** mit Erklaerung statt stiller + Annahme. + - Verifiziert gegen seinen Aufbau (10 Checkpoints, Rewind auf 7, DB passend + gekuerzt, frischer Auditoren-Klon): + nicht gesetzt → **exit 3** (vorher 0) · 10 → Alarm 2 · 8 → Alarm 2 · + 7 (veraltet) → 0, aber **mit Vorbehalt** (vorher ohne) · 0 / `xyz` → + **exit 1** (vorher stilles 0) · Rewind-Schutz zugesichert → 0 ohne + Vorbehalt. Labor und Port geraeumt. + - Seine Positivkontrolle bestaetigt und uebernommen: „in der Mitte gewaschen“ + ist dicht, weil der Kopf-Hash bei `atId=letzter.maxId` alles darunter + abdeckt – die Reconciliation nur des letzten Checkpoints genuegt. + - [x] **🔄 Gegenbuch: Rewind auf signierten Praefix benennbar gemacht (Pentest R178-01)** (2026-08-18) - Fund: Mein Signatur-Gate faengt Force-Push mit **fremder oder unsignierter** Historie – aber ein **Rewind auf einen aelteren, echt signierten Stand** diff --git a/tools/audit-notary/README.md b/tools/audit-notary/README.md index ddaa33d8..e3b1dd25 100644 --- a/tools/audit-notary/README.md +++ b/tools/audit-notary/README.md @@ -61,9 +61,23 @@ Deshalb zusätzlich: NOTARY_MIN_SEQ=42 node notary.mjs --check ``` -Ohne `NOTARY_MIN_SEQ` weist die Erfolgsmeldung ausdrücklich darauf hin, dass -ein Zurückspulen nicht erkennbar war. Ein „OK" ohne diesen Zusatz bedeutet -mehr als eines mit. +**`NOTARY_MIN_SEQ` ersetzt die serverseitige Sperre nicht.** Der Wert ist eine +*Untergrenze* und immer nur so frisch wie deine letzte Beobachtung. Wer +stündlich beglaubigt, aber wöchentlich prüft, läuft mit einem Wert herum, der +um rund 168 hinterherhinkt – ein Zurückspulen in dieses Fenster bliebe +unbemerkt und der gewaschene Stand würde sogar ausdrücklich bestätigt. + +Ist der Rewind-Schutz auf dem Server tatsächlich eingerichtet, bestätige das: + +```bash +NOTARY_REWIND_PROTECTED=true +``` + +**Nur diese Zusicherung** blendet den Vorbehalt aus – nicht die bloße Angabe +einer Zahl. Solange sie fehlt, endet jeder Lauf mit **Code 3** statt 0: +„nicht abschließend feststellbar". Das ist Absicht, denn ein Cronjob liest den +Rückgabecode, nicht den Fließtext – ein Hinweis, der nur in der Ausgabe steht, +erreicht die Überwachung nie. ## Der entscheidende Punkt: es wird tatsächlich geprüft @@ -117,7 +131,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 | -| 3 | beglaubigter Stand nicht feststellbar (Remote fehlt oder unerreichbar) | +| 3 | beglaubigter Stand nicht abschließend feststellbar – Remote fehlt/unerreichbar, **oder** ein Zurückspulen lässt sich nicht ausschließen | Für Cron gilt: **jeder** Code außer 0 gehört gemeldet. Code 2 ist der Alarm, Code 3 heißt „ich weiß es nicht" – und das ist ausdrücklich kein Freibrief. diff --git a/tools/audit-notary/notary.mjs b/tools/audit-notary/notary.mjs index 27597939..bfa6c5cd 100755 --- a/tools/audit-notary/notary.mjs +++ b/tools/audit-notary/notary.mjs @@ -73,7 +73,35 @@ let PIN = (process.env.NOTARY_SIGNER_FINGERPRINT || '').trim() || null; // Wer die zuletzt gesehene seq kennt (aus Monitoring, Protokoll, Mail), kann // sie hier mitgeben – dann faellt das Zurueckspulen auch ohne lokalen Zustand // auf. -const MIN_SEQ = Number.parseInt(process.env.NOTARY_MIN_SEQ || '', 10); +const MIN_SEQ_ROH = (process.env.NOTARY_MIN_SEQ || '').trim(); +let MIN_SEQ = Number.NaN; +if (MIN_SEQ_ROH) { + MIN_SEQ = Number.parseInt(MIN_SEQ_ROH, 10); + // Laut ablehnen statt still verschlucken (Pentest R179-01): ein Tippfehler + // ergab frueher `NaN`, wurde ignoriert – und weil die blosse PRAESENZ eines + // Werts den Vorbehalt ausblendete, sah das Ergebnis anschliessend + // selbstbewusster aus als ganz ohne Angabe. `0` war sogar ein Freibrief. + if (!Number.isFinite(MIN_SEQ) || MIN_SEQ <= 0) { + console.error( + `NOTARY_MIN_SEQ ist kein sinnvoller Wert: "${MIN_SEQ_ROH}".\n` + + 'Erwartet wird die zuletzt bekannte Checkpoint-Nummer als positive Zahl.\n' + + 'Lieber weglassen als raten – dann sagt die Prüfung ehrlich, dass ein ' + + 'Zurückspulen nicht erkennbar war.', + ); + process.exit(1); + } +} + +// Zusicherung des Betreibers, dass der Server non-fast-forward-Pushes +// verbietet (Pentest R179-01/-b). +// +// NUR diese Zusicherung darf den Rewind-Vorbehalt ausblenden – nicht die +// blosse Anwesenheit einer Zahl in NOTARY_MIN_SEQ. Der Grund: MIN_SEQ ist eine +// UNTERGRENZE und immer nur so frisch wie die letzte Beobachtung. Wer stuendlich +// beglaubigt, aber woechentlich prueft, laeuft mit einem Wert herum, der um +// ~168 hinterherhinkt – jeder Rewind in dieses Fenster passiert lautlos, und +// der gewaschene Stand wird sogar ausdruecklich bestaetigt. +const REWIND_GESCHUETZT = process.env.NOTARY_REWIND_PROTECTED === 'true'; if (!SIGN && process.env.NOTARY_INSECURE_ACK !== 'mir-ist-klar-dass-das-ungeschuetzt-ist') { console.error( 'NOTARY_SIGN=false deaktiviert den gesamten Schutz des Gegenbuchs.\n' + @@ -178,12 +206,29 @@ const OHNE_ANKER = () => // signierten Stand unerkennbar (R178-01). Das gehoert in die Erfolgsmeldung, // sonst liest sich „OK“ als mehr Gewissheit, als vorhanden ist. const REWIND_VORBEHALT = () => - Number.isFinite(MIN_SEQ) + REWIND_GESCHUETZT ? '' - : '\n Hinweis: Ein Zurückspulen der Reihe auf einen älteren, ebenfalls signierten Stand ' + - 'ist hieraus NICHT erkennbar.\n' + - ' Dagegen hilft serverseitiger Rewind-Schutz (non-fast-forward verbieten) oder ' + - 'NOTARY_MIN_SEQ mit der zuletzt bekannten Nummer.'; + : '\n Offen: Ein Zurückspulen der Reihe auf einen älteren, ebenfalls signierten Stand ' + + 'lässt sich hier nicht ausschließen.' + + (Number.isFinite(MIN_SEQ) + ? `\n NOTARY_MIN_SEQ=${MIN_SEQ} deckt nur ab, was UNTERHALB dieser Nummer fehlt – ` + + 'ein Rewind oberhalb bliebe unbemerkt, und der Wert hinkt naturgemäß hinterher.' + : '\n Ohne NOTARY_MIN_SEQ gibt es dafür gar keinen Bezugspunkt.') + + '\n Verlässlich hilft nur serverseitiger Rewind-Schutz (non-fast-forward verbieten);' + + ' ist er eingerichtet, mit NOTARY_REWIND_PROTECTED=true bestätigen.'; + + +// Rueckgabecode statt blosser Prosa (Pentest R179-b). +// +// Der Vorbehalt stand in stdout, der Exit blieb 0 – und Cron greppt den Code, +// nicht den Text. Der Zustand „Rewind nicht ausschliessbar“ erreichte die +// Automation damit nie. Code 0 gibt es deshalb nur noch bei belegter +// Gewissheit: entweder ist serverseitiger Rewind-Schutz zugesichert, oder eine +// mitgegebene Mindesthoehe wurde erfuellt. Sonst Code 3 – „nicht abschliessend +// feststellbar“, dieselbe Bedeutung wie beim unerreichbaren Remote und +// bewusst NICHT mit dem Manipulationsalarm (2) verschmolzen. +const abschliessendSicher = (anzahl) => + REWIND_GESCHUETZT || (Number.isFinite(MIN_SEQ) && anzahl >= MIN_SEQ); const alarm = (text) => { console.error(`ALARM: ${text}`); process.exit(2); }; @@ -384,13 +429,14 @@ if (letzter) { // 5) Anhaengen – mit Ruecknahme, falls der signierte Commit scheitert. // --------------------------------------------------------------------------- if (NUR_PRUEFEN) { + const sicher = abschliessendSicher(bisher.length); console.log( `OK: ${bisher.length} Checkpoint(s) geprüft – Signaturen gültig, Reihe lückenlos, ` + `Arbeitsdatei deckungsgleich mit dem signierten Stand` + (letzter ? `, CRM stimmt mit Checkpoint ${letzter.seq} überein (maxId ${letzter.maxId}).` : '.') + OHNE_ANKER() + REWIND_VORBEHALT(), ); - process.exit(0); + process.exit(sicher ? 0 : 3); } const eintrag = { @@ -461,5 +507,7 @@ if (PUSH && !NUR_LOKAL) { console.log( `OK: Checkpoint ${eintrag.seq} beglaubigt (maxId ${eintrag.maxId}).` + OHNE_ANKER() + + REWIND_VORBEHALT() + `\n Zuletzt bekannte Nummer für die nächste Prüfung: NOTARY_MIN_SEQ=${eintrag.seq}`, ); +process.exit(abschliessendSicher(eintrag.seq) ? 0 : 3);