Files
Shade/docs/key-transparency.md
Sterister e6fdf31b49
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.

Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
  Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
  bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
  shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
  non-realtime stack.

Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
  version 4.0.0 on /health.

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

349 lines
12 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.
# Key Transparency (V3.12)
> **Status:** v0.4.0+ — opt-in. Server runs unchanged when KT is off.
> Klient ignorerer proof-felt når KT-config mangler. Trygg å rulle ut
> uten klient-update.
Shades prekey-server er sannhetskilde for hvilket bundle som er
publisert for hver adresse. Uten Key Transparency (KT) kan en
ondsinnet eller kompromittert server bytte ut et bundle uten at noen
oppdager det. Med KT er hvert bundle som leveres **kryptografisk
forpliktet** i en append-only Merkle log som tredjeparts-witnesses kan
auditere.
Se også `docs/V3.12-DESIGN.md` for designnotat med trusselmodell og
beslutningsspor.
---
## Hva KT garanterer
| Angrep | Detektert? |
|---|---|
| Server gir Bob feil bundle for `alice` | **Ja** — inklusjons-proof matcher ikke |
| Server gir Bob og Charlie ulike bundles for `alice` | **Ja** — witness-gossip ser to STH-er på samme `tree_size` |
| Server skriver om historikk for å skjule tidligere svik | **Ja** — konsistens-proof feiler |
| Server signerer "stale" STH for å holde et tidsvindu åpent | **Ja** — klient avviser STH eldre enn `maxStaleMs` (default 24t) |
| Førstegangs-impersonering av en helt ny adresse | **Nei** — KT ser bare etter at adressen er i loggen, ikke at den er "riktig" person. Bruk V3.3 (fingerprint-gate) + V3.10 (social recovery) for det. |
---
## Operatør: skru på KT
KT er opt-in og krever:
1. **Et Ed25519 signing-keypair** for STH-signering. Dette er
*operatørens* nøkkel og må beskyttes som en code-signing-key.
2. **En persistent KTLogStore.** I produksjon: `PostgresKTLogStore`.
I test/dev: `MemoryKTLogStore`.
3. **At klienter pinner samme `logPublicKey`** OOB (typisk via
`Shade.config`-bundling i appen).
### Generere signing-key
```sh
bun run scripts/generate-kt-key.ts > kt-key.json
```
(Eller kjør manuelt: `crypto.generateEd25519KeyPair()` i en Bun REPL.)
Lagre `privateKey` i operatørens secret-store. Distribuér `publicKey`
til klienter sammen med app-config.
### Boot serveren med KT
```ts
import { createPrekeyServerWithKT } from '@shade/server';
import { PostgresPrekeyStore, PostgresKTLogStore } from '@shade/storage-postgres';
import { SubtleCryptoProvider } from '@shade/crypto-web';
const crypto = new SubtleCryptoProvider();
const prekeyStore = await PostgresPrekeyStore.create(process.env.DATABASE_URL!);
const ktStore = await PostgresKTLogStore.create(process.env.DATABASE_URL!);
const { app, kt } = await createPrekeyServerWithKT({
crypto,
store: prekeyStore,
keyTransparency: {
store: ktStore,
signingPrivateKey: loadFromSecret('SHADE_KT_SIGNING_PRIVATE_KEY'),
signingPublicKey: loadFromSecret('SHADE_KT_SIGNING_PUBLIC_KEY'),
heartbeatIntervalMs: 10 * 60 * 1000, // default; 0 = off
},
});
export default { port: 3900, fetch: app.fetch };
```
Når KT er på blir disse rutene tilgjengelig:
| Route | Hva den returnerer |
|---|---|
| `GET /v1/kt/log_id` | `{ logId, publicKey }` (begge base64) |
| `GET /v1/kt/sth` | Siste signed tree head |
| `GET /v1/kt/sth/:treeSize` | Historisk STH for et bestemt tree_size |
| `GET /v1/kt/consistency?from=N1&to=N2` | Konsistens-proof N1 → N2 |
Bundle-fetch (`GET /v1/keys/bundle/:address`) får nå et `ktProof`-felt
i responsen.
### Migrasjon fra ikke-KT
KT er bakoverkompatibel:
1. Skru på KT-config i serveren. Restart.
2. Eksisterende klienter ignorerer proof-feltene (`ktProof`, `ktSth`).
3. Etter hvert som klienter oppgraderes med KT-config (`mode: 'observe'`),
begynner de å verifisere.
4. Når øko-systemet er vant til det, eskalér klienter til
`'observe-strict'` for å avvise prekey-server-svar uten proof.
Ved første boot scanner KT-tjenesten ikke automatisk eksisterende
prekey-store-tilstand inn i loggen. **Re-registrering** av eksisterende
adresser (dvs. en `POST /v1/keys/register`-runde fra hver klient) er
det som backfiller. For et større deployment: anbefalt at en operatør
varsler brukerne om å re-registrere innen et tidsvindue. Klienter som
ikke re-registrerer vil feile `observe-strict`-fetch til de får ny key
fra peer.
---
## Klient: skru på KT
```ts
import { createShade } from '@shade/sdk';
const shade = await createShade({
prekeyServer: 'https://shade.example.com',
address: 'alice',
keyTransparency: {
mode: 'observe-strict', // eller 'observe'
logPublicKey: KT_LOG_PUBLIC_KEY_BASE64, // eller Uint8Array
maxStaleMs: 24 * 60 * 60 * 1000, // default 24t
},
});
```
`shade.getKTWitness()` returnerer `LightWitness`-instansen som
samler observerte STH-er. Bruk `.compare(otherSth)` for manuell
gossip-sjekk mot peers.
### `mode: 'observe'`
- Verifiserer proof når serveren leverer det.
- Skipper verifisering hvis `ktProof` mangler i bundle-respons.
- Anbefalt under første utrulling der ikke alle klienter har
re-registrert ennå.
### `mode: 'observe-strict'`
- Krever proof på hver `200`-respons. Mangler proof → kast `KTVerificationError`.
- Krever proof på hver `404`-respons også (for absence/tombstone-pinning).
- Anbefalt produksjons-modus når KT-økosystemet er etablert.
---
## Witness / auditor
`@shade/key-transparency` eksporterer `LightWitness`. Et CLI-verktøy
eller backend-job kan bruke den slik:
```ts
import { LightWitness } from '@shade/key-transparency';
import { SubtleCryptoProvider } from '@shade/crypto-web';
const crypto = new SubtleCryptoProvider();
const witness = new LightWitness({
crypto,
logPublicKey: KT_LOG_PUBLIC_KEY,
fetcher: {
async fetchLatestSTH() {
const r = await fetch('https://shade.example.com/v1/kt/sth');
return r.json();
},
async fetchConsistencyProof(from, to) {
const r = await fetch(`https://shade.example.com/v1/kt/consistency?from=${from}&to=${to}`);
return r.json();
},
},
});
// Poll periodically (e.g. every 5 minutes)
setInterval(async () => {
try {
const sth = await witness.pollOnce();
console.log(`Observed STH: tree_size=${sth.treeSize}, root=${Buffer.from(sth.rootHash).toString('hex').slice(0, 16)}`);
} catch (err) {
console.error('Witness alarm:', err);
// Send to PagerDuty / Slack / whatever
}
}, 5 * 60 * 1000);
```
Witness-koden detekterer:
- **Stale STH** — server publiserer ikke nye STH-er i tide.
- **Split view** — to STH-er ved samme `tree_size` med ulik root.
- **Re-write** — konsistens-proof feiler.
- **Wrong key** — `log_id` matcher ikke pinnet `logPublicKey`.
---
## Operatørkost (estimat)
For et deployment med:
- **100k registrerte adresser**
- **1 identitets-rotasjon per år** per bruker
- **52 replenish per år** (én i uka, *ikke* committed til loggen — bare register/delete er)
| Ressurs | Per år | Kommentar |
|---|---|---|
| Log-rader | ~100k | bare register/delete |
| Lagring (leaves+index) | ~25 MB | base64-kodet |
| STH-rows | ~52k | én per heartbeat (10 min) |
| STH-storage | ~7 MB | |
| CPU per STH | ~1ms | Ed25519-signing er trivielt |
| Bundle-fetch overhead | <2ms | inkluderer audit-path-bygg |
**Backup:** behandle KT-tabellene som "kan ikke gjenopprettes" data —
`shade_kt_leaves` har en database-trigger som forbyr UPDATE/DELETE i
PostgreSQL-implementasjonen. Backup-strategi:
- Daglig full backup av `shade_kt_*` tabellene.
- WAL-shipping anbefalt (tap < 60 s i verste fall).
- **Test recovery** kvartalsvis. Recovery-prosedyre står under.
---
## Recovery
### Scenario 1 — STH-signing-key tapt eller kompromittert
Loggen forblir konsistent (alle gamle STH-er er allerede signert), men
nye STH-er kan ikke signeres med samme key.
**Steg:**
1. Generer ny Ed25519-keypair.
2. Skriv inn et "rotation breaks here"-leaf i loggen (operasjon = 0x03
på en spesiell `__log__`-adresse) — operasjonen er rent
informativ, men gjør rotasjonen synlig i tree.
3. Re-konfigurer serveren med ny key. Restart.
4. Server publiserer en ny STH; den vil ha en ny `log_id` (siden
`log_id = SHA-256(publicKey)`).
5. **Klienter må eksplisitt akseptere ny key.** Inntil de pinner ny
`logPublicKey`, vil deres `LightWitness` kaste
`KTLogIdMismatchError`. Operatør publiserer ny key OOB med
"rotated from `<gammel logId>`"-melding signert med gammel key
(siste handling før gammel key zeroizes).
### Scenario 2 — KT-database korrumpert / tapt før backup
Dette er **det verste utfallet**. Loggen er per design ikke
gjenopprettbar — å "rekonstruere" den fra prekey-store ville bryte
selve invarianten KT lover.
**Steg:**
1. Stopp serveren.
2. Deklarer en "log-restart event" via offentlig kanal (status-side,
release-notes, Twitter, etc.) — inkluder timestamp, tapte tree_size
(siste backup-bare snapshot om mulig), og ny `logPublicKey`.
3. Generer ny KT-keypair (ikke bruk gamle).
4. Boot serveren tom (tom `shade_kt_*` tabell). Første STH er fra
`tree_size = 0`.
5. Be brukerne om å re-registrere identitetene sine. Klientene vil
trigge V3.3 fingerprint-gate på første re-meldings-flyt etterpå
siden rotasjons-fingerprintet endres.
6. Auditor-organisasjoner kan publisere "vi observerte gammel log
inntil tree_size N, ny log starter på 0 fra T+0" — dette gir
sluttbruker mulighet til å vurdere hvor stort hullet er.
**Beskytt mot dette:** WAL-shipping + off-site backup. Aldri kjør KT
med kun én database-instans uten replicas.
### Scenario 3 — Witness oppdager split-view
Witness kaster `KTSplitViewError` i `LightWitness.observe()` eller
`KTVerificationError` i transport. Dette betyr:
- Operatøren har enten
(a) hatt en software-bug som signerte to ulike STH-er ved samme
tree_size, eller
(b) er kompromittert / ondsinnet.
**Operatør-handling:**
1. Pause `POST /v1/keys/register`, `DELETE`, og bundle-fetch
umiddelbart (return 503).
2. Audit `shade_kt_sths` — hvis du finner to rader med samme
`tree_size` men ulik `root_hash`, har serveren gjort feil. Dette er
alvorlig — finn root cause før du fortsetter.
3. Kommuniser ut til brukerne. Forutsett at en angriper har vært
inne; trigge en bredere reset (recovery scenario 2) hvis det er
mistanke om tampering.
**Klient-handling:**
- `LightWitness` har allerede holdt brukeren tilbake.
- SDK-en surfacer feilen som `KTSplitViewError` til app-koden.
- App-en bør vise advarsel: "Operatørens server kan ikke verifiseres.
Avstå fra sending av sensitive meldinger inntil videre."
---
## Sikkerhets-anbefalinger
1. **Kjør minst én uavhengig witness.** Operatørens egen "witness"
teller ikke — det må være en separat prosess på separate
infrastruktur eid av en separat aktør (community-medlem, security
firm, e.l.).
2. **Pin `logPublicKey` i app-binær eller signert config.** En
man-in-the-middle som kan bytte både prekey-server og KT-key
fanges ikke av KT alene.
3. **Loggrotasjon krever menneske-i-løkken.** Ikke automatiser
key-rotation for KT — den eksplisitte breaking-event er en feature.
4. **`maxStaleMs` bør samsvare med din heartbeat.** 24t default tåler
en heartbeat-pause på opptil et døgn; senk til 14t hvis du har
strenge krav til friskhet.
5. **`observe-strict` bør være standard når økosystemet er etablert.**
Default `'observe'` er en operasjonell overgangsmodus, ikke et
sluttmål.
---
## Kjente begrensninger
- **Federation mellom flere prekey-servere** er ikke støttet i V3.12.
Hver Shade-deployment har én log eller ingen.
- **Sparse Merkle tree for adresse-index** brukes ikke i V3.12 —
fravær-proof er foreløpig nabopar-bevis. <100 KB ved 100k adresser
er akseptabelt; sparse tree blir relevant fra ~10M+ adresser.
- **One-time prekey-rotasjon committes ikke** til loggen. OTP er
ephemerale og inkludering ville støy-fylle loggen. Dette betyr at
en server som svarer med riktig identitet men feil OTP fanges ikke
av KT — forsvar mot dette ligger i V3.3 fingerprint-gate (samme
identitet) + sesjons-etableringens X3DH (feil OTP gir feil shared
secret → første melding feiler decryption).
---
## Tester og test-vektorer
- `packages/shade-key-transparency/tests/` — RFC 6962-kompatibel
Merkle-log + STH + index-proofs (58 tests).
- `packages/shade-server/tests/kt.test.ts` — server-integrasjon (8
tests).
- `packages/shade-transport/tests/kt-transport.test.ts` — klient-
verifikasjon over HTTP (4 tests).
- `packages/shade-transport/tests/kt-split-view-e2e.test.ts`
V3.12-akseptanse split-view-deteksjon (3 tests).
- `packages/shade-sdk/tests/kt.test.ts` — SDK-config + witness wiring
(3 tests).
Totalt 76 tester dedikert til KT.