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
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>
349 lines
12 KiB
Markdown
349 lines
12 KiB
Markdown
# 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 1–4t 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.
|