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
+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);