# 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 ``"-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.