# Eine StandardOS-Prüfkette verifizieren

Jeder StandardOS-Export enthält ein Array `audit_trail`. Dieses Dokument legt
genau fest, wie jeder Eintrag gehasht wird, damit Sie die Kette selbst
nachrechnen können, in jeder Programmiersprache, ohne StandardOS auszuführen
und ohne uns zu vertrauen.

Eine vollständige Implementierung in etwa hundert Zeilen JavaScript ohne
Abhängigkeiten ist neben diesem Dokument unter
https://getstandardos.com/verify-chain.mjs veröffentlicht. Sie ist zum Lesen geschrieben; wer sie lieber selbst implementiert, dem genügt dieses Dokument.

## Was die Kette garantiert

Jeder Eintrag versiegelt den Eintrag davor. Ändern Sie ein beliebiges Feld eines
historischen Eintrags, stimmt sein `row_hash` nicht mehr, und weil der
`prev_hash` des nächsten Eintrags genau dieser Hash ist, bricht auch jeder
folgende Eintrag. Das Löschen eines Eintrags bricht die Verbindung über die
Lücke. Das Anhängen eines erfundenen Eintrags setzt nur die Kenntnis des aktuellen Kopfes voraus, und das ist in Ordnung. Die Kette beweist, dass die Geschichte nicht *umgeschrieben* wurde,
nicht, dass ein bestimmtes Ereignis stattgefunden hat.

## Was sie für sich allein nicht garantiert

Die Hash-Regel ist öffentlich und ohne Schlüssel, und das ist Absicht: Eine
Regel mit Schlüssel wäre nur für den prüfbar, der den Schlüssel hält, und das
wären wir. Die Folge ist, dass jeder, der in die Datenbank schreiben kann, auch
jeden folgenden Hash neu berechnen kann, und die Kette allein zeigt das nicht.

Dafür gibt es die **Anker**. Siehe „Anker“ weiter unten. Prüfen Sie die Kette
gegen eine Ankerquittung, die Sie bereits besitzen, und der Angriff durch
Neuberechnung ist geschlossen, denn eine Quittung, die schon in Ihrem Postfach
liegt, können wir nicht mehr ändern.

## Das Urbild

Für jeden Eintrag lautet der Hash:

```
row_hash = lowercase_hex( sha256( utf8( preimage ) ) )
```

Das Urbild ist die Verkettung der folgenden Teile, **ohne Trennzeichen außer
dort, wo sie gezeigt werden**:

| # | Teil | Hinweise |
|---|------|-------|
| 1 | `prev_hash` | 64 hexadezimale Kleinbuchstaben |
| 2 | `old_data` | die Zeichenkette wie exportiert, oder `""` bei null |
| 3 | `new_data` | die Zeichenkette wie exportiert, oder `""` bei null |
| 4 | `action` | `INSERT`, `UPDATE` oder `DELETE` |
| 5 | `table_name` | z. B. `risks` |
| 6 | *(nur hash_version ≥ 2)* `actor` + `"\|"` + `record_id` | `actor` ist `""` bei null |
| 7 | *(nur hash_version ≥ 3)* `"\|"` + `principal` | `""` bei null |
| 8 | `occurred_at` | genau wie unten formatiert |

Das Urbild jeder Version ist eine strikte Präfix-Erweiterung der vorigen, ein
v1-Eintrag wird also genau so gehasht wie vor v2. **Wenden Sie die Regel an, die
die eigene `hash_version` der Zeile nennt.** Die Geschichte wird nie neu
gehasht, wenn sich die Regel ändert; deshalb gibt es die Spalte.

### Formatierung von `occurred_at`

Das ist der Teil, den viele falsch machen. Der Zeitstempel wird **in UTC**
gerendert als:

```
YYYY-MM-DDTHH:MM:SS.ffffffZ
```

Genau sechs Nachkommastellen der Sekunde, immer vorhanden, immer mit Nullen aufgefüllt, zum Beispiel `2026-07-30T17:41:39.558458Z`. Postgres erzeugt das mit
`to_char(occurred_at at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`.

Beachten Sie, dass `Date.toISOString()` in JavaScript **drei** Nachkommastellen
ausgibt, nicht sechs, und deshalb den falschen Hash liefert. Das exportierte
`occurred_at` ist eine ISO-Zeichenkette mit Mikrosekunden; verwenden Sie ihre
Ziffern direkt, statt sie durch einen Datumstyp zu schicken, der sie nicht
halten kann.

### `old_data` / `new_data`

Absichtlich als **Zeichenketten** exportiert, nicht als Objekte. Die gehashten
Bytes sind die kanonische `jsonb`-Darstellung von Postgres: Objektschlüssel
sortiert nach Länge, dann byteweise, ein Leerzeichen nach jedem `:` und `,`.
`JSON.stringify` reproduziert das nicht. Hashen Sie die Zeichenkette genau wie
exportiert. Parsen Sie sie nur, wenn Sie sie lesen wollen.

Ein `INSERT` hat kein `old_data`; ein `DELETE` hat kein `new_data`. Beide sind
im Export `null` und tragen die leere Zeichenkette zum Urbild bei.

## Die Kette prüfen

1. Sortieren Sie die Einträge nach `id` aufsteigend, innerhalb einer `org_id`.
2. Der `prev_hash` des ersten Eintrags muss dem **Genesis-Seed** entsprechen:
   `sha256(utf8(org_id))` als hexadezimale Kleinbuchstaben, wobei `org_id` die
   UUID in ihrer kanonischen Form mit Bindestrichen und Kleinbuchstaben ist.
3. Für jeden Eintrag: `row_hash` aus dem Urbild neu berechnen und vergleichen;
   dann bestätigen, dass der `prev_hash` des nächsten Eintrags dem `row_hash`
   dieses Eintrags entspricht.

Schlägt Schritt 2 fehl, wurden Einträge am Anfang entfernt. Schlägt Schritt 3
bei Eintrag *n* fehl, ist die Kette für die ersten *n−1* Einträge intakt und
etwas bei *n* stimmt nicht mit dem überein, was aufgezeichnet wurde.

## Anker

Jeden Tag zeichnet StandardOS den Kopf der Kette jeder Organisation auf und
schickt dieser Organisation per E-Mail eine Quittung mit dem Kopf-Hash, der
Anzahl der Einträge und dem Datum. Diese Quittungen sind der unabhängige Teil:
Sie liegen in Ihrem Postfach, nicht in unserer Datenbank.

Nehmen Sie dazu den Kopf-Hash aus einer Quittung, die älter ist als der
Zeitraum, um den es Ihnen geht, suchen Sie den Eintrag mit diesem `row_hash` in
Ihrem Export und prüfen Sie die Kette bis zu diesem Punkt. Eine Neuberechnung
nach dem Versand der Quittung kann diesen Kopf-Hash nicht noch einmal erzeugen.

Bewahren Sie die Quittungen auf. Sie sind der Grund, warum „selbst wir können es
nicht unbemerkt ändern“ eine Aussage über Mathematik ist und nicht über unsere
guten Absichten.

### Was ein Anker nicht abdeckt

Ein Anker um 03:40 sagt nichts über Ereignisse, die um 09:00 geschrieben
wurden. Zwischen zwei Ankerläufen gibt es ein Fenster, in dem ein Ereignis
geschrieben und wieder entfernt werden kann, ohne dass etwas dem widerspricht:
Die Hash-Kette bemerkt es nicht, weil das Löschen der neuesten Einträge keine
Verbindung bricht, und kein Anker sie je abgedeckt hat.

Zwei Dinge verengen dieses Fenster, ohne es zu schließen. Jeder nächtliche Lauf
prüft, dass der *vorige* Anker noch gilt, das heißt, dass das aufgezeichnete
Ereignis mit demselben Hash noch vorhanden ist und dass die Kette nicht unter
die verankerte Anzahl geschrumpft ist, und zeichnet andernfalls einen Alarm auf.
Alles, was einen Anker überdauert, ist von da an geschützt. Und der tiefe
Durchlauf leitet jede Kette täglich von der Genesis her neu ab, was Inhalte
erkennt, die ohne Neuberechnung geändert wurden.

Unabgedeckt bleibt ein Ankerintervall, für Ereignisse, die innerhalb davon
erzeugt und wieder vernichtet wurden. Wir schreiben das lieber auf, als einen
Leser etwas anderes annehmen zu lassen.

### Was die Hash-Regel abdeckt, je Version

Die Regel wurde zweimal erweitert, und jede Zeile hält fest, welche Version sie
geschrieben hat.

| Version | Versiegelt |
|---|---|
| 1 | Inhalt, Aktion, Tabelle, Zeitstempel und die Verbindung zum vorigen Eintrag |
| 2 | das Obige, plus `actor` und `record_id` |
| 3 | das Obige, plus `principal` |

Die Geschichte wird nie neu gehasht, wenn sich die Regel ändert. Ein
Nur-Anhängen-Protokoll so umzuschreiben, dass es unter einer neuen Regel
verifiziert, ist genau die Operation, die das Protokoll erkennbar machen soll.
Die Folge ist, dass in einem v1- oder v2-Eintrag die Zuordnungsspalten
*aufgezeichnet, aber nicht versiegelt* sind: Sie können geändert werden, ohne
die Verifikation zu brechen. Der Export markiert das je Zeile
(`principal_attested`, `actor_attested`), und `verify-chain.mjs` gibt aus, wie
viele Einträge betroffen sind. Behandeln Sie eine unbestätigte Zuordnung als
Behauptung, nicht als Beweis, wer gehandelt hat. Inhalt, Reihenfolge und
Zeitpunkt jedes Eintrags werden unabhängig von der Version verifiziert.

## Testvektoren

Wenn Sie die obige Regel implementiert haben, prüfen Sie sie gegen diese
Vektoren, bevor Sie Ihrer Implementierung bei einem echten Export vertrauen.
Jeder Eintrag ist eine vollständige Prüfpfad-Zeile mit dem `row_hash`, den sie
erzeugen muss. Sie decken alle drei Hash-Versionen ab, sowohl leeres als auch
gefülltes `old_data`/`new_data`, und einen Sekundenbruchteil, der nicht schon
sechs Ziffern hat.

Das ist keine Dekoration. Unsere CI berechnet jeden dieser Vektoren bei jedem
Push mit dem veröffentlichten Verifizierer neu, sodass eine Änderung der
Hash-Regel, die dieses Dokument nicht aktualisiert, unseren Build scheitern
lässt und nicht Ihr Audit.

```json
[
  {
    "hash_version": 1,
    "prev_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "old_data": null,
    "new_data": "{\"id\":1}",
    "action": "INSERT",
    "table_name": "risks",
    "actor": null,
    "record_id": null,
    "principal": null,
    "occurred_at": "2026-07-30T17:41:39.558458Z",
    "row_hash": "62586c3fab919870dc2a5eec406fde33ddc31f73b92d5d851acbf3e822b15704"
  },
  {
    "hash_version": 2,
    "prev_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
    "old_data": "{\"id\":1}",
    "new_data": "{\"id\":2}",
    "action": "UPDATE",
    "table_name": "soa_entries",
    "actor": "11111111-1111-1111-1111-111111111111",
    "record_id": "22222222-2222-2222-2222-222222222222",
    "principal": null,
    "occurred_at": "2026-07-30T17:41:39.5Z",
    "row_hash": "6aad0bb2c1edf6053b297d5a38b88bcc9146fcfb6caa4e0f47dd009bcda60674"
  },
  {
    "hash_version": 3,
    "prev_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
    "old_data": "{\"id\":3}",
    "new_data": null,
    "action": "DELETE",
    "table_name": "documents",
    "actor": null,
    "record_id": "33333333-3333-3333-3333-333333333333",
    "principal": "job:soa-justification-source",
    "occurred_at": "2026-08-03T09:00:00Z",
    "row_hash": "d895d6280468e1a0600034f15dc3a884a123333a5b73a2df7cf8e470c6cf2fe4"
  }
]
```

## Eine Abweichung melden

Schlägt eine Verifikation fehl und Sie halten die Daten für echt, schreiben Sie
uns an security@getstandardos.com mit der `id` des Eintrags und dem, was Sie
berechnet haben. Eine Kette, die nicht verifiziert, ist entweder Manipulation
oder ein Fehler in unserem Hashing, und wir wollen so dringend wie Sie wissen,
welches von beiden.
