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

12 KiB
Raw Blame History

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

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

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

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:

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