security: Add proper authentication, RBAC, and tenant isolation
- Add password hashing with bcrypt - Add AuthService with proper login - Add password strength validation - Add RBAC middleware (AdminOnly, ManagerOrAdmin) - Add tenant isolation middleware - Update CRM handler with tenant filtering - Add JWT fallback for development mode - Add user context helpers - Build successful
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
# quiXzoom — Evidence Graph & Verified Claim
|
||||
*Beslutad: 2026-06-17. Ägare: Erik Svensson.*
|
||||
*Bygger på: QUIXZOOM_VISION.md, QUIXZOOM_VERIFIED_REALITY.md, QUIXZOOM_AVO_MASTERPROMPT_V2.md*
|
||||
|
||||
---
|
||||
|
||||
## Enheten är utsagan — inte bilden, inte evidenspaketet
|
||||
|
||||
Det ni säljer, lagrar, fakturerar och kan bli stämda över är **påståendet**:
|
||||
|
||||
> "Livbojen vid kaj 14B existerade och var i korrekt skick 2026-06-17 kl 12:22 UTC."
|
||||
|
||||
Det är en `verified_claim`. Allt annat — frames, OCR, gyrodata, liveness — är evidens *för* utsagan.
|
||||
|
||||
---
|
||||
|
||||
## Claim-schemat
|
||||
|
||||
```json
|
||||
{
|
||||
"claim_id": "clm_9f3a8c1b",
|
||||
"claim_type": "object_existence_and_condition",
|
||||
"claim_text": "Livbojen vid kaj 14B existerar och är i korrekt skick.",
|
||||
"control_point": "lifebuoy_kaj14b",
|
||||
"knowledge_pack_id": "maritime_safety_v2",
|
||||
|
||||
"trust_threshold": 0.92,
|
||||
"trust_achieved": 0.961,
|
||||
"decision": "verified",
|
||||
|
||||
"evidence_bundle_id": "evb_7d2f1a9e",
|
||||
"evidence_bundle_hash": "sha256:a3f8b2...",
|
||||
|
||||
"zoomer_id": "z_9981",
|
||||
"device_id": "dev_ios_xyz",
|
||||
"session_id": "sess_abc123",
|
||||
|
||||
"geo": { "lat": 59.334, "lng": 18.063, "accuracy_m": 4 },
|
||||
"timestamp_start": "2026-06-17T12:20:41Z",
|
||||
"timestamp_decision": "2026-06-17T12:22:04Z",
|
||||
|
||||
"signature": "ed25519:9f3a...",
|
||||
"immutable": true
|
||||
}
|
||||
```
|
||||
|
||||
`trust_threshold` ägs av kunskapspaketet — per claim-typ, aldrig globalt.
|
||||
`decision` är append-only. Korrektion sker som ny claim som refererar bakåt.
|
||||
|
||||
---
|
||||
|
||||
## Sufficiency-funktionen — förtroende-ackumulator
|
||||
|
||||
Systemet samlar evidens tills `trust_achieved ≥ trust_threshold`. Sedan stannar det.
|
||||
|
||||
```
|
||||
trust_achieved = Σ (weight_i × confidence_i × liveness_factor)
|
||||
```
|
||||
|
||||
Varje evidenselement bidrar med viktad konfidens:
|
||||
|
||||
| Element | Vikt (example) | Bidrar till |
|
||||
|---------|---------------|-------------|
|
||||
| Visual coverage frame | 0.15 | Objekt existerar |
|
||||
| OCR serial match | 0.25 | Objekt identifierat |
|
||||
| Liveness Score >90 | 0.20 | Observation äkta |
|
||||
| GPS match ±15m | 0.15 | Objekt på rätt plats |
|
||||
| App Attest valid | 0.10 | Enhet omodifierad |
|
||||
| Challenge passed (per st) | 0.08 | Anti-spoofing |
|
||||
| Reference match | 0.07 | Korrekt skick |
|
||||
|
||||
**Vikterna och trösklarna ägs av kunskapspaketet**, inte av AVO-koden.
|
||||
|
||||
### Konsekvensen
|
||||
|
||||
Ibland räcker 4 frames och 2 challenges. Ibland krävs 40 frames och 5 challenges.
|
||||
Systemet vet när det är klart — Zoomern vet det aldrig förrän det är grönt.
|
||||
|
||||
### Sufficiency kan misslyckas
|
||||
|
||||
Om systemet inte når tröskeln inom:
|
||||
- Max scan-tid (konfigurerbart, default 120s)
|
||||
- Max challenges (default 5)
|
||||
- Max frames (default 600)
|
||||
|
||||
→ `decision: insufficient_evidence` → eskalera till manuell granskning.
|
||||
|
||||
**Aldrig:** tvinga fram ett svagt godkännande för att undvika eskalering.
|
||||
|
||||
```json
|
||||
{
|
||||
"claim_id": "clm_4b2e9d7a",
|
||||
"decision": "insufficient_evidence",
|
||||
"trust_achieved": 0.71,
|
||||
"trust_threshold": 0.92,
|
||||
"limiting_factor": "liveness_score_too_low",
|
||||
"escalation": "manual_review",
|
||||
"photographer_directive": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Evidence Bundle — schemat
|
||||
|
||||
Det fullständiga, signerade paketet som claim:en refererar till:
|
||||
|
||||
```json
|
||||
{
|
||||
"bundle_id": "evb_7d2f1a9e",
|
||||
"claim_id": "clm_9f3a8c1b",
|
||||
"created_at": "2026-06-17T12:22:04Z",
|
||||
|
||||
"liveness": {
|
||||
"score": 97,
|
||||
"signals": {
|
||||
"gyro_natural": true,
|
||||
"parallax_verified": true,
|
||||
"focus_changes": 4,
|
||||
"gps_match": true,
|
||||
"app_attest": true,
|
||||
"stream_continuous": true,
|
||||
"challenges_issued": 3,
|
||||
"challenges_passed": 3
|
||||
}
|
||||
},
|
||||
|
||||
"evidence_elements": [
|
||||
{
|
||||
"element_id": "e1",
|
||||
"type": "visual_frame",
|
||||
"frame_index": 142,
|
||||
"timestamp_ms": 3820,
|
||||
"confidence": 0.97,
|
||||
"weight": 0.15,
|
||||
"contribution": 0.1455,
|
||||
"hash": "sha256:f1a2b3..."
|
||||
},
|
||||
{
|
||||
"element_id": "e2",
|
||||
"type": "ocr_extraction",
|
||||
"frame_index": 198,
|
||||
"timestamp_ms": 5180,
|
||||
"confidence": 0.94,
|
||||
"weight": 0.25,
|
||||
"contribution": 0.2350,
|
||||
"extracted_value": "SE-4821-9938-01",
|
||||
"hash": "sha256:c4d5e6..."
|
||||
},
|
||||
{
|
||||
"element_id": "e3",
|
||||
"type": "challenge_response",
|
||||
"challenge_issued_at_ms": 6200,
|
||||
"challenge_directive": "Gå ett steg bakåt",
|
||||
"challenge_class": "liveness",
|
||||
"response_detected_at_ms": 8100,
|
||||
"confidence": 0.96,
|
||||
"weight": 0.08,
|
||||
"contribution": 0.0768
|
||||
}
|
||||
],
|
||||
|
||||
"trust_accumulation": [
|
||||
{ "after_element": "e1", "trust_so_far": 0.1455 },
|
||||
{ "after_element": "e2", "trust_so_far": 0.3805 },
|
||||
{ "after_element": "e3", "trust_so_far": 0.4573 }
|
||||
],
|
||||
|
||||
"bundle_hash": "sha256:a3f8b2...",
|
||||
"signature": "ed25519:9f3a...",
|
||||
"immutable": true
|
||||
}
|
||||
```
|
||||
|
||||
`trust_accumulation` loggar hur förtroendet byggdes upp steg för steg.
|
||||
Vid återgranskning kan man se exakt varför beslutet fattades — och vid vilken evidens tröskeln passerades.
|
||||
|
||||
---
|
||||
|
||||
## Challenge-direktivets vokabulär
|
||||
|
||||
**Designkrav:** Zoomern kan aldrig avgöra om ett direktiv är guidning eller liveness-challenge.
|
||||
|
||||
```
|
||||
"Flytta kameran 30 cm åt vänster." ← kan vara guidning (framing_ok)
|
||||
eller challenge (liveness, kräver parallax)
|
||||
|
||||
"Höj kameran lite." ← kan vara guidning (angle_ok)
|
||||
eller challenge (liveness, kräver rörelse)
|
||||
|
||||
"Visa objektets nedre del." ← kan vara guidning (coverage)
|
||||
eller challenge (liveness, kräver ny vinkel)
|
||||
```
|
||||
|
||||
Samma fras, samma röst, samma UI. Challenge-klassen är intern — aldrig exponerad.
|
||||
|
||||
```json
|
||||
{
|
||||
"directive": "Gå ett steg bakåt",
|
||||
"directive_class": "liveness_challenge", ← INTERN, visas aldrig för Zoomer
|
||||
"expected_signal": "zoom_out + parallax_change",
|
||||
"timeout_ms": 8000
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Korrektion av en verified_claim
|
||||
|
||||
En verifierad utsaga är immutable. Korrektion sker som ny claim som refererar bakåt:
|
||||
|
||||
```json
|
||||
{
|
||||
"claim_id": "clm_4c7f2b8d",
|
||||
"claim_type": "correction",
|
||||
"corrects_claim_id": "clm_9f3a8c1b",
|
||||
"correction_reason": "Manuell granskning: serienummer OCR-fel, korrekt värde SE-4821-9938-02",
|
||||
"corrected_by": "reviewer_human_012",
|
||||
"timestamp": "2026-06-17T15:33:11Z",
|
||||
"signature": "ed25519:7b2c...",
|
||||
"immutable": true
|
||||
}
|
||||
```
|
||||
|
||||
Originalclaim:en finns kvar. Granskningskedjan är komplett.
|
||||
|
||||
---
|
||||
|
||||
## Kunskapspaketet äger tröskeln
|
||||
|
||||
```json
|
||||
{
|
||||
"object": "lifebuoy",
|
||||
"claim_type": "existence_and_condition",
|
||||
"trust_threshold": 0.92,
|
||||
"evidence_weights": {
|
||||
"visual_frame": 0.15,
|
||||
"ocr_serial": 0.25,
|
||||
"liveness_score_component": 0.20,
|
||||
"gps_match": 0.15,
|
||||
"app_attest": 0.10,
|
||||
"challenge_response": 0.08,
|
||||
"reference_match": 0.07
|
||||
},
|
||||
"min_liveness_score": 80,
|
||||
"max_scan_seconds": 120,
|
||||
"compliance": "maritime_safety"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"object": "passport",
|
||||
"claim_type": "identity_document_authenticity",
|
||||
"trust_threshold": 0.97,
|
||||
"evidence_weights": {
|
||||
"mrz_validation": 0.30,
|
||||
"photo_match": 0.25,
|
||||
"tamper_detection": 0.20,
|
||||
"liveness_score_component": 0.15,
|
||||
"challenge_response": 0.10
|
||||
},
|
||||
"min_liveness_score": 95,
|
||||
"max_scan_seconds": 60,
|
||||
"compliance": "regulated"
|
||||
}
|
||||
```
|
||||
|
||||
Passet kräver 97% förtroende och minst 95 i liveness.
|
||||
Livbojen kräver 92% och minst 80 i liveness.
|
||||
Samma motor — olika parametrar. Aldrig hårdkodad logik.
|
||||
|
||||
---
|
||||
|
||||
## Persistens och juridisk hållbarhet
|
||||
|
||||
| Krav | Implementation |
|
||||
|------|---------------|
|
||||
| Immutability | Append-only storage (S3 Object Lock / DynamoDB Streams) |
|
||||
| Signering | Ed25519 per claim och per bundle |
|
||||
| Återgranskningsbarhet | `trust_accumulation`-log i varje bundle |
|
||||
| Temporal binding | Timestamp signerat med bundle-hash — kan inte backdateras |
|
||||
| Separation | Bundle sparas separat från claim — kan granskas oberoende |
|
||||
|
||||
**Samma mönster som ledger:** en post ändras aldrig. Korrektion = ny post med bakåtreferens.
|
||||
|
||||
---
|
||||
|
||||
## Vad AVO v2-kontraktet fortfarande styr
|
||||
|
||||
Evidence Graph och Verified Claim är ett lager *ovanpå* AVO v2 — inte en ersättning.
|
||||
|
||||
```
|
||||
Smart Scan (insamling)
|
||||
↓
|
||||
AVO Fas A — Liveguidning (on-device, per frame)
|
||||
↓
|
||||
AVO Fas B — Slutanalys (server, per exponering)
|
||||
↓
|
||||
Sufficiency-funktion (ackumulerar trust mot claim-tröskel)
|
||||
↓
|
||||
Evidence Bundle (signerat paket)
|
||||
↓
|
||||
Verified Claim (persisterad, immutable utsaga)
|
||||
↓
|
||||
QuickSum (summary till uppdragsgivare)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Öppna frågor
|
||||
|
||||
1. **Vem äger kunskapspaketen?** quiXzoom sätter defaults — kan uppdragsgivare överskriva? Sätt ett golv.
|
||||
2. **Juridisk beviskraft** — ed25519-signatur + immutable storage: räcker det i EU-rätt för försäkringsärenden? Behöver vi en ackrediterad tidsstämpel (RFC 3161)?
|
||||
3. **RFC 3161 trusted timestamp** — binds claim-hashen till en extern tidsstämpelserver ger oberoende temporalt bevis. Troligtvis nödvändigt för regulated-klassen.
|
||||
4. **Liveness-score för offline** — App Attest kräver nät. Offline-scanning ger lägre liveness-score. Accepteras av knowledge_pack?
|
||||
5. **Granskningsgränssnitt** — hur ser det ut när en försäkringshandläggare öppnar ett bundle 6 månader senare? UI behöver designas för människa, inte för AVO.
|
||||
Reference in New Issue
Block a user