verify meldet, dass die Kette neu berechnet wurde
Im Betrieb entdeckt, nicht im Test. Das Staging-Gegenbuch meldete "Der beglaubigte Eintrag 5352 existiert nicht mehr". Rekonstruktion aus dem Protokoll: am 22.08. wurden die Aufbewahrungsfristen auf 0 gesetzt, zwei Cleanups loeschten 3155 Eintraege (id 1-5356), danach lief ein Rehash. Seither meldet die Pruefung "Alle Eintraege sind unveraendert und lueckenlos verkettet". Wahr - und praktisch das Gegenteil dessen, was ein Leser mitnimmt. Der Rehash verknuepft alles neu; die rund 700 Kettenluecken, die davor bestanden, sind seitdem unsichtbar. Nachweisbar im Vorbefund, den der Rehash selbst mitschreibt (R170-01) - nur schaute den nie jemand an. Das Gegenbuch war der einzige Zeuge; innerhalb des CRM war die Loeschung nicht mehr feststellbar. verifyIntegrity sammelt jetzt die Rehash-Marker; die Antwort enthaelt rehashes[] mit Zeitpunkt, Anzahl, Signatur und Vorbefund. Die Meldung nennt sie IMMER, auch im gruenen Fall, und der Einstiegssatz lautet dann "...lueckenlos verkettet - allerdings erst seit der letzten Neuberechnung". valid bleibt unberuehrt. Ein Rehash ist eine legitime Massnahme; ihn dauerhaft als Befund zu fuehren waere der Dauer-Alarm, den wir mit den beglaubigten Luecken gerade beseitigt haben. Melden, nicht alarmieren. Umgekehrte Beweislast als bei Manifest und Siegel: Dort zaehlen nur signierte Traeger, weil ein gefaelschter Marker Luecken wegerklaeren koennte. Hier erzeugt ein Marker eine Warnung - wuerden nur signierte zaehlen, koennte man einen Rehash unsichtbar machen, indem man seine Signatur zerstoert. Deshalb zaehlt jeder auswertbare Marker; eine fehlende Signatur wird zusaetzlich gemeldet. Getestet ueber HTTP gegen eine Wegwerf-DB mit echtem Rehash ueber den regulaeren Endpunkt: sauberer Vorzustand, Loeschung+Rehash (der Staging-Ablauf im Kleinen), und zerstoerte Signatur. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -909,6 +909,24 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
* nicht mehr (siehe ausfuehrliche Begruendung an der Berechnung unten).
|
||||
*/
|
||||
attestedGaps: number[];
|
||||
/**
|
||||
* Protokollierte Neuberechnungen der Kette (Rehash), aelteste zuerst.
|
||||
*
|
||||
* Ein Rehash verknuepft alle Eintraege neu. Danach ist die Kette
|
||||
* zwangslaeufig stimmig - auch ueber Loeschungen hinweg, die vorher als
|
||||
* Luecken sichtbar waren. „Lueckenlos verkettet“ heisst nach einem Rehash
|
||||
* also nur noch: seit dem Rehash. Wer das nicht mitliest, nimmt eine
|
||||
* Entwarnung mit, die es nicht gibt.
|
||||
*/
|
||||
rehashes: Array<{
|
||||
id: number;
|
||||
zeitpunkt: string;
|
||||
neuBerechnet: number | null;
|
||||
/** Marker mit gueltiger HMAC-Signatur? Siehe Kommentar an der Auswertung. */
|
||||
signiert: boolean;
|
||||
/** Befund unmittelbar VOR dem Rehash - was also uebertuencht wurde. */
|
||||
vorbefund: { manipuliert: number; luecken: number } | null;
|
||||
}>;
|
||||
/**
|
||||
* Zustand des Bestandssiegels ueber den nicht signierbaren Altbestand.
|
||||
* `kein_siegel` = nie erstellt. `entfernt` = Blaetter vorhanden, aber kein
|
||||
@@ -1044,6 +1062,68 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
const gapErklaert = (prevId: number, curId: number) =>
|
||||
deletionRanges.some((r) => r.from <= curId && r.to >= prevId);
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Protokollierte Neuberechnungen (Rehash) einsammeln.
|
||||
//
|
||||
// Ein Rehash macht die Kette rechnerisch stimmig - auch dort, wo vorher
|
||||
// Loeschungen als Luecken sichtbar waren. Genau das ist auf Staging passiert:
|
||||
// zwei Cleanups mit Aufbewahrung 0 entfernten 3155 Eintraege, der folgende
|
||||
// Rehash liess rund 700 Kettenluecken verschwinden, und die Pruefung meldete
|
||||
// danach „unveraendert und lueckenlos verkettet“. Wahr - und trotzdem das
|
||||
// Gegenteil dessen, was ein Leser mitnimmt.
|
||||
//
|
||||
// Umgekehrte Beweislast als bei Manifest und Siegel: Dort zaehlen NUR
|
||||
// signierte Traeger, weil ein gefaelschter Marker Luecken wegerklaeren
|
||||
// koennte - Misstrauen ist die sichere Richtung. Hier erzeugt ein Marker eine
|
||||
// WARNUNG. Wuerden wir nur signierte gelten lassen, koennte jemand einen
|
||||
// Rehash unsichtbar machen, indem er dessen Signatur zerstoert. Deshalb
|
||||
// zaehlt hier jeder auswertbare Marker; ob er signiert ist, wird nur
|
||||
// mitgeteilt.
|
||||
const rehashKandidaten = await prisma.auditLog.findMany({
|
||||
where: { resourceType: 'AuditLog', action: 'UPDATE', endpoint: '/api/audit-logs/rehash' },
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
const rehashSchluessel = [auditHmacKey(), ...auditHmacKeysOld()].filter(
|
||||
(k): k is string => !!k,
|
||||
);
|
||||
const rehashes: Array<{
|
||||
id: number;
|
||||
zeitpunkt: string;
|
||||
neuBerechnet: number | null;
|
||||
signiert: boolean;
|
||||
vorbefund: { manipuliert: number; luecken: number } | null;
|
||||
}> = [];
|
||||
for (const row of rehashKandidaten) {
|
||||
let neuBerechnet: number | null = null;
|
||||
let vorbefund: { manipuliert: number; luecken: number } | null = null;
|
||||
try {
|
||||
const nach = JSON.parse(row.changesAfter || '{}');
|
||||
if (typeof nach.neuBerechnet !== 'number') continue; // kein Rehash-Marker
|
||||
neuBerechnet = nach.neuBerechnet;
|
||||
const vor = JSON.parse(row.changesBefore || '{}');
|
||||
if (Array.isArray(vor.manipuliert) && Array.isArray(vor.ketten_luecken)) {
|
||||
vorbefund = {
|
||||
manipuliert: vor.manipuliert.length,
|
||||
luecken: vor.ketten_luecken.length,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const signiert =
|
||||
row.hashVersion >= 3 &&
|
||||
rehashSchluessel.some(
|
||||
(k) => row.hash === generateHashV3(row as unknown as AuditHashV2Input, k),
|
||||
);
|
||||
rehashes.push({
|
||||
id: row.id,
|
||||
zeitpunkt: row.createdAt.toISOString(),
|
||||
neuBerechnet,
|
||||
signiert,
|
||||
vorbefund,
|
||||
});
|
||||
}
|
||||
|
||||
const tamperedEntries: number[] = [];
|
||||
const chainGaps: number[] = [];
|
||||
const unexplainedGaps: number[] = [];
|
||||
@@ -1349,6 +1429,7 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
unexplainedGaps,
|
||||
unverifiableEntries,
|
||||
attestedGaps,
|
||||
rehashes,
|
||||
backlogSealStatus,
|
||||
backlogTampered,
|
||||
backlogMissing,
|
||||
|
||||
Reference in New Issue
Block a user