Files
Shade/docs/archive/V3.12-DESIGN.md
Sterister f301b391a5
Some checks failed
Test / test (push) Has been cancelled
docs(archive): close out Status fields on V2.x backlog + V3.12 design notat
V4.0 acceptance §"All docs/V*.md arkivert med DONE-status" requires
every archived plan to carry an explicit Status field. V2.1 / V2.2 /
V2.3 inherited their pre-status format; V3.12-DESIGN was still
"Approved". Mark all four as Done with a one-line pointer to where
the work actually landed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:04:47 +02:00

558 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# V3.12 — Key Transparency: Designnotat
**Status:** Done — implementert i `@shade/key-transparency` 0.4.0, frosset i 4.0 GA.
**Forfatter:** Shade-teamet
**Reviewer-mål:** ekstern crypto-orientert reviewer før produksjons-deploy.
**Implementasjons-target:** `@shade/key-transparency` + utvidelser i
`@shade/server`, `@shade/transport`, `@shade/sdk`.
---
## 1. Mål og ikke-mål
### Mål
Bytt ut "blind tillit til prekey-server" med en **verifiserbar
append-only log**. Når en klient mottar et prekey-bundle skal den ha
kryptografisk bevis for at:
1. Bundlen er **commit'et** i en tidstemplet log (Signed Tree Head).
2. Den eksakte (adresse, identityKey, signedPreKey)-mappingen står i
den loggen — _eller_ den står ikke (fravær-bevis).
3. Loggen har ikke skrevet om historie siden forrige fetch
(konsistens-bevis).
4. Andre klienter ser **samme** log (split-view-deteksjon via
witness-gossip).
Dette er **CT-style transparens** (RFC 6962-prinsipper) tilpasset
prekey-distribusjon.
### Ikke-mål (eksplisitt ut)
- **Federert log mellom flere prekey-servere.** Hver Shade-deployment
har én log (eller ingen). Multi-server gossip er V3.13+.
- **Løse MITM-på-første-kontakt fullstendig.** KT fanger split-view og
re-write, men ikke det at en angriper publiserer en forfalsket
identitet ved første registrering. Det er V3.3 (fingerprint-gate)
+ V3.10 (social recovery).
- **Legal/compliance audit-log.** Loggen er kryptografisk, ikke juridisk.
- **Klient-styrt sletting.** Append-only — DELETE skriver
tombstone-entry, fjerner ikke historikk.
### Beslutningskriterium for implementasjon
Når dette notatet er godkjent _og_ alle åpne spørsmål under §11 har
konkrete svar (ikke bare "vi finner ut av det senere"), kan kode
skrives. Det notatet ligger på når §11 lukkes er det vi bygger.
---
## 2. Trusselmodell-tillegg
Eksisterende THREAT-MODEL.md dekker prekey-server som "honest-but-curious"
+ tilstede TOFU. KT utvider modellen til **fully-malicious server**:
| Angrep | Pre-V3.12 | Post-V3.12 |
|---|---|---|
| Server returnerer feil bundle for én klient | Uoppdaget til OOB-verifisering | Klient kan be om proof; mismatch oppdages |
| Server bytter en allerede registrert identityKey | TOFU-fingerprint endres → V3.3-gate slår inn (men brukerinitiert) | Loggen vil vise to entries med samme adresse → witness oppdager |
| Server gir `alice` ulike identityKeys til Bob og Charlie (split-view) | Uoppdaget til OOB | Witness-gossip avslører to ulike STH-er |
| Server skriver om historikk for å skjule tidligere svik | Mulig | Konsistens-proof feiler → klient varsler |
| Server nekter å publisere ny STH | Mulig | "Stale STH"-detekteres av friskhetsbevis (max age) |
| Server kompromitterer signing-key for STH | KT-trygghet brutt | Witness gossip om gammel STH-kjede; rotasjon krever ny genesis |
KT løser **ikke**:
- Førstegangs-impersonering av en helt ny adresse (intet historisk
bevismateriale).
- Kollusjon mellom server og _alle_ witnesses.
- Klient som glemmer cached STH og må re-bootstrappe.
---
## 3. Datastruktur-valg
Vi velger **RFC 6962-stil append-only Merkle log** + **ekstern
adresse-index** med commitment-bevis. Begrunnelse:
### Vurderte alternativer
1. **Pure CT-log (RFC 6962):** Simple append-only Merkle tree.
Inklusjonsbevis trivielle. Fravær-bevis _ikke_ støttet
nativt (må scanne hele loggen).
2. **CONIKS-tre (sparse Merkle tree over adresser):** Native fravær-bevis,
men mye mer kompleks (epoch-baserte snapshots, prefix-trees,
placeholder-nodes). Overkill for første iterasjon.
3. **Hybrid (RFC 6962 log + side-index):** Loggen er sannhetskilde,
indexen er en _commitment_-mapping `address → leaf_index`. Server
beviser inklusjon via leaf-path, fravær via "denne adressen er ikke
i indexen ved tree_size T" + signert STH.
**Valg: alternativ 3.** Det gir CT-stil enkelthet, samt fravær-bevis
nesten gratis (commitment til indexen er en del av hver STH).
### Konkret format
#### Leaf
Hver leaf representerer én registrering eller revoke:
```
leaf = SHA256(
0x00 || // leaf prefix (RFC 6962)
uint64_be(timestamp_ms) ||
byte(operation) || // 0x01 register, 0x02 replenish, 0x03 delete
uint16_be(len(address)) || address_bytes ||
uint16_be(len(bundle_hash)) || bundle_hash // 32 bytes SHA-256 over canonical bundle
)
```
`bundle_hash` er deterministisk hash av:
```
canonical_bundle = SHA256(
0x01 || // bundle prefix
identitySigningKey (32) ||
identityDHKey (32) ||
uint32_be(signedPreKey.keyId) ||
signedPreKey.publicKey (32) ||
signedPreKey.signature (64)
)
```
One-time prekeys er **ikke** med i bundle-hashen — de er ephemerale og
ville lekket OTP-rotasjons-mønstre.
#### Tree
Merkle-tre over leaf-array, RFC 6962 §2.1:
- `MTH(empty) = SHA256()`
- `MTH({d}) = SHA256(0x00 || d)` (already hashed leaf)
- `MTH(D[n]) = SHA256(0x01 || MTH(D[0:k]) || MTH(D[k:n]))` der
`k` er største 2-potens < n.
#### Signed Tree Head (STH)
```
sth = {
tree_size: uint64,
timestamp: uint64_ms,
root_hash: bytes(32),
index_root: bytes(32), // commitment til adresse-index ved denne tree_size
log_id: bytes(32), // SHA-256 av server-public-key (stabil ID)
signature: bytes(64) // Ed25519 over canonical(rest)
}
```
`canonical(sth)` for signing:
```
0x02 || // sth prefix
uint64_be(tree_size) ||
uint64_be(timestamp) ||
root_hash (32) ||
index_root (32) ||
log_id (32)
```
#### Inklusjons-bevis
Standard RFC 6962 audit-path: liste med søsken-hasher fra leaf til root,
slik at klient re-beregner root og sammenligner med STH.
#### Konsistens-bevis
Standard RFC 6962 §2.1.2: bevis at tree med `tree_size = N1` er prefix
av tree med `tree_size = N2 > N1`. Klient bruker dette for å detektere
re-write.
#### Fravær-bevis
Adresse-indexen er en sortert liste `(address, leaf_index_of_latest)`
serialized og hashet. `index_root` i STH er commitment.
For å bevise fravær av adresse `addr` ved tree_size `N`:
- Server returnerer hele indexen ved tree_size `N` (sortert), eller
- (effektivt:) Returnerer naboparet `(addr_prev, addr_next)` der
`addr_prev < addr < addr_next` lexikografisk, sammen med en
Merkle-path i en sparse Merkle tree over indexen.
Første iterasjon: vi serialiserer hele indexen og lar klienten
laste den (kompakt: <100 KB selv for 100k adresser). Senere
optimaliserer vi til sparse Merkle tree hvis dataset vokser.
---
## 4. Friskhetsbevis (Signed Tree Heads)
### Frekvens
- **Min:** Ny STH ved hver mutasjon (register/replenish/delete) — synkront
i write-pathen.
- **Maks-stale:** Selv uten mutasjoner skal en STH publiseres minst hver
**10. minutt** ("heartbeat STH" — samme tree_size, oppdatert timestamp).
Dette gir klienter mulighet til å detektere "død" log uten å bekymre
seg om hvorvidt logen faktisk har endret seg.
### Klient-akseptansevindue
Klient avviser STH eldre enn `now - 24 timer` (default, konfigurerbar).
Dette beskytter mot replay av gamle STH-er som "skjuler" en mutasjon
gjort i ettertid.
### Stale-STH som soft-fail
Hvis STH er stale men gyldig signert: klient logger advarsel,
returnerer bundle med `proof.staleness = 'warn'` (V1) eller blokkerer
(V2 etter dogfooding). Vi starter med _warn_, eskalerer til _block_
når witness-økosystem er etablert.
---
## 5. Klient-verifikasjonssteg
På hver `fetchBundle(address)`:
1. Server returnerer `{ bundle, proof: { sth, leaf, audit_path, leaf_index, address_index_proof } }`.
2. Klient verifiserer:
- `sth.signature` mot kjent `log_public_key` (pinnet ved første
bootstrap).
- `sth.timestamp >= now - max_age_ms` (default 24t).
- Re-beregner `leaf_hash` fra bundle og sammenligner med `proof.leaf`.
- Re-beregner `root_hash` fra `audit_path + leaf` og sammenligner med
`sth.root_hash`.
- Verifiserer `address_index_proof` mot `sth.index_root`.
3. Hvis klient har en cached forrige STH: sjekk **konsistens-proof**
mellom forrige og denne. Server publiserer dette i
`GET /v1/kt/consistency?from=<size>&to=<size>`.
4. Hvis klient har en cached STH for samme `tree_size` med ulik
`root_hash`**split-view alarm**.
### Probabilistisk vs. obligatorisk verifisering
Vi velger **obligatorisk** ved hver bundle-fetch. Bundle-fetch er sjelden
(per ny peer, ikke per melding) — kostnaden er <100ms. Probabilistisk
verifisering ville la klienter bli lurt av "én dårlig fetch" uten
deteksjon.
### Bootstrap
Første gang en klient møter en log: pinner `log_public_key` etter å ha
hentet det fra et **ut-av-bånd**-pinningendepunkt eller fra `Shade.config`
(operatør sender den med klient-config). Etterfølgende rotasjon krever
ny genesis-STH med eksplisitt break-event signert av forrige nøkkel.
---
## 6. Witness/auditor-rolle
### Hva en witness gjør
- Periodisk poll: `GET /v1/kt/sth` (hent siste STH).
- Lagrer alle observerte STH-er i append-only lokal store.
- Eksponerer `GET /witness/sth?log_id=...&tree_size=...` slik at andre
klienter kan sammenligne hva _denne_ witnessen har sett.
- Verifiserer konsistens mellom hver ny STH og forrige.
### Klient-witness-gossip
Klient-bibliotek kan operere i tre moduser:
1. **Observe-only:** verifiserer kun bundle den selv henter, ingen
gossip.
2. **Light-witness:** poller STH hver `Xt` og lagrer lokalt; sammenligner
med STH levert ved bundle-fetch.
3. **Full-witness:** publiserer signerte STH-observasjoner til en
konfigurert peer-liste eller offentlig endpoint.
V1 leverer 1 og 2. Mode 3 (full-witness publication-protocol) er V2
hvis økosystem trenger det.
### Hvem kjører witnesses?
- Shade-prosjektet kjører **referanse-witness** på offentlig endpoint
(separate-from-prekey-server).
- Power-users / operatører kan kjøre egne via `@shade/key-transparency/witness`-
API.
- Tredjeparts auditors (typisk security-research) er invitert.
Vi krever **ikke** federation/konsensus mellom witnesses i V1 — gossip
er rent "har du sett samme STH som meg?".
---
## 7. Operatørkost
### Lagring
- **Per leaf:** 32 bytes (hash) + ~80 bytes adresse-index entry =
~112 bytes.
- **100k adresser, 1 rotasjon/år, 1 replenish/uke:** ~5.4M leaves =
~600 MB log. Tre-strukturen er beregnet on-demand, ikke lagret.
- **Index:** ~100k × 80B = 8 MB i minne (cacheable).
### CPU
- STH-signing: 1 Ed25519-signering per mutasjon + heartbeat = <1k/dag for
små deployments. Trivielt.
- Audit-path-beregning: O(log N) ved fetch. <1ms.
- Konsistens-proof: O(log N).
### Backup
Logen MÅ aldri miste data — sletting eller corruption ødelegger
integritet permanent. Strategi:
- Loggen lagres som append-only tabell `shade_kt_log` (PG) med
`(leaf_index, leaf_hash, leaf_data_json)`.
- Backup hver time + WAL-shipping anbefalt.
- Ved corruption: se §10 Recovery.
### STH-signing-key
- Genereres ved første KT-aktivering, lagres i operatør-styrt secret
(env, KMS, eller på disk for hjemme-server).
- Rotasjon: **breaking event** — krever ny genesis-STH der ny key
signerer melding "rotated from ${old_key}" med _gammel_ key. Klient
må eksplisitt akseptere rotasjonen.
---
## 8. Migrasjon
### Server-side
KT er **opt-in** på operatør-nivå. `createPrekeyServer({ keyTransparency:
{ enabled, store, signingKey } })`. Når slått på:
1. Server skriver alle eksisterende identiteter inn som genesis-leaves
ved boot.
2. Første STH publiseres med `tree_size = N` der N er antall
eksisterende adresser.
3. Klient som henter bundle får proof; klient som ikke støtter KT
ignorerer proof-felt (forward-compatible).
### Klient-side
`@shade/sdk`-config:
```ts
createShade({
keyTransparency: {
mode: 'observe' | 'light-witness' | 'off',
logPublicKey: '<base64>',
maxStaleMs: 86_400_000,
},
// ...
})
```
`mode: 'off'` (default for backward-compat første release) — ignorerer
proof. Ny SDK med `mode: 'observe'` verifiserer men feiler ikke harde
hvis proof mangler. `mode: 'observe-strict'` (senere) krever proof.
### Eksisterende deployments
Operatør kan rulle KT inn på live server uten klient-update:
1. Skru på KT i server-config → server begynner å produsere proofs.
2. Gamle klienter ignorerer proof-felt (de er additive i bundle-respons).
3. Nye klienter med `mode: 'observe'` begynner å verifisere.
4. Når operatør har testet og publisert log-public-key OOB, kan brukere
skifte til `'light-witness'`.
---
## 9. Akseptansekriterier
- [ ] `@shade/key-transparency` pakke leverer:
- Merkle log core (RFC 6962 hash-funksjoner).
- STH-signing/verifikasjon.
- Inklusjons-bevis generering + verifisering.
- Konsistens-bevis generering + verifisering.
- Adresse-index med commitment.
- Witness-light klient.
- Cross-platform (TS-only, ingen native deps).
- [ ] `@shade/server` integrasjon:
- `KTLogStore`-interface (memory + postgres).
- Routes: `GET /v1/kt/sth`, `GET /v1/kt/sth/:tree_size`,
`GET /v1/kt/consistency`, `GET /v1/kt/inclusion/:address`.
- Bundle-fetch returnerer `{ bundle, proof }` når KT aktivert.
- Heartbeat-STH-publisering hver 10. minutt (configurable).
- [ ] `@shade/transport` `ShadeFetchTransport`:
- Aksepterer optional `keyTransparency`-verifier.
- `fetchBundle()` returnerer `{ bundle, proof?: KTProof }`.
- [ ] `@shade/sdk` `Shade`:
- `keyTransparency`-config.
- Verifiserer proof ved hver bundle-fetch når aktivert.
- Cacher STH for split-view-deteksjon.
- [ ] **End-to-end test: split-view detection.**
- Test-server gir Bob bundle X, Charlie bundle Y for samme adresse `alice`.
- Bob+Charlie kjører som light-witness, gossiper STH-er.
- Test asserter at mismatch detekteres innen N polls.
- [ ] **End-to-end test: log re-write detection.**
- Server skriver om historie (test-only API).
- Konsistens-proof feiler på neste fetch.
- [ ] Operatør-doc dekker recovery-strategi.
- [ ] CHANGELOG, README, ROADMAP oppdatert.
- [ ] Cross-platform vector-test for Merkle hash + STH (Android/TS
paritet, samme som V3.5-tradisjonen).
---
## 10. Recovery
### Log corruption
Hvis log-data tapes (disk-feil før backup): **kan ikke gjenopprettes
uten å miste integritet** — det er hele poenget.
Recovery-prosedyre:
1. Operatør publiserer "log-restart" event signert med STH-keyen.
2. Genesis-STH genereres på nytt med ny `log_id` (= ny offentlig nøkkel
eller eksplisitt versjon).
3. Klienter som har cached STH-er fra gammel log varsles via
eksplisitt diskrepans i `log_id`.
4. Brukere som er bekymret må OOB-verifisere identiteter (V3.3-gate
trigges automatisk for fingerprint-rotasjon).
### Stale signing-key
Hvis STH-keyen lekkes: rotasjon krever break-event (§7). Inntil
brukerne aksepterer ny key, oppfører cient-bibliotek seg som om STH
mangler (soft-fail i `observe`-mode, blokkerer i `observe-strict`).
---
## 11. Åpne spørsmål (lukket før kode)
| Spørsmål | Svar |
|---|---|
| Hvordan distribueres `log_public_key` til klient første gang? | Operatør embedder i `Shade.config` ved app-init. OOB-pinning er fallback. |
| Skal one-time prekeys være med i bundle-hash? | Nei — ephemerale, og deres rotasjon ville støy-fylle loggen. |
| Konflikt: STH ved hver mutasjon vs. batched STH? | Per mutasjon. Heartbeat hver 10 min uansett. Batching vurderes som optimalisering hvis throughput blir et problem (ikke nå). |
| Hva skjer ved replenish (kun OTP-tilført)? | Skriver ikke til log (bundle-hash uendret). Heartbeat-STH dekker friskhet. |
| Hva med DELETE? | Skriver tombstone-leaf med `operation = 0x03`. Identiteten i indexen markeres som "deleted at tree_size N". |
| Sparse Merkle tree for index-proof? | Senere — V1 bruker hele indexen i fravær-proof. <100 KB ved 100k adresser er akseptabelt. |
| Klient-cache eviction-policy for STH? | LRU på `log_id`, last-N (default 100). Klient holder _alltid_ siste sett STH. |
| Witness-publication-protokoll? | V1 har poll-only (`GET /witness/sth`); push-publication er V2. |
Alle åpne spørsmål har konkrete svar. Implementasjon kan starte.
---
## 12. Pakke-struktur
```
packages/shade-key-transparency/
├── package.json # @shade/key-transparency, v0.4.0
├── src/
│ ├── index.ts # Public exports
│ ├── hashes.ts # RFC 6962 leaf/node hashing
│ ├── log.ts # MerkleLog (in-memory) + audit-path
│ ├── consistency.ts # Consistency-proof gen/verify
│ ├── sth.ts # STH sign / verify / canonical bytes
│ ├── index-tree.ts # Address index commitment
│ ├── proof.ts # KTProof type + bundle-proof verifier
│ ├── store.ts # KTLogStore interface (server-side)
│ ├── memory-store.ts # In-memory KTLogStore
│ ├── witness.ts # Light-witness client
│ └── errors.ts # KT-specific error types
└── tests/
├── hashes.test.ts
├── log.test.ts # RFC 6962 test vectors
├── consistency.test.ts
├── sth.test.ts
├── index-tree.test.ts
├── proof.test.ts
└── split-view.test.ts # End-to-end split-view detection
```
Server-integrasjon i `@shade/server`:
```
packages/shade-server/src/
├── kt-routes.ts # /v1/kt/* routes
├── kt-integration.ts # Hook bundle-fetch + register/delete to log
└── ...
```
Postgres-implementasjon i `@shade/storage-postgres`:
```
packages/shade-storage-postgres/src/
├── postgres-kt-store.ts # KTLogStore on PG
└── ...
```
Klient-integrasjon i `@shade/transport` + `@shade/sdk`:
```
packages/shade-transport/src/
├── kt-verifier.ts # Proof-verifier for fetchBundle
└── ...
packages/shade-sdk/src/
├── kt.ts # Shade.keyTransparency config + cache
└── ...
```
---
## 13. Test-strategi
1. **RFC 6962 test-vektorer:** importer kjente vektorer fra
<https://datatracker.ietf.org/doc/html/rfc6962#appendix-A>.
2. **Property-tests (fast-check):** for hver tree_size N og hvert
leaf-index i: `verify(audit_path(i, N), leaf, sth) === true`.
3. **Konsistens-bevis property-tests:** for N1 < N2:
`verify_consistency(proof, sth1, sth2) === true`.
4. **Split-view e2e:** to klienter, ondsinnet test-server, witness
gossip oppdager mismatch.
5. **Re-write-detection e2e:** server muterer log-historie, klient
neste fetch får konsistens-proof som feiler.
6. **Cross-platform:** Android (Kotlin) + TS gir samme leaf-hash for
samme bundle (V3.5-paritet er forutsetning, så dette må også gå
gjennom kotlin-port; for V3.12 første release dekker vi TS — Android
port er V3.13).
7. **Stale STH:** klient avviser STH > max_age.
8. **Bootstrap-pinning:** klient feiler hvis log_public_key ikke matcher.
---
## 14. Sikkerhetsvurdering
- **Falsk trygghet hvis halvveis:** Avhjelpes ved at default-mode er `'off'`,
bare _eksplisitt_ aktivert KT gir hardere garantier. Dokumentasjon
fremhever at `'observe'` er observasjon, ikke obstruksjon, til
økosystemet er etablert.
- **Server-side mutability av historie:** Avhjelpes ved at `KTLogStore`
kun har `append()` — ingen `update()`/`delete()` på historiske leaves.
PG-tabellen har CHECK constraint og BEFORE-triggers for ekstra defense
in depth (se §7).
- **STH-key compromise:** dokumentert §10. Operatør-ansvar.
- **DoS via massive index-proofs:** index-proof er i V1 hele indexen.
100 KB per fetch er overkommelig; rate-limiteren dekker excess.
- **Replay av gammel proof:** STH-timestamp + max_age beskytter.
---
## 15. Approval
Når dette notatet er reviewed (in-tree review er nok for å kommitte
første implementasjon; ekstern crypto-review er pre-deploy-krav per
V3.12 §"Pre-requisite designnotat"), kan implementasjon starte.
**Implementasjon-rekkefølge** (alle commits i samme branch):
1. `@shade/key-transparency` core (Merkle log, STH, proofs).
2. Server-integrasjon (`@shade/server` + memory/postgres KTLogStore).
3. Klient-integrasjon (`@shade/transport` verifier + `@shade/sdk` config).
4. Witness-light + e2e split-view-test.
5. Operatør-doc + CHANGELOG + README + ROADMAP.
— end of design —