code wiki / (root) / nx_tls13_chrome_hello.nx

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}