# Security Model

Two encrypted transports: **TLS 1.3** on the TCP control channel, and an encrypted **UDP**
media channel. Plus server identity, authentication, accounts at rest, and anti-replay.

> **Encryption is mandatory — there is no unencrypted mode.** The server has no plaintext
> listener, the client has no "insecure" option, and there is no config flag to turn either
> off. A connection is encrypted or it does not exist. This is a hard product rule, not a
> default. It is also *zero-config* (see §1): the server generates its own key/cert on first
> run, so "secured by default" never costs the operator a setup step.

## 1. Control channel — TLS 1.3 (settled)

- TCP control is wrapped in **TLS 1.3** (TLS 1.2 disabled). AEAD cipher suites only
  (AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305). X25519 key exchange.
- Library: **mbedTLS 3.6 LTS** — Apache-2.0 (permissive, fine for an eventual closed-source
  distribution), TLS 1.3 client+server, and `mbedtls_ssl_export_keying_material()` for the
  media path (§2). It also **static-links cleanly into a single self-host binary**, which is
  a deliberate choice in service of the easy-deploy goal. (OpenSSL 3.x, also permissive
  Apache-2.0, is a drop-in alternative behind the same internal interface.)
- **Zero-config TLS:** on first launch the server auto-generates a self-signed certificate
  bound to a freshly generated **Ed25519 identity key** and persists both. The operator does
  nothing. Clients pin the identity on first connect (TOFU, §1.1). A server *with* a domain
  can drop in a CA cert later, but it is never required to be encrypted.
- All authentication and account material crosses the wire only inside this tunnel.

### 1.1 Server identity — two modes

Self-hosting means most servers won't have a CA-signed cert for a hostname. We support
both, advertised in `ServerHello`:

1. **TOFU (Trust On First Use)** — default for hobby servers. On first connect the client
   shows an identity dialog and, if accepted, pins the value locally. Subsequent connects
   verify the pin silently; a changed value warns loudly (`MISMATCH`).
2. **PKI** — a server with a domain can use a normal CA-signed cert (e.g. Let's Encrypt);
   clients validate the chain conventionally. TOFU pinning still applies on top.

**What is actually pinned (M4 implementation):** the **TLS leaf certificate's SHA-256
fingerprint** — verifiable directly from the TLS handshake before any application data is
trusted. The server also declares an Ed25519 identity fingerprint in `ServerHello`, but this
value is **display-only** and is *not* the value that is pinned or verified. Reason: the TLS
cert and the Ed25519 identity key are generated independently with no cryptographic binding
between them, so pinning the self-declared Ed25519 value (sent *inside* the channel being
trust-decided) would be circular — an attacker who impersonates the server at the TLS level
would supply whatever Ed25519 value they like. Pinning the TLS cert fingerprint is the only
value that is genuinely verifiable at the moment of trust decision.

This is a known limitation of the current design. Closing it properly requires binding the
Ed25519 key into the TLS cert (e.g. as a SubjectAltName or extension), which is a planned
future improvement. Until then, clients display both values but gate on the cert fingerprint.

Client certificates are reserved for a future "key-based identity" option (see roadmap) but
are not required in v1.

## 2. Media channel — UDP encryption (settled: exported-keys + AEAD)

The UDP media path uses **TLS-exported keys + per-packet AEAD** (an SRTP-style design),
mandatory from the first build. This was chosen over DTLS after weighing two findings:

> **Finding 1 — DTLS 1.3 (RFC 9147) is not in stable OpenSSL or mbedTLS.** It ships
> production-ready only in **wolfSSL**, which is **GPLv2-or-commercial** — disqualified,
> because the code will eventually be distributed in closed-source form (no GPL/LGPL deps).
>
> **Finding 2 — mbedTLS 3.6 LTS already exposes `mbedtls_ssl_export_keying_material()`**
> (RFC 5705 / RFC 8446 §7.5 exporter). So we can derive media keys from the existing TLS 1.3
> control session with *zero* extra handshake and *zero* extra dependency.

### How it works

1. During the TLS 1.3 control handshake, both sides call the keying-material exporter with a
   fixed label (`"voicecat media v1"`) to derive independent **send/recv media keys** and a
   salt. No second handshake, no certificates on the UDP path — the UDP channel inherits the
   authenticated, MITM-resistant TLS session's trust.
2. Each UDP voice frame is sealed with **ChaCha20-Poly1305** (libsodium, ISC license).
3. The readable routing field (`ssrc`) is passed as AEAD **associated data** so the relay can
   route without decrypting and an attacker cannot tamper with it undetected.

This keeps the entire crypto surface on two permissive libraries (mbedTLS + libsodium), adds
no handshake latency to voice startup, and is small enough to audit fully. It is abstracted
behind a `MediaCrypto { seal(frame)->bytes; open(bytes)->frame }` interface, so a future
DTLS 1.3 backend could slot in later if a permissive implementation matures — but nothing in
the design depends on that.

### Per-frame protections

- **AEAD** (ChaCha20-Poly1305) over each voice frame — confidentiality + integrity.
- **Associated data:** the `ssrc` (and version/flags) are authenticated-but-visible so the
  relay routes without decrypting; everything else is encrypted.
- **Nonce discipline:** `nonce = direction_bit ‖ ssrc ‖ monotonic_packet_counter`. The
  counter never repeats under one key; the session **rekeys** (re-derives via the exporter
  with a bumped epoch) well before counter exhaustion or on a time/byte budget.
- **Anti-replay:** a 64-bit sliding-window replay filter keyed on the packet counter (à la
  IPsec). The window is **advanced only after the AEAD tag verifies** (RFC 3711 §3.3 order:
  replay-check → authenticate → update). The counter is read from the unauthenticated
  header, so advancing the high-water mark *before* authentication would let a single
  corrupted or forged packet jump it far ahead, after which every legitimate packet is
  rejected as "too old" — a permanent denial of the whole stream. Failed-auth packets leave
  the window untouched. Replays and out-of-window packets are dropped before decode.

## 3. UDP session binding

UDP packets are not individually authenticated to a *user* beyond the transport session.
Binding works as:

1. `AuthResult.udp_token` (issued over TLS) is a short-lived, single-use, random token tied
   to `session_id`.
2. Client's first UDP message is `UdpBinding{udp_token}`, sent as the first AEAD media frame
   using the keys exported from the TLS session.
3. Server validates the token, binds the **5-tuple → session_id**, and discards the token.
4. Thereafter, frames are accepted only on that bound tuple; ssrcs are checked against the
   streams the session announced. Source-address spoofing can't hijack a session because the
   attacker lacks the media key and the token.

## 4. Authentication & accounts (settled: guests + local accounts)

- **Guests:** toggled by server config (`allow_guests`). A guest picks a nickname and joins;
  no persistent identity. Nicknames are non-reserved and may be uniquified by the server.
- **Local accounts:** username + password, **admin-provisioned** (no self-serve registration
  in v1). An admin creates/resets/deletes accounts either via the `voicecat-admin` CLI or the
  in-app admin interface, which sends the privileged `CreateAccount`/`ResetPassword`/
  `DeleteAccount` messages (protocol.md §3, permission-gated). Stored in **SQLite**; passwords
  hashed with **Argon2id** (via libsodium `crypto_pwhash`) using per-install-tuned memory/time
  parameters; never stored or logged in plaintext. Verification runs on the worker pool (it's
  deliberately slow) to avoid stalling the net thread.
- **Channel passwords:** hashed at rest with **BLAKE2b** (libsodium `crypto_generichash`) plus a per-channel salt. BLAKE2b is used instead of Argon2id here because channel-password checks happen on the net thread during `JoinChannelRequest`; a slow hash would block real-time message processing. The password itself still crosses the wire only inside TLS 1.3.
- **Brute-force defense:** per-IP and per-account rate limiting on auth attempts with
  exponential backoff; configurable lockout. Generic `auth_request` failures return a
  non-enumerating error ("invalid credentials") to avoid username probing.

```
accounts(   id INTEGER PK, username TEXT UNIQUE,
            pw_argon2id TEXT,            -- encoded hash incl. params + salt
            created_at, last_login, flags )
bans(       id, subject_type, subject, reason, expires_at, created_at )
```

## 5. Permissions (scaffold for v1, enforced server-side)

A `Permissions` set is attached to each session at auth time and is the *only* authority —
clients never self-grant. v1 needs a minimal set (join channel, send text, create temporary
channel, kick/move if moderator); the model is a role/flag bitset that the moderation
milestone expands. All privileged operations (`CreateChannel`, `Kick`, `Ban`,
`MoveUser`, server-mute) are checked against it server-side regardless of client UI.

## 6. Threat model & non-goals

**In scope:**
- Passive eavesdropping on either transport → defeated by TLS 1.3 (control) and the
  exported-key AEAD (media).
- Active MITM on first connect → mitigated by TOFU pin + Ed25519 identity (user must verify
  fingerprint out-of-band for the strongest guarantee).
- UDP source spoofing / session hijack → defeated by token binding + media-key secrecy +
  anti-replay.
- Password theft at rest → mitigated by Argon2id; in transit → only inside TLS.

**Explicit non-goals (v1):**
- **End-to-end encryption between users.** The server relays Opus and can see who talks to
  whom; with the SFU relay it does *not* decode audio, but the media key is per
  client↔server, not per pair. True E2EE (server can't read media) is a possible future
  feature, not v1.
- Anonymity / metadata hiding. The server, by design, knows the channel graph.
- DoS resilience at scale beyond basic rate limiting and the bounded-frame guards.

## 7. Crypto dependency summary

Two libraries, both permissive (no GPL/LGPL), so a future closed-source distribution stays
clean:

- **TLS 1.3:** **mbedTLS 3.6 LTS** (Apache-2.0). Control-channel TLS + the keying-material
  exporter that seeds the media path. Static-links into a single binary. (OpenSSL 3.x,
  Apache-2.0, is an interchangeable alternative.)
- **Primitives & password hashing:** **libsodium** (ISC) — Argon2id (account passwords),
  ChaCha20-Poly1305 (the media AEAD), Ed25519 (server identity), X25519, secure RNG. All
  non-TLS crypto goes through libsodium so we never hand-roll a primitive.
