Revision 3: härdningen granskad — fyra fynd i en dag gammal kod, alla åtgärdade

Revisionens föremål är hashkedjan, förseglingen och säkerhetstaket från
igår. Skälet att granska det nyaste först: det farligaste ögonblicket för
en skyddsmekanism är veckan efter att den byggts, när testerna är skrivna
av samma person som skrev mekanismen och provar samma antaganden.

K-1, KRITISK. Förseglingen larmade falskt i normal drift. Offline-synk
kan lämna in händelser EFTER att ärendet stängts — en sen bild, ett sent
kundbesked — och verifieringen jämförde förseglingens rot med kedjans
NUVARANDE rot. Varje legitim efterhändelse gav OGILTIG. Reproducerad i
integrationsmiljön innan den kallades fynd. En verifiering som larmar
falskt avfärdas snart som trasig, och därefter avfärdas även de äkta
larmen — falsklarmet är inte en mindre bugg i ett skydd, det är det som
dödar skyddet. Åtgärd: förseglingen täcker sitt PREFIX. Den förseglade
roten ska vara en länk i den omräknade kedjan; det som kom efter
redovisas öppet i `efterForsegling` i stället för att smittas eller
smitta. Motvikten är prövad: sabotage INUTI prefixet fäller fortfarande
både kedjan och förseglingen — annars vore "täcker sitt prefix" bara ett
artigare ord för "täcker ingenting".

m-1. Kedjelänken vilade på ett tidsformat som garanterades tre filer
bort. Inte utlösbart i dag — alla vägar går genom tillPost — men första
nya skrivväg utan millisekunder hade gett en kedja som aldrig verifierar.
Normaliseringen bor nu i skrivKedjat, i samma uttryck som länken och
databasraden får den ur.

m-2. Protokollvägens skrivna/dubbletter räknades med två count(*) runt
anropet; en samtidig skrivning från någon annan hamnade i vår siffra.
skrivKedjat returnerar nu räkningen ur transaktionen som gjorde jobbet.

m-3. Läsordning och kedjeordning var två ordningar. Kedjan ordnas av
sekvens; GET, grinden och delningsvyn sorterade på tidpunkt och id — inom
en batch kunde id:ts bokstavsordning avgöra vilken händelse som var
"senast". Alla läsvägar sorterar nu i kedjeordning.

Granskat utan anmärkning, bokfört så nästa revision vet att "hittade
inget" betyder "letade": numerisk rundresa genom jsonb (provad med 1e-7
och 0.10000000000000009, inte antagen), dubbletter flyttar inte kedjan,
krypto-shredding mot kedjan, signaturens serverägdhet, takets monotoni.

Mönstret i alla fyra fynd är detsamma och står i rapporten: testerna som
skrevs med mekanismen provade manipulation, som är det man tänker på när
man bygger ett skydd. Det som gick sönder var normal drift — sen synk,
samtidiga grannar, en batch i samma millisekund. Skydd fallerar oftare
genom att larma falskt än genom att missa angrepp.

766 tester, 206 integrationskontroller, genomgång 4/4, portalspärr,
typkontroll, lint och artefaktmätning gröna. API-specen dokumenterar
prefixsemantiken och efterForsegling. docs/QUALITY-AUDIT-3.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-06 20:03:43 +00:00
parent 1ba5aaaefa
commit 0941e53c50
4 changed files with 270 additions and 43 deletions
+135
View File
@@ -0,0 +1,135 @@
# Kvalitetsrevision · Revision 3
**ALVA 3.3 · 2026-08-06 · ALVA-DOC-0006**
Revisionens föremål är **härdningen själv**: hashkedjan, förseglingen och
säkerhetstaket som infördes efter panelgranskningen, plus deras sömmar mot
resten av systemet. Skälet att revidera det nyaste först är erfarenheten
från revision 1 och 2: det farligaste ögonblicket för en skyddsmekanism är
veckan efter att den byggdes, när alla tester är skrivna av samma person
som skrev mekanismen och provar samma antaganden.
Metoden är densamma som tidigare: varje misstanke reproduceras innan den
kallas fynd, varje fynd åtgärdas eller bokförs, och varje åtgärd
mutationsprövas eller motviktsprövas. Ett fynd utan repro är en åsikt.
---
## K-1 · KRITISK — Förseglingen larmade falskt i normal drift
**Fyndet.** Offline-synk är en kärnfunktion: en annan enhet kan lämna in
händelser *efter* att någon stängt ärendet — en sen bild, ett sent
kundbesked. Verifieringen jämförde förseglingens rot med kedjans
*nuvarande* rot. Varje legitim efterhändelse flyttade kedjan förbi den
förseglade punkten, och förseglingen rapporterades **OGILTIG**.
**Reproducerad** i integrationsmiljön: ett komplett ärende avslutades och
förseglades (`giltig`), en observation synkades in efteråt, och samma
anrop svarade `OGILTIG`.
**Varför det är kritiskt, fast inget manipulerats.** En verifiering som
larmar falskt i normal drift avfärdas snart som trasig — och därefter
avfärdas även de äkta larmen. Falsklarmet är inte en mindre bugg i ett
skydd; det är det som dödar skyddet.
**Åtgärden.** Förseglingen täcker sitt **prefix**. Den förseglade roten
ska vara *en länk i* den omräknade kedjan; är den det bevisar
förseglingen loggen fram till avslutet, och det som kom efter redovisas
öppet i fältet `efterForsegling` i stället för att smittas eller smitta.
Prefixet prövas separat, så ett kedjebrott *efter* förseglingspunkten
inte heller det fäller ett bevisat prefix — brottet pekas ut för sig.
**Motvikten** — prövad, inte antagen: en rad ändrad som databasägare
*inuti* prefixet fäller både kedjan och förseglingen. Utan det testet
hade "täcker sitt prefix" kunnat vara ett artigare ord för "täcker
ingenting".
**Kvarstående begränsning, avsiktlig:** efterhändelser förseglas inte om
— triggern tillåter inte en andra försegling, och en försegling som kan
skrivas om är ingen försegling. Att `efterForsegling` är synlig är
hanteringen: läsaren ser exakt hur långt beviset når.
---
## m-1 — Kedjelänken vilade på ett tidsformat
**Fyndet.** Länken räknas över tidpunkten som sträng. Vid skrivning
användes strängen som kom in; vid verifiering den som kom *ur databasen*
(`toISOString()`, millisekunder). Alla nuvarande vägar går genom
`tillPost`, som redan producerar exakt den formen — så felet var inte
utlösbart i dag. Men garantin låg i en konvention tre filer bort, och
den första nya skrivvägen som skickade `"…T10:00:00Z"` utan millisekunder
hade gett en kedja som aldrig verifierar. Ett falsklarm igen — se K-1
för varför det är den farligaste sortens fel här.
**Åtgärden.** `skrivKedjat` normaliserar tidpunkten själv, i samma
uttryck som både länken och databasraden får den ur. Garantin bor nu där
den används.
## m-2 — Protokollvägens räkning kunde störas av grannen
**Fyndet.** `skrivna`/`dubbletter` räknades som skillnaden mellan två
`count(*)` runt anropet. En samtidig skrivning från någon annan — sen
synk, kundbesked — hamnade i vår siffra. Fel räkning är inte kosmetik i
ett 207-svar vars poäng är att mottagaren ska veta vad som inte kom in.
**Åtgärden.** `skrivKedjat` äger transaktionen och returnerar nu
räkningen ur den: `{rot, skrivna, dubbletter}`.
## m-3 — Läsordning och kedjeordning var två ordningar
**Fyndet.** Kedjan ordnas av `sekvens` (insättningsordning). GET,
grinden, sammanfattningen och delningsvyn sorterade på `tidpunkt, id`.
Inom en batch kan två händelser dela millisekund, och då avgjorde id:ts
*bokstavsordning* vilken som var "senast" — grindens `.at(-1)` kunde läsa
en annan ordning än den loggen skrevs i. Latent (batchar med två
händelser av samma typ i samma millisekund är sällsynta), men två
sanningar om ordning i ett system vars bevis *är* ordningen är en för
mycket.
**Åtgärden.** Alla läsvägar sorterar `sekvens nulls first, tidpunkt, id`
— kedjeordningen, med tidsordning som arv för rader från före kedjan.
---
## Granskat utan anmärkning
Bokförs för att nästa revision ska veta vad som redan är prövat — och
för att "hittade inget" ska betyda "letade", inte "antog".
| Område | Hur det prövades |
|---|---|
| Numerisk rundresa genom jsonb | Kedjans digest räknas om ur lagrad jsonb, och Postgres normaliserar tal. Provat med de former som brukar gå sönder: `0.55`, `1e-7`, `0.10000000000000009`. Kedjan verifierar — normaliseringarna är samstämmiga eftersom allt passerat JS `JSON.parse` vid intaget. Integrationskontroll, inte antagande. |
| Sabotage som databasägare | Trigger släppt, rad ändrad, trigger återskapad — brottet pekas ut med radens id. Provat både utanför (18c) och inuti (18f) det förseglade prefixet. |
| Dubbletter och kedjan | En omsänd händelse skrivs inte och flyttar inte kedjan — roten är identisk före och efter omsändningen. |
| Samtidighet i kedjan | Radlås per ärende (`select … for update`) serialiserar batchar; en fork hade sett ut som manipulation utan att vara det. Konstruktionsgranskad; lasttest återstår (känt sedan panelen, §5.2). |
| Krypto-shredding mot kedjan | Radering förstör nycklar, inte rader. Digest tas över den lagrade (krypterade) formen, så kedjan verifierar även efter en radering. |
| Signaturen | `arende_avslutat.signatur` skrivs ur verifierad token; klientens värde ignoreras. Mutationsprövad. |
| Säkerhetstakets monotoni | Taket beräknas ur händelser som bara kan tillkomma, och varje villkor kan bara slå om till sant — taket kan aldrig sjunka under ett redan valt värde. Ett valt värde förblir därmed alltid inom tak. |
| Gallring mot förseglingskolumnerna | Gallringen raderar hela ärenden; förseglingen följer raden. Ingen särbehandling krävs. |
## Kända och accepterade
| | Beslut |
|---|---|
| Två samtidiga avslut kan båda passera grinden | Grindprövning och skrivning är inte atomära. Följden är två avslutshändelser, varav den första förseglar. Ofarligt för beviset (allt är kedjat), förvirrande i loggen. Accepterat tills verkliga data visar att det inträffar. |
| Extern förankring saknas | Förseglingen är serverns egen nyckel. RFC 3161 kvarstår från panelens lista — utan den är serverns klocka vår klocka. |
| Klienthash av foto vid upptagning | Kvarstår från panelens lista. Kedjan förseglar innehållet från mottagandet; upptagningsögonblicket är fortfarande obevisat. |
---
## Sammanfattning
Ett kritiskt fynd, tre mindre — **alla fyra i kod som var en dag gammal
och redan hade gröna tester.** Det är inte ett argument mot härdningen;
det är argumentet för revisionen. Testerna som skrevs med mekanismen
provade manipulation, som är det man tänker på när man bygger ett skydd.
Det som gick sönder var i stället normal drift: sen synk, samtidiga
grannar, en batch i samma millisekund. Skydd fallerar oftare genom att
larma falskt än genom att missa angrepp, och tre av fyra fynd var just
falsklarmsrisker.
Efter åtgärd: 766 enhetstester, 206 integrationskontroller mot riktig
Postgres — varav sabotage som databasägare i två varianter, numerisk
rundresa och skrivning efter avslut — genomgång 4/4, portalspärr,
typkontroll, lint och artefaktmätning gröna.