REALITY.4b: client-side relay verification (ECDSA-P256 CertificateVerify, default ON, fail-closed) #82

Merged
vxfemboy merged 9 commits from feat/reality-4b-relay-verify into main 2026-07-17 15:02:00 +00:00
vxfemboy commented 2026-07-17 14:32:19 +00:00 (Migrated from github.com)

REALITY.4b — client-side relay verification (default ON)

PR 2 of the REALITY.4 pair (stacked on REALITY.4a, merged). Closes the REALITY loop: a yip client cryptographically verifies the relay holds relay_reality_priv — not a MITM/impostor — without weakening the un-authed-prober camouflage. verify=on|off on the reality:// URL, default ON, client-local (never negotiated on the wire).

Mechanism

The only value tied to relay_reality_priv is the seal's ECDH shared secret (X25519(client_eph, relay_reality_priv), which the client also computes). So:

  • Server always binds on each authed connection: derive an ECDSA-P256 key from shared (HKDF-Expand(shared,"yip-reality-cert-v1",48B) → constant-time wide reduction mod the P-256 order n, domain-separated from the seal AEAD key), re-forge the borrowed-identity leaf per-connection with it (cached StolenFields, no dest re-fetch), and let BoringSSL sign the standard CertificateVerify with it.
  • Client (hand-rolled yip_utls) re-derives the same key, pins the leaf to it, and verifies the CertificateVerify — scheme hard-pinned ecdsa_secp256r1_sha256 (the announced algorithm is never read → no algorithm confusion), RFC 8446 §4.4.3 signed content, fail-closed on every edge with no hostile-byte panic, run before any yip Register frame.

A MITM holds neither private key, cannot compute shared, and (transcript binding) cannot relay/replay/reflect the genuine relay's signature → cannot be accepted. On failure the client sends a browser-faithful TLS alert and jittered give-up (300s base + 0–120s, distinct from the sub-5s ladder → no fleet-wide fixed tell), revealing no yip-specific bytes.

Why ECDSA-P256 (not Ed25519)

An adversarial advisor + cross-model review caught that Ed25519 would break the cleartext Chrome ClientHello fingerprint (Chrome doesn't advertise ed25519) — a passive, keyless distinguisher that defeats REALITY. ECDSA-P256 (ecdsa_secp256r1_sha256, already in the Chrome-faithful sig-algs) keeps the ClientHello byte-faithful (JA4 diff stays green); it's what real Xray uses.

Proven end-to-end

The netns verify=on money test (two UDP-blocked peers tunnel through a real REALITY relay) + wrong-relay-key fail-closed negative (wrong key → seal-open fails → spliced to dest → no tunnel, no retry-storm) ran live under sudo. The money test cross-validates the CertVerify §4.4.3 boundary against BoringSSL's signer (an independent-stack KAT in practice). Plus unit tests: client/server derive the same key; verify accepts a correct binder and rejects wrong-key / bad-sig / missing-CertVerify; a KAT guards the transcript boundary.

Preserved

Un-authed-prober camouflage unchanged (spliced to dest); REALITY.3 anti-replay untouched and still runs before the per-connection re-forge (a replay can't force a bind); verify=false / the 4a tls:// path / the sync data plane byte-unchanged; JA3/JA4 fidelity intact.

Deferred (documented)

  • Cert-flight SIZE-padding → REALITY.5 (#76). The re-forged single self-signed leaf is shorter than dest's real leaf+chain; the encrypted Certificate-message length is a residual distinguisher (pre-existing since REALITY.3; not introduced here). REALITY.5 closes it holistically with the server flight.
  • Alert value live-capture + middlebox-CCS-before-encrypted-alert fidelity → REALITY.5.
  • Non-blocking nits: a hermetic byte-vector CertVerify KAT; simplify the wide reduction to p256's hash2curve; rename decide_authed's sni_has_acceptor param.

Tests

yip-utls 47 + KATs, yip-rendezvous 80, yipd 198+27 (incl. live netns) — all green. clippy --workspace -D warnings + fmt clean. forbid-unsafe (outside yip-io/yip-device); no as; no bare #[allow].

## REALITY.4b — client-side relay verification (default ON) PR 2 of the REALITY.4 pair (stacked on REALITY.4a, merged). Closes the REALITY loop: a yip client **cryptographically verifies the relay holds `relay_reality_priv`** — not a MITM/impostor — without weakening the un-authed-prober camouflage. `verify=on|off` on the `reality://` URL, **default ON**, client-local (never negotiated on the wire). ### Mechanism The only value tied to `relay_reality_priv` is the seal's ECDH `shared` secret (`X25519(client_eph, relay_reality_priv)`, which the client also computes). So: - **Server always binds** on each authed connection: derive an **ECDSA-P256** key from `shared` (`HKDF-Expand(shared,"yip-reality-cert-v1",48B)` → constant-time wide reduction mod the P-256 order n, domain-separated from the seal AEAD key), **re-forge the borrowed-identity leaf per-connection** with it (cached `StolenFields`, no dest re-fetch), and let BoringSSL sign the standard `CertificateVerify` with it. - **Client** (hand-rolled `yip_utls`) re-derives the same key, **pins the leaf** to it, and verifies the `CertificateVerify` — scheme **hard-pinned** `ecdsa_secp256r1_sha256` (the announced algorithm is never read → no algorithm confusion), RFC 8446 §4.4.3 signed content, **fail-closed on every edge** with no hostile-byte panic, run **before** any yip Register frame. A MITM holds neither private key, cannot compute `shared`, and (transcript binding) cannot relay/replay/reflect the genuine relay's signature → cannot be accepted. On failure the client sends a browser-faithful TLS alert and **jittered give-up** (300s base + 0–120s, distinct from the sub-5s ladder → no fleet-wide fixed tell), revealing no yip-specific bytes. ### Why ECDSA-P256 (not Ed25519) An adversarial advisor + cross-model review caught that Ed25519 would break the **cleartext** Chrome ClientHello fingerprint (Chrome doesn't advertise `ed25519`) — a passive, keyless distinguisher that defeats REALITY. ECDSA-P256 (`ecdsa_secp256r1_sha256`, already in the Chrome-faithful sig-algs) keeps the ClientHello byte-faithful (JA4 diff stays green); it's what real Xray uses. ### Proven end-to-end The netns **verify=on money test** (two UDP-blocked peers tunnel through a real REALITY relay) + **wrong-relay-key fail-closed negative** (wrong key → seal-open fails → spliced to dest → no tunnel, no retry-storm) **ran live under sudo**. The money test cross-validates the CertVerify §4.4.3 boundary against BoringSSL's signer (an independent-stack KAT in practice). Plus unit tests: client/server derive the same key; verify accepts a correct binder and rejects wrong-key / bad-sig / missing-CertVerify; a KAT guards the transcript boundary. ### Preserved Un-authed-prober camouflage unchanged (spliced to dest); **REALITY.3 anti-replay untouched** and still runs before the per-connection re-forge (a replay can't force a bind); `verify=false` / the 4a `tls://` path / the sync data plane byte-unchanged; JA3/JA4 fidelity intact. ### Deferred (documented) - **Cert-flight SIZE-padding → REALITY.5 (#76).** The re-forged single self-signed leaf is shorter than dest's real leaf+chain; the *encrypted* Certificate-message length is a residual distinguisher (pre-existing since REALITY.3; not introduced here). REALITY.5 closes it holistically with the server flight. - Alert value live-capture + middlebox-CCS-before-encrypted-alert fidelity → REALITY.5. - Non-blocking nits: a hermetic byte-vector CertVerify KAT; simplify the wide reduction to p256's `hash2curve`; rename `decide_authed`'s `sni_has_acceptor` param. ### Tests `yip-utls` 47 + KATs, `yip-rendezvous` 80, `yipd` 198+27 (incl. live netns) — all green. `clippy --workspace -D warnings` + `fmt` clean. `forbid-unsafe` (outside yip-io/yip-device); no `as`; no bare `#[allow]`.
Sign in to join this conversation.
No description provided.