nx_tls13_chrome_hello.nx source
↩ module page · 107 lines · 7823 B
1// nx_tls13_chrome_hello.nx -- emit a ClientHello whose JA3 fingerprint MATCHES a real Chrome (~v120), so
2// Cloudflare-class anti-bot CDNs (which drop our minimal 2-cipher/6-extension/no-GREASE hello at the ClientHello)
3// classify us as a browser and complete the handshake. This is R2f-A (the curl-impersonate/uTLS approach, native).
4// ADDITIVE (Cardinal 19): the fleet's live tls13_client_hello_emit2 is UNTOUCHED; only the manga-fetch path calls
5// this. Emitted INLINE (not via the shared ext emitters) for byte-exact JA3 control. GREASE (0x0a0a/0x1a1a) is
6// placed in ciphers[0] + a leading extension + supported_groups[0] + key_share[0] + supported_versions[0] (JA3
7// filters GREASE, so the specific value is irrelevant to the hash). Cipher order + extension order + curves +
8// point-formats reproduce Chrome's JA3 string exactly (gated in nx_tls13_chrome_hello_gate: our JA3 == the known
9// Chrome JA3). Cipher coverage: we advertise Chrome's full list incl 0x1302 (AES-256-GCM) -- with nx_aes256_gcm
10// now shipped we can complete whichever TLS-1.3 suite CF picks (0x1301/0x1302/0x1303). license_tier: ORIGINAL
11import "nx_syscalls.nx"
12
13const CH_GREASE_A: i64 = 0x0a0a
14const CH_GREASE_B: i64 = 0x1a1a
15
16func ch_u16(out: *u8, o: i64, v: i64) -> i64 { out[o] = ((v >> 8) & 0xff) as u8; out[o+1] = (v & 0xff) as u8; return o + 2 }
17func ch_u8(out: *u8, o: i64, v: i64) -> i64 { out[o] = (v & 0xff) as u8; return o + 1 }
18
19// emit the extensions block into out starting at o (SNI + x25519 pubkey supplied). returns new o.
20func ch_exts(out: *u8, o0: i64, sni: *u8, sni_len: i64, pub32: *u8, p256_pub65: *u8) -> i64 {
21 var o: i64 = o0
22 // 1. GREASE (empty)
23 o = ch_u16(out, o, CH_GREASE_A); o = ch_u16(out, o, 0)
24 // 2. server_name (0x0000): list_len(2) [ type(1)=0 host_len(2) host ]
25 o = ch_u16(out, o, 0x0000)
26 o = ch_u16(out, o, sni_len + 5) // ext data len
27 o = ch_u16(out, o, sni_len + 3) // server_name_list len
28 o = ch_u8(out, o, 0) // name_type host_name
29 o = ch_u16(out, o, sni_len)
30 var i: i64 = 0; while i < sni_len { out[o] = sni[i]; o = o + 1; i = i + 1 }
31 // 3. extended_master_secret (0x0017), empty
32 o = ch_u16(out, o, 0x0017); o = ch_u16(out, o, 0)
33 // 4. renegotiation_info (0xff01): 1 byte 0x00
34 o = ch_u16(out, o, 0xff01); o = ch_u16(out, o, 1); o = ch_u8(out, o, 0)
35 // 5. supported_groups (0x000a): GREASE + x25519(29) secp256r1(23) secp384r1(24)
36 o = ch_u16(out, o, 0x000a); o = ch_u16(out, o, 10); o = ch_u16(out, o, 8)
37 o = ch_u16(out, o, CH_GREASE_A); o = ch_u16(out, o, 29); o = ch_u16(out, o, 23); o = ch_u16(out, o, 24)
38 // 6. ec_point_formats (0x000b): 1 fmt = uncompressed(0)
39 o = ch_u16(out, o, 0x000b); o = ch_u16(out, o, 2); o = ch_u8(out, o, 1); o = ch_u8(out, o, 0)
40 // 7. session_ticket (0x0023), empty
41 o = ch_u16(out, o, 0x0023); o = ch_u16(out, o, 0)
42 // 8. ALPN (0x0010): http/1.1 ONLY (JA3 does NOT hash ALPN contents, so the fingerprint is unchanged; offering
43 // only http/1.1 makes the server speak HTTP/1.1 to our HTTP/1.1 GET instead of negotiating h2 we don't parse).
44 o = ch_u16(out, o, 0x0010); o = ch_u16(out, o, 11); o = ch_u16(out, o, 9)
45 o = ch_u8(out, o, 8); out[o]=104;out[o+1]=116;out[o+2]=116;out[o+3]=112;out[o+4]=47;out[o+5]=49;out[o+6]=46;out[o+7]=49; o = o + 8 // "http/1.1"
46 // 9. status_request (0x0005): OCSP, responder+ext empty -> 01 00 00 00 00
47 o = ch_u16(out, o, 0x0005); o = ch_u16(out, o, 5); o = ch_u8(out, o, 1); o = ch_u16(out, o, 0); o = ch_u16(out, o, 0)
48 // 10. signature_algorithms (0x000d): Chrome's 8: 0403 0804 0401 0503 0805 0501 0806 0601
49 o = ch_u16(out, o, 0x000d); o = ch_u16(out, o, 18); o = ch_u16(out, o, 16)
50 o = ch_u16(out, o, 0x0403); o = ch_u16(out, o, 0x0804); o = ch_u16(out, o, 0x0401); o = ch_u16(out, o, 0x0503)
51 o = ch_u16(out, o, 0x0805); o = ch_u16(out, o, 0x0501); o = ch_u16(out, o, 0x0806); o = ch_u16(out, o, 0x0601)
52 // 11. signed_certificate_timestamp (0x0012), empty
53 o = ch_u16(out, o, 0x0012); o = ch_u16(out, o, 0)
54 // 12. key_share (0x0033): GREASE(1B) + x25519(29,32B) + secp256r1(23,65B) -- dual share avoids a HelloRetryRequest
55 // if the server prefers P-256 (JA3 does NOT hash key_share CONTENTS, so the fingerprint is unchanged).
56 o = ch_u16(out, o, 0x0033); o = ch_u16(out, o, 0x70) // ext data len = 112
57 o = ch_u16(out, o, 0x6e) // client_shares len = 110
58 o = ch_u16(out, o, CH_GREASE_A); o = ch_u16(out, o, 1); o = ch_u8(out, o, 0) // GREASE share (1 byte)
59 o = ch_u16(out, o, 29); o = ch_u16(out, o, 32) // x25519, key_len 32
60 i = 0; while i < 32 { out[o] = pub32[i]; o = o + 1; i = i + 1 }
61 o = ch_u16(out, o, 23); o = ch_u16(out, o, 65) // secp256r1, key_len 65
62 i = 0; while i < 65 { out[o] = p256_pub65[i]; o = o + 1; i = i + 1 }
63 // 13. psk_key_exchange_modes (0x002d): 01 01 (psk_dhe_ke)
64 o = ch_u16(out, o, 0x002d); o = ch_u16(out, o, 2); o = ch_u8(out, o, 1); o = ch_u8(out, o, 1)
65 // 14. supported_versions (0x002b): GREASE + 0304 (TLS 1.3)
66 o = ch_u16(out, o, 0x002b); o = ch_u16(out, o, 5); o = ch_u8(out, o, 4)
67 o = ch_u16(out, o, CH_GREASE_A); o = ch_u16(out, o, 0x0304)
68 // 15. compress_certificate (0x001b): DIAG-DISABLED (Cloudflare honors it -> sends a big compressed-cert record)
69 // o = ch_u16(out, o, 0x001b); o = ch_u16(out, o, 3); o = ch_u8(out, o, 2); o = ch_u16(out, o, 2)
70 // 16. application_settings (0x4469 = 17513): supported ALPN list "h2"
71 o = ch_u16(out, o, 0x4469); o = ch_u16(out, o, 5); o = ch_u16(out, o, 3); o = ch_u8(out, o, 2); out[o]=104; out[o+1]=50; o = o + 2
72 // 17. trailing GREASE (empty)
73 o = ch_u16(out, o, CH_GREASE_B); o = ch_u16(out, o, 0)
74 return o
75}
76
77// emit the full Chrome-JA3 ClientHello into out. random32 + x25519 pub32 + SNI supplied. returns total len, or -1.
78func tls13_chrome_hello_emit(random32: *u8, sni: *u8, sni_len: i64, pub32: *u8, p256_pub65: *u8, out: *u8, out_cap: i64) -> i64 {
79 if out_cap < 700 + sni_len { return 0 - 1 }
80 if sni_len < 1 { return 0 - 1 }
81 var o: i64 = 4 // reserve handshake header
82 o = ch_u16(out, o, 0x0303) // legacy_version
83 var i: i64 = 0; while i < 32 { out[o+i] = random32[i]; i = i + 1 } o = o + 32
84 // legacy_session_id: EMPTY. (Chrome sends 32 random bytes for middlebox-compat, but our session runner does
85 // not send the client ChangeCipherSpec that compat mode expects; an empty sid avoids that path. JA3 does NOT
86 // include the session_id, so the fingerprint is unchanged.)
87 o = ch_u8(out, o, 0)
88 // cipher_suites: GREASE + Chrome's 15 (len = 32)
89 o = ch_u16(out, o, 32)
90 o = ch_u16(out, o, CH_GREASE_A)
91 o = ch_u16(out, o, 0x1301); o = ch_u16(out, o, 0x1302); o = ch_u16(out, o, 0x1303)
92 o = ch_u16(out, o, 0xc02b); o = ch_u16(out, o, 0xc02f); o = ch_u16(out, o, 0xc02c); o = ch_u16(out, o, 0xc030)
93 o = ch_u16(out, o, 0xcca9); o = ch_u16(out, o, 0xcca8); o = ch_u16(out, o, 0xc013); o = ch_u16(out, o, 0xc014)
94 o = ch_u16(out, o, 0x009c); o = ch_u16(out, o, 0x009d); o = ch_u16(out, o, 0x002f); o = ch_u16(out, o, 0x0035)
95 // legacy_compression_methods = 0x01 0x00
96 o = ch_u8(out, o, 1); o = ch_u8(out, o, 0)
97 // extensions: reserve list_len(2), fill after
98 let ext_len_off: i64 = o; o = o + 2
99 let ext_start: i64 = o
100 o = ch_exts(out, o, sni, sni_len, pub32, p256_pub65)
101 ch_u16(out, ext_len_off, o - ext_start) // backfill extensions list length
102 // backfill handshake header: msg_type=1 (ClientHello) + uint24 body length
103 let body: i64 = o - 4
104 out[0] = 1 as u8
105 out[1] = ((body >> 16) & 0xff) as u8; out[2] = ((body >> 8) & 0xff) as u8; out[3] = (body & 0xff) as u8
106 return o
107}