Gegenbuch: Pruefmodus schreibt nicht mehr, Widerspruch aufgeloest (R181)

R181-01: Der Pruefmodus sagte zu, nichts zu veraendern und kein Schreibrecht zu
brauchen - und pushte trotzdem, weil ankerNachziehen() in jedem Modus lief. Ein
read-only Audit mutierte damit still das geteilte Substrat.

R181-02: Ein Widerspruch zwischen den eigenen Fixes. R180-01 erhebt die
Serversperre auf refs/notary/* zur tragenden Pflicht, R180-02 verlangt dort
Schreibrecht zur Selbstheilung. Sobald je ein Anker fehlte, bekam jeder
read-only pruefende Auditor dauerhaft einen Fehler auf einer voellig gueltigen
Kette, den er nicht beheben konnte. Fix: Reparieren nur im Notar-Schreiblauf,
im Pruefmodus wird der fehlende Anker gemeldet.

Eigener Rueckgabecode 5: "Anker unvollstaendig" ist nicht "nicht feststellbar" -
die Kette ist gueltig, nur das Substrat-Gedaechtnis unvollstaendig, ein
benannter reparierbarer Defekt. Dieselbe Trennung wie bei Genesis/Adoption.

Empirisch beantwortet: Die Notar-Identitaet laesst sich eng auf das Anlegen von
refs/notary/* beschraenken, ohne Loeschen oder Ueberschreiben - serverseitig
unterscheidbar an der Null-OID. Mit pre-receive-Hook verifiziert: Backfill
greift, Loeschen und Force-Overwrite bleiben abgewiesen. Hook als Beispiel in
der README.

Verifiziert: read-only --check mit fehlendem Anker -> Code 5, nichts gepusht;
Notar-Schreiblauf unter derselben ACL -> Anker nachgetragen, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 17:56:36 +02:00
co-authored by Claude Opus 5
parent a954f0f736
commit 773033936d
3 changed files with 110 additions and 12 deletions
+22 -2
View File
@@ -103,6 +103,19 @@ Standardzustand.
> einen niedrigeren stehen lässt, senkt damit den Vergleichswert selbst und
> in-repo ist das von „gab es nie" nicht zu unterscheiden.
>
> **Die Notar-Identität braucht dort eine eng gefasste Ausnahme:** Sie muss
> `refs/notary/*` **anlegen** dürfen, aber ausdrücklich **nicht löschen oder
> überschreiben**. Sonst hebelt eine kompromittierte Notar-Identität genau den
> Schutz aus, um den es hier geht. Server-seitig ist das unterscheidbar ein
> `pre-receive`-Hook sieht beim Anlegen die Null-OID als alten Wert:
>
> ```sh
> # refs/notary/*: nur anlegen, niemals ändern oder löschen
> case "$ref" in refs/notary/*)
> [ "$old" = "0000000000000000000000000000000000000000" ] || exit 1 ;;
> esac
> ```
>
> 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
@@ -198,6 +211,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 |
| 5 | **Anker unvollständig** die Kette ist gültig, aber `refs/notary/seq-N` fehlt. Reparierbar durch einen Notar-Schreiblauf |
| 4 | **Wächter-Gedächtnis fehlt** Erstinbetriebnahme unbestätigt, oder Speicher nach der Etablierung verloren |
| 3 | beglaubigter Stand nicht abschließend feststellbar Remote fehlt/unerreichbar, erste Beobachtung, Zurückspulen nicht ausschließbar, **oder** Checkpoint erstellt aber nicht verankert |
@@ -210,8 +224,14 @@ Code 3 heißt „ich weiß es nicht" und das ist ausdrücklich kein Freibrie
node notary.mjs --check
```
Führt alle Kontrollen aus, hängt aber nichts an und braucht kein Schreibrecht.
Geeignet für jemanden, der die Kette unabhängig nachvollziehen will.
Führt alle Kontrollen aus, **verändert nichts** und braucht kein Schreibrecht
auch dann nicht, wenn dabei ein fehlender Anker auffällt. Der wird gemeldet
(Code 5), nicht repariert; Reparieren ist Sache des Notar-Laufs.
Das ist kein Detail: Die Serversperre für `refs/notary/*` lehnt Schreibzugriffe
ja gerade ab. Würde der Prüfmodus dort nachtragen wollen, bekäme jeder
read-only prüfende Auditor dauerhaft einen Fehler auf einer völlig gültigen
Kette und könnte ihn nicht beheben.
## Anhängen ist selbst ein Beglaubigungsakt
+55 -10
View File
@@ -497,30 +497,58 @@ const beobachtungMerken = () => {
// 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.
let fehlendeAnker = [];
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;
fehlendeAnker = [];
for (let n = 1; n <= bisherLaenge; n++) if (!vorhanden.has(n)) fehlendeAnker.push(n);
if (!fehlendeAnker.length) return;
// Im PRUEFMODUS wird NICHT nachgezogen (Pentest R181-01/-02).
//
// Zwei Gruende, und beide sind grundsaetzlich:
// 1. Der Pruefmodus sagt zu, nichts zu veraendern und kein Schreibrecht zu
// brauchen. Ein Audit, das dabei still das Substrat mutiert, bricht
// genau diese Zusage.
// 2. `refs/notary/*` MUSS serverseitig gegen Schreibzugriff gesperrt sein
// (R180-01) dieselbe Sperre lehnt den Nachtrag ab. Ein read-only
// pruefender Auditor bekaeme dadurch dauerhaft einen Fehler auf einer
// voellig gueltigen Kette, den er gar nicht beheben kann.
//
// Der fehlende Anker wird gemeldet, nicht repariert. Reparieren ist Sache
// des Notar-Schreiblaufs, dessen Identitaet in der Serversperre eine
// Ausnahme fuer das ANLEGEN von `refs/notary/*` braucht (nicht fuers
// Loeschen oder Ueberschreiben sonst hebelt eine kompromittierte
// Notar-Identitaet den Schutz aus R180-01 selbst aus).
if (NUR_PRUEFEN || NUR_LOKAL || !PUSH) 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 nachgetragen = new Set();
for (const n of fehlendeAnker) {
const sha = liste[n - 1];
if (!sha) continue;
if (gitStill('push', REMOTE, `${sha}:refs/notary/seq-${n}`).ok) nachgezogen++;
if (gitStill('push', REMOTE, `${sha}:refs/notary/seq-${n}`).ok) {
nachgezogen++;
nachgetragen.add(n);
}
}
if (nachgezogen) console.log(`Fehlende Anker nachgetragen: ${nachgezogen}`);
if (nachgezogen < fehlend.length) {
fehlendeAnker = fehlendeAnker.filter((n) => !nachgetragen.has(n));
if (fehlendeAnker.length) {
console.error(
`${fehlend.length - nachgezogen} Anker-Ref(s) fehlen und ließen sich nicht nachtragen.\n` +
`${fehlendeAnker.length} Anker-Ref(s) fehlen und ließen sich nicht nachtragen: ` +
`${fehlendeAnker.join(', ')}\n` +
'Ohne sie ist der beglaubigte Höchststand nicht aus dem Server rekonstruierbar \n' +
'genau die Eigenschaft, auf der dieser Schutz beruht.',
'genau die Eigenschaft, auf der dieser Schutz beruht.\n' +
'Wahrscheinlichste Ursache: Die Serversperre für refs/notary/* lässt auch die\n' +
'Notar-Identität nicht anlegen. Sie braucht dort eine Ausnahme zum ANLEGEN \n' +
'ausdrücklich nicht zum Löschen oder Überschreiben.',
);
process.exit(3);
process.exit(5);
}
}
@@ -666,6 +694,22 @@ if (letzter) {
// ---------------------------------------------------------------------------
if (NUR_PRUEFEN) {
const sicher = abschliessendSicher(bisher.length);
// Eigener Code fuer den fehlenden Anker (Pentest R181-02, Frage b).
//
// „Anker fehlt“ ist NICHT dasselbe wie „nicht feststellbar“: Die Kette ist
// geprueft und gueltig, nur das Substrat-Gedaechtnis ist unvollstaendig
// ein bekannter, benannter, durch einen Notar-Lauf reparierbarer Defekt.
// Ihn in Code 3 zu werfen hiesse, eine konkrete Handlungsanweisung als
// Ungewissheit zu melden; dieselbe Trennung wie bei Genesis/Adoption.
if (fehlendeAnker.length) {
console.log(
`Anker unvollständig: refs/notary/seq-${fehlendeAnker.join(', seq-')} fehlt/fehlen.\n` +
' Die Kette selbst ist geprüft und gültig aber der beglaubigte Höchststand ist\n' +
' ohne diese Refs nicht vollständig aus dem Server rekonstruierbar.\n' +
' Behebbar durch einen Notar-Schreiblauf; im Prüfmodus wird bewusst nichts\n' +
' gepusht (dieser Modus verändert nichts und braucht kein Schreibrecht).',
);
}
console.log(
`OK: ${bisher.length} Checkpoint(s) geprüft Signaturen gültig, Reihe lückenlos, ` +
`Arbeitsdatei deckungsgleich mit dem signierten Stand` +
@@ -673,6 +717,7 @@ if (NUR_PRUEFEN) {
OHNE_ANKER() + REWIND_VORBEHALT(),
);
beobachtungMerken();
if (fehlendeAnker.length) process.exit(5);
process.exit(sicher ? 0 : 3);
}