R177-01 (MEDIUM): Der Pin war optional. Ohne ihn war der Vertrauensanker die gesamte allowed_signers-Menge, nicht der eine Notar-Schluessel: ein zweiter dort gelisteter Schluessel konnte das Gegenbuch waschen und force-pushen, und %G? war G. R176-01 hatte "jeder selbst erzeugte Schluessel" geschlossen, "jeder erlaubte Schluessel" blieb offen. Fix: Pin wird aus user.signingkey abgeleitet; laesst er sich nicht bestimmen, wird abgebrochen statt die ganze Liste zu akzeptieren. R177-02 (MEDIUM): Der Schreib-Lauf prueft den frischen Commit nur auf %G?, nicht auf den Pin. Ein Notar-Host mit falsch konfiguriertem Schluessel meldete "beglaubigt" und pushte - und ab da war die Kette dauerhaft rot, behebbar nur per Force-Push, den die Branch-Protection gerade verhindern soll. Fix: Pin-Abgleich am frischen Commit vor dem Push, bei Abweichung Ruecknahme. Kleinkram: NOTARY_ALLOW_LOCAL faerbt Erfolgsmeldungen ein und pusht nicht mehr ins Leere; CRM-Fehler liefern eine erklaerende Zeile statt Node-Stacktrace. Verifiziert mit drei SSH-Schluesseln gegen echten Remote: keyC-Angriff -> Alarm exit 2 auch ohne gesetzten Pin; Schreiblauf mit falschem Schluessel -> zurueckgerollt, nichts gepusht; CRM nicht erreichbar/401 -> saubere Meldung; Lokalmodus eingefaerbt; saubere Historie ohne Fehlalarm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
411 lines
17 KiB
JavaScript
Executable File
411 lines
17 KiB
JavaScript
Executable File
#!/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.
|
||
let PIN = process.env.NOTARY_SIGNER_FINGERPRINT || null;
|
||
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.' : '';
|
||
|
||
const alarm = (text) => { console.error(`ALARM: ${text}`); process.exit(2); };
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 1) Vertrauenswuerdigen Stand bestimmen: bevorzugt der Remote-Kopf.
|
||
// ---------------------------------------------------------------------------
|
||
const zweig = git('rev-parse', '--abbrev-ref', 'HEAD');
|
||
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 damit ` +
|
||
`nur lokal – entweder pushen oder untersuchen, wie sie entstanden sind.`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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}).`);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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(),
|
||
);
|
||
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());
|