mirror of
https://github.com/lwthiker/curl-impersonate.git
synced 2026-09-06 00:41:52 +00:00
Impersonate Chrome 110 (#148)
Add support for impersonating Chrome 110. Chrome 110 comes with TLS extension permutation enabled by default. We mimic this behavior in libcurl with the new CURLOPT_SSL_PERMUTE_EXTENSIONS option, which enables the corresponding flag in BoringSSL. --------- Co-authored-by: Johann Saunier <[email protected]>
This commit is contained in:
co-authored by
Johann Saunier
parent
b2b1ea0f8e
commit
9d05a81030
+67
-11
@@ -1,7 +1,7 @@
|
||||
import enum
|
||||
import struct
|
||||
import collections
|
||||
from typing import List, Any
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -45,6 +45,7 @@ class TLSExtensionType(enum.Enum):
|
||||
record_size_limit = 28
|
||||
delegated_credentials = 34
|
||||
session_ticket = 35
|
||||
pre_shared_key = 41
|
||||
supported_versions = 43
|
||||
psk_key_exchange_modes = 45
|
||||
keyshare = 51
|
||||
@@ -648,7 +649,20 @@ class TLSClientHelloSignature():
|
||||
def extension_list(self):
|
||||
return list(map(lambda ext: ext.ext_type, self.extensions))
|
||||
|
||||
def _compare_extensions(self, other: 'TLSClientHelloSignature'):
|
||||
def _is_permuted_extension(self, ext: TLSExtensionSignature):
|
||||
# Chrome permutes all TLS extensions except for GREASE and pre_shared_key
|
||||
# (and the trailing padding)
|
||||
return ext.ext_type not in [
|
||||
TLSExtensionType.GREASE,
|
||||
TLSExtensionType.pre_shared_key,
|
||||
TLSExtensionType.padding
|
||||
]
|
||||
|
||||
def _compare_extensions(
|
||||
self,
|
||||
other: 'TLSClientHelloSignature',
|
||||
allow_tls_permutation: bool = False
|
||||
):
|
||||
"""Compare the TLS extensions of two Client Hello messages."""
|
||||
# Check that the extension lists are identical in content.
|
||||
if set(self.extension_list) != set(other.extension_list):
|
||||
@@ -658,15 +672,23 @@ class TLSClientHelloSignature():
|
||||
return False, (f"TLS extension lists differ: "
|
||||
f"Symmatric difference {symdiff}")
|
||||
|
||||
if self.extension_list != other.extension_list:
|
||||
if not allow_tls_permutation and self.extension_list != other.extension_list:
|
||||
return False, "TLS extension lists identical but differ in order"
|
||||
|
||||
# Check the extensions' parameters.
|
||||
for i, ext in enumerate(self.extensions):
|
||||
if not ext.equals(other.extensions[i]):
|
||||
if allow_tls_permutation and self._is_permuted_extension(ext):
|
||||
# If TLS extension permutation is enabled, locate this extension
|
||||
# in the other signature by type.
|
||||
other_ext = next(
|
||||
e for e in other.extensions if e.ext_type == ext.ext_type
|
||||
)
|
||||
else:
|
||||
other_ext = other.extensions[i]
|
||||
if not ext.equals(other_ext):
|
||||
ours = ext.to_dict()
|
||||
ours.pop("type")
|
||||
theirs = other.extensions[i].to_dict()
|
||||
theirs = other_ext.to_dict()
|
||||
theirs.pop("type")
|
||||
msg = (f"TLS extension {ext.ext_type.name} is different. "
|
||||
f"{ours} != {theirs}")
|
||||
@@ -674,7 +696,11 @@ class TLSClientHelloSignature():
|
||||
|
||||
return True, None
|
||||
|
||||
def _equals(self, other: 'TLSClientHelloSignature', reason: bool = False):
|
||||
def _equals(
|
||||
self,
|
||||
other: 'TLSClientHelloSignature',
|
||||
allow_tls_permutation: bool = False
|
||||
):
|
||||
"""Check if another TLSClientHelloSignature is identical."""
|
||||
if self.record_version != other.record_version:
|
||||
msg = (f"TLS record versions differ: "
|
||||
@@ -700,20 +726,32 @@ class TLSClientHelloSignature():
|
||||
msg = f"TLS compression methods differ in contents or order. "
|
||||
return False, msg
|
||||
|
||||
return self._compare_extensions(other)
|
||||
return self._compare_extensions(other, allow_tls_permutation)
|
||||
|
||||
def equals(self, other: 'TLSClientHelloSignature', reason: bool = False):
|
||||
def equals(
|
||||
self,
|
||||
other: 'TLSClientHelloSignature',
|
||||
allow_tls_permutation: bool = False,
|
||||
reason: bool = False
|
||||
):
|
||||
"""Checks whether two Client Hello messages have the same signature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
other : TLSClientHelloSignature
|
||||
The signature of the other Client Hello message.
|
||||
allow_tls_permutation : bool
|
||||
Allow TLS extension permutations. If set to True, and the TLS
|
||||
extensions are identical between the signatures but differ in
|
||||
order, the signatures will be considered equal.
|
||||
reason : bool
|
||||
If True, returns an additional string describing the reason of the
|
||||
difference in case of a difference, and None otherwise.
|
||||
"""
|
||||
equal, msg = self._equals(other)
|
||||
equal, msg = self._equals(
|
||||
other,
|
||||
allow_tls_permutation=allow_tls_permutation
|
||||
)
|
||||
if reason:
|
||||
return equal, msg
|
||||
else:
|
||||
@@ -939,13 +977,17 @@ class BrowserSignature:
|
||||
http2 : HTTP2Signature
|
||||
The HTTP/2 signature of the browser.
|
||||
Can be None, in which case it is ignored.
|
||||
options: dict
|
||||
Optional parameters specifying how to
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
tls_client_hello: TLSClientHelloSignature = None,
|
||||
http2: HTTP2Signature = None):
|
||||
http2: HTTP2Signature = None,
|
||||
options: Dict = None):
|
||||
self.tls_client_hello = tls_client_hello
|
||||
self.http2 = http2
|
||||
self.options = options
|
||||
|
||||
def _equals(self, other: 'BrowserSignature'):
|
||||
# If one is None, so must be the other
|
||||
@@ -968,6 +1010,14 @@ class BrowserSignature:
|
||||
if not equal:
|
||||
return equal, msg
|
||||
|
||||
if (self.options is None) != (other.options is None):
|
||||
return False, "Options present in one signature but not the other"
|
||||
|
||||
if self.options is not None:
|
||||
if self.options != other.options:
|
||||
msg = (f"Options differ: {self.options} != {other.options}")
|
||||
return False, msg
|
||||
|
||||
return True, None
|
||||
|
||||
def equals(self, other: 'BrowserSignature', reason: bool = False):
|
||||
@@ -990,6 +1040,8 @@ class BrowserSignature:
|
||||
def to_dict(self):
|
||||
"""Serialize to a dict object."""
|
||||
d = {}
|
||||
if self.options is not None:
|
||||
d["options"] = self.options
|
||||
if self.tls_client_hello is not None:
|
||||
d["tls_client_hello"] = self.tls_client_hello.to_dict()
|
||||
if self.http2 is not None:
|
||||
@@ -1011,4 +1063,8 @@ class BrowserSignature:
|
||||
else:
|
||||
http2 = None
|
||||
|
||||
return BrowserSignature(tls_client_hello=tls_client_hello, http2=http2)
|
||||
return BrowserSignature(
|
||||
tls_client_hello=tls_client_hello,
|
||||
http2=http2,
|
||||
options=d.get("options")
|
||||
)
|
||||
|
||||
@@ -576,6 +576,104 @@ signature:
|
||||
- 'accept-encoding: gzip, deflate, br'
|
||||
- 'accept-language: en-US,en;q=0.9'
|
||||
---
|
||||
name: chrome_110.0.5481.177_win10
|
||||
browser:
|
||||
name: chrome
|
||||
version: 110.0.5481.177
|
||||
os: win10
|
||||
mode: regular
|
||||
signature:
|
||||
options:
|
||||
tls_permute_extensions: true
|
||||
tls_client_hello:
|
||||
record_version: 'TLS_VERSION_1_0'
|
||||
handshake_version: 'TLS_VERSION_1_2'
|
||||
session_id_length: 32
|
||||
ciphersuites: [
|
||||
'GREASE',
|
||||
0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030,
|
||||
0xcca9, 0xcca8, 0xc013, 0xc014, 0x009c, 0x009d, 0x002f,
|
||||
0x0035
|
||||
]
|
||||
comp_methods: [0x00]
|
||||
extensions:
|
||||
- type: GREASE
|
||||
length: 0
|
||||
- type: server_name
|
||||
- type: extended_master_secret
|
||||
length: 0
|
||||
- type: renegotiation_info
|
||||
length: 1
|
||||
- type: supported_groups
|
||||
length: 10
|
||||
supported_groups: [
|
||||
'GREASE',
|
||||
0x001d, 0x0017, 0x0018
|
||||
]
|
||||
- type: ec_point_formats
|
||||
length: 2
|
||||
ec_point_formats: [0]
|
||||
- type: session_ticket
|
||||
length: 0
|
||||
- type: application_layer_protocol_negotiation
|
||||
length: 14
|
||||
alpn_list: ['h2', 'http/1.1']
|
||||
- type: status_request
|
||||
length: 5
|
||||
status_request_type: 0x01
|
||||
- type: signature_algorithms
|
||||
length: 18
|
||||
sig_hash_algs: [
|
||||
0x0403, 0x0804, 0x0401, 0x0503,
|
||||
0x0805, 0x0501, 0x0806, 0x0601
|
||||
]
|
||||
- type: signed_certificate_timestamp
|
||||
length: 0
|
||||
- type: keyshare
|
||||
length: 43
|
||||
key_shares:
|
||||
- group: GREASE
|
||||
length: 1
|
||||
- group: 29
|
||||
length: 32
|
||||
- type: psk_key_exchange_modes
|
||||
length: 2
|
||||
psk_ke_mode: 1
|
||||
- type: supported_versions
|
||||
length: 7
|
||||
supported_versions: [
|
||||
'GREASE', 'TLS_VERSION_1_3', 'TLS_VERSION_1_2'
|
||||
]
|
||||
- type: compress_certificate
|
||||
length: 3
|
||||
algorithms: [0x02]
|
||||
- type: application_settings
|
||||
length: 5
|
||||
alps_alpn_list: ['h2']
|
||||
- type: GREASE
|
||||
length: 1
|
||||
data: !!binary AA==
|
||||
- type: padding
|
||||
http2:
|
||||
pseudo_headers:
|
||||
- ':method'
|
||||
- ':authority'
|
||||
- ':scheme'
|
||||
- ':path'
|
||||
headers:
|
||||
- 'sec-ch-ua: "Chromium";v="110", "Not A(Brand";v="24", "Google Chrome";v="110"'
|
||||
- 'sec-ch-ua-mobile: ?0'
|
||||
- 'sec-ch-ua-platform: "Windows"'
|
||||
- 'upgrade-insecure-requests: 1'
|
||||
- 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
|
||||
- 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'
|
||||
- 'sec-fetch-site: none'
|
||||
- 'sec-fetch-mode: navigate'
|
||||
- 'sec-fetch-user: ?1'
|
||||
- 'sec-fetch-dest: document'
|
||||
- 'accept-encoding: gzip, deflate, br'
|
||||
- 'accept-language: en-US,en;q=0.9'
|
||||
---
|
||||
name: chrome_99.0.4844.73_android12-pixel6
|
||||
browser:
|
||||
name: chrome
|
||||
|
||||
@@ -148,6 +148,7 @@ class TestImpersonation:
|
||||
("curl_chrome101", None, None, "chrome_101.0.4951.67_win10"),
|
||||
("curl_chrome104", None, None, "chrome_104.0.5112.81_win10"),
|
||||
("curl_chrome107", None, None, "chrome_107.0.5304.107_win10"),
|
||||
("curl_chrome110", None, None, "chrome_110.0.5481.177_win10"),
|
||||
("curl_chrome99_android", None, None, "chrome_99.0.4844.73_android12-pixel6"),
|
||||
("curl_edge99", None, None, "edge_99.0.1150.30_win10"),
|
||||
("curl_edge101", None, None, "edge_101.0.1210.47_win10"),
|
||||
@@ -203,6 +204,14 @@ class TestImpersonation:
|
||||
"libcurl-impersonate-chrome",
|
||||
"chrome_107.0.5304.107_win10"
|
||||
),
|
||||
(
|
||||
"minicurl",
|
||||
{
|
||||
"CURL_IMPERSONATE": "chrome110"
|
||||
},
|
||||
"libcurl-impersonate-chrome",
|
||||
"chrome_110.0.5481.177_win10"
|
||||
),
|
||||
(
|
||||
"minicurl",
|
||||
{
|
||||
@@ -556,7 +565,15 @@ class TestImpersonation:
|
||||
["tls_client_hello"]
|
||||
)
|
||||
|
||||
equals, msg = sig.equals(expected_sig, reason=True)
|
||||
allow_tls_permutation=browser_signatures[expected_signature] \
|
||||
["signature"] \
|
||||
.get("options", {}) \
|
||||
.get("tls_permute_extensions", False)
|
||||
equals, msg = sig.equals(
|
||||
expected_sig,
|
||||
allow_tls_permutation=allow_tls_permutation,
|
||||
reason=True
|
||||
)
|
||||
assert equals, msg
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user