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:
@@ -97,6 +97,37 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ 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)
|
- [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**
|
- Fund: Mein Signatur-Gate faengt Force-Push mit **fremder oder unsignierter**
|
||||||
Historie – aber ein **Rewind auf einen aelteren, echt signierten Stand**
|
Historie – aber ein **Rewind auf einen aelteren, echt signierten Stand**
|
||||||
|
|||||||
@@ -61,9 +61,23 @@ Deshalb zusätzlich:
|
|||||||
NOTARY_MIN_SEQ=42 node notary.mjs --check
|
NOTARY_MIN_SEQ=42 node notary.mjs --check
|
||||||
```
|
```
|
||||||
|
|
||||||
Ohne `NOTARY_MIN_SEQ` weist die Erfolgsmeldung ausdrücklich darauf hin, dass
|
**`NOTARY_MIN_SEQ` ersetzt die serverseitige Sperre nicht.** Der Wert ist eine
|
||||||
ein Zurückspulen nicht erkennbar war. Ein „OK" ohne diesen Zusatz bedeutet
|
*Untergrenze* und immer nur so frisch wie deine letzte Beobachtung. Wer
|
||||||
mehr als eines mit.
|
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
|
## 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) |
|
| 0 | alles in Ordnung, Checkpoint angehängt (bzw. Prüfung bestanden) |
|
||||||
| 1 | Betriebsfehler (Konfiguration, Commit oder Push fehlgeschlagen) |
|
| 1 | Betriebsfehler (Konfiguration, Commit oder Push fehlgeschlagen) |
|
||||||
| 2 | **Befund** – Widerspruch zwischen CRM und Gegenbuch, oder ungültige Signatur |
|
| 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,
|
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.
|
Code 3 heißt „ich weiß es nicht" – und das ist ausdrücklich kein Freibrief.
|
||||||
|
|||||||
@@ -73,7 +73,35 @@ let PIN = (process.env.NOTARY_SIGNER_FINGERPRINT || '').trim() || null;
|
|||||||
// Wer die zuletzt gesehene seq kennt (aus Monitoring, Protokoll, Mail), kann
|
// Wer die zuletzt gesehene seq kennt (aus Monitoring, Protokoll, Mail), kann
|
||||||
// sie hier mitgeben – dann faellt das Zurueckspulen auch ohne lokalen Zustand
|
// sie hier mitgeben – dann faellt das Zurueckspulen auch ohne lokalen Zustand
|
||||||
// auf.
|
// 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') {
|
if (!SIGN && process.env.NOTARY_INSECURE_ACK !== 'mir-ist-klar-dass-das-ungeschuetzt-ist') {
|
||||||
console.error(
|
console.error(
|
||||||
'NOTARY_SIGN=false deaktiviert den gesamten Schutz des Gegenbuchs.\n' +
|
'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,
|
// signierten Stand unerkennbar (R178-01). Das gehoert in die Erfolgsmeldung,
|
||||||
// sonst liest sich „OK“ als mehr Gewissheit, als vorhanden ist.
|
// sonst liest sich „OK“ als mehr Gewissheit, als vorhanden ist.
|
||||||
const REWIND_VORBEHALT = () =>
|
const REWIND_VORBEHALT = () =>
|
||||||
Number.isFinite(MIN_SEQ)
|
REWIND_GESCHUETZT
|
||||||
? ''
|
? ''
|
||||||
: '\n Hinweis: Ein Zurückspulen der Reihe auf einen älteren, ebenfalls signierten Stand ' +
|
: '\n Offen: Ein Zurückspulen der Reihe auf einen älteren, ebenfalls signierten Stand ' +
|
||||||
'ist hieraus NICHT erkennbar.\n' +
|
'lässt sich hier nicht ausschließen.' +
|
||||||
' Dagegen hilft serverseitiger Rewind-Schutz (non-fast-forward verbieten) oder ' +
|
(Number.isFinite(MIN_SEQ)
|
||||||
'NOTARY_MIN_SEQ mit der zuletzt bekannten Nummer.';
|
? `\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); };
|
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.
|
// 5) Anhaengen – mit Ruecknahme, falls der signierte Commit scheitert.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
if (NUR_PRUEFEN) {
|
if (NUR_PRUEFEN) {
|
||||||
|
const sicher = abschliessendSicher(bisher.length);
|
||||||
console.log(
|
console.log(
|
||||||
`OK: ${bisher.length} Checkpoint(s) geprüft – Signaturen gültig, Reihe lückenlos, ` +
|
`OK: ${bisher.length} Checkpoint(s) geprüft – Signaturen gültig, Reihe lückenlos, ` +
|
||||||
`Arbeitsdatei deckungsgleich mit dem signierten Stand` +
|
`Arbeitsdatei deckungsgleich mit dem signierten Stand` +
|
||||||
(letzter ? `, CRM stimmt mit Checkpoint ${letzter.seq} überein (maxId ${letzter.maxId}).` : '.') +
|
(letzter ? `, CRM stimmt mit Checkpoint ${letzter.seq} überein (maxId ${letzter.maxId}).` : '.') +
|
||||||
OHNE_ANKER() + REWIND_VORBEHALT(),
|
OHNE_ANKER() + REWIND_VORBEHALT(),
|
||||||
);
|
);
|
||||||
process.exit(0);
|
process.exit(sicher ? 0 : 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
const eintrag = {
|
const eintrag = {
|
||||||
@@ -461,5 +507,7 @@ if (PUSH && !NUR_LOKAL) {
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`OK: Checkpoint ${eintrag.seq} beglaubigt (maxId ${eintrag.maxId}).` + OHNE_ANKER() +
|
`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}`,
|
`\n Zuletzt bekannte Nummer für die nächste Prüfung: NOTARY_MIN_SEQ=${eintrag.seq}`,
|
||||||
);
|
);
|
||||||
|
process.exit(abschliessendSicher(eintrag.seq) ? 0 : 3);
|
||||||
|
|||||||
Reference in New Issue
Block a user