Files
opencrm/tools/audit-notary/notary.mjs
T
duffyduckandClaude Opus 5 d50d8f6036 Gegenbuch: Rewind auf signierten Praefix benennbar gemacht (Pentest R178-01)
Das Signatur-Gate faengt Force-Push mit fremder oder unsignierter Historie -
aber ein Rewind auf einen aelteren, echt signierten Stand ist signaturseitig
einwandfrei. Angreifer spult origin/main auf einen frueheren Checkpoint zurueck
und kuerzt die Datenbank passend: alle Signaturen G, Pin korrekt, Reihe
lueckenlos. Ein Notar-Klon mit lokalem Vorlauf merkt es, ein frischer
Auditoren-Klon meldete OK - ausgerechnet im dokumentierten Pruef-Fall.

Das laesst sich im Skript nicht kryptographisch erkennen, die Historie ist echt.
Deshalb zwei Dinge statt eines Scheinfixes: NOTARY_MIN_SEQ als Bezugspunkt (ist
die Reihe kuerzer, Alarm; jeder Lauf nennt die Nummer), und ohne diesen
Bezugspunkt sagt die Erfolgsmeldung ausdruecklich, dass ein Zurueckspulen nicht
erkennbar war.

README: serverseitiger Rewind-Schutz (non-fast-forward verbieten) jetzt als
Pflicht formuliert, samt Begruendung und dem Hinweis, dass ein frischer Klon
den Rewind nicht sieht.

Kleinkram: NOTARY_SIGNER_FINGERPRINT wird beim Einlesen getrimmt (ein
Zeilenumbruch loeste 4/4 Fehlalarme aus, die auf den korrekten Fingerabdruck
zeigten); die "nie gepusht"-Meldung priorisiert Untersuchen statt Pushen.
Beim Testen selbst gefunden: der erste git-Aufruf war ungeschuetzt und warf bei
kaputtem Klon einen Stacktrace.

Verifiziert: Rewind 3->1 mit gekuerzter DB -> frischer Klon ohne Bezugspunkt OK
mit Vorbehalt, mit NOTARY_MIN_SEQ=3 -> Alarm exit 2; Pin mit Zeilenumbruch kein
Fehlalarm; regulaerer Lauf unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 21:46:02 +02:00

466 lines
20 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* OpenCRM Gegenbuch (externe Notarisierung)
* ===========================================
*
* Laeuft NICHT auf dem CRM-Server, sondern auf einem zweiten Rechner. Der
* Signaturschluessel und das Push-Recht leben dort nicht in den
* Deploy-Secrets des CRM.
*
* ZENTRALE LEHRE (Pentest R175-01): Append-only und Signieren allein reichen
* nicht es braucht einen VERIFIZIERENDEN LESER. Die erste Fassung las ihre
* Wahrheit aus der lokalen Arbeitsdatei und hat nie eine Signatur geprueft;
* das `-S` war write-only ohne Konsument. Wer den Notar-Host beschreiben
* konnte, hat dem Skript beliebige „beglaubigte“ Zeilen untergeschoben und den
* abgeschnittenen Zustand als neue Wahrheit zementiert ohne Alarm.
*
* Deshalb gilt jetzt:
* - Wahrheitsquelle ist der SIGNIERTE Commit-Baum (bevorzugt der Remote-Kopf),
* nicht die Arbeitsdatei.
* - JEDER Commit der Historie muss eine gute Signatur tragen.
* - Weicht die Arbeitsdatei vom signierten Stand ab, wird abgebrochen.
* - Geschrieben wird erst nach erfolgreichem, signiertem Commit; schlaegt er
* fehl, wird die Datei zurueckgerollt (Pentest R175-03).
*/
import { execFileSync } from 'node:child_process';
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { resolve, basename } from 'node:path';
const CRM_URL = process.env.CRM_URL;
const CRM_TOKEN = process.env.CRM_TOKEN;
const LEDGER = resolve(process.env.LEDGER_FILE || 'checkpoints.jsonl');
const LEDGER_NAME = basename(LEDGER);
const PUSH = process.env.NOTARY_PUSH !== 'false';
const REMOTE = process.env.NOTARY_REMOTE || 'origin';
// Reiner Pruefmodus fuer Auditoren: fuehrt alle Kontrollen aus, haengt aber
// nichts an und braucht kein Schreibrecht.
const NUR_PRUEFEN = process.argv.includes('--check') || process.env.NOTARY_CHECK_ONLY === 'true';
// Wird gesetzt, wenn ohne externen Anker gearbeitet wird faerbt jede
// Erfolgsmeldung ein (Pentest R177, Kleinkram).
let NUR_LOKAL = false;
// Ohne Signatur ist das Gegenbuch wertlos: dann kann jeder mit Schreibrecht
// die Reihe faelschen. Der Schalter existiert nur fuer Tests und verlangt eine
// zusaetzliche, bewusste Bestaetigung der Reflex "Cron hakt, also abschalten"
// soll nicht ausreichen.
const SIGN = process.env.NOTARY_SIGN !== 'false';
// Fingerabdruck des erwarteten Signierschluessels (`%GF`).
//
// NICHT optional (Pentest R177-01): Ohne Pin ist der Vertrauensanker die
// gesamte `allowed_signers`-MENGE, nicht der eine Notar-Schluessel. Jeder dort
// gelistete Zweitschluessel anderer Nutzer, Alt-Key, global geteilte Datei
// kann das Gegenbuch dann waschen und force-pushen, und die Pruefung meldet
// „gültig“. Live gezeigt.
//
// Ist nichts gesetzt, wird der Pin aus `user.signingkey` abgeleitet. Klappt
// auch das nicht, wird abgebrochen statt stillschweigend die ganze Menge zu
// akzeptieren.
// `.trim()` beim EINLESEN (Pentest R178): ein Zeilenumbruch aus der
// Umgebungsvariable liess sonst jeden Commit als „anderer Schlüssel“ auflaufen
// ein Fehlalarm, der auf den korrekten Fingerabdruck zeigt und damit maximal
// verwirrt.
let PIN = (process.env.NOTARY_SIGNER_FINGERPRINT || '').trim() || null;
// Bekannte Mindesthoehe der Checkpoint-Reihe (Pentest R178-01).
//
// Ein Angreifer mit Force-Push-Recht kann die Reihe auf einen AELTEREN,
// echt signierten Praefix zurueckspulen und die Datenbank passend kuerzen.
// Alle Signaturen bleiben gueltig, der Pin passt, die Reihe ist lueckenlos
// signaturseitig ist daran nichts auszusetzen. Ein Notar-Klon mit lokalem
// Vorlauf merkt es (er erinnert die hoehere seq); ein FRISCHER Auditoren-Klon
// merkt es nicht.
//
// 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);
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' +
'Wenn das wirklich beabsichtigt ist (nur für Tests), zusätzlich setzen:\n' +
' NOTARY_INSECURE_ACK=mir-ist-klar-dass-das-ungeschuetzt-ist',
);
process.exit(1);
}
if (!CRM_URL || !CRM_TOKEN) {
console.error('CRM_URL und CRM_TOKEN müssen gesetzt sein.');
process.exit(1);
}
// stderr wird bewusst abgefangen statt durchgereicht: sonst stehen git-eigene
// Fehlermeldungen VOR der Erklaerung dieses Skripts und der Betreiber liest
// zuerst etwas Kryptisches. Relevante git-Ausgaben geben wir gezielt weiter.
const GIT_STDIO = ['ignore', 'pipe', 'pipe'];
const git = (...args) =>
execFileSync('git', args, { encoding: 'utf8', stdio: GIT_STDIO }).trim();
const gitStill = (...args) => {
try { return { ok: true, out: git(...args) }; }
catch (e) { return { ok: false, out: String(e.stderr || e.message).trim() }; }
};
// Pin aus der lokalen Signierkonfiguration ableiten, wenn nicht vorgegeben.
if (SIGN && !PIN) {
const konf = gitStill('config', 'user.signingkey');
const kandidat = konf.ok ? konf.out.trim().replace(/^key::/, '') : '';
if (kandidat && existsSync(kandidat)) {
try {
// `ssh-keygen -lf` liefert z. B. "256 SHA256:abc… kommentar (ED25519)".
// Gebraucht wird genau das mittlere Feld dasselbe Format, das git als
// %GF ausgibt.
const zeile = execFileSync('ssh-keygen', ['-lf', kandidat], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
}).trim();
const treffer = zeile.split(/\s+/).find((t) => t.startsWith('SHA256:'));
if (treffer) PIN = treffer;
} catch {
/* faellt unten in den Abbruch */
}
}
if (!PIN) {
console.error(
'Der erwartete Signierschlüssel steht nicht fest.\n' +
'Ohne ihn wäre der Vertrauensanker die gesamte allowed_signers-Liste und nicht\n' +
'der eine Notar-Schlüssel jeder dort gelistete Zweitschlüssel könnte das\n' +
'Gegenbuch umschreiben, ohne dass es auffällt.\n' +
'Abhilfe: NOTARY_SIGNER_FINGERPRINT setzen (git log -1 --format=%GF)\n' +
'oder user.signingkey auf die .pub-Datei des Notar-Schlüssels zeigen lassen.',
);
process.exit(1);
}
}
async function hole(pfad) {
// Fehler werden hier zu einer erklaerenden Zeile frueher flog ein
// Node-Stacktrace hoch, also genau die kryptische erste Zeile, die fuer
// git-Meldungen schon abgestellt war.
let r;
try {
r = await fetch(`${CRM_URL}${pfad}`, { headers: { Authorization: `Bearer ${CRM_TOKEN}` } });
} catch (e) {
console.error(
`Das CRM ist nicht erreichbar (${CRM_URL}).\n` +
` Grund: ${e instanceof Error ? e.message : String(e)}\n` +
'Ohne aktuellen Kontrollwert kann nichts beglaubigt werden das ist ein ' +
'Verfügbarkeitsproblem, kein Befund.',
);
process.exit(1);
}
if (r.status === 401 || r.status === 403) {
console.error(
`Das CRM hat den Zugriff abgelehnt (HTTP ${r.status}).\n` +
'Meist ist der Token abgelaufen oder ihm fehlt das Recht audit:read.',
);
process.exit(1);
}
if (!r.ok) {
console.error(`Unerwartete Antwort des CRM: ${pfad} -> HTTP ${r.status}`);
process.exit(1);
}
let j;
try {
j = await r.json();
} catch {
console.error(`Antwort des CRM ist kein JSON (${pfad}) steht ein Proxy oder eine Fehlerseite davor?`);
process.exit(1);
}
if (!j.success) {
console.error(`Das CRM meldet einen Fehler: ${j.error}`);
process.exit(1);
}
return j.data;
}
const OHNE_ANKER = () =>
NUR_LOKAL ? ' ACHTUNG: nur lokal, kein externer Anker, also kein Manipulationsschutz.' : '';
// Ohne bekannte Mindesthoehe bleibt ein Rewind auf einen aelteren, echt
// 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)
? ''
: '\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.';
const alarm = (text) => { console.error(`ALARM: ${text}`); process.exit(2); };
// ---------------------------------------------------------------------------
// 1) Vertrauenswuerdigen Stand bestimmen: bevorzugt der Remote-Kopf.
// ---------------------------------------------------------------------------
// Auch der allererste git-Aufruf muss erklaerend scheitern koennen sonst
// steht bei einem kaputten Klon ein Node-Stacktrace statt einer Diagnose.
const zweigErmittelt = gitStill('rev-parse', '--abbrev-ref', 'HEAD');
if (!zweigErmittelt.ok || !zweigErmittelt.out || zweigErmittelt.out === 'HEAD') {
console.error(
'Kein ausgecheckter Branch gefunden ist das hier wirklich ein Klon des Gegenbuchs?\n' +
(zweigErmittelt.ok ? '' : ` git-Meldung: ${zweigErmittelt.out}`),
);
process.exit(1);
}
const zweig = zweigErmittelt.out;
let ref = 'HEAD';
const hatRemote = gitStill('remote', 'get-url', REMOTE).ok;
if (hatRemote) {
const f = gitStill('fetch', '--quiet', REMOTE);
if (f.ok && gitStill('rev-parse', '--verify', `${REMOTE}/${zweig}`).ok) {
ref = `${REMOTE}/${zweig}`;
} else {
// FAIL-CLOSED (Pentest R176-02): Frueher fiel der Ablauf hier still auf
// `HEAD` zurueck ausgerechnet unter der Bedingung, die einen vorherigen
// Push-Fehlschlag verursacht hat. Ein nie gepushter Checkpoint galt dann
// als beglaubigt, und `--check` meldete waehrend eines Remote-Ausfalls
// gruenes Licht auf nicht notarisiertem Zustand.
//
// Ohne erreichbaren Remote laesst sich der beglaubigte Stand schlicht
// nicht feststellen. Das ist kein Grund, dem lokalen Stand zu glauben.
console.error(
`Der beglaubigte Stand ist nicht feststellbar: ${REMOTE}/${zweig} nicht erreichbar.\n` +
`Ohne Remote kann nicht entschieden werden, ob der lokale Stand jemals notarisiert wurde.\n` +
`Das ist ein Verfügbarkeitsproblem kein Freibrief. Erst Verbindung klären, dann erneut prüfen.\n` +
(f.ok ? '' : ` git-Meldung: ${f.out}\n`),
);
process.exit(3);
}
} else if (process.env.NOTARY_ALLOW_LOCAL === 'true') {
// Der Schalter darf nicht zu einem glatten „OK“ fuehren: Leckt er je ins
// Produktivumfeld, gaebe es gruenes Licht auf nicht notarisiertem Stand.
NUR_LOKAL = true;
} else {
console.error(
`Kein Remote '${REMOTE}' konfiguriert. Ein Gegenbuch, das nur lokal liegt, schützt nichts \n` +
`es steht im selben Zugriffsbereich wie alles andere auf diesem Rechner.\n` +
`Für einen bewussten lokalen Testlauf: NOTARY_ALLOW_LOCAL=true`,
);
process.exit(3);
}
// Lokale Commits, die dem Remote voraus sind, sind nicht beglaubigt.
if (ref !== 'HEAD') {
const voraus = gitStill('rev-list', '--count', `${ref}..HEAD`);
if (voraus.ok && Number(voraus.out) > 0) {
alarm(
`${voraus.out} lokale(r) Commit(s) wurden nie gepusht das Gegenbuch ist insoweit ` +
`nicht beglaubigt.\n` +
`ZUERST PRÜFEN, wie sie entstanden sind: ein abgebrochener Lauf hinterlässt so einen ` +
`Commit ebenso wie ein falsch signierter (dann darf er NICHT gepusht werden, sonst ist ` +
`die Kette dauerhaft rot).\n` +
` Ansehen mit: git log ${ref}..HEAD --format='%h %G? %GF %s'`,
);
}
}
// ---------------------------------------------------------------------------
// 2) Signaturen der GESAMTEN Historie pruefen das war der fehlende Leser.
// ---------------------------------------------------------------------------
const commits = gitStill('log', '--format=%H %G?', ref);
if (!commits.ok) alarm(`Historie nicht lesbar: ${commits.out}`);
const zeilen = commits.out.split('\n').filter((z) => z.trim());
if (SIGN) {
// NUR `G` (Pentest R176-01). Bei SSH-Signaturen bedeutet `U` woertlich
// „gute Signatur, aber kein passender Principal“ also: der Schluessel
// steht NICHT in `allowed_signers`. Wer `U` akzeptiert, akzeptiert jeden
// selbst erzeugten Schluessel und macht damit genau den Vertrauensanker
// wirkungslos, den er zu pruefen glaubt. Live gezeigt: mit einem fremden
// Schluessel re-signiertes, force-gepushtes Gegenbuch lief als „gültig“
// durch.
//
// Hinweis fuer GPG statt SSH: dort liefert ein gueltiger, aber nicht
// ownertrusted Schluessel ebenfalls `U`. Dann Ownertrust auf den
// Notar-Schluessel setzen NICHT `U` wieder zulassen.
const schlecht = zeilen
.map((z) => z.split(' '))
.filter(([, status]) => status !== 'G');
// Der allererste (leere) init-Commit darf unsigniert sein, sonst nichts.
const relevant = schlecht.filter(([sha]) => {
const dateien = gitStill('show', '--name-only', '--format=', sha);
return dateien.ok && dateien.out.includes(LEDGER_NAME);
});
// Optional schaerfer: den erwarteten Signierschluessel festnageln, statt sich
// allein auf die Bewertung von git zu verlassen.
if (PIN) {
const fremde = zeilen
.map(([, ] , i) => i)
.map((i) => zeilen[i].split(' ')[0])
.filter((sha) => {
const dateien = gitStill('show', '--name-only', '--format=', sha);
if (!dateien.ok || !dateien.out.includes(LEDGER_NAME)) return false;
const fp = gitStill('log', '-1', '--format=%GF', sha);
return !fp.ok || fp.out.trim() !== PIN;
});
if (fremde.length) {
alarm(
`${fremde.length} Commit(s) wurden mit einem anderen als dem festgelegten ` +
`Schlüssel signiert (erwartet: ${PIN}):\n ` +
fremde.map((sha) => sha.slice(0, 12)).join('\n '),
);
}
}
if (relevant.length) {
alarm(
`${relevant.length} Commit(s) mit Gegenbuch-Änderungen tragen KEINE gültige Signatur:\n ` +
relevant.map(([sha, st]) => `${sha.slice(0, 12)} (%G?=${st})`).join('\n ') +
`\nDie Reihe ist damit nicht vertrauenswürdig.`,
);
}
}
// ---------------------------------------------------------------------------
// 3) Wahrheit aus dem signierten Baum lesen NICHT aus der Arbeitsdatei.
// ---------------------------------------------------------------------------
const ausBaum = gitStill('show', `${ref}:${LEDGER_NAME}`);
const beglaubigterInhalt = ausBaum.ok ? ausBaum.out : '';
const arbeitsInhalt = existsSync(LEDGER) ? readFileSync(LEDGER, 'utf8').trim() : '';
if (arbeitsInhalt !== beglaubigterInhalt.trim()) {
alarm(
'Die lokale Gegenbuch-Datei weicht vom signierten Stand ab. Entweder wurde sie ' +
'von Hand verändert, oder ein früherer Lauf ist zwischen Schreiben und Commit ' +
'abgebrochen. Nichts wird angehängt.\n' +
` Wiederherstellen mit: git checkout ${ref} -- ${LEDGER_NAME}`,
);
}
const bisher = beglaubigterInhalt
.split('\n')
.filter((z) => z.trim())
.map((z) => JSON.parse(z));
for (let i = 0; i < bisher.length; i++) {
if (bisher[i].seq !== i + 1) {
alarm(`Lücke in der Checkpoint-Reihe an Position ${i + 1} (seq=${bisher[i].seq}).`);
}
}
if (Number.isFinite(MIN_SEQ) && bisher.length < MIN_SEQ) {
alarm(
`Die Checkpoint-Reihe ist kürzer als bekannt: erwartet mindestens ${MIN_SEQ}, ` +
`vorhanden ${bisher.length}.\n` +
`Die vorhandenen Einträge sind zwar korrekt signiert aber es fehlen welche am Ende. ` +
`Das ist das Bild eines Rewinds per Force-Push.`,
);
}
// ---------------------------------------------------------------------------
// 4) Abgleich mit dem CRM, VOR dem Anhaengen.
// ---------------------------------------------------------------------------
const letzter = bisher[bisher.length - 1];
const aktuell = await hole('/api/audit-logs/checkpoint');
if (letzter) {
if (aktuell.maxId !== null && aktuell.maxId < letzter.maxId) {
alarm(
`Das Audit-Log ist geschrumpft. Beglaubigt war maxId=${letzter.maxId}, ` +
`jetzt maxId=${aktuell.maxId}. Es wurden Einträge entfernt.`,
);
}
const rueck = await hole(`/api/audit-logs/checkpoint?atId=${letzter.maxId}`);
if (rueck.atHash === null) alarm(`Der beglaubigte Eintrag ${letzter.maxId} existiert nicht mehr.`);
if (rueck.atHash !== letzter.chainHead) {
alarm(
`Der Eintrag ${letzter.maxId} wurde nachträglich verändert.\n` +
` beglaubigt: ${letzter.chainHead}\n jetzt : ${rueck.atHash}`,
);
}
if (letzter.sealRoot && !aktuell.sealRoot) {
alarm('Das Bestandssiegel ist verschwunden (vorher beglaubigt vorhanden).');
}
if (letzter.sealLeafCount > 0 && aktuell.sealLeafCount === 0) {
alarm('Die Blattwerte des Bestandssiegels wurden entfernt.');
}
if (letzter.sealRoot && aktuell.sealRoot && letzter.sealRoot !== aktuell.sealRoot) {
console.warn(
`HINWEIS: Das Bestandssiegel wurde erneuert (${letzter.sealRoot.slice(0, 12)}… → ` +
`${aktuell.sealRoot.slice(0, 12)}…). Legitim nach einem Retention-Lauf sonst prüfen.`,
);
}
}
// ---------------------------------------------------------------------------
// 5) Anhaengen mit Ruecknahme, falls der signierte Commit scheitert.
// ---------------------------------------------------------------------------
if (NUR_PRUEFEN) {
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);
}
const eintrag = {
seq: bisher.length + 1,
ts: aktuell.ts,
maxId: aktuell.maxId,
chainHead: aktuell.chainHead,
sealRoot: aktuell.sealRoot,
sealLeafCount: aktuell.sealLeafCount,
};
const neuerInhalt = [...bisher, eintrag].map((e) => JSON.stringify(e)).join('\n') + '\n';
const zuruecknehmen = () => {
// Ohne das bliebe eine uncommittete Zeile liegen, die der naechste Lauf
// absorbiert und damit zementiert (Pentest R175-03).
const r = gitStill('checkout', ref, '--', LEDGER_NAME);
if (!r.ok && !ausBaum.ok) writeFileSync(LEDGER, '');
};
writeFileSync(LEDGER, neuerInhalt);
try {
git('add', LEDGER);
git('commit', ...(SIGN ? ['-S'] : []), '-m',
`Checkpoint ${eintrag.seq} maxId ${eintrag.maxId} ${eintrag.ts}`);
} catch (e) {
zuruecknehmen();
console.error(`Commit fehlgeschlagen, Gegenbuch unverändert zurückgesetzt.\n${e.stderr || e.message}`);
process.exit(1);
}
// Signatur des frischen Commits gegenpruefen sonst waere `-S` wieder nur
// Absichtserklaerung.
if (SIGN) {
const st = git('log', '-1', '--format=%G?', 'HEAD');
if (st !== 'G') {
git('reset', '--hard', 'HEAD~1');
alarm(`Der neue Commit trägt keine gültige Signatur (%G?=${st}). Zurückgerollt.`);
}
// Auch der FRISCHE Commit muss vom festgelegten Schluessel stammen
// (Pentest R177-02). Vorher wurde hier nur `%G?` geprueft: Ein Notar-Host
// mit falsch konfiguriertem Schluessel meldete „beglaubigt“ und pushte
// und ab da war die Kette bei jeder Folgepruefung dauerhaft rot, behebbar
// nur per Force-Push, den die Branch-Protection gerade verhindern soll.
// Deshalb: pruefen und zuruecknehmen, BEVOR gepusht wird.
if (PIN) {
const fp = git('log', '-1', '--format=%GF', 'HEAD').trim();
if (fp !== PIN) {
git('reset', '--hard', 'HEAD~1');
alarm(
`Der neue Commit wurde mit einem anderen Schlüssel signiert als festgelegt.\n` +
` erwartet: ${PIN}\n verwendet: ${fp}\n` +
`Zurückgerollt, nichts gepusht bitte user.signingkey auf dem Notar-Rechner prüfen.`,
);
}
}
}
if (!SIGN) console.warn('WARNUNG: Commit wurde NICHT signiert das Gegenbuch ist ungeschützt.');
if (PUSH && !NUR_LOKAL) {
const p = gitStill('push', REMOTE, zweig);
if (!p.ok) {
console.error(
`Push fehlgeschlagen der Checkpoint liegt nur lokal und gilt nicht als beglaubigt.\n${p.out}`,
);
process.exit(1);
}
}
console.log(
`OK: Checkpoint ${eintrag.seq} beglaubigt (maxId ${eintrag.maxId}).` + OHNE_ANKER() +
`\n Zuletzt bekannte Nummer für die nächste Prüfung: NOTARY_MIN_SEQ=${eintrag.seq}`,
);