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:
@@ -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.
|
||||||
@@ -1022,6 +1022,52 @@ OMSKRIVNING=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform
|
|||||||
"update felsokning_arenden set forsegling = 'ffff' where id = 'arende-forseglat'" 2>&1 || true)
|
"update felsokning_arenden set forsegling = 'ffff' where id = 'arende-forseglat'" 2>&1 || true)
|
||||||
kontroll "förseglingen kan inte skrivas om" "$(echo "$OMSKRIVNING" | grep -c 'kan inte ändras')" "1"
|
kontroll "förseglingen kan inte skrivas om" "$(echo "$OMSKRIVNING" | grep -c 'kan inte ändras')" "1"
|
||||||
|
|
||||||
|
# 18e. Skrivning efter avslut får inte ogiltigförklara förseglingen
|
||||||
|
#
|
||||||
|
# Offline-synk är en kärnfunktion: en annan enhet kan lämna in händelser
|
||||||
|
# EFTER att någon stängt ärendet. Förseglingen intygar loggen fram till
|
||||||
|
# avslutet — den får varken larma falskt för att livet fortsatte, eller
|
||||||
|
# tyst låtsas täcka det som kom efteråt.
|
||||||
|
curl -s -o /dev/null -X POST "$BAS/api/arenden/arende-forseglat/handelser" -H "Authorization: Bearer $TOKEN_A" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"handelser":[{"id":"sen-1","tidpunkt":"2026-08-06T12:00:00Z","anvandare":"Johan","handelse":{"typ":"observation","text":"Sent synkad observation fran en annan enhet."}}]}'
|
||||||
|
EFTER=$(curl -s "$BAS/api/arenden/arende-forseglat/kedja" -H "Authorization: Bearer $TOKEN_A")
|
||||||
|
kontroll "kedjan verifierar med efterhandelsen" "$(echo "$EFTER" | falt .ok)" "true"
|
||||||
|
kontroll "förseglingen är fortfarande giltig — den täcker sitt prefix" "$(echo "$EFTER" | falt .forsegling)" "giltig"
|
||||||
|
kontroll "men efterhandelserna redovisas" "$(echo "$EFTER" | falt .efterForsegling)" "1"
|
||||||
|
|
||||||
|
# 18f. Prefixsemantikens motvikt: sabotage INUTI prefixet fäller den
|
||||||
|
#
|
||||||
|
# Prefixfixen (K-1) får inte vara för snäll. En rad ändrad FÖRE
|
||||||
|
# förseglingspunkten ska fälla både kedjan och förseglingen — annars
|
||||||
|
# vore "täcker sitt prefix" bara ett artigare ord för "täcker ingenting".
|
||||||
|
PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning -q <<'SQL'
|
||||||
|
drop trigger handelser_append_only on felsokning_handelser;
|
||||||
|
update felsokning_handelser
|
||||||
|
set handelse = jsonb_set(handelse, '{beskrivning}', '"Manipulerad reproducering."')
|
||||||
|
where arende_id = 'arende-forseglat' and handelse->>'typ' = 'reproducering';
|
||||||
|
create trigger handelser_append_only
|
||||||
|
before update or delete on felsokning_handelser
|
||||||
|
for each row execute function forbjud_andring();
|
||||||
|
SQL
|
||||||
|
SABOT2=$(curl -s "$BAS/api/arenden/arende-forseglat/kedja" -H "Authorization: Bearer $TOKEN_A")
|
||||||
|
kontroll "sabotage i prefixet bryter kedjan" "$(echo "$SABOT2" | falt .ok)" "false"
|
||||||
|
kontroll "sabotage i prefixet fäller förseglingen" "$(echo "$SABOT2" | falt .forsegling)" "OGILTIG"
|
||||||
|
|
||||||
|
# 18g. Siffror i händelsen överlever databasens rundresa
|
||||||
|
#
|
||||||
|
# Kedjans digest räknas om ur lagrad jsonb. Tal normaliseras av Postgres
|
||||||
|
# — om normaliseringen skilde sig från JavaScripts vore varje händelse
|
||||||
|
# med ett tal ett falsklarm. Konfidensvärden är de enda talen i schemat,
|
||||||
|
# så de provas med de former som brukar gå sönder: decimaler, exponenter,
|
||||||
|
# stora tal.
|
||||||
|
curl -s -X POST "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"id":"arende-tal","nummer":90,"skapad":"2026-08-06T10:00:00Z"}' >/dev/null
|
||||||
|
curl -s -o /dev/null -X POST "$BAS/api/arenden/arende-tal/handelser" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"handelser":[{"id":"tal-1","tidpunkt":"2026-08-06T10:01:00Z","anvandare":"Anna","handelse":{"typ":"arbetsorder_skannad","falt":[{"id":"kund_namn","varde":"Test","konfidens":0.9},{"id":"fordon_regnr","varde":"ABC123","konfidens":0.55},{"id":"ao_nummer","varde":"1","konfidens":1},{"id":"fordon_vin","varde":"X","konfidens":1e-7},{"id":"fordon_marke","varde":"Y","konfidens":0.10000000000000009}]}}]}'
|
||||||
|
TAL=$(curl -s "$BAS/api/arenden/arende-tal/kedja" -H "Authorization: Bearer $TOKEN_A")
|
||||||
|
kontroll "kedjan överlever numerisk rundresa genom jsonb" "$(echo "$TAL" | falt .ok)" "true"
|
||||||
|
|
||||||
# 19. Support och felanmälan (ALVA-PROC-0050)
|
# 19. Support och felanmälan (ALVA-PROC-0050)
|
||||||
kill "$SERVER_PID" 2>/dev/null || true
|
kill "$SERVER_PID" 2>/dev/null || true
|
||||||
wait "$SERVER_PID" 2>/dev/null || true
|
wait "$SERVER_PID" 2>/dev/null || true
|
||||||
|
|||||||
@@ -776,7 +776,19 @@ paths:
|
|||||||
den är.
|
den är.
|
||||||
forsegling:
|
forsegling:
|
||||||
type: string
|
type: string
|
||||||
description: saknas · giltig · OGILTIG · kan inte prövas
|
description: >
|
||||||
|
saknas · giltig · OGILTIG · kan inte prövas.
|
||||||
|
Förseglingen täcker sitt PREFIX — loggen fram till
|
||||||
|
avslutet. Händelser som tillkommit efter (sen
|
||||||
|
offline-synk, sena kundbesked) gör den inte
|
||||||
|
ogiltig; de redovisas i `efterForsegling`.
|
||||||
|
efterForsegling:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
description: >
|
||||||
|
Kedjade händelser efter förseglingspunkten. Noll är
|
||||||
|
det normala; ett tal är inte ett fel utan ett
|
||||||
|
faktum läsaren ska se.
|
||||||
bevisar: { type: string }
|
bevisar: { type: string }
|
||||||
"401": { $ref: "#/components/responses/Fel" }
|
"401": { $ref: "#/components/responses/Fel" }
|
||||||
"404": { $ref: "#/components/responses/Fel" }
|
"404": { $ref: "#/components/responses/Fel" }
|
||||||
|
|||||||
@@ -516,22 +516,31 @@ async function skrivKedjat(arendeId, poster) {
|
|||||||
);
|
);
|
||||||
let sekvens = Number(sista.rows[0]?.sekvens ?? 0);
|
let sekvens = Number(sista.rows[0]?.sekvens ?? 0);
|
||||||
let h = sista.rows[0]?.kedjehash ?? grund(arendeId);
|
let h = sista.rows[0]?.kedjehash ?? grund(arendeId);
|
||||||
|
let skrivna = 0;
|
||||||
|
|
||||||
for (const p of poster) {
|
for (const p of poster) {
|
||||||
const digest = innehallsHash(kanoniskt(p.handelse));
|
const digest = innehallsHash(kanoniskt(p.handelse));
|
||||||
const nastaH = lank(h, { id: p.id, tidpunkt: p.tidpunkt, anvandare: p.anvandare, digest });
|
// Tidpunkten normaliseras HÄR, till exakt den form verifieringen
|
||||||
|
// räknar om ur databasen (millisekunders ISO). Alla nuvarande
|
||||||
|
// vägar går genom tillPost som redan ger den formen — men länken
|
||||||
|
// får inte vila på att det förblir sant. En kedja som bryts av ett
|
||||||
|
// tidsformat är ett falsklarm, och falsklarm dödar förtroendet
|
||||||
|
// för verifieringen (revision 3, m-1).
|
||||||
|
const tid = new Date(p.tidpunkt).toISOString();
|
||||||
|
const nastaH = lank(h, { id: p.id, tidpunkt: tid, anvandare: p.anvandare, digest });
|
||||||
const r = await db.query(
|
const r = await db.query(
|
||||||
`insert into felsokning_handelser
|
`insert into felsokning_handelser
|
||||||
(id, arende_id, tidpunkt, anvandare, handelse, klientdigest, sekvens, kedjehash)
|
(id, arende_id, tidpunkt, anvandare, handelse, klientdigest, sekvens, kedjehash)
|
||||||
values ($1, $2, $3, $4, $5, $6, $7, $8)
|
values ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
on conflict (arende_id, id) do nothing`,
|
on conflict (arende_id, id) do nothing`,
|
||||||
[p.id, arendeId, p.tidpunkt, p.anvandare, p.handelse, p.klientdigest ?? null, sekvens + 1, nastaH],
|
[p.id, arendeId, tid, p.anvandare, p.handelse, p.klientdigest ?? null, sekvens + 1, nastaH],
|
||||||
);
|
);
|
||||||
// En dubblett skrivs inte och får därför inte heller flytta kedjan:
|
// En dubblett skrivs inte och får därför inte heller flytta kedjan:
|
||||||
// länken ovan förbrukas bara när raden faktiskt sattes in.
|
// länken ovan förbrukas bara när raden faktiskt sattes in.
|
||||||
if (r.rowCount > 0) {
|
if (r.rowCount > 0) {
|
||||||
sekvens += 1;
|
sekvens += 1;
|
||||||
h = nastaH;
|
h = nastaH;
|
||||||
|
skrivna += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,7 +565,7 @@ async function skrivKedjat(arendeId, poster) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.query("commit");
|
await db.query("commit");
|
||||||
return { rot: h };
|
return { rot: h, skrivna, dubbletter: poster.length - skrivna };
|
||||||
} catch (fel) {
|
} catch (fel) {
|
||||||
await db.query("rollback").catch(() => {});
|
await db.query("rollback").catch(() => {});
|
||||||
throw fel;
|
throw fel;
|
||||||
@@ -569,7 +578,7 @@ async function grindHinder(pool, arendeId, nya, sprak = STANDARD) {
|
|||||||
const rader = await pool.query(
|
const rader = await pool.query(
|
||||||
`select h.handelse, a.metodik_id from felsokning_handelser h
|
`select h.handelse, a.metodik_id from felsokning_handelser h
|
||||||
join felsokning_arenden a on a.id = h.arende_id
|
join felsokning_arenden a on a.id = h.arende_id
|
||||||
where h.arende_id = $1 order by h.tidpunkt, h.id`,
|
where h.arende_id = $1 order by h.sekvens nulls first, h.tidpunkt, h.id`,
|
||||||
[arendeId],
|
[arendeId],
|
||||||
);
|
);
|
||||||
const handelser = [...rader.rows.map((r) => r.handelse), ...nya.map((p) => p.handelse)];
|
const handelser = [...rader.rows.map((r) => r.handelse), ...nya.map((p) => p.handelse)];
|
||||||
@@ -1288,12 +1297,12 @@ export function skapaServer() {
|
|||||||
? await pool.query(
|
? await pool.query(
|
||||||
`select id, tidpunkt, anvandare, handelse from felsokning_handelser
|
`select id, tidpunkt, anvandare, handelse from felsokning_handelser
|
||||||
where arende_id = $1 and handelse->>'typ' = any($2)
|
where arende_id = $1 and handelse->>'typ' = any($2)
|
||||||
order by tidpunkt, id`,
|
order by sekvens nulls first, tidpunkt, id`,
|
||||||
[arendeId, synliga],
|
[arendeId, synliga],
|
||||||
)
|
)
|
||||||
: await pool.query(
|
: await pool.query(
|
||||||
`select id, tidpunkt, anvandare, handelse from felsokning_handelser
|
`select id, tidpunkt, anvandare, handelse from felsokning_handelser
|
||||||
where arende_id = $1 order by tidpunkt, id`,
|
where arende_id = $1 order by sekvens nulls first, tidpunkt, id`,
|
||||||
[arendeId],
|
[arendeId],
|
||||||
);
|
);
|
||||||
return svara(res, 200, { arende: arende.rows[0], handelser: handelser.rows, niva });
|
return svara(res, 200, { arende: arende.rows[0], handelser: handelser.rows, niva });
|
||||||
@@ -2305,7 +2314,7 @@ export function skapaServer() {
|
|||||||
}
|
}
|
||||||
const rader = await pool.query(
|
const rader = await pool.query(
|
||||||
`select tidpunkt, anvandare, handelse from felsokning_handelser
|
`select tidpunkt, anvandare, handelse from felsokning_handelser
|
||||||
where arende_id = $1 order by tidpunkt, id`,
|
where arende_id = $1 order by sekvens nulls first, tidpunkt, id`,
|
||||||
[sammanfattningVag[1]],
|
[sammanfattningVag[1]],
|
||||||
);
|
);
|
||||||
const nycklar = await nycklarFor(anspr.org);
|
const nycklar = await nycklarFor(anspr.org);
|
||||||
@@ -2380,23 +2389,11 @@ export function skapaServer() {
|
|||||||
poster.push(post);
|
poster.push(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
let skrivna = 0;
|
// Kedjat via samma väg som allt annat, och räkningen kommer ur
|
||||||
let dubbletter = 0;
|
// transaktionen som gjorde jobbet — inte ur två count-frågor
|
||||||
{
|
// runtomkring, där en samtidig skrivning från någon annan
|
||||||
// Kedjat via samma väg som allt annat. Räkningen görs mot
|
// hamnade i vår siffra (revision 3, m-2).
|
||||||
// loggen efteråt — skrivKedjat äger transaktionen.
|
const { skrivna, dubbletter } = await skrivKedjat(protokollVag[1], poster);
|
||||||
const fore = await pool.query(
|
|
||||||
`select count(*)::int as n from felsokning_handelser where arende_id = $1`,
|
|
||||||
[protokollVag[1]],
|
|
||||||
);
|
|
||||||
await skrivKedjat(protokollVag[1], poster);
|
|
||||||
const efter = await pool.query(
|
|
||||||
`select count(*)::int as n from felsokning_handelser where arende_id = $1`,
|
|
||||||
[protokollVag[1]],
|
|
||||||
);
|
|
||||||
skrivna = efter.rows[0].n - fore.rows[0].n;
|
|
||||||
dubbletter = poster.length - skrivna;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delvis lyckad inläsning är inte 200. Mottagaren ska behöva
|
// Delvis lyckad inläsning är inte 200. Mottagaren ska behöva
|
||||||
// hantera att något inte kom in.
|
// hantera att något inte kom in.
|
||||||
@@ -2565,31 +2562,64 @@ export function skapaServer() {
|
|||||||
);
|
);
|
||||||
const okedjade = rader.rows.filter((r) => r.sekvens == null).length;
|
const okedjade = rader.rows.filter((r) => r.sekvens == null).length;
|
||||||
|
|
||||||
// Förseglingen prövas mot den OMRÄKNADE roten, inte den lagrade:
|
// Förseglingen täcker sitt PREFIX, inte kedjans nuvarande rot.
|
||||||
// en angripare som räknat om kedjan och roten stoppas av att
|
//
|
||||||
// HMAC:en inte går att räkna om utan nyckeln.
|
// Skillnaden är inte akademisk. Offline-synk är en kärnfunktion:
|
||||||
|
// en annan enhet kan lämna in händelser EFTER att ärendet
|
||||||
|
// stängts, och då flyttar kedjans rot förbi den förseglade.
|
||||||
|
// Revision 3 (K-1) fann att verifieringen då svarade OGILTIG —
|
||||||
|
// ett falsklarm i normal drift, och en verifiering som larmar
|
||||||
|
// falskt avfärdas snart som trasig. Det är så ett skydd dör.
|
||||||
|
//
|
||||||
|
// Rätt semantik: den förseglade roten ska vara EN LÄNK I den
|
||||||
|
// omräknade kedjan. Är den det bevisar förseglingen loggen fram
|
||||||
|
// till den punkten, och det som kom efter redovisas som
|
||||||
|
// oförseglat i stället för att smittas eller smitta. En
|
||||||
|
// angripare som räknat om kedjan stoppas fortfarande: den
|
||||||
|
// omräknade kedjan innehåller inte den gamla roten, och en ny
|
||||||
|
// försegling kan inte skapas utan nyckeln.
|
||||||
const arende = await pool.query(
|
const arende = await pool.query(
|
||||||
`select kedjerot, forsegling, forseglad from felsokning_arenden where id = $1`,
|
`select kedjerot, forsegling, forseglad from felsokning_arenden where id = $1`,
|
||||||
[kedjeVag[1]],
|
[kedjeVag[1]],
|
||||||
);
|
);
|
||||||
const a = arende.rows[0] ?? {};
|
const a = arende.rows[0] ?? {};
|
||||||
let forsegling = "saknas";
|
let forsegling = "saknas";
|
||||||
|
let efterForsegling = null;
|
||||||
if (a.forsegling) {
|
if (a.forsegling) {
|
||||||
if (!FORSEGLING_NYCKEL) forsegling = "kan inte prövas — FORSEGLING_NYCKEL saknas";
|
if (!FORSEGLING_NYCKEL) {
|
||||||
else if (
|
forsegling = "kan inte prövas — FORSEGLING_NYCKEL saknas";
|
||||||
kedja.ok &&
|
|
||||||
a.kedjerot === kedja.rot &&
|
|
||||||
provaForsegling(
|
|
||||||
FORSEGLING_NYCKEL,
|
|
||||||
kedjeVag[1],
|
|
||||||
a.kedjerot,
|
|
||||||
new Date(a.forseglad).toISOString(),
|
|
||||||
a.forsegling,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
forsegling = "giltig";
|
|
||||||
} else {
|
} else {
|
||||||
forsegling = "OGILTIG";
|
const kedjade = rader.rows.filter((r) => r.sekvens != null);
|
||||||
|
const idx = kedjade.findIndex((r) => r.kedjehash === a.kedjerot);
|
||||||
|
// Prefixet prövas för sig: ett brott EFTER förseglingspunkten
|
||||||
|
// gör inte prefixet mindre bevisat — brottet pekas ut ovan.
|
||||||
|
const prefixOk =
|
||||||
|
idx >= 0 &&
|
||||||
|
verifieraKedja(
|
||||||
|
kedjeVag[1],
|
||||||
|
kedjade.slice(0, idx + 1).map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
tidpunkt: new Date(r.tidpunkt).toISOString(),
|
||||||
|
anvandare: r.anvandare,
|
||||||
|
digest: innehallsHash(kanoniskt(r.handelse)),
|
||||||
|
kedjehash: r.kedjehash,
|
||||||
|
})),
|
||||||
|
).ok;
|
||||||
|
if (
|
||||||
|
prefixOk &&
|
||||||
|
provaForsegling(
|
||||||
|
FORSEGLING_NYCKEL,
|
||||||
|
kedjeVag[1],
|
||||||
|
a.kedjerot,
|
||||||
|
new Date(a.forseglad).toISOString(),
|
||||||
|
a.forsegling,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
forsegling = "giltig";
|
||||||
|
efterForsegling = kedjade.length - (idx + 1);
|
||||||
|
} else {
|
||||||
|
forsegling = "OGILTIG";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2604,6 +2634,10 @@ export function skapaServer() {
|
|||||||
kedjade: rader.rows.length - okedjade,
|
kedjade: rader.rows.length - okedjade,
|
||||||
okedjade,
|
okedjade,
|
||||||
forsegling,
|
forsegling,
|
||||||
|
// Hur många kedjade händelser som ligger EFTER förseglingen.
|
||||||
|
// Noll är det normala; ett tal här är inte ett fel utan ett
|
||||||
|
// faktum som läsaren ska se: sen synk, sena kundbesked.
|
||||||
|
efterForsegling,
|
||||||
bevisar:
|
bevisar:
|
||||||
"Content unchanged since receipt, in the recorded order. Nothing about the time before receipt, nothing about accuracy.",
|
"Content unchanged since receipt, in the recorded order. Nothing about the time before receipt, nothing about accuracy.",
|
||||||
});
|
});
|
||||||
@@ -2618,7 +2652,7 @@ export function skapaServer() {
|
|||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
const rader = await pool.query(
|
const rader = await pool.query(
|
||||||
`select id, tidpunkt, anvandare, handelse from felsokning_handelser
|
`select id, tidpunkt, anvandare, handelse from felsokning_handelser
|
||||||
where arende_id = $1 order by tidpunkt, id`,
|
where arende_id = $1 order by sekvens nulls first, tidpunkt, id`,
|
||||||
[handelserVag[1]],
|
[handelserVag[1]],
|
||||||
);
|
);
|
||||||
const nycklar = await nycklarFor(anspr.org);
|
const nycklar = await nycklarFor(anspr.org);
|
||||||
|
|||||||
Reference in New Issue
Block a user