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 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 22:10:36 +02:00
co-authored by Claude Opus 5
parent d50d8f6036
commit 8d1ffc0df8
3 changed files with 104 additions and 11 deletions
+31
View File
@@ -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**
+18 -4
View File
@@ -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.
+55 -7
View File
@@ -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);