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.
|