|
| 1 | +# Threat model |
| 2 | + |
| 3 | +Phase 0 of `docs/AUDIT-PLAN.md`. One page, adversary first. Tessera is a single-implementation research |
| 4 | +prototype, wire format v0, unaudited; nothing below has been independently reviewed. Claims about mechanisms |
| 5 | +are made against the code in `core/src/main/kotlin/tessera/core/`, not the prose docs; where the two disagree, |
| 6 | +the code is cited and the disagreement noted. |
| 7 | + |
| 8 | +## 1. Assets protected |
| 9 | + |
| 10 | +- **Payload confidentiality**, including against a recorder who stores traffic today and decrypts with a |
| 11 | + quantum computer later. The handshake secret is hybrid X25519 + ML-KEM-768; breaking confidentiality of a |
| 12 | + recorded session requires breaking both. |
| 13 | +- **Payload and header integrity.** Every post-handshake packet is ChaCha20-Poly1305 AEAD with the header as |
| 14 | + AAD; an on-path modification fails authentication and the packet is dropped. |
| 15 | +- **Connection identity.** Packets are bound to a connection by keys, not by address; only a holder of the |
| 16 | + connection's keys (or of the server's stateless-reset secret) can terminate or speak on a connection. |
| 17 | +- **Bounded reflection.** A spoofed source cannot use the server as a >3x amplifier, and cannot (beyond fixed |
| 18 | + rate budgets) use it as a CPU oracle for ML-KEM decapsulations (`AddressValidation.kt`). |
| 19 | + |
| 20 | +**Not protected:** |
| 21 | + |
| 22 | +- **Traffic analysis.** Packet sizes, timing, rates, and direction are visible. No padding beyond the minimum |
| 23 | + needed for the header-protection sample; no cover traffic. |
| 24 | +- **Connection linkability at the network layer.** The 4-byte short connection id rides in the clear on every |
| 25 | + short packet (deliberately, per `PacketCrypto.kt`: header protection masks only the packet number and two |
| 26 | + flag bits; the pathId bits and connId stay readable for load balancers). An observer can trivially track a |
| 27 | + connection across its lifetime, and across a client rebind if the connId is kept. |
| 28 | +- **Peer anonymity.** IP addresses are not hidden; that is not this layer's job. |
| 29 | +- **Full forward secrecy of the first flight.** The session key is DH(client-ephemeral, server-static) plus |
| 30 | + KEM against the server-static ML-KEM key (`Handshake.kt`). There is no server ephemeral in the exchange — |
| 31 | + the code is "Noise-IK-shaped", not full IK with an `ee` token as SPEC's shorthand might suggest — so an |
| 32 | + adversary who later obtains **both** server static private keys and recorded the traffic recovers the |
| 33 | + session key. Key rotation (`secret_{n+1} = HKDF(secret_n)`) chains forward from that same secret and does |
| 34 | + not repair this. Resumed sessions inherit the same bound via the ticket (below). |
| 35 | + |
| 36 | +## 2. Adversaries |
| 37 | + |
| 38 | +**Passive recorder (incl. harvest-now-decrypt-later).** Records everything, forever. Stopped by the hybrid |
| 39 | +key exchange: recovering plaintext requires the X25519 shared secret *and* the ML-KEM-768 shared secret |
| 40 | +(`Handshake.kt` concatenates both into HKDF). A future quantum attacker breaks X25519 but not ML-KEM; a |
| 41 | +classical attacker with a lattice break gets the KEM but not X25519. Caveat: a recorder who later steals the |
| 42 | +server's static keys wins (see forward-secrecy note above); a recorder who later steals a ticket key reads |
| 43 | +that ticket's resumed 0-RTT flights. |
| 44 | + |
| 45 | +**On-path active attacker.** Can drop, delay, reorder, modify, inject. Modification and injection fail the |
| 46 | +AEAD (per-attempt forgery 2^-128 at tag 16, 2^-64 at tag 8). Flipping the key-phase bit to provoke a spurious |
| 47 | +rotation is specifically handled: `KeyPhaseState.open` follows an update only after a packet authenticates |
| 48 | +under the pre-derived next-generation keys. Dropping and delaying are availability attacks and are not |
| 49 | +prevented — an on-path attacker can always kill a connection. |
| 50 | + |
| 51 | +**Off-path spoofer.** Sends packets with forged sources. Cannot forge data packets (no keys), cannot forge a |
| 52 | +stateless reset (the token is delivered inside the encrypted handshake reply and is an HMAC under a |
| 53 | +server-held secret, `StatelessReset.kt`), cannot amplify (3x bound before path validation, `PathValidation`; |
| 54 | +a Retry packet at 31 B against a >=1.2 KB initial is a deamplifier), and cannot burn KEM CPU past the fixed |
| 55 | +per-source and global token buckets — under pressure an unvalidated source gets a Retry token it never |
| 56 | +receives at a spoofed address (`AddressValidation.kt`). Known residue, documented in SPEC "Address |
| 57 | +validation": the Retry itself is unauthenticated, so an off-path guesser can inject one; the client accepts |
| 58 | +at most one per connect, from the addressed server only, and its retransmit train continues — cost is one |
| 59 | +extra initial, not a failed connect. |
| 60 | + |
| 61 | +**Malicious peer.** Holds valid keys; the crypto does not constrain it. It can lie in every frame: ACK |
| 62 | +inflation, credit/grant manipulation, ECN lies, bogus PMTUD, junk frames. Defences are parsers hardened by |
| 63 | +fuzzing (findings recorded in-code, e.g. the empty-packet check in `PacketCrypto.checkShort`), bounded |
| 64 | +decoder state, and rate/credit sanity limits — but a peer-driven resource-exhaustion review is exactly what |
| 65 | +an audit is for. Assume a malicious peer can waste this endpoint's memory and CPU up to the audited bounds |
| 66 | +and no confidentiality beyond its own connection. |
| 67 | + |
| 68 | +**Restarted / keyless server.** A server that crashed and lost all connection state can still emit a valid |
| 69 | +stateless reset: the reset secret derives from the operator-provided ticket key, which survives restarts, and |
| 70 | +the token is HMAC(secret, shortConnId) where the connId is readable on the client's retransmits |
| 71 | +(`StatelessReset.kt`). Reset emission is itself bounded (min-size packet requirement plus a 2000/s global |
| 72 | +bucket, SPEC "stateless reset") so the mechanism is not a reflector. The failure mode without it — client |
| 73 | +retransmits into a black hole until the 10 s idle timeout — is availability, not secrecy. |
| 74 | + |
| 75 | +## 3. Out of scope |
| 76 | + |
| 77 | +- **Key distribution.** Noise IK premise: the client already holds the server's static X25519 and ML-KEM |
| 78 | + public keys, obtained out of band (pinning, TOFU, delegated credential). There is no PKI, no certificates, |
| 79 | + no in-band identity. A client that accepts the wrong static key has lost before the first packet. |
| 80 | +- **DoS beyond the stated bounds.** The 3x amplification bound, the KEM admission budgets, and the reset/Retry |
| 81 | + rate caps are the whole DoS story. Volumetric flooding of the link, state exhaustion by a peer with valid |
| 82 | + keys past audited limits, and everything an on-path attacker can do by dropping are out of scope. |
| 83 | +- **Side channels beyond constant-time primitives.** Tag and token comparisons are constant-time |
| 84 | + (`Arrays.constantTimeAreEqual`, the local `eq` in `AddressValidation.kt`); the crypto is BouncyCastle's. |
| 85 | + Cache-timing of the JVM, the Rust SIMD datapath, and ML-KEM implementation side channels are not analyzed. |
| 86 | +- **Traffic analysis** of sizes, timing, and connection linkage (Section 1). |
| 87 | + |
| 88 | +## 4. Known accepted weaknesses |
| 89 | + |
| 90 | +- **`tagLen = 8` truncated-tag mode.** Negotiated in `ConnParams` (`Params.kt`): forgery ~2^-64 per attempt, |
| 91 | + documented in-code as acceptable for media/game state, **not for transactions**. Truncation happens on the |
| 92 | + wire only; the keystream and confidentiality argument are unchanged (SPEC v0.9). The truncated-tag open |
| 93 | + path recomputes the full Poly1305 tag and compares a prefix in constant time (`PacketCrypto.openTruncated`). |
| 94 | +- **±10 s 0-RTT replay window with a seen-set.** `ZeroRtt.Server` and `Resumption.Server` reject timestamps |
| 95 | + outside ±10 s and any fingerprint already seen inside the window. The fingerprint is 8 bytes (first 8 of |
| 96 | + the random ePub; ticket-nonce xor client-nonce for resumption) — collision, not forgery, resistance; the |
| 97 | + seen-set is pruned only above 100k entries. Within-window replay from a clock-skewed pair, and the fact |
| 98 | + that 0-RTT data must be idempotent at the application layer regardless, are accepted and documented in the |
| 99 | + code. A resumed connection additionally binds ts+nonce into the session key so a replayed ticket under a |
| 100 | + fresh nonce yields a distinct key (`Resumption.sessionKey`). |
| 101 | +- **Single implementation, no interop evidence.** The v0 wire format has never been parsed by an independent |
| 102 | + decoder (AUDIT-PLAN Phase 2 / TODO item 7). Every compatibility claim is self-referential. |
| 103 | +- **No audit.** No external cryptographic or implementation review has occurred. The handshake deviates from |
| 104 | + proven Noise IK (hybrid KEM injection, no server ephemeral, 0-RTT layer) and those deltas are undocumented |
| 105 | + formally — that is Phase 1. |
| 106 | + |
| 107 | +## 5. Mechanism inventory |
| 108 | + |
| 109 | +| Mechanism | Where | Notes | |
| 110 | +|---|---|---| |
| 111 | +| Hybrid handshake: X25519 + ML-KEM-768 into HKDF-SHA256 | `core/Handshake.kt` | Client ephemeral only; server static keys pinned out of band | |
| 112 | +| Packet AEAD (ChaCha20-Poly1305), header protection, truncated-tag open | `core/PacketCrypto.kt` (`PacketKeys`, `PacketProtection`) | RFC 9001 §5.4-shape HP; flags mask 0x63 leaves connId/pathId in clear by design | |
| 113 | +| Key update via phase bit; one retained previous generation; follow only after next-key auth | `core/PacketCrypto.kt` (`KeyPhaseState`) | HP key fixed at generation 0 for the connection's lifetime | |
| 114 | +| Automatic rotation policy: 2^20 packets or 1 GiB sealed per tx generation | SPEC v0.9; trigger in `transport` `transmit`, machinery `KeyPhaseState` | Policy, not a derived AEAD limit; counters freeze while an update is pending | |
| 115 | +| 0-RTT replay defence: ±10 s window + seen-set | `core/ZeroRtt.kt`, `core/Resumption.kt` | App-layer idempotence still required | |
| 116 | +| Stateless resumption tickets (server-encrypted, 7-day lifetime) | `core/Resumption.kt` | Ticket-key compromise reads resumed 0-RTT flights | |
| 117 | +| Stateless reset tokens: HMAC(derived secret, connId), constant-time match | `core/StatelessReset.kt` | Token delivered inside encrypted handshake reply; survives restart via ticket key | |
| 118 | +| Amplification bound (3x before path validation) + path challenge | `core/PathValidation.kt` | Bounds reflected bytes, not CPU | |
| 119 | +| KEM admission control: per-source/global buckets, pressure-triggered Retry tokens | `core/AddressValidation.kt` | Bounds CPU; fixed 8192-slot keyed table, no growth | |
0 commit comments