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:
@@ -700,7 +700,7 @@ export async function getCheckpoint(atId?: number): Promise<{
|
||||
maxId: number | null;
|
||||
chainHead: string | null;
|
||||
sealRoot: string | null;
|
||||
sealStatus: string;
|
||||
sealLeafCount: number;
|
||||
atId?: number;
|
||||
atHash?: string | null;
|
||||
}> {
|
||||
@@ -708,22 +708,60 @@ export async function getCheckpoint(atId?: number): Promise<{
|
||||
orderBy: { id: 'desc' },
|
||||
select: { id: true, hash: true },
|
||||
});
|
||||
const pruefung = await verifyIntegrity();
|
||||
|
||||
// BEWUSST KEIN verifyIntegrity() hier (Pentest R175-02): das laeuft ueber
|
||||
// alle Zeilen und kostet bei grossen Logs Sekunden je Aufruf – ein
|
||||
// authentifizierter Leser koennte damit die Datenbank in die Knie zwingen.
|
||||
// Gebraucht wird von der Vollpruefung ohnehin nur die Siegel-Wurzel, und die
|
||||
// steht im juengsten gueltigen Marker. Der Kopf-Hash kommt direkt aus der
|
||||
// Kopfzeile.
|
||||
const c3 = await prisma.auditLog.aggregate({
|
||||
where: { hashVersion: { gte: 3 } },
|
||||
_min: { id: true },
|
||||
});
|
||||
const v3FromId = c3._min.id;
|
||||
const schluessel = [auditHmacKey(), ...auditHmacKeysOld()].filter(
|
||||
(k): k is string => !!k,
|
||||
);
|
||||
const markerKandidaten = await prisma.auditLog.findMany({
|
||||
where: { resourceType: BACKLOG_SEAL_RESOURCE, endpoint: BACKLOG_SEAL_ENDPOINT },
|
||||
orderBy: { id: 'desc' },
|
||||
take: 20,
|
||||
});
|
||||
let sealRoot: string | null = null;
|
||||
if (schluessel.length && v3FromId !== null) {
|
||||
for (const r of markerKandidaten) {
|
||||
if (r.id < v3FromId || r.hashVersion < 3) continue;
|
||||
if (!schluessel.some((k) => r.hash === generateHashV3(r as unknown as AuditHashV2Input, k))) continue;
|
||||
try {
|
||||
const m = JSON.parse(r.changesAfter || '{}');
|
||||
if (typeof m.root === 'string' && m.root.length > 0) {
|
||||
sealRoot = m.root;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
/* naechster Kandidat */
|
||||
}
|
||||
}
|
||||
}
|
||||
const blattAnzahl = await prisma.auditBacklogSeal.count();
|
||||
|
||||
const ergebnis: {
|
||||
ts: string;
|
||||
maxId: number | null;
|
||||
chainHead: string | null;
|
||||
sealRoot: string | null;
|
||||
sealStatus: string;
|
||||
sealLeafCount: number;
|
||||
atId?: number;
|
||||
atHash?: string | null;
|
||||
} = {
|
||||
ts: new Date().toISOString(),
|
||||
maxId: kopf?.id ?? null,
|
||||
chainHead: kopf?.hash ?? null,
|
||||
sealRoot: pruefung.backlogSealRoot,
|
||||
sealStatus: pruefung.backlogSealStatus,
|
||||
sealRoot,
|
||||
// Reicht der Gegenstelle, um das Verschwinden des Siegels zu bemerken:
|
||||
// Blaetter ohne Wurzel = Marker entfernt.
|
||||
sealLeafCount: blattAnzahl,
|
||||
};
|
||||
|
||||
if (atId !== undefined && Number.isFinite(atId)) {
|
||||
|
||||
Reference in New Issue
Block a user