2121 try_decode_packet /2 ,
2222 decode_all /2 ,
2323 encode_packet /2 ]).
24- -ifdef (TEST ).
2524-export ([make_client_hello /2 ,
26- make_client_hello /3 ,
2725 make_client_hello /4 ,
28- make_client_hello /5 ,
2926 parse_server_hello /1 ]).
30- -endif .
3127
3228-export_type ([codec / 0 , meta / 0 ]).
3329
@@ -294,63 +290,159 @@ make_srv_hello(Digest, SessionId, {KeyShareGroup, KeyShareKey}) ->
294290 | Extensions ],
295291 [<<? TLS_TAG_SRV_HELLO , (iolist_size (Payload )):? u24 >> | Payload ].
296292
297- - ifdef (TEST ).
298- % % Generate Fake-TLS "ClientHello". Used for tests only.
293+ % % Generate Fake-TLS "ClientHello".
299294make_client_hello (Secret , SniDomain ) ->
300295 make_client_hello (erlang :system_time (second ),
301296 crypto :strong_rand_bytes (32 ),
302297 Secret , SniDomain ).
303298
304- % % Generate Fake-TLS "ClientHello" with custom TLS packet length. Used for tests only.
305- make_client_hello (Secret , SniDomain , TlsPacketLen ) ->
306- make_client_hello (erlang :system_time (second ),
307- crypto :strong_rand_bytes (32 ),
308- Secret , SniDomain , TlsPacketLen ).
309-
310299make_client_hello (Timestamp , SessionId , HexSecret , SniDomain ) when byte_size (HexSecret ) == 32 ->
311300 make_client_hello (Timestamp , SessionId , mtp_handler :unhex (HexSecret ), SniDomain );
312301make_client_hello (Timestamp , SessionId , Secret , SniDomain ) when byte_size (SessionId ) == 32 ,
313302 byte_size (Secret ) == 16 ->
314- make_client_hello (Timestamp , SessionId , Secret , SniDomain , 512 ).
315-
316- % % @doc Generate ClientHello with custom TLS packet length (for testing variable-length support)
317- make_client_hello (Timestamp , SessionId , HexSecret , SniDomain , TlsPacketLen ) when byte_size (HexSecret ) == 32 ->
318- make_client_hello (Timestamp , SessionId , mtp_handler :unhex (HexSecret ), SniDomain , TlsPacketLen );
319- make_client_hello (Timestamp , SessionId , Secret , SniDomain , TlsPacketLen ) when byte_size (SessionId ) == 32 ,
320- byte_size (Secret ) == 16 ,
321- TlsPacketLen >= 512 ->
322- % % Wireshark capture from Telegram Desktop
303+ % % Modern ClientHello following tdesktop b72deb1 + tdlib d0de8a7.
304+ % % Variable length (no fixed padding); proxy only validates the HMAC in Random.
305+ GREASE = <<16#ea , 16#ea >>,
306+
307+ % % Cipher suites: GREASE + 15 standard suites (TLS_RSA_WITH_3DES removed)
323308 CipherSuites =
324- mtp_handler :unhex (<<" eaea130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f0035000a" >>),
325- CSLen = byte_size (CipherSuites ),
309+ <<GREASE /binary ,
310+ 16#13 , 16#01 , % TLS_AES_128_GCM_SHA256
311+ 16#13 , 16#02 , % TLS_AES_256_GCM_SHA384
312+ 16#13 , 16#03 , % TLS_CHACHA20_POLY1305_SHA256
313+ 16#c0 , 16#2b , % ECDHE_ECDSA_AES128_GCM_SHA256
314+ 16#c0 , 16#2f , % ECDHE_RSA_AES128_GCM_SHA256
315+ 16#c0 , 16#2c , % ECDHE_ECDSA_AES256_GCM_SHA384
316+ 16#c0 , 16#30 , % ECDHE_RSA_AES256_GCM_SHA384
317+ 16#cc , 16#a9 , % ECDHE_ECDSA_CHACHA20_POLY1305
318+ 16#cc , 16#a8 , % ECDHE_RSA_CHACHA20_POLY1305
319+ 16#c0 , 16#13 , % ECDHE_RSA_AES128_CBC_SHA
320+ 16#c0 , 16#14 , % ECDHE_RSA_AES256_CBC_SHA
321+ 16#00 , 16#9c , % RSA_AES128_GCM_SHA256
322+ 16#00 , 16#9d , % RSA_AES256_GCM_SHA384
323+ 16#00 , 16#2f , % RSA_AES128_CBC_SHA
324+ 16#00 , 16#35 >>, % RSA_AES256_CBC_SHA
326325
327326 SNI = make_sni ([SniDomain ]),
328- % % Wireshark capture from Telegram Desktop
329- KeyShare =
330- mtp_handler :unhex (
331- <<" 0033002b00295a5a000100001d0020a4146c3e8573565bb5f5c877a88a98dcbbd46a9b3ca1ab3df7217cc33b4b6d2c" >>),
327+
328+ SigAlgos =
329+ <<16#00 , 16#0d , % signature_algorithms
330+ 16#00 , 16#20 , % ext length 32 (2 list-len + 30 entries)
331+ 16#00 , 16#1e , % sig-algo list length 30 (15 algos × 2)
332+ 16#04 , 16#03 , % ecdsa_secp256r1_sha256
333+ 16#05 , 16#03 , % ecdsa_secp384r1_sha384
334+ 16#06 , 16#03 , % ecdsa_secp521r1_sha512
335+ 16#02 , 16#03 , % ecdsa_sha1
336+ 16#08 , 16#04 , % rsa_pss_rsae_sha256
337+ 16#08 , 16#05 , % rsa_pss_rsae_sha384
338+ 16#08 , 16#06 , % rsa_pss_rsae_sha512
339+ 16#04 , 16#01 , % rsa_pkcs1_sha256
340+ 16#05 , 16#01 , % rsa_pkcs1_sha384
341+ 16#06 , 16#01 , % rsa_pkcs1_sha512
342+ 16#02 , 16#01 , % rsa_pkcs1_sha1
343+ 16#04 , 16#02 ,
344+ 16#03 , 16#02 ,
345+ 16#02 , 16#02 ,
346+ 16#03 , 16#01 >>,
347+
348+ % % supported_groups: added X25519MLKEM768 (0x11ec), per tdesktop b72deb1
349+ SupportedGroups =
350+ <<16#00 , 16#0a , % supported_groups
351+ 16#00 , 16#0c , % ext length 12 (2 list-len + 10 entries)
352+ 16#00 , 16#0a , % named-group list length 10 (5 groups × 2)
353+ GREASE /binary , % GREASE named group
354+ 16#11 , 16#ec , % X25519MLKEM768 (new)
355+ 16#00 , 16#1d , % x25519
356+ 16#00 , 16#17 , % secp256r1
357+ 16#00 , 16#18 >>, % secp384r1
358+
332359 SupportedVersions =
333- mtp_handler :unhex (<<" 002b000b0a1a1a0304030303020301" >>),
334- % % Calculate extensions length based on desired TLS packet length
335- % % TLS Frame = Type(1) + Version(2) + Length(2) + Payload
336- % % Payload = Hello(1) + HelloLen(3) + Version(2) + Random(32) + SessIdLen(1) + SessId
337- % % + CSLen(2) + CS + CompMethodsLen(1) + CompMethods(1) + ExtLen(2) + Extensions
338- % % TlsPacketLen = Payload size (everything after the Length field)
339- HelloLen = TlsPacketLen - 4 , % Subtract Hello(1) + HelloLen(3) = 4
340- ExtLen = TlsPacketLen - (1 + 3 + 2 + 32 + 1 + byte_size (SessionId ) + 2 + CSLen + 1 + 1 + 2 ),
341- RealExtensions = <<KeyShare /binary , SupportedVersions /binary , SNI /binary >>,
342- Extensions = add_padding_ext (RealExtensions , ExtLen ),
343- (ExtLen == byte_size (Extensions )) orelse error ({bad_ext_len , byte_size (Extensions ), ExtLen }),
344-
345- SessIdLen = byte_size (SessionId ),
360+ <<16#00 , 16#2b , % supported_versions
361+ 16#00 , 16#0b , % ext length 11 (1 list-len + 10 entries)
362+ 16#0a , % version list length 10 (5 versions × 2)
363+ GREASE /binary , % GREASE version
364+ 16#03 , 16#04 , % TLS 1.3
365+ 16#03 , 16#03 , % TLS 1.2
366+ 16#03 , 16#02 , % TLS 1.1
367+ 16#03 , 16#01 >>, % TLS 1.0
368+
369+ % % key_share: GREASE + X25519MLKEM768 (1184+32 bytes) + standalone X25519 (32 bytes)
370+ % % The proxy and server-side make_key_share/1 will pick the standalone X25519 entry.
371+ % % ML-KEM-768 public key: 1184 random bytes (proxy does not validate key material)
372+ MlKem768Key = crypto :strong_rand_bytes (1184 ),
373+ KSKey1 = crypto :strong_rand_bytes (32 ), % X25519 component of the hybrid entry
374+ KSKey2 = crypto :strong_rand_bytes (32 ), % standalone X25519 key (picked by proxy)
375+ KeyShareEntries =
376+ <<GREASE /binary , 16#00 , 16#01 , 16#00 , % GREASE: group + key_len=1 + 0x00
377+ 16#11 , 16#ec , 16#04 , 16#c0 , % X25519MLKEM768: group + key_len=1216
378+ MlKem768Key /binary , % ML-KEM-768 public key (1184 bytes)
379+ KSKey1 /binary , % X25519 component (32 bytes)
380+ 16#00 , 16#1d , 16#00 , 16#20 , % x25519: group + key_len=32
381+ KSKey2 /binary >>, % standalone X25519 public key
382+ KSListLen = byte_size (KeyShareEntries ), % 5 + 1220 + 36 = 1261
383+ KeyShare =
384+ <<16#00 , 16#33 , % key_share
385+ (KSListLen + 2 ):? u16 , % ext length = list-len field (2) + entries
386+ KSListLen :? u16 ,
387+ KeyShareEntries /binary >>,
388+
389+ % % Encrypted Client Hello outer (0xfe0d), per tdlib d0de8a7
390+ % % (previously 0xfe02; the \x00\x20 + 32-byte field was also extended from 20 to 32 bytes)
391+ EchRand1 = crypto :strong_rand_bytes (1 ),
392+ EchRand32 = crypto :strong_rand_bytes (32 ),
393+ EchPayload = crypto :strong_rand_bytes (176 ), % fixed choice from {144,176,208,240}
394+ EchContent =
395+ <<16#00 , 16#00 , 16#01 , 16#00 , 16#01 , % fixed ECH outer header
396+ EchRand1 /binary , % 1 random byte
397+ 16#00 , 16#20 ,
398+ EchRand32 /binary , % 32 random bytes
399+ (byte_size (EchPayload )):? u16 ,
400+ EchPayload /binary >>,
401+ ECH =
402+ <<16#fe , 16#0d , % ech_outer_extensions (0xfe0d)
403+ (byte_size (EchContent )):? u16 ,
404+ EchContent /binary >>,
405+
406+ Extensions =
407+ [<<GREASE /binary , 0 :16 >>, % leading GREASE extension (empty)
408+ SNI , % server_name (0x0000)
409+ <<16#00 , 16#17 , 0 :16 >>, % extended_master_secret (empty)
410+ <<16#00 , 16#23 , 0 :16 >>, % session_ticket (empty)
411+ SigAlgos , % signature_algorithms (0x000d)
412+ <<16#00 , 16#05 , 16#00 , 16#05 , % status_request (OCSP)
413+ 16#01 , 0 :32 >>, % type=ocsp(1) + empty responder list(2) + empty exts(2)
414+ SupportedGroups , % supported_groups (0x000a)
415+ <<16#00 , 16#0b , 16#00 , 16#02 , 16#01 , 16#00 >>, % ec_point_formats: uncompressed
416+ <<16#00 , 16#12 , 0 :16 >>, % signed_certificate_timestamp (empty)
417+ <<16#00 , 16#10 , 16#00 , 16#0e , % application_layer_protocol_negotiation
418+ 16#00 , 16#0c , % protocol list length 12
419+ 16#02 , $h , $2 , % "h2"
420+ 16#08 , $h , $t , $t , $p , $/ , $1 , $. , $1 >>, % "http/1.1"
421+ <<16#44 , 16#cd , 16#00 , 16#05 , % application_layer_protocol_settings
422+ 16#00 , 16#03 , 16#02 , $h , $2 >>, % (type updated 0x4469→0x44cd in b72deb1)
423+ SupportedVersions , % supported_versions (0x002b)
424+ <<16#00 , 16#2d , 16#00 , 16#02 , 16#01 , 16#01 >>, % psk_key_exchange_modes: psk_dhe_ke
425+ KeyShare , % key_share (0x0033)
426+ ECH , % encrypted_client_hello (0xfe0d)
427+ <<16#ff , 16#01 , 16#00 , 16#01 , 16#00 >>, % renegotiation_info (empty)
428+ <<GREASE /binary , 16#00 , 16#01 , 16#00 >>], % trailing GREASE extension
429+
430+ ExtBin = iolist_to_binary (Extensions ),
431+ CSLen = byte_size (CipherSuites ),
432+ SessIdLen = byte_size (SessionId ), % always 32
433+ ExtLen = byte_size (ExtBin ),
434+ % % HelloBodyLen: TLS version(2) + Random(32) + SessIdLen(1) + SessId + CSLen(2) + CS
435+ % % + CompMethodsLen(1) + CompMethod(1) + ExtLen(2) + Extensions
436+ HelloBodyLen = 2 + 32 + 1 + SessIdLen + 2 + CSLen + 1 + 1 + 2 + ExtLen ,
437+ TlsLen = HelloBodyLen + 4 , % +4 for handshake type(1) + handshake length(3)
346438 Pack = fun (FakeRandom ) ->
347- <<? TLS_REC_HANDSHAKE , ? TLS_10_VERSION , TlsPacketLen :? u16 ,
348- ? TLS_TAG_CLI_HELLO , HelloLen :? u24 , ? TLS_12_VERSION ,
439+ <<? TLS_REC_HANDSHAKE , ? TLS_10_VERSION , TlsLen :? u16 ,
440+ ? TLS_TAG_CLI_HELLO , HelloBodyLen :? u24 , ? TLS_12_VERSION ,
349441 FakeRandom :? DIGEST_LEN /binary ,
350- SessIdLen , SessionId : SessIdLen /binary ,
351- CSLen :? u16 , CipherSuites : CSLen /binary ,
352- 1 , 0 , % Compression methods
353- ExtLen :? u16 , Extensions : ExtLen /binary >>
442+ SessIdLen , SessionId /binary ,
443+ CSLen :? u16 , CipherSuites /binary ,
444+ 1 , 0 , % 1 compression method: null(0)
445+ ExtLen :? u16 , ExtBin /binary >>
354446 end ,
355447 FakeRandom0 = binary :copy (<<0 >>, ? DIGEST_LEN ),
356448 Hello0 = Pack (FakeRandom0 ),
@@ -365,15 +457,7 @@ make_sni(Domains) ->
365457 ItemsLen = byte_size (SniListItems ),
366458 <<? EXT_SNI :? u16 , (ItemsLen + 2 ):? u16 , ItemsLen :? u16 , SniListItems /binary >>.
367459
368- add_padding_ext (RealExtensions , ExtLen ) ->
369- RealExtLen = byte_size (RealExtensions ),
370- PadSize = ExtLen - RealExtLen - 4 ,
371- PaddingExt = <<21 :? u16 , % EXT_PADDING
372- PadSize :? u16 ,
373- (binary :copy (<<0 >>, PadSize ))/binary >>,
374- <<RealExtensions /binary , PaddingExt /binary >>.
375-
376- % % Parses "ServerHello" (the one produced by from_client_hello/2). Used for tests only.
460+ % % Parses "ServerHello" (the one produced by from_client_hello/2).
377461parse_server_hello (<<? TLS_REC_HANDSHAKE , ? TLS_12_VERSION , HSLen :? u16 , Handshake :HSLen /binary ,
378462 ? TLS_REC_CHANGE_CIPHER , ? TLS_12_VERSION , CCLen :? u16 , ChangeCipher :CCLen /binary ,
379463 ? TLS_REC_DATA , ? TLS_12_VERSION , DLen :? u16 , Data :DLen /binary ,
@@ -382,8 +466,6 @@ parse_server_hello(<<?TLS_REC_HANDSHAKE, ?TLS_12_VERSION, HSLen:?u16, Handshake:
382466parse_server_hello (B ) when byte_size (B ) < (512 + 5 ) ->
383467 incomplete .
384468
385- - endif .
386-
387469% % Data stream codec
388470
389471- spec new () -> codec ().
0 commit comments