yip_utls: hello::craft GREASE bookends collide ~6% (RFC8446 §4.2 violation + fingerprint tell) #78

Closed
opened 2026-07-16 10:23:16 +00:00 by vxfemboy · 0 comments
vxfemboy commented 2026-07-16 10:23:16 +00:00 (Migrated from github.com)

Summary

yip_utls::hello::craft (REALITY.2, crates/yip-utls/src/hello.rs) draws the leading and trailing GREASE extension codepoints independently (two separate write_grease_extension/grease(rng) calls, ~lines 127 and 132). Since GREASE has only 16 valid values, the two bookends collide ~1/16 (~6%) of the time — producing two extensions with the same extension type.

This was discovered while wiring the REALITY client into yipd's relay-dial (REALITY.4a); the netns/in-process tests there had to add retry-accept loops to tolerate the ~6% reject rate, which is a symptom, not a fix.

Why it matters (two real consequences)

  1. Malformed ClientHello → RFC 8446 §4.2 violation. Two extensions of the same type in a single ClientHello is illegal; a strict TLS 1.3 server rejects it with DECODE_ERROR. So ~6% of REALITY handshakes fail outright. In production this self-heals via reconnect-with-backoff, but it's a reliability tax (~6% of connections need a retry) — bad for a latency-sensitive tunnel.
  2. Fingerprint tell. Real Chrome/BoringSSL guarantees the two extension GREASE values differ (index-based distinctness). Identical bookends are therefore something real Chrome never emits — a positive "not-Chrome" signal to a sophisticated DPI. (Note: JA3/JA4 normalize GREASE away, so this is invisible to JA3/JA4 but visible to a raw byte-level fingerprinter.)

For an anti-censorship transport whose entire premise is byte-faithful Chrome mimicry, emitting a malformed, non-Chrome hello 6% of the time is a genuine liability.

Note

The same file already coordinates a single drawn GREASE value across the supported_groups/key_share pair — so the authors clearly know GREASE codepoints must be coordinated; the extension bookends were simply missed.

Fix

  • In hello::craft, draw the trailing GREASE codepoint distinct from the leading one (mirror BoringSSL's index-based distinctness — e.g. pick two distinct indices into the 16 GREASE values).
  • Add a regression test asserting leading_grease != trailing_grease across many seeds.
  • Re-run the JA4-stability diff test (JA4 should be unaffected since it normalizes GREASE; confirm no drift).

References

  • crates/yip-utls/src/hello.rs (craft, the two extension-GREASE bookend calls)
  • RFC 8701 (GREASE), RFC 8446 §4.2 (no duplicate extension types)
  • Surfaced by REALITY.4a (yipd client wiring); reviewer severity: Important.
## Summary `yip_utls::hello::craft` (REALITY.2, `crates/yip-utls/src/hello.rs`) draws the **leading** and **trailing** GREASE *extension* codepoints **independently** (two separate `write_grease_extension`/`grease(rng)` calls, ~lines 127 and 132). Since GREASE has only 16 valid values, the two bookends **collide ~1/16 (~6%)** of the time — producing two extensions with the **same extension type**. This was discovered while wiring the REALITY client into yipd's relay-dial (REALITY.4a); the netns/in-process tests there had to add retry-accept loops to tolerate the ~6% reject rate, which is a symptom, not a fix. ## Why it matters (two real consequences) 1. **Malformed ClientHello → RFC 8446 §4.2 violation.** Two extensions of the same type in a single ClientHello is illegal; a strict TLS 1.3 server rejects it with `DECODE_ERROR`. So ~6% of REALITY handshakes fail outright. In production this self-heals via reconnect-with-backoff, but it's a **reliability tax** (~6% of connections need a retry) — bad for a latency-sensitive tunnel. 2. **Fingerprint tell.** Real Chrome/BoringSSL **guarantees** the two extension GREASE values differ (index-based distinctness). Identical bookends are therefore something real Chrome *never* emits — a positive "not-Chrome" signal to a sophisticated DPI. (Note: JA3/JA4 normalize GREASE away, so this is invisible to JA3/JA4 but visible to a raw byte-level fingerprinter.) For an anti-censorship transport whose entire premise is byte-faithful Chrome mimicry, emitting a malformed, non-Chrome hello 6% of the time is a genuine liability. ## Note The same file already coordinates a *single* drawn GREASE value across the `supported_groups`/`key_share` pair — so the authors clearly know GREASE codepoints must be coordinated; the extension bookends were simply missed. ## Fix - In `hello::craft`, draw the **trailing** GREASE codepoint **distinct from** the leading one (mirror BoringSSL's index-based distinctness — e.g. pick two distinct indices into the 16 GREASE values). - Add a **regression test** asserting `leading_grease != trailing_grease` across many seeds. - Re-run the **JA4-stability** diff test (JA4 should be unaffected since it normalizes GREASE; confirm no drift). ## References - `crates/yip-utls/src/hello.rs` (`craft`, the two extension-GREASE bookend calls) - RFC 8701 (GREASE), RFC 8446 §4.2 (no duplicate extension types) - Surfaced by REALITY.4a (yipd client wiring); reviewer severity: Important.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
femboy/yip#78
No description provided.