Gegenbuch: verifizierender Leser statt Absichtserklaerung (Pentest R175)
R175-01 (HIGH): Die erste Fassung signierte zwar, prueft aber nie. Sie las ihre Wahrheit per readFileSync aus der lokalen Arbeitsdatei, nirgends gab es ein git verify-commit - das -S war write-only ohne Konsument. Live reproduziert: DB-Tail abgeschnitten und die lokale Ledger-Zeile angepasst -> "OK, Checkpoint beglaubigt", exit 0, kein Alarm. Fix: Wahrheitsquelle ist der signierte Commit-Baum (bevorzugt der Remote-Kopf); jeder Commit mit Gegenbuch-Aenderung muss eine gueltige Signatur tragen; weicht die Arbeitsdatei vom signierten Stand ab, wird abgebrochen; lokale, nie gepushte Commits gelten nicht als beglaubigt; die Signatur des frisch erzeugten Commits wird gegengeprueft. Dazu ein Pruefmodus --check fuer Auditoren. R175-02 (MEDIUM): /checkpoint fuhr je Aufruf ein volles verifyIntegrity (O(n), 0,85 s bei 16k Zeilen) - authentifizierte DoS-Verstaerkung. Gebraucht wurde nur die Siegel-Wurzel. Jetzt Kopf-Hash aus der Kopfzeile, Wurzel aus dem juengsten gueltigen Marker, sealLeafCount statt des teuren Status. R175-03: writeFileSync lief vor dem Commit, eine verwaiste Zeile wurde vom Folgelauf zementiert. Jetzt Ruecknahme bei Fehlschlag, und NOTARY_SIGN=false verlangt zusaetzlich NOTARY_INSECURE_ACK. Verifiziert mit echtem SSH-Signaturschluessel: stilles Waschen -> Alarm; erfundene Zeile -> Alarm; unsignierter Commit -> Alarm; Commit-Fehlschlag -> zurueckgerollt; Reflex-Schalter verweigert; Pruefmodus haengt nichts an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,32 @@ Als Cronjob, stündlich:
|
||||
nur geliehen – wer Schreibrecht auf das Repository erlangt, schreibt die
|
||||
Historie sonst einfach um. Bei GitHub/GitLab: Branch-Protection auf `main`.
|
||||
|
||||
## Der entscheidende Punkt: es wird tatsächlich geprüft
|
||||
|
||||
Die erste Fassung dieses Skripts hat signiert – aber **nie eine Signatur
|
||||
geprüft**. Es las seine Wahrheit aus der lokalen Arbeitsdatei. Wer den
|
||||
Notar-Rechner beschreiben konnte, hat ihm damit beliebige „beglaubigte"
|
||||
Zeilen untergeschoben und einen abgeschnittenen Datenbestand als neue Wahrheit
|
||||
festgeschrieben, ohne Alarm. Signieren ohne prüfenden Leser ist wertlos.
|
||||
|
||||
Deshalb gilt jetzt:
|
||||
|
||||
- Wahrheitsquelle ist der **signierte Commit-Baum** (bevorzugt der Remote-Kopf),
|
||||
nicht die Arbeitsdatei
|
||||
- **Jeder** Commit, der das Gegenbuch ändert, muss eine gültige Signatur tragen
|
||||
- Weicht die Arbeitsdatei vom signierten Stand ab, wird abgebrochen
|
||||
- Geschrieben wird erst nach erfolgreichem, signiertem Commit – scheitert er,
|
||||
wird die Datei zurückgerollt
|
||||
|
||||
## Prüfmodus für Auditoren
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
## Was das Skript erkennt
|
||||
|
||||
| Angriff | Erkennung |
|
||||
@@ -51,6 +77,10 @@ Historie sonst einfach um. Bei GitHub/GitLab: Branch-Protection auf `main`.
|
||||
| Einträge am Ende abgeschnitten | aktuelle höchste ID kleiner als die beglaubigte |
|
||||
| Bestandssiegel verschwunden | vorher beglaubigt, jetzt nicht mehr vorhanden |
|
||||
| Gegenbuch selbst gekürzt | Lücke in der fortlaufenden Nummer |
|
||||
| Gegenbuch lokal manipuliert | Arbeitsdatei weicht vom signierten Stand ab |
|
||||
| Untergeschobener Commit | Commit ohne gültige Signatur in der Historie |
|
||||
| Nie gepushte lokale Commits | Abgleich gegen den Remote-Kopf |
|
||||
| Bestandssiegel-Blätter entfernt | beglaubigte Blattzahl auf null gefallen |
|
||||
|
||||
Bei jedem dieser Fälle bricht das Skript mit **Exit-Code 2** ab und **hängt
|
||||
nichts an** – der manipulierte Zustand wird also nicht als neue Wahrheit
|
||||
@@ -67,5 +97,11 @@ im Log unter.
|
||||
CRM – dort ist ja nichts konfiguriert. Der Ausfall muss auf dem
|
||||
Gegenbuch-Rechner überwacht werden (z. B. Alarm, wenn der jüngste Commit
|
||||
älter als zwei Intervalle ist).
|
||||
- **`NOTARY_SIGN=false`** existiert nur für Tests ohne hinterlegten Schlüssel.
|
||||
Im Betrieb niemals setzen.
|
||||
- **`NOTARY_SIGN=false`** existiert nur für Tests ohne hinterlegten Schlüssel
|
||||
und verlangt zusätzlich `NOTARY_INSECURE_ACK=…`. Der Reflex „Cron hakt, also
|
||||
Signatur abschalten" soll bewusst nicht ausreichen – er würde eine
|
||||
ungeschützte Kette festschreiben.
|
||||
- **Der Remote-Server muss Force-Push verhindern.** Das Skript prüft die
|
||||
Signaturen, aber wenn jemand die Historie am Server überschreiben kann,
|
||||
ersetzt er auch die signierten Commits durch eigene. Branch-Protection ist
|
||||
Teil des Schutzes, nicht Zubehör.
|
||||
|
||||
+174
-69
@@ -3,109 +3,172 @@
|
||||
* OpenCRM – Gegenbuch (externe Notarisierung)
|
||||
* ===========================================
|
||||
*
|
||||
* WICHTIG: Dieses Skript laeuft NICHT auf dem CRM-Server. Es gehoert auf einen
|
||||
* zweiten Rechner. Der Signaturschluessel und das Push-Recht auf das private
|
||||
* Repository leben dort – nicht in den Deploy-Secrets des CRM.
|
||||
* 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.
|
||||
*
|
||||
* Der Grund: Alle Anker im CRM selbst (Hash-Kette, Versionsgrenze, Manifest,
|
||||
* Bestandssiegel) sitzen in derselben Datenbank, die sie schuetzen sollen. Wer
|
||||
* dort schreiben kann, kommt am Ende an jeden davon heran. Ein Gegenbuch an
|
||||
* einem fremden Ort durchbricht das – aber NUR, solange der Schluessel dieses
|
||||
* Ortes nicht im selben Zugriffsbereich liegt. Liegt er es doch, sieht es nach
|
||||
* doppeltem Boden aus und ist keiner.
|
||||
* 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.
|
||||
*
|
||||
* Ablauf je Lauf:
|
||||
* 1. Kontrollwert beim CRM abholen (nur lesend)
|
||||
* 2. GEGEN die eigene Historie pruefen, BEVOR etwas angehaengt wird
|
||||
* 3. Nur bei Unauffaelligkeit anhaengen, signiert committen, pushen
|
||||
* 4. Bei Widerspruch: Exit-Code 2 und nichts schreiben
|
||||
*
|
||||
* Aufruf (Beispiel-Cron, stuendlich):
|
||||
* 0 * * * * cd /pfad/zum/gegenbuch && CRM_URL=https://crm.example.de \
|
||||
* CRM_TOKEN=... node /pfad/notary.mjs >> notary.log 2>&1
|
||||
* 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 } from 'node:path';
|
||||
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';
|
||||
// NUR fuer Tests ohne hinterlegten Schluessel. Im Betrieb NIEMALS setzen: ohne
|
||||
// Signatur kann jeder, der Schreibrecht auf das Repository erlangt, die
|
||||
// Checkpoint-Reihe faelschen - dann ist das Gegenbuch wertlos.
|
||||
const SIGN = process.env.NOTARY_SIGN !== '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';
|
||||
|
||||
// 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';
|
||||
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);
|
||||
}
|
||||
|
||||
const git = (...args) => execFileSync('git', args, { encoding: 'utf8' }).trim();
|
||||
const gitStill = (...args) => {
|
||||
try { return { ok: true, out: git(...args) }; }
|
||||
catch (e) { return { ok: false, out: String(e.stderr || e.message) }; }
|
||||
};
|
||||
|
||||
async function hole(pfad) {
|
||||
const r = await fetch(`${CRM_URL}${pfad}`, {
|
||||
headers: { Authorization: `Bearer ${CRM_TOKEN}` },
|
||||
});
|
||||
const r = await fetch(`${CRM_URL}${pfad}`, { headers: { Authorization: `Bearer ${CRM_TOKEN}` } });
|
||||
if (!r.ok) throw new Error(`${pfad} -> HTTP ${r.status}`);
|
||||
const j = await r.json();
|
||||
if (!j.success) throw new Error(`${pfad} -> ${j.error}`);
|
||||
return j.data;
|
||||
}
|
||||
|
||||
function ledgerLesen() {
|
||||
if (!existsSync(LEDGER)) return [];
|
||||
return readFileSync(LEDGER, 'utf8')
|
||||
.split('\n')
|
||||
.filter((z) => z.trim())
|
||||
.map((z) => JSON.parse(z));
|
||||
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';
|
||||
if (gitStill('remote', 'get-url', REMOTE).ok) {
|
||||
const f = gitStill('fetch', '--quiet', REMOTE);
|
||||
if (!f.ok) console.warn(`HINWEIS: fetch von ${REMOTE} fehlgeschlagen – prüfe gegen lokalen Stand.\n${f.out}`);
|
||||
else if (gitStill('rev-parse', '--verify', `${REMOTE}/${zweig}`).ok) ref = `${REMOTE}/${zweig}`;
|
||||
}
|
||||
|
||||
const bisher = ledgerLesen();
|
||||
|
||||
// --- seq lueckenlos und monoton pruefen -------------------------------------
|
||||
// Ohne diese Pruefung koennte jemand die Checkpoint-Reihe selbst hinten
|
||||
// kuerzen; die Luecke faellt dann nicht auf.
|
||||
for (let i = 0; i < bisher.length; i++) {
|
||||
if (bisher[i].seq !== i + 1) {
|
||||
console.error(`ALARM: Lücke in der Checkpoint-Reihe an Position ${i + 1} (seq=${bisher[i].seq}).`);
|
||||
process.exit(2);
|
||||
// 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) {
|
||||
const schlecht = zeilen
|
||||
.map((z) => z.split(' '))
|
||||
.filter(([, status]) => status !== 'G' && status !== 'U');
|
||||
// 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);
|
||||
});
|
||||
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');
|
||||
|
||||
// --- Abgleich VOR dem Anhaengen ---------------------------------------------
|
||||
if (letzter) {
|
||||
// 1) Abschneiden am Ende: der beglaubigte Stand darf nie schrumpfen.
|
||||
if (aktuell.maxId !== null && aktuell.maxId < letzter.maxId) {
|
||||
console.error(
|
||||
`ALARM: Das Audit-Log ist geschrumpft. Beglaubigt war maxId=${letzter.maxId}, ` +
|
||||
alarm(
|
||||
`Das Audit-Log ist geschrumpft. Beglaubigt war maxId=${letzter.maxId}, ` +
|
||||
`jetzt maxId=${aktuell.maxId}. Es wurden Einträge entfernt.`,
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// 2) Umschreiben: der frueher beglaubigte Kopf muss reproduzierbar bleiben.
|
||||
const rueck = await hole(`/api/audit-logs/checkpoint?atId=${letzter.maxId}`);
|
||||
if (rueck.atHash === null) {
|
||||
console.error(`ALARM: Der beglaubigte Eintrag ${letzter.maxId} existiert nicht mehr.`);
|
||||
process.exit(2);
|
||||
}
|
||||
if (rueck.atHash === null) alarm(`Der beglaubigte Eintrag ${letzter.maxId} existiert nicht mehr.`);
|
||||
if (rueck.atHash !== letzter.chainHead) {
|
||||
console.error(
|
||||
`ALARM: Der Eintrag ${letzter.maxId} wurde nachträglich verändert.\n` +
|
||||
alarm(
|
||||
`Der Eintrag ${letzter.maxId} wurde nachträglich verändert.\n` +
|
||||
` beglaubigt: ${letzter.chainHead}\n jetzt : ${rueck.atHash}`,
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// 3) Bestandssiegel darf nicht klammheimlich verschwinden.
|
||||
if (letzter.sealRoot && !aktuell.sealRoot) {
|
||||
console.error('ALARM: Das Bestandssiegel ist verschwunden (vorher beglaubigt vorhanden).');
|
||||
process.exit(2);
|
||||
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(
|
||||
@@ -115,23 +178,65 @@ if (letzter) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Anhaengen, signieren, pushen -------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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}).` : '.'),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const eintrag = {
|
||||
seq: bisher.length + 1,
|
||||
ts: aktuell.ts,
|
||||
maxId: aktuell.maxId,
|
||||
chainHead: aktuell.chainHead,
|
||||
sealRoot: aktuell.sealRoot,
|
||||
sealStatus: aktuell.sealStatus,
|
||||
sealLeafCount: aktuell.sealLeafCount,
|
||||
};
|
||||
writeFileSync(LEDGER, [...bisher, eintrag].map((e) => JSON.stringify(e)).join('\n') + '\n');
|
||||
const neuerInhalt = [...bisher, eintrag].map((e) => JSON.stringify(e)).join('\n') + '\n';
|
||||
|
||||
git('add', LEDGER);
|
||||
// -S signiert den Commit mit dem Schluessel DIESES Rechners.
|
||||
const commitArgs = ['commit', ...(SIGN ? ['-S'] : []), '-m',
|
||||
`Checkpoint ${eintrag.seq} – maxId ${eintrag.maxId} – ${eintrag.ts}`];
|
||||
if (!SIGN) console.warn('WARNUNG: Commit wird NICHT signiert (NOTARY_SIGN=false).');
|
||||
git(...commitArgs);
|
||||
if (PUSH) git('push');
|
||||
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' && st !== 'U') {
|
||||
git('reset', '--hard', 'HEAD~1');
|
||||
alarm(`Der neue Commit trägt keine gültige Signatur (%G?=${st}). Zurückgerollt.`);
|
||||
}
|
||||
}
|
||||
if (!SIGN) console.warn('WARNUNG: Commit wurde NICHT signiert – das Gegenbuch ist ungeschützt.');
|
||||
|
||||
if (PUSH) {
|
||||
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}).`);
|
||||
|
||||
Reference in New Issue
Block a user