# 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. The server has a long-lived **Ed25519 identity key**; its fingerprint is shown to the user on first connect (like SSH host keys / TeamSpeak server keys) and pinned locally. Subsequent connects verify the pin; a changed key warns loudly. The TLS cert is self-signed and bound to this identity key. 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. 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 sliding-window replay filter per ssrc (à la IPsec) keyed on the packet counter. 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 too; join attempts compare server-side. - **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 ) ``` ## 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.