0941e53c50
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
136 lines
7.6 KiB
Markdown
136 lines
7.6 KiB
Markdown
# 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.
|