REALITY.5b: byte-matching ServerHello emission + server key schedule (4588+X25519, round-trip proven) #85

Closed
vxfemboy wants to merge 7 commits from feat/reality-5b-serverhello-emit into feat/reality-5a-dest-flight-capture
vxfemboy commented 2026-07-18 01:14:36 +00:00 (Migrated from github.com)

REALITY.5b — byte-matching ServerHello emission + server key schedule

Stacked on #83 (REALITY.5a). Base is feat/reality-5a-dest-flight-capture; review the 5b delta here, merge after #83. PR 2 of 4 for REALITY.5 (#76).

The server-side counterpart to REALITY.2's client. Given the dest template captured in 5a, the relay emits a byte-matching ServerHello — structurally identical to dest's (cipher, ordered extensions incl. GREASE, chosen group) but keyed on the relay's own ephemeral, so the relay holds the session keys. A passive DPI byte-matches the cleartext ServerHello; dest's real key_share/random are per-connection random too, so substituting the relay's own values is indistinguishable.

What's here

  • yip-utls::server::server_key_share — the relay's server-side KEX.
    • Group 4588 (X25519MLKEM768): ML-KEM Encapsulate against the client's ek (the mirror of REALITY.2's client Decapsulate) + X25519 DH; server_key_share = ct(1088) ‖ x25519_pub(32), shared = mlkem_ss ‖ x25519_ss — the same combiner order the client uses.
    • Group 29 (X25519): plain X25519.
    • Any other group → Err(UnsupportedGroup) (5d degrades to splice-only; P256/P384 + HelloRetryRequest deferred to #84).
  • yip-utls::server::emit_server_hello — rebuilds the ServerHello verbatim from the captured ServerHelloShape, substituting only a fresh 32-byte random, the client's legacy_session_id echo, and the key_share ext body (the relay's own). Derives handshake keys over transcript = ClientHello ‖ ServerHello via REALITY.2's derive_handshake_keys (reused unchanged).
  • yip-rendezvous::realityClientHelloInfo.key_share_mlkem_ek extracted from the group-4588 key_share entry (mlkem_ek(1184) ‖ x25519(32)), fail-closed on a hostile/malformed ClientHello. This is what 5d feeds to emit_server_hello.

Correctness gate: the round-trip

The proof is a genuine round-trip (not a tautology): a test client — REALITY.2's own parse_server_hello + the client KEX (decapsulate/DH, separate RNGs) + derive_handshake_keys — parses the relay's emitted ServerHello and derives the byte-identical HandshakeKeys, for both 4588 and X25519. If the combiner order or X25519 direction were wrong, no keys would match. Plus a byte-match test asserting only random/session_id/key_share differ from the template (GREASE and extension order preserved).

Scope / non-goals

  • 4588 + X25519 only. P256/P384 + HelloRetryRequest → #84.
  • No encrypted-flight emission (5c) and no tls_front wiring (5d) here — additive, server-side only.

5d hand-off notes

  • group-4588 X25519 sourcing: the DH's X25519 half should key against the x25519 bundled in the 4588 entry (…‖ x25519(32) tail), not the standalone 0x001d entry. Safe within 5b because yip's own client reuses one ephemeral across both shares; 5d must either thread the 4588-tail x25519 through or assert that invariant. Fails closed (dead session), no security downgrade, if ever violated.
  • RNG: server_key_share's rng drives ML-KEM encapsulation + the X25519 ephemeral; 5d must wire an OS-CSPRNG-backed RandomSource.

Tests

  • server_key_share shapes/secret-lengths for 4588 + 29; unsupported group + missing ek → Err.
  • emit_server_hello round-trips (4588 + 29) with identical keys; byte-match except substituted fields; u24 body / u16 ext length guards.
  • parse_client_hello extracts the 1184-byte mlkem_ek; wrong-length 4588 entry → None; distinct x25519 in the two entries proves key_share_x25519 stays sourced from 0x001d.

Final whole-branch review (opus): READY TO MERGE — KEX verified end-to-end, every network-reachable path fail-closed, ServerHello byte-fidelity preserved, forbid-unsafe / no as / no bare #[allow].

## REALITY.5b — byte-matching ServerHello emission + server key schedule **Stacked on #83 (REALITY.5a).** Base is `feat/reality-5a-dest-flight-capture`; review the 5b delta here, merge after #83. PR 2 of 4 for REALITY.5 (#76). The server-side counterpart to REALITY.2's client. Given the `dest` template captured in 5a, the relay emits a **byte-matching ServerHello** — structurally identical to `dest`'s (cipher, ordered extensions incl. GREASE, chosen group) but keyed on the **relay's own** ephemeral, so the relay holds the session keys. A passive DPI byte-matches the cleartext ServerHello; `dest`'s real `key_share`/`random` are per-connection random too, so substituting the relay's own values is indistinguishable. ### What's here - **`yip-utls::server::server_key_share`** — the relay's server-side KEX. - Group **4588 (X25519MLKEM768):** ML-KEM **Encapsulate** against the client's ek (the mirror of REALITY.2's client `Decapsulate`) + X25519 DH; `server_key_share = ct(1088) ‖ x25519_pub(32)`, `shared = mlkem_ss ‖ x25519_ss` — the **same combiner order** the client uses. - Group **29 (X25519):** plain X25519. - Any other group → `Err(UnsupportedGroup)` (5d degrades to splice-only; P256/P384 + HelloRetryRequest deferred to #84). - **`yip-utls::server::emit_server_hello`** — rebuilds the ServerHello verbatim from the captured `ServerHelloShape`, substituting **only** a fresh 32-byte random, the client's `legacy_session_id` echo, and the `key_share` ext body (the relay's own). Derives handshake keys over transcript = ClientHello ‖ ServerHello via REALITY.2's `derive_handshake_keys` (reused unchanged). - **`yip-rendezvous::reality`** — `ClientHelloInfo.key_share_mlkem_ek` extracted from the group-4588 key_share entry (`mlkem_ek(1184) ‖ x25519(32)`), fail-closed on a hostile/malformed ClientHello. This is what 5d feeds to `emit_server_hello`. ### Correctness gate: the round-trip The proof is a genuine round-trip (not a tautology): a test **client** — REALITY.2's own `parse_server_hello` + the client KEX (decapsulate/DH, **separate RNGs**) + `derive_handshake_keys` — parses the relay's emitted ServerHello and derives the **byte-identical** `HandshakeKeys`, for **both** 4588 and X25519. If the combiner order or X25519 direction were wrong, no keys would match. Plus a byte-match test asserting only random/session_id/key_share differ from the template (GREASE and extension order preserved). ### Scope / non-goals - 4588 + X25519 only. P256/P384 + HelloRetryRequest → #84. - No encrypted-flight emission (5c) and no `tls_front` wiring (5d) here — additive, server-side only. ### 5d hand-off notes - **group-4588 X25519 sourcing:** the DH's X25519 half should key against the x25519 **bundled in the 4588 entry** (`…‖ x25519(32)` tail), not the standalone `0x001d` entry. Safe within 5b because yip's own client reuses one ephemeral across both shares; 5d must either thread the 4588-tail x25519 through or assert that invariant. Fails **closed** (dead session), no security downgrade, if ever violated. - **RNG:** `server_key_share`'s `rng` drives ML-KEM encapsulation + the X25519 ephemeral; 5d must wire an OS-CSPRNG-backed `RandomSource`. ### Tests - `server_key_share` shapes/secret-lengths for 4588 + 29; unsupported group + missing ek → `Err`. - `emit_server_hello` round-trips (4588 + 29) with identical keys; byte-match except substituted fields; u24 body / u16 ext length guards. - `parse_client_hello` extracts the 1184-byte mlkem_ek; wrong-length 4588 entry → `None`; distinct x25519 in the two entries proves `key_share_x25519` stays sourced from `0x001d`. Final whole-branch review (opus): **READY TO MERGE** — KEX verified end-to-end, every network-reachable path fail-closed, ServerHello byte-fidelity preserved, `forbid-unsafe` / no `as` / no bare `#[allow]`.

Pull request closed

Sign in to join this conversation.
No description provided.