Specifikation

Sådan verificerer du en StandardOS-revisionskæde

Enhver StandardOS-eksport indeholder et array audit_trail. Dette dokument fastlægger præcis, hvordan hver post hashes, så du selv kan genberegne kæden, i et hvilket som helst sprog, uden at køre StandardOS og uden at stole på os.

En komplet implementering på omkring hundrede linjer JavaScript uden afhængigheder er udgivet sammen med dette dokument på https://getstandardos.com/verify-chain.mjs. Den er skrevet for at blive læst, men vil du hellere implementere den selv, er dette dokument nok.

Hvad kæden garanterer

Hver post forsegler posten før den. Ændr et vilkårligt felt i en vilkårlig historisk post, og dens row_hash passer ikke længere, og fordi den næste posts prev_hash er netop den hash, bryder alle efterfølgende poster også. Sletning af en post bryder leddet hen over hullet. At tilføje en opdigtet post kræver kun kendskab til det aktuelle hoved, og det er i orden. Kæden beviser, at historien ikke er omskrevet, ikke at en bestemt hændelse har fundet sted.

Hvad den ikke garanterer alene

Hashreglen er offentlig og uden nøgle, og det er bevidst: en regel med nøgle kunne kun verificeres af den, der har nøglen, og det ville være os. Følgen er, at enhver, der kan skrive til databasen, også kan genberegne alle efterfølgende hashes, og kæden alene viser det ikke.

Det er det, ankrene er til. Se "Ankre" nedenfor. Verificér kæden mod en ankerkvittering, du allerede har, og genberegningsangrebet er lukket, fordi vi ikke kan ændre en kvittering, der allerede ligger i din indbakke.

Preimage

For hver post er hashen:

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

Preimage er sammenkædningen af følgende dele, uden skilletegn undtagen hvor det er vist:

# Del Noter
1 prev_hash 64 hexadecimale tegn med små bogstaver
2 old_data strengen som eksporteret, eller "" ved null
3 new_data strengen som eksporteret, eller "" ved null
4 action INSERT, UPDATE eller DELETE
5 table_name f.eks. risks
6 (kun hash_version ≥ 2) actor + "|" + record_id actor er "" ved null
7 (kun hash_version ≥ 3) "|" + principal "" ved null
8 occurred_at formateret præcis som nedenfor

Hver versions preimage er en streng præfiksudvidelse af den forrige, så en v1-post hashes præcis, som den blev, før v2 fandtes. Anvend den regel, som rækkens egen hash_version nævner. Historien hashes aldrig om, når reglen ændres; det er derfor, kolonnen findes.

Formatering af occurred_at

Det er den del, folk får galt. Tidsstemplet gengives i UTC som:

YYYY-MM-DDTHH:MM:SS.ffffffZ

Præcis seks decimaler på sekundet, altid til stede, altid udfyldt med nuller, for eksempel 2026-07-30T17:41:39.558458Z. Postgres laver det med to_char(occurred_at at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"').

Bemærk, at JavaScripts Date.toISOString() giver tre decimaler, ikke seks, og derfor giver den forkerte hash. Det eksporterede occurred_at er en ISO-streng med mikrosekunder; brug dens cifre direkte i stedet for at sende dem gennem en datotype, der ikke kan rumme dem.

old_data / new_data

Eksporteres bevidst som strenge, ikke objekter. De bytes, der hashes, er Postgres' kanoniske jsonb-gengivelse: objektnøgler sorteret efter længde og derefter bytevis, ét mellemrum efter hvert : og ,. JSON.stringify gengiver ikke det. Hash strengen præcis som eksporteret. Parse den kun, hvis du vil læse den.

Et INSERT har ingen old_data; et DELETE har ingen new_data. Begge er null i eksporten og bidrager med den tomme streng til preimage.

Kontrol af kæden

  1. Sortér posterne efter id stigende inden for ét org_id.
  2. Den første posts prev_hash skal være lig med genesis-frøet: sha256(utf8(org_id)) som hexadecimal med små bogstaver, hvor org_id er UUID'et i sin kanoniske form med bindestreger og små bogstaver.
  3. For hver post: genberegn row_hash fra preimage og sammenlign; bekræft derefter, at den næste posts prev_hash er lig med denne posts row_hash.

Fejler trin 2, er der fjernet poster fra begyndelsen. Fejler trin 3 ved post n, er kæden intakt for de første n−1 poster, og noget ved n stemmer ikke med det, der blev registreret.

Ankre

Hver dag registrerer StandardOS hovedet af hver organisations kæde og sender organisationen en kvittering på mail med hovedhashen, antallet af poster og datoen. De kvitteringer er den uafhængige del: de ligger i din indbakke, ikke i vores database.

For at bruge en tager du hovedhashen fra en kvittering, der er ældre end den periode, du interesserer dig for, finder posten med den row_hash i din eksport og verificerer kæden op til det punkt. En genberegning udført efter kvitteringens afsendelse kan ikke frembringe den hovedhash igen.

Gem kvitteringerne. De er grunden til, at "selv vi kan ikke ændre det uopdaget" er et udsagn om matematik og ikke om vores gode hensigter.

Hvad et anker ikke dækker

Et anker taget kl. 03:40 siger intet om hændelser skrevet kl. 09:00. Mellem to ankerkørsler er der et vindue, hvor en hændelse kan skrives og fjernes igen, uden at noget modsiger det: hashkæden opdager det ikke, fordi sletning af de nyeste poster ikke bryder noget led, og intet anker nogensinde dækkede dem.

To ting indsnævrer vinduet uden at lukke det. Hver natlig kørsel kontrollerer, at det forrige anker stadig holder, altså at den registrerede hændelse stadig findes med samme hash, og at kæden ikke er skrumpet under det forankrede antal, og registrerer en alarm, hvis ikke. Så alt, hvad der overlever ét anker, er beskyttet fra da af. Og den dybe gennemgang udleder hver kæde fra genesis dagligt, hvilket fanger indhold, der er ændret uden genberegning.

Det, der forbliver udækket, er ét ankerinterval, for hændelser skabt og tilintetgjort inden for det. Vi vil hellere skrive det ned end lade en læser antage noget andet.

Hvad hashreglen dækker, pr. version

Reglen er udvidet to gange, og hver række registrerer, hvilken version der skrev den.

Version Forsegler
1 indhold, handling, tabel, tidsstempel og leddet til den forrige post
2 ovenstående plus actor og record_id
3 ovenstående plus principal

Historien hashes aldrig om, når reglen ændres. At omskrive en log, der kun kan tilføjes til, så den verificerer under en ny regel, er præcis den operation, loggen findes for at gøre opdagelig. Følgen er, at på en v1- eller v2-post er tilskrivningskolonnerne registreret, men ikke forseglet: de kan ændres uden at bryde verifikationen. Eksporten markerer det pr. række (principal_attested, actor_attested), og verify-chain.mjs udskriver, hvor mange poster det gælder. Behandl en uattesteret tilskrivning som en påstand, ikke som bevis for, hvem der handlede. Indhold, rækkefølge og tidspunkt for hver post verificeres uanset version.

Testvektorer

Har du implementeret reglen ovenfor, så kontrollér den mod disse, før du stoler på din implementering på en rigtig eksport. Hver post er en komplet revisionssporsrække og den row_hash, den skal give. De dækker alle tre hashversioner, både tomme og udfyldte old_data/new_data, og en sekundbrøkdel, der ikke allerede har seks cifre.

Det er ikke pynt. Vores CI genberegner hver eneste af dem ved hvert push med den udgivne verifikator, så en ændring af hashreglen, der glemte at opdatere dette dokument, får vores build til at fejle i stedet for din revision.

[
  {
    "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"
  }
]

Sådan melder du en uoverensstemmelse

Fejler en verifikation, og mener du, at dataene er ægte, så skriv til os på security@getstandardos.com med postens id og det, du beregnede. En kæde, der ikke verificerer, er enten manipulation eller en fejl i vores hashing, og vi vil lige så gerne som du vide hvilken.

Denne side på:EnglishDeutschFrançaisNederlandsEspañol