Gegenbuch: nur vertrauenswuerdige Signaturen, fail-closed ohne Remote (R176)
R176-01 (HIGH): Das Signatur-Gate akzeptierte %G? = G ODER U. Bei SSH-Signaturen bedeutet U woertlich "gute Signatur, aber kein passender Principal" - der Schluessel steht also NICHT in allowed_signers. Damit passierte jeder selbst erzeugte Schluessel das Gate und der einzige In-System-Vertrauensanker war wirkungslos. End-to-end reproduziert: Gegenbuch mit fremdem Schluessel re-signiert und force-gepusht -> "OK", exit 0. Vorbedingung war nur Remote-Schreibrecht, kein Host-Zugriff. Fix: nur G an beiden Stellen, dazu optionales Pinnen des erwarteten Signierschluessels ueber NOTARY_SIGNER_FINGERPRINT (%GF). R176-02 (MEDIUM): War der Remote unerreichbar, fiel der Ablauf still auf HEAD zurueck und die "nie gepusht"-Pruefung wurde uebersprungen - ausgerechnet unter der Bedingung, die einen Push-Fehlschlag verursacht. --check meldete waehrend eines Ausfalls gruenes Licht auf nicht notarisiertem Zustand. Fix: fail-closed mit Code 3, ebenso ohne konfigurierten Remote (Testlauf nur mit NOTARY_ALLOW_LOCAL=true). Nebenbei: git-eigene Fehlermeldungen standen vor der eigenen Erklaerung, stderr wird jetzt abgefangen und gezielt weitergereicht. Verifiziert mit zwei SSH-Schluesseln gegen echten Remote: Angriff mit fremdem Key + Force-Push -> Alarm exit 2 (vorher OK); Remote unerreichbar -> exit 3; kein Remote -> exit 3; falscher Fingerabdruck-Pin -> Alarm; saubere Historie ohne Fehlalarm. Rueckgabecodes in der README dokumentiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,8 @@ const NUR_PRUEFEN = process.argv.includes('--check') || process.env.NOTARY_CHECK
|
||||
// zusaetzliche, bewusste Bestaetigung – der Reflex "Cron hakt, also abschalten"
|
||||
// soll nicht ausreichen.
|
||||
const SIGN = process.env.NOTARY_SIGN !== 'false';
|
||||
// Optionaler Fingerabdruck des erwarteten Signierschluessels (`%GF`).
|
||||
const 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' +
|
||||
@@ -54,10 +56,15 @@ if (!CRM_URL || !CRM_TOKEN) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const git = (...args) => execFileSync('git', args, { encoding: 'utf8' }).trim();
|
||||
// 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) }; }
|
||||
catch (e) { return { ok: false, out: String(e.stderr || e.message).trim() }; }
|
||||
};
|
||||
|
||||
async function hole(pfad) {
|
||||
@@ -75,10 +82,36 @@ const alarm = (text) => { console.error(`ALARM: ${text}`); process.exit(2); };
|
||||
// ---------------------------------------------------------------------------
|
||||
const zweig = git('rev-parse', '--abbrev-ref', 'HEAD');
|
||||
let ref = 'HEAD';
|
||||
if (gitStill('remote', 'get-url', REMOTE).ok) {
|
||||
const hatRemote = gitStill('remote', 'get-url', REMOTE).ok;
|
||||
|
||||
if (hatRemote) {
|
||||
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}`;
|
||||
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') {
|
||||
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.
|
||||
@@ -99,14 +132,46 @@ 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
|
||||
// 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' && status !== 'U');
|
||||
.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 ` +
|
||||
@@ -222,7 +287,7 @@ try {
|
||||
// Absichtserklaerung.
|
||||
if (SIGN) {
|
||||
const st = git('log', '-1', '--format=%G?', 'HEAD');
|
||||
if (st !== 'G' && st !== 'U') {
|
||||
if (st !== 'G') {
|
||||
git('reset', '--hard', 'HEAD~1');
|
||||
alarm(`Der neue Commit trägt keine gültige Signatur (%G?=${st}). Zurückgerollt.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user