Worum es geht
Eine Hash-Kette ist eine Liste, in der jeder Eintrag den Hash seines Vorgängers enthält. Änderst du einen Eintrag, passt sein Hash nicht mehr zum nächsten Eintrag. Die Kette ist an dieser Stelle gebrochen, und jede Prüfung findet das.
CRMS führt zwei Arten solcher Ketten:
- Die Objektkette: eine je Vorlagenvariante und eine je Dokumentkette. Sie hält die Geschichte genau eines Objekts.
- Das Journal: eines je Mandant und eines für das System. Es hält jedes nachweisrelevante Ereignis aller Objekte dieses Mandanten.
Wie die Ketten zusammenhängen
flowchart LR
subgraph O["Objektkette (eine Dokumentkette)"]
R1["Revision 1"] --> R2["Revision 2"] --> H["Legal Hold gesetzt"]
end
subgraph J["Journal des Mandanten"]
J1["template-published"] --> J2["document-stored"] --> J3["document-stored"] --> J4["document-legal-hold-set"]
end
R1 -. "Hash" .-> J2
R2 -. "Hash" .-> J3
H -. "Hash" .-> J4
J4 -. "Kopf" .-> A["Anker mit Zeitstempel"]
Jeder Journal-Eintrag trägt den Hash des Objektketten-Eintrags, über den er berichtet. Wer eine Revision ändern will, muss also ihre Objektkette und das Journal umschreiben. Und das Journal ist nach außen verankert, siehe Zeitstempel und Anker.
Was ein Hash verknüpft
Beide Ketten folgen derselben Regel:
hash_n = SHA-256( kanonisch(metadaten_n) ‖ inhalts_hash_n ‖ hash_n-1 )
- kanonisch heißt: Die Metadaten werden als JSON nach RFC 8785 geschrieben. Gleiche Daten ergeben immer dieselben Bytes, egal in welcher Reihenfolge die Felder standen.
- inhalts_hash ist bei einer Dokumentrevision der Hash der Dokument-Bytes. Im Journal ist es der Hash des gemeldeten Objektketten-Eintrags.
- hash_n-1 ist der Hash des Vorgängers. Der erste Eintrag einer Kette hat keinen und nimmt stattdessen lauter Null-Bytes.
‖heißt: hintereinander gehängt. Die beiden Hashes gehen als rohe Bytes ein, nicht als Text.
Das Verfahren steht vor jedem Hash (sha-256:…). Wer Metadaten, Inhalt und Kette hat, kann jeden Hash selbst nachrechnen, mit nichts als einem JSON-Kanonisierer und SHA-256.
Was in einen Eintrag der Dokumentkette eingeht
Metadaten eines Eintrags (kanonisiert, dann gehasht){
"chain": "5d0c…", ← die Kette
"sequence": 2, ← Position, ab 1, ohne Lücken
"type": "REVISION", ← REVISION, LEGAL_HOLD_SET, LEGAL_HOLD_RELEASED, ERASED
"revision": "7c1e…", ← die betroffene Revision
"actor": "u-3f2a…", ← wer es ausgelöst hat
"at": "2026-10-17T09:12:44.123456Z",
"reason": null, ← Grund als Schlüssel, bei Hold und Löschung
"attributes": { "number": 2, "template": "audit-report", "payloadHash": "sha-256:…", … }
}Jeder Eintrag trägt den Inhalts-Hash der Revision, um die es geht. Auch eine Löschung oder ein Legal Hold ist damit fest an genau ein Dokument gebunden.
Eine Dokumentkette kennt vier Eintragsarten:
| Art | Bedeutung |
|---|---|
REVISION | ein endgültiges Dokument wurde als nächste Revision gespeichert |
LEGAL_HOLD_SET | ein Legal Hold wurde auf die Kette gesetzt |
LEGAL_HOLD_RELEASED | der Legal Hold wurde aufgehoben |
ERASED | die Bytes einer Revision wurden gelöscht, ihr Schlüssel vernichtet |
Ob eine Revision gelöscht ist oder ein Hold gilt, speichert CRMS nicht in einem Feld daneben. Es liest den Zustand aus den Einträgen. Was dort nicht steht, gilt nicht.
Was im Journal steht
Jedes Journal ist eine eigene Kette. Ein Eintrag nennt Art, betroffenes Objekt, Benutzer und Zeit:
| Art | Betroffenes Objekt | Wann |
|---|---|---|
template-revision | template-variant:<id> | eine Vorlagen-Revision wurde eingereicht |
template-approved | template-variant:<id> | eine Freigabestufe wurde erteilt |
template-rejected | template-variant:<id> | eine Revision wurde abgelehnt |
template-published | template-variant:<id> | eine Revision wurde veröffentlicht |
template-superseded | template-variant:<id> | die veröffentlichte Revision wurde abgelöst |
template-retired | template-variant:<id> | die veröffentlichte Revision wurde ohne Nachfolger zurückgezogen |
document-stored | document-chain:<id> | ein endgültiges Dokument wurde gespeichert |
document-legal-hold-set | document-chain:<id> | ein Legal Hold wurde gesetzt |
document-legal-hold-released | document-chain:<id> | ein Legal Hold wurde aufgehoben |
document-erased | document-chain:<id> | eine Revision wurde gelöscht |
document-key-destroyed | document-key:<id> | ein Dokumentschlüssel wurde vernichtet |
document-read | document-chain:<id> | ein endgültiges Dokument wurde gelesen, nur mit CODAMAI_CRMS_AUDIT_READS=true |
Wie die Vorlagen-Einträge entstehen, steht unter Lebenszyklus einer Vorlage.
Wo die Ketten liegen
| Was | Wo |
|---|---|
| Journal eines Mandanten | in der Datenbank des Mandanten |
Journal des Systems (Schlüssel system) | in der System-Datenbank |
| Dokumentketten | beim Journal ihres Eigentümers |
Alle Tabellen werden nur ergänzt, nie geändert: Einträge kommen dazu, keiner wird überschrieben oder gelöscht. Nur der Kopf des Journals, der die letzte Nummer und den letzten Hash hält, wird fortgeschrieben. Mehr unter Datenbank.
Gleichzeitiges Schreiben
Zwei Vorgänge im selben Mandanten schreiben nacheinander ins Journal. CRMS sperrt dafür den Kopf des Journals, bis die Transaktion endet. Rollt ein Vorgang zurück, verschwindet sein Journal-Eintrag mit ihm.
| Was trifft aufeinander | Was passiert |
|---|---|
| zwei Schreiber in einem bestehenden Journal | der zweite wartet und schreibt danach |
| zwei Schreiber legen im selben Moment dasselbe neue Journal an | einer bekommt 409 journal-concurrent-change und kann es erneut versuchen |
| zwei Schreiber hängen gleichzeitig an dieselbe Dokumentkette an | einer bekommt 409 document-concurrent-change und kann es erneut versuchen |
Eine Kette verzweigt sich nie.
Was die Prüfung einer Kette findet
Die Prüfung rechnet jeden Hash vom ersten Eintrag an neu und vergleicht ihn mit dem gespeicherten.
| Manipulation | gefunden durch |
|---|---|
| ein Byte eines Eintrags geändert | die Kette selbst |
| ein Eintrag entfernt oder die Reihenfolge vertauscht | die Kette selbst |
| der letzte Eintrag des Journals entfernt | den Vergleich mit dem Kopf |
| das Journal ab einer Stelle stimmig neu berechnet, Kopf eingeschlossen | nur den Anker mit Zeitstempel |