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:
2026-08-22 10:59:07 +02:00
co-authored by Claude Opus 5
parent 1a252c5059
commit a954f0f736
3 changed files with 114 additions and 12 deletions
+16 -2
View File
@@ -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.
> **Diese Refs gehören ebenfalls serverseitig geschützt** (kein Löschen, kein
> Überschreiben). Sonst verschiebt sich das Problem nur eine Ebene weiter.
> ### ⚠ Ohne serverseitigen Schutz dieser Refs bringt das Verfahren nichts
>
> `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
+63 -10
View File
@@ -228,7 +228,7 @@ 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 = () =>
REWIND_GESCHUETZT || ankerBelegt || ffBelegt
REWIND_GESCHUETZT || ffBelegt
? ''
: '\n Offen: Ein Zurückspulen der Reihe auf einen älteren, ebenfalls signierten Stand ' +
'lässt sich hier nicht ausschließen.' +
@@ -251,9 +251,14 @@ const REWIND_VORBEHALT = () =>
// bewusst NICHT mit dem Manipulationsalarm (2) verschmolzen.
const abschliessendSicher = (anzahl) =>
REWIND_GESCHUETZT ||
// Verankerte Refs sind das belastbarste Signal: sie liegen im Substrat und
// ueberleben einen Force-Push auf den Zweig.
ankerBelegt ||
// BEWUSST NICHT `ankerBelegt` (Pentest R180-01): Die Anker-Refs zeigen nur
// den HOECHSTEN NOCH VORHANDENEN Stand nicht den hoechsten je erreichten.
// 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
// Nachweis, kein Versprechen deshalb zaehlt er (R179 (a)).
ffBelegt ||
@@ -407,7 +412,13 @@ let ffBelegt = false;
// Deshalb: kein stilles Neu-Baselinen. Eine Baseline zu setzen heisst „ich
// 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;
if (remoteHatHistorie && process.env.NOTARY_ADOPT_ACK !== 'true') {
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.
// ---------------------------------------------------------------------------
@@ -562,6 +607,8 @@ for (let i = 0; i < bisher.length; i++) {
}
}
ankerNachziehen(bisher.length);
if (hoechsterAnker && bisher.length < hoechsterAnker.seq) {
alarm(
`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 a = gitStill('push', REMOTE, `HEAD:${ankerRef}`);
if (!a.ok) {
console.warn(
`HINWEIS: Anker-Ref ${ankerRef} konnte nicht gesetzt werden.\n` +
` Der Checkpoint ist gepusht, aber ohne diesen Ref bleibt das Gedächtnis an\n` +
` diesen Rechner gebunden geht es verloren, ist der Stand nicht mehr aus dem\n` +
` Server rekonstruierbar. Grund: ${a.out}`,
// Kein blosser Hinweis mehr (R180-02): Der Anker IST die tragende
// Eigenschaft. Fehlt er, ist der Checkpoint zwar im Zweig, aber der
// Hoechststand nicht mehr aus dem Server rekonstruierbar und der
// naechste Auditor saehe einen um eins zu niedrigen Stand als vollstaendig.
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;
beobachtungMerken();