Gegenbuch: Anker belegen nichts mehr, Anker-Verlust wird laut (R180-01/-02)
R180-01: Der Code nahm den hoechsten noch vorhandenen refs/notary/seq-* als "hoechsten je". Loescht ein Angreifer nur die oberen Anker und laesst einen niedrigeren stehen, senkt er den Vergleichswert selbst - ein frischer Auditoren-Klon meldete OK, exit 0 auf gewaschenem Stand. Perverser Gradient: Wer alle Refs loeschte, flog auf (Code 4); wer weniger loeschte, kam durch, weil ankerBelegt sowohl die Gewissheit begruendete als auch den Code-4-Diskriminator kurzschloss. Fix: Anker begruenden keine Gewissheit mehr. Sie koennen ein Zurueckspulen widerlegen, aber nie Unversehrtheit belegen. Der Code-4-Diskriminator haengt nicht mehr an ihnen und greift nur im Schreiblauf. R180-02: Der Anker-Push-Fehlschlag war nur eine Warnung mit exit 0 - ausgerechnet bei der tragenden Eigenschaft. Der Normalbetrieb senkte damit den Hoechststand still um eins. Fix: exit 3 bei Fehlschlag, und jeder Lauf zieht fehlende Anker nach, bevor er etwas als OK meldet. Verifiziert: Teil-Loeschung mit frischem Auditoren-Klon -> exit 3 mit Vorbehalt statt exit 0; Anker-Push per Hook abgelehnt -> exit 3 statt Hinweis; Folgelauf traegt den fehlenden Anker nach. README: Der Schutz von refs/notary/* gegen Loeschen und Ueberschreiben ist von der Fussnote zur tragenden Voraussetzung erhoben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,41 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [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**
|
||||||
|
`refs/notary/seq-*` als „hoechsten je“. Loescht ein Angreifer nur die
|
||||||
|
OBEREN Anker und laesst einen niedrigeren stehen, senkt er den
|
||||||
|
Vergleichswert selbst – ein frischer Auditoren-Klon meldete `OK`, exit 0
|
||||||
|
auf gewaschenem Stand.
|
||||||
|
Perverser Gradient: Wer ALLE Refs loeschte, flog auf (Code 4); wer weniger
|
||||||
|
loeschte, kam durch – weil `ankerBelegt` sowohl die Gewissheit begruendete
|
||||||
|
als auch den Code-4-Diskriminator kurzschloss.
|
||||||
|
Fix: **Anker begruenden keine Gewissheit mehr.** Sie koennen ein
|
||||||
|
Zurueckspulen *widerlegen* (hoher Anker nicht mehr im Kopf → Alarm), aber
|
||||||
|
nie Unversehrtheit *belegen*. Der Code-4-Diskriminator haengt nicht mehr an
|
||||||
|
ihnen und greift nur noch im Schreiblauf (ein Auditor hatte nie ein
|
||||||
|
Gedaechtnis, ihm Code 4 vorzuwerfen waere Laerm).
|
||||||
|
- **R180-02 (LOW→MED)** – Der Anker-Push-Fehlschlag war nur eine Warnung mit
|
||||||
|
exit 0, ausgerechnet bei der tragenden Eigenschaft: `main` stand auf dem
|
||||||
|
neuen Checkpoint, der Anker fehlte, niemand holte es nach. Der
|
||||||
|
**Normalbetrieb** senkte damit den Hoechststand still um eins und riss das
|
||||||
|
R180-01-Fenster ohne Angriff auf.
|
||||||
|
Fix: Anker-Push-Fehlschlag → **exit 3**; und jeder Lauf **zieht fehlende
|
||||||
|
Anker nach**, bevor er irgendetwas als OK meldet (Zuordnung ueber die
|
||||||
|
n-te Aenderung am Gegenbuch).
|
||||||
|
- Verifiziert: Teil-Loeschung (main auf seq-2, Anker 3–5 weg, DB gekuerzt),
|
||||||
|
frischer Auditoren-Klon → **exit 3 mit Vorbehalt statt exit 0** (das falsche
|
||||||
|
Gruen ist weg; erkannt wird es in-repo weiterhin nicht, das geht ohne
|
||||||
|
Ref-Schutz nicht); Anker-Push per Hook abgelehnt → **exit 3** statt
|
||||||
|
Hinweis; Folgelauf → „Fehlende Anker nachgetragen: 1“, Anker wieder
|
||||||
|
vollstaendig.
|
||||||
|
- **README:** Der Schutz von `refs/notary/*` gegen Loeschen und Ueberschreiben
|
||||||
|
ist von der Fussnote zur **tragenden Voraussetzung** erhoben – mit der
|
||||||
|
Begruendung, warum Anker nichts belegen koennen, und dem ausdruecklichen
|
||||||
|
Hinweis, dass ein `--check` aus frischem Klon eine Anker-Loeschung nicht
|
||||||
|
erkennt.
|
||||||
|
|
||||||
- [x] **🧠 Gegenbuch: Gedaechtnis ins Substrat verlegt, Verlust laut behandelt (Pentest R180)** (2026-08-18)
|
- [x] **🧠 Gegenbuch: Gedaechtnis ins Substrat verlegt, Verlust laut behandelt (Pentest R180)** (2026-08-18)
|
||||||
- Sein Durchrechnen war zwingend: „erste Beobachtung → exit 3“ galt **genau
|
- Sein Durchrechnen war zwingend: „erste Beobachtung → exit 3“ galt **genau
|
||||||
einen Lauf**. Danach baselinete der Waechter auf den AKTUELLEN Kopf – nach
|
einen Lauf**. Danach baselinete der Waechter auf den AKTUELLEN Kopf – nach
|
||||||
|
|||||||
@@ -91,8 +91,22 @@ als Ausgangspunkt übernehmen. Ab da wäre alles dauerhaft grün. Ein gutartiger
|
|||||||
Standardzustand, den ein Angreifer gezielt herstellen kann, ist kein sicherer
|
Standardzustand, den ein Angreifer gezielt herstellen kann, ist kein sicherer
|
||||||
Standardzustand.
|
Standardzustand.
|
||||||
|
|
||||||
> **Diese Refs gehören ebenfalls serverseitig geschützt** (kein Löschen, kein
|
> ### ⚠ Ohne serverseitigen Schutz dieser Refs bringt das Verfahren nichts
|
||||||
> Überschreiben). Sonst verschiebt sich das Problem nur eine Ebene weiter.
|
>
|
||||||
|
> `refs/notary/*` muss auf dem Server **gegen Löschen und Überschreiben**
|
||||||
|
> gesperrt sein. Das ist keine Empfehlung, sondern die tragende Voraussetzung –
|
||||||
|
> genauso wie die Rewind-Sperre auf `main`.
|
||||||
|
>
|
||||||
|
> Der Grund ist eine unangenehme Asymmetrie: Das Skript kennt nur den
|
||||||
|
> **höchsten noch vorhandenen** Anker, nicht den höchsten je erzeugten. Wer
|
||||||
|
> *alle* Anker löscht, fliegt auf (Code 4). Wer nur die **obersten** löscht und
|
||||||
|
> einen niedrigeren stehen lässt, senkt damit den Vergleichswert selbst – und
|
||||||
|
> in-repo ist das von „gab es nie" nicht zu unterscheiden.
|
||||||
|
>
|
||||||
|
> 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
|
||||||
|
> eine Anker-Löschung **nicht** – es meldet dann Code 3 mit Vorbehalt, nicht 0.
|
||||||
|
|
||||||
### Wenn das Gedächtnis trotzdem fehlt
|
### Wenn das Gedächtnis trotzdem fehlt
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ 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 = () =>
|
||||||
REWIND_GESCHUETZT || ankerBelegt || ffBelegt
|
REWIND_GESCHUETZT || ffBelegt
|
||||||
? ''
|
? ''
|
||||||
: '\n Offen: Ein Zurückspulen der Reihe auf einen älteren, ebenfalls signierten Stand ' +
|
: '\n Offen: Ein Zurückspulen der Reihe auf einen älteren, ebenfalls signierten Stand ' +
|
||||||
'lässt sich hier nicht ausschließen.' +
|
'lässt sich hier nicht ausschließen.' +
|
||||||
@@ -251,9 +251,14 @@ const REWIND_VORBEHALT = () =>
|
|||||||
// bewusst NICHT mit dem Manipulationsalarm (2) verschmolzen.
|
// bewusst NICHT mit dem Manipulationsalarm (2) verschmolzen.
|
||||||
const abschliessendSicher = (anzahl) =>
|
const abschliessendSicher = (anzahl) =>
|
||||||
REWIND_GESCHUETZT ||
|
REWIND_GESCHUETZT ||
|
||||||
// Verankerte Refs sind das belastbarste Signal: sie liegen im Substrat und
|
// BEWUSST NICHT `ankerBelegt` (Pentest R180-01): Die Anker-Refs zeigen nur
|
||||||
// ueberleben einen Force-Push auf den Zweig.
|
// den HOECHSTEN NOCH VORHANDENEN Stand – nicht den hoechsten je erreichten.
|
||||||
ankerBelegt ||
|
// Wer die obersten Refs loescht, senkt damit den Vergleichswert selbst; ein
|
||||||
|
// niedrigerer Anker bleibt stehen und sah bisher wie Bestaetigung aus.
|
||||||
|
// Perverser Gradient: Wer ALLE Refs loescht, flog auf (Code 4); wer nur die
|
||||||
|
// oberen loeschte, kam durch. Anker koennen ein Zurueckspulen also WIDERLEGEN
|
||||||
|
// (siehe Alarm oben), aber nie Unversehrtheit BELEGEN.
|
||||||
|
ffBelegt ||
|
||||||
// Belegte Fast-Forward-Kette seit der ersten Beobachtung: das ist ein
|
// Belegte Fast-Forward-Kette seit der ersten Beobachtung: das ist ein
|
||||||
// Nachweis, kein Versprechen – deshalb zaehlt er (R179 (a)).
|
// Nachweis, kein Versprechen – deshalb zaehlt er (R179 (a)).
|
||||||
ffBelegt ||
|
ffBelegt ||
|
||||||
@@ -407,7 +412,13 @@ let ffBelegt = false;
|
|||||||
// Deshalb: kein stilles Neu-Baselinen. Eine Baseline zu setzen heisst „ich
|
// Deshalb: kein stilles Neu-Baselinen. Eine Baseline zu setzen heisst „ich
|
||||||
// verbuerge mich, DAS ist der wahre Kopf“ – dieser Akt gehoert bestaetigt.
|
// verbuerge mich, DAS ist der wahre Kopf“ – dieser Akt gehoert bestaetigt.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
if (!letzteBeobachtung && !ankerBelegt) {
|
// Nur der SCHREIBLAUF setzt eine Baseline – nur dort ist die Sperre noetig.
|
||||||
|
// Ein Auditor mit frischem Klon hatte nie ein Gedaechtnis; ihm Code 4
|
||||||
|
// vorzuwerfen waere Laerm. Er bekommt stattdessen exit 3 mit Vorbehalt.
|
||||||
|
//
|
||||||
|
// Das Kurzschliessen ueber `ankerBelegt` ist entfallen (R180-01): ein stehen
|
||||||
|
// gelassener niedriger Anker durfte die Sperre nicht aufheben.
|
||||||
|
if (!letzteBeobachtung && !NUR_PRUEFEN) {
|
||||||
const remoteHatHistorie = gitStill('show', `${ref}:${LEDGER_NAME}`).ok;
|
const remoteHatHistorie = gitStill('show', `${ref}:${LEDGER_NAME}`).ok;
|
||||||
if (remoteHatHistorie && process.env.NOTARY_ADOPT_ACK !== 'true') {
|
if (remoteHatHistorie && process.env.NOTARY_ADOPT_ACK !== 'true') {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -479,6 +490,40 @@ const beobachtungMerken = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fehlende Anker nachziehen (Pentest R180-02).
|
||||||
|
//
|
||||||
|
// Scheitert der Anker-Push, stand `main` schon auf dem neuen Checkpoint,
|
||||||
|
// waehrend der Anker fehlte – und niemand holte das nach. Der Normalbetrieb
|
||||||
|
// 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.
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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 sha = liste[n - 1];
|
||||||
|
if (!sha) continue;
|
||||||
|
if (gitStill('push', REMOTE, `${sha}:refs/notary/seq-${n}`).ok) nachgezogen++;
|
||||||
|
}
|
||||||
|
if (nachgezogen) console.log(`Fehlende Anker nachgetragen: ${nachgezogen}`);
|
||||||
|
if (nachgezogen < fehlend.length) {
|
||||||
|
console.error(
|
||||||
|
`${fehlend.length - nachgezogen} Anker-Ref(s) fehlen und ließen sich nicht nachtragen.\n` +
|
||||||
|
'Ohne sie ist der beglaubigte Höchststand nicht aus dem Server rekonstruierbar –\n' +
|
||||||
|
'genau die Eigenschaft, auf der dieser Schutz beruht.',
|
||||||
|
);
|
||||||
|
process.exit(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 2) Signaturen der GESAMTEN Historie pruefen – das war der fehlende Leser.
|
// 2) Signaturen der GESAMTEN Historie pruefen – das war der fehlende Leser.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -562,6 +607,8 @@ for (let i = 0; i < bisher.length; i++) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ankerNachziehen(bisher.length);
|
||||||
|
|
||||||
if (hoechsterAnker && bisher.length < hoechsterAnker.seq) {
|
if (hoechsterAnker && bisher.length < hoechsterAnker.seq) {
|
||||||
alarm(
|
alarm(
|
||||||
`Die Checkpoint-Reihe ist kürzer als der Server beglaubigt hat: ` +
|
`Die Checkpoint-Reihe ist kürzer als der Server beglaubigt hat: ` +
|
||||||
@@ -728,12 +775,18 @@ if (PUSH && !NUR_LOKAL) {
|
|||||||
const ankerRef = `refs/notary/seq-${eintrag.seq}`;
|
const ankerRef = `refs/notary/seq-${eintrag.seq}`;
|
||||||
const a = gitStill('push', REMOTE, `HEAD:${ankerRef}`);
|
const a = gitStill('push', REMOTE, `HEAD:${ankerRef}`);
|
||||||
if (!a.ok) {
|
if (!a.ok) {
|
||||||
console.warn(
|
// Kein blosser Hinweis mehr (R180-02): Der Anker IST die tragende
|
||||||
`HINWEIS: Anker-Ref ${ankerRef} konnte nicht gesetzt werden.\n` +
|
// Eigenschaft. Fehlt er, ist der Checkpoint zwar im Zweig, aber der
|
||||||
` Der Checkpoint ist gepusht, aber ohne diesen Ref bleibt das Gedächtnis an\n` +
|
// Hoechststand nicht mehr aus dem Server rekonstruierbar – und der
|
||||||
` diesen Rechner gebunden – geht es verloren, ist der Stand nicht mehr aus dem\n` +
|
// naechste Auditor saehe einen um eins zu niedrigen Stand als vollstaendig.
|
||||||
` Server rekonstruierbar. Grund: ${a.out}`,
|
console.error(
|
||||||
|
`Checkpoint ${eintrag.seq} ist gepusht, aber der Anker ${ankerRef} konnte NICHT ` +
|
||||||
|
`gesetzt werden.\n` +
|
||||||
|
`Damit ist der beglaubigte Höchststand nicht mehr aus dem Server rekonstruierbar. ` +
|
||||||
|
`Der nächste Lauf versucht, ihn nachzutragen – bis dahin gilt der Stand als nicht ` +
|
||||||
|
`vollständig verankert.\n git-Meldung: ${a.out}`,
|
||||||
);
|
);
|
||||||
|
process.exit(3);
|
||||||
}
|
}
|
||||||
verankert = true;
|
verankert = true;
|
||||||
beobachtungMerken();
|
beobachtungMerken();
|
||||||
|
|||||||
Reference in New Issue
Block a user