chrome-fp — 与真 Chrome 逐字节同指纹的纯 Python 请求库, 用法和 requests 一样
用 Python 发 HTTP 请求, 但 TLS/HTTP2 指纹跟本机真 Chrome(153.0.8010.48)一致: JA4、HTTP/2 Akamai 指纹、请求头顺序与取值、扩展集合、GREASE、ALPS、trust_anchors、 PQ 混合密钥共享(X25519MLKEM768)全部对齐。
import chrome_fp as requests # 就当成 requests 用
r = requests.get("https://example.com/", params={"a": 1}, timeout=10)
print(r.status_code, r.headers["Content-Type"], r.elapsed)
print(r.text, r.json() if "json" in r.headers.get("content-type", "") else "")
with requests.Session() as s:
s.headers.update({"x-token": "abc"})
r = s.post("https://httpbin.org/post", json={"a": 1})
r.raise_for_status()
本轮做了什么:拿真机抓包把库对齐
本机是非管理员,Npcap/dumpcap 直接拒绝抓包
(You do not have permission to capture on device)。所以改用应用层旁路抓包:
Chrome ──CONNECT──> tools/tap_proxy.py ──> 真实服务器 / 本地 h2 服务器
│
├─ 原样录下双向字节(不改一个字节)
├─ 合成 TCP 头写成 .pcap ──> tshark / Wireshark 打开
└─ Chrome 的 SSLKEYLOGFILE ──> 解密看 HTTP/2 帧
产物在 capture/chrome153/(另有 capture/library/ 是本库走同一流程的抓包,用来对比):
| 文件 | 内容 |
|---|---|
chrome_tap.pcap |
合成 TCP 的抓包文件,Wireshark 可直接打开(304 包 / 23 条隧道) |
sslkeylog.txt |
Chrome 自己写的 TLS 会话密钥 |
hello/*.bin |
真 Chrome 发出的 ClientHello 原始字节(17 条样本) |
streams/*.bin |
每条隧道双向的原始字节流 |
h2_client.json |
解密后解出的请求头(顺序 + 取值) |
summary.txt |
按资源类型归类的头顺序汇总 |
tls_diff.txt |
与库的逐扩展对比结论(完全一致) |
report.json |
tshark 判定结果(JA4/JA3/扩展/key_share…) |
抓包自己也能复现:
python tools/run_capture.py --out capture/chrome153 --sites https://example.com/
python tools/analyze_capture.py --dir capture/chrome153 # tshark 出 JA4 等
python tools/tls13_decrypt.py capture/chrome153 # 自己解密出 h2 明文
python tools/h2_frames.py capture/chrome153 # 解 HTTP/2 帧与请求头
python tools/diff_fp.py --dir capture/chrome153 # 与库逐扩展对比
tshark 4.6 能把服务端方向的 TLS 解出来,但客户端方向一直停在
Encrypted Application Data。抓包本身没问题(record 结构完整、密钥齐全), 所以tools/tls13_decrypt.py按 RFC 8446 自己解,并与 tshark 对服务端的判定互相印证。
在 Wireshark / tshark 里直接看
capture/chrome153/chrome_tap.pcap 是标准 pcap(以太网链路层 + 完整 TCP 三次握手,
tshark 能正常重组),双击就能用 Wireshark 打开:
# 1) 让 Wireshark 用 Chrome 的 keylog 解密
# GUI: 编辑 → 首选项 → Protocols → TLS → (Pre)-Master-Secret log filename
# 选 capture/chrome153/sslkeylog.txt
# tshark 等价写法: -o tls.keylog_file:capture/chrome153/sslkeylog.txt
# 2) 看真 Chrome 的 JA4(tshark 4.6 原生支持)
tshark -r capture/chrome153/chrome_tap.pcap \
-o tls.keylog_file:capture/chrome153/sslkeylog.txt \
-Y "tls.handshake.type==1" \
-T fields -e tls.handshake.extensions_server_name -e tls.handshake.ja4
# 3) 看解密后的 HTTP/2 请求头(顺序就是 Chrome 的真实顺序)
tshark -r capture/chrome153/chrome_tap.pcap \
-o tls.keylog_file:capture/chrome153/sslkeylog.txt \
-Y "http2.header.name" -T fields -e http2.streamid -e http2.header.name -e http2.header.value
本机是非管理员,
dumpcap.exe -i <任意网卡>会直接报You do not have permission to capture on device ... Admin-only Mode, 所以抓包驱动用不了(Npcap 的 NPCAP 组也不在当前令牌里,UAC 又不方便点)。 旁路代理拿到的字节和网卡抓包看到的完全一样,只是没有 TCP 重传之类的噪声; 如果你想要网卡级抓包,用管理员权限跑dumpcap -i <网卡> -w out.pcapng即可,分析流程完全一样。
抓出来的差异(已全部修掉)
| # | 项目 | 真 Chrome 153 | 旧库 (0.2.3) | 处理 |
|---|---|---|---|---|
| 1 | trust_anchors(0xca34) ID 数量 |
28 个 | 32 个(152 的集合) | 去掉 d6790902/03/09/0e 四个 |
| 2 | HTTP/2 PRIORITY 帧 |
一个都不发 | 默认发 3/5/7/9 四帧 | send_priority_tree 默认关 |
| 3 | HEADERS 帧标志 |
不带 PRIORITY 前缀 | 带 5 字节 weight 前缀 | 默认不写 |
| 4 | Akamai 指纹第 3/4 段 | 0 / m,a,s,p |
会被 #2 改成 00:256,... |
修正;m,a,s,p 其实是伪头顺序 |
| 5 | sec-ch-ua |
"Google Chrome";v="153", "Not_A Brand";v="8", "Chromium";v="153" |
152 的品牌、顺序也不同 | 更新 |
| 6 | sec-ch-ua-platform |
"Windows" |
"Linux" |
更新 |
| 7 | user-agent |
Windows NT 10.0; Win64; x64 + Chrome/153 | X11; Linux x86_64 + Chrome/152 | 更新 |
| 8 | 请求头顺序 | 导航 / 子资源两套不同顺序 | 只有一套写死的顺序 | 按 dest/mode 选 profile |
| 9 | accept / priority / referer / origin |
随资源类型变 | 全部写死 | 按 dest 取值 |
| 10 | sec-fetch-user |
只有顶层 document 导航才有 | 导航一律带 | 修正 |
TLS 层其余部分(15 个密码套件、17 个扩展+2 GREASE、11 个签名算法、 4 个 group、ECH GREASE 结构、ALPS、compress_certificate、status_request、 EMS、session_id…)经逐字节比对与 Chrome 153 完全一致。
验证结果
python tools/verify_live.py
让本库走同一个代理访问同一个本地 h2 服务器,再和 Chrome 的抓包对比:
[2/4] TLS ClientHello 对比 密码套件/扩展集合/扩展数量/trust_anchors/groups/sigalgs/versions 全部 OK
[3/4] HTTP/2 指纹对比 Akamai = 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p 一致
PRIORITY 帧数量 = 0 一致
[4/4] 请求头逐条对比 document / style / font / script / image / iframe / fetch / xhr / POST
—— 9 类请求的头顺序与取值全部逐条相同
实测结果
| 项目 | 真 Chrome 153 | 本库 |
|---|---|---|
| JA4 | t13d1517h2_8daaf6152771_<随机> |
一致(17/17 条样本) |
| HTTP/2 Akamai | 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p |
一致(10/10 条连接) |
| 扩展数 | 17 真实 + 2 GREASE | 一致 |
| trust_anchors | 28 个 ID(集合固定、顺序随机) | 一致 |
| 请求头顺序/取值 | 导航与子资源各一套 | 一致(逐条比对) |
实网站点:python tools/live_sites.py → 5/5
(example.com / taobao h2+TLS1.3,baidu / qq / sohu TLS1.2 回落 + HTTP/1.1)
requests 兼容的用法
Session / Response 的属性与方法刻意对齐 requests:
import chrome_fp as requests
from chrome_fp.exceptions import HTTPError, TooManyRedirects
s = requests.Session()
s.headers.update({"x-token": "abc"}) # 大小写不敏感, 单次请求头可覆盖
s.params = {"v": "1"}
s.cookies.set("sid", "x", domain="example.com")
s.auth = ("user", "pass") # 或 basic_auth/自定义可调用对象
s.proxies = {"https": "http://127.0.0.1:7892"}
s.max_redirects = 10
r = s.request("POST", "https://example.com/api",
params={"p": 1}, json={"a": 1}, timeout=(5, 20),
headers={"x-extra": "1"}, cookies={"k": "v"},
allow_redirects=True, hooks={"response": [lambda r: None]})
r.status_code; r.reason; r.ok; r.headers["CONTENT-TYPE"]; r.text; r.json()
r.content; r.raw; r.url; r.encoding; r.apparent_encoding; r.elapsed; r.history
r.cookies; r.request; r.is_redirect; r.links; r.next
r.raise_for_status(); list(r.iter_content(4096)); list(r.iter_lines())
模块级 chrome_fp.get/post/put/patch/delete/head/options/request 与 requests 同名同签名。
异常层次在 chrome_fp.exceptions(RequestException / HTTPError / Timeout /
TooManyRedirects / MissingSchema / InvalidURL / JSONDecodeError …)。
几处刻意的 requests 语义(不是 bug):
r.encoding按 requests 规则来:Content-Type 带 charset 就用它;text/*没写 charset 时是ISO-8859-1;application/json是utf-8。想要正确的中文文本就r.encoding = r.apparent_encoding(用 charset_normalizer,和 requests 一样)。r.json()解析失败抛chrome_fp.exceptions.JSONDecodeError(同时是json.JSONDecodeError)。r.iter_lines()在decode_unicode=False时给 bytes。
指纹控制(requests 没有的额外关键字)
Session(
proxy="http://127.0.0.1:7892", # 也支持 proxies={...} 和 HTTP(S)_PROXY 环境变量
mode="cors", # navigate | cors | no-cors —— 决定 sec-fetch-*
dest="empty", # document|iframe|empty|script|style|image|font|preflight
user_agent=None, # 覆盖 UA
origin=None, referer=None, # CORS / 子资源请求的上下文
sec_fetch_site=None, # none|same-origin|same-site|cross-site
priority=None, # 覆盖 RFC 9218 priority 头
send_priority_tree=False, # 只有模拟 Chrome 152 才打开
verify=True, ca_file=None, allow_tls12=True,
timeout=30, max_redirects=30, trust_env=True,
keylog_file=None, # 写 NSS key log, Wireshark 可直接解密本库流量
)
单次请求也能覆盖:s.get(url, mode="navigate", dest="document", referer=..., origin=..., priority=...)。
sec-fetch-site 需要调用方给上下文(库不知道"发起方页面"是谁),默认:
顶层 document 导航 = none,其余 = same-origin;跨站时显式传 cross-site。
底层构造单条 ClientHello(不含网络):
from chrome_fp import build_client_hello, ja4_from_hello
ch = build_client_hello("example.com")
print(ja4_from_hello(ch.record), len(ch.record))
ch.record # 可直接写 socket 的原始字节
include_mlkem=False:不发 X25519MLKEM768grease_parts=():完全关闭 GREASEpermute_extensions=False:关闭扩展随机置换(调试用)
测试
python -m unittest discover -s tests -v # 38 个用例
python tools/verify_live.py # 端到端: 本库 vs 真 Chrome 抓包
python tools/live_sites.py # 实网站点冒烟
tests/test_chrome153_fingerprint.py 直接读 capture/chrome153/ 的真机数据,
把库的 ClientHello 与请求头钉死在真 Chrome 上(没有抓包数据会自动跳过)。
构建
python -m pip install build wheel
python -m build # 产出 dist/chrome_fp-0.3.0-py3-none-any.whl 和 .tar.gz
python tools/verify_build.py # 校验产物(清单/METADATA/隔离安装冒烟/sdist 自举)
verify_build.py 会把 wheel 解到临时目录,用那个副本跑一遍真实功能
(拼 ClientHello、算 JA4、requests 风格 prepare_request),确保校验的不是源码树;
再确认 sdist 解包后能自己重新构建出 wheel。
- wheel 里只有
chrome_fp包(14 个模块)+ LICENSE + METADATA - sdist 额外带
tests/、tools/,但不含体积大的capture/抓包数据 chrome_fp.zip是同样内容的便携 zip(源码 + README + pyproject + LICENSE)
目录结构
chrome_fp/
spec.py Chrome 153 指纹常量 + 每种资源类型的头顺序 profile(全部标注出处)
hello.py 按 BoringSSL ssl_add_clienthello_tlsext 规则拼 ClientHello
fingerprint.py 解析 + JA3/JA4 计算
tls13.py 纯 Python TLS 1.3 客户端(record 层/密钥调度/CV 校验/证书链校验/KeyUpdate/keylog)
tls12.py TLS 1.2 回落(ECDHE/RSA + GCM/ChaCha20, EMS, ServerKeyExchange 验签)
client.py 发同一个 ClientHello, 按 ServerHello 自动分派 1.3 / 1.2
mlkem.py ML-KEM-768 (FIPS 203) 纯 Python
http2.py HTTP/2 客户端(Chrome 帧序/SETTINGS/头顺序)
http1.py HTTP/1.1 客户端(ALPN=http/1.1 时, 头名用 Title-Case)
session.py requests 风格的 Session / Response / PreparedRequest
api.py 模块级 get/post/...(与 requests 同名同签名)
structures.py CaseInsensitiveDict / RequestsCookieJar
exceptions.py 与 requests.exceptions 对应的异常层次
tools/ 抓包(代理/本地h2服务器/解密)、分析(tshark/HTTP2)、对比、实网冒烟
tests/ 单元 + 与真机抓包的回归测试
capture/ 本轮真 Chrome 153 的抓包与判定结果
已知边界
- TLS 1.2 回落已实现(ECDHE + AES-GCM / ChaCha20-Poly1305,含 RSA 密钥交换、 EMS/ALPN/证书校验)。用同一个 ClientHello 回落,所以 JA4 等指纹不变。 不支持:CBC 类套件(极少见,会明确报错)、会话恢复、0-RTT、客户端证书。
- 客户端证书未实现:传
cert=会抛NotImplementedRequestError(不静默忽略)。 files=的 multipart/form-data 已实现;stream=只是接收(响应始终缓存,iter_content仍可用)。- 已知问题:
www.google.com/www.googleapis.com握手能完成(JA4 一致), 但 GFE 随后用unexpected_message断链 —— 它在等某个特定形态的客户端消息。 其余实测站点正常。 - JA3 每次连接都不同,这是真 Chrome 的行为(扩展顺序随机置换),不是 bug; 稳定的标识是 JA4。
- 扩展顺序、GREASE 取值、ECH GREASE 长度、trust_anchors 顺序都是每次连接随机的 (与 Chrome 相同分布)。
- QUIC/HTTP3 未实现(Chrome 走 TCP 时就是这套指纹)。
真值来源
- 本机真 Chrome 153.0.8010.48(Windows x64)的抓包:22 条 ClientHello +
10 条 HTTP/2 连接(
capture/chrome153/),JA4 由 tshark 4.6.4 判定 - 本地 Chromium/BoringSSL 源码:
- 扩展表顺序与置换:
boringssl/src/ssl/extensions.cc:4067-4295, 4306-4328 - GREASE 首尾与 padding 规则:
extensions.cc:4489-4560 - ECH GREASE 长度:
boringssl/src/ssl/encrypted_client_hello.cc:732-784 - trust_anchors(0xca34):
include/openssl/tls1.h:141+extensions.cc:2948-2965 - 混合密钥共享拼接顺序:
boringssl/src/ssl/ssl_key_share.cc:308-346 - TLS 客户端配置:
net/socket/ssl_client_socket_impl.cc
- 扩展表顺序与置换:
Metadata
Release files for chrome-fp 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| chrome_fp-0.3.0.tar.gz | 105.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| chrome_fp-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 172.7 kB
Release files / chrome_fp-0.3.0.tar.gz
| Download URL | chrome_fp-0.3.0.tar.gz |
|---|---|
| Size | 105.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7eb333f6a6e91a3241ac3c0e7ac56006a9af24fd9fb1c0034e384f9012527d4f
|
|
BLAKE2b-256 checksum How to use checksums |
bfc4b6eb971bea764b1ffc52351d8ec92f845936720bf1a66c12796ce2e1cb6d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / chrome_fp-0.3.0-py3-none-any.whl
| Download URL | chrome_fp-0.3.0-py3-none-any.whl |
|---|---|
| Size | 67.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5770be8804305174fb385fde08b4cbb72df076c36bffe7b55658d3b1ecf27def
|
|
BLAKE2b-256 checksum How to use checksums |
244b436bd22c11fa5db10901c0610adf0feaba0b0dd8d96a00774d2062efc2ff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|