Skip to main content

chrome-fp — 与真 Chrome 逐字节同指纹的纯 Python 请求库, 用法和 requests 一样

用 Python 发 HTTP 请求, 但 TLS/HTTP2 指纹跟本机真 Chrome(默认 154.0.8037.98, 可用 chrome_version="153" 切到 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()

版本 profile:Chrome 154(默认)/ 153

Session(chrome_version=...) 选版本,默认跟着最新 Stable(154,本机 154.0.8037.98):

from chrome_fp import Session
Session()                        # Chrome 154(默认)
Session(chrome_version="153")    # Chrome 153(153.0.8010.48)

2026-10-03 在本机做了 153 ↔ 154 的 A/B 实抓:Chrome for Testing 153.0.8010.47 与 本机 Stable 154.0.8037.98 各抓 20+ 条真实连接(tools/run_capture.py 起本地 TLS/HTTP2 探针 + 拉真 Chrome,tools/analyze_hello.py 解析):

项目 Chrome 153 Chrome 154 结论
JA4_a / JA4_b t13d1517h2 / 8daaf6152771 完全相同 不变
cipher / supported_groups / sig_algs — 完全相同 不变
扩展集合(17 个) — 完全相同 不变
trust_anchors(0xca34) 28 个 ID 同一集合 不变
H2 SETTINGS / WINDOW_UPDATE / 帧序 — 完全相同 不变
sec-ch-ua 品牌 "Google Chrome";v="153", "Not_A Brand";v="8", "Chromium";v="153" "Chromium";v="154", "Google Chrome";v="154", "Not A(Brand";v="99" 随版本变
UA / full-version Chrome/153.0.0.0 Chrome/154.0.0.0 随版本变

153 → 154 的 TLS/HTTP2 线格式没有变化,差异只在版本字符串与 UA 品牌串。 profile 以 dataclasses.replace 从 153 派生 154,spec.PROFILES 里继续加版本即可。

实现细节:spec 模块级常量(CHROME_VERSION、CIPHER_SUITES…)跟随"当前生效 profile"(PEP 562 模块级 __getattr__ + ContextVar),所以 Session(chrome_version="153") 建出来之后,同线程里读 spec.CHROME_VERSION 会得到 153;每个 Session.request() 也会 重新切回自己的版本,多版本可共存于同一进程。旧代码里的全大写常量名继续可用。

同一套探针也用来验证本库自己:把库指向探针抓一遍,与真 Chrome 154 逐项对比 —— ClientHello 的 cipher / groups / sig_algs / 扩展集合 / trust_anchors / session_id / key_share / ALPN 8 项全一致,JA4(含去 GREASE 的 c 段)三处字符串完全相同。

本轮还修掉一处旧实现与真机不符的地方:真 Chrome 的 HEADERS 帧带 PRIORITY 标志 (flags 0x25),前面有 5 字节 E=1 / depends_on=0 / weight=按 RFC 9218 urgency 查表 (u=0→256、u=1→220、u=3→147)。153/154 实测都带,之前实现少发这 5 字节。

抓包与验证工具链(2026-10 现状)

旧的 tools/ 在目录清理中丢失,本轮重写了一条不依赖 tshark、也不需要管理员权限的探针链, 153 / 154 两个版本都用它校对:

# 1) 抓真 Chrome: 起本地 TLS/HTTP2 探针 + 拉 Chrome(临时 profile, 不碰你在用的配置)
python tools/run_capture.py --out capture/chrome154 --ports 8443-8462 --seconds 25
#    Chrome for Testing 等非默认安装: --chrome <path> --extra=--no-sandbox

# 2) 解析 ClientHello / JA4 / trust_anchors, 并对着 profile 逐项 diff
python tools/analyze_hello.py --dir capture/chrome154 --diff

# 3) 把**本库**指向同一个探针, 和真 Chrome 的字节逐项对比
python tools/tap_probe.py --ports 8443-8446 --out capture/library154 --wait 45
python tools/verify_against_probe.py --port 8443
工具 作用
tools/tap_probe.py 本地 TLS/HTTP2 探针:MemoryBIO 驱动,握手期的原始字节逐字节留档,并解出 H2 帧与请求头
tools/run_capture.py 起探针 + 拉真 Chrome + 收日志(Chrome 是 GUI 子系统程序,PowerShell 抓不到它的 stdout)
tools/launch_chrome.py 单独启动 Chrome 打探针(host-resolver-rules + 临时 profile + --no-proxy-server)
tools/analyze_hello.py 解析 ClientHello / JA4 / 扩展 / trust_anchors,--diff 直接与 profile 对比
tools/verify_against_probe.py 用本库发请求走同一探针,与真 Chrome 逐项比字节

抓包证据放在 capture/(chrome154*、chrome153cft、library154);按"仓库只留代码"的取舍, capture/ 在 .gitignore 里,没有提交。

第 1 轮(0.2.x → 0.3.0):对齐 Chrome 153(历史记录)

下面这段是当时(0.3.0)的做法与结论,保留作历史记录。它引用的 tools/tap_proxy.py、analyze_capture.py、tls13_decrypt.py、h2_frames.py、diff_fp.py 和 capture/chrome153/ 已经在目录清理中丢失,当前可用的工具见上一节。

本机是非管理员,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 即可,分析流程完全一样。

抓出来的差异(0.3.0 已修;其中 #3 在 0.6.0 修正为相反结论)

# 项目 真 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 前缀(flags 0x25) 带 5 字节 weight 前缀 0.3.0 当时误判为"不带"、把前缀删了;0.6.0 实抓修正为带上(与 #2 的"不发 PRIORITY 帧"不矛盾:帧不发,但 HEADERS 的标志位和前缀要带)
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 完全一致。

验证结果

当时用 tools/verify_live.py 让本库走同一个代理访问同一个本地 h2 服务器,再和 Chrome 的抓包对比 (该脚本已丢失;现在的等价做法是 tools/verify_against_probe.py):

[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 类请求的头顺序与取值全部逐条相同

实测结果

下表是本轮(2026-10-03)两个版本各自的样本量;"本库"一列是把库指向同一探针抓出来的结果。

项目 真 Chrome 153(20~22 条样本) 真 Chrome 154(22 条样本 / 20 条连接) 本库
JA4 t13d1517h2_8daaf6152771_<随机> 同左(逐字符相同,含去 GREASE 的 c 段) 一致
HTTP/2 Akamai 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p 同左 一致
扩展集合 17 真实 + 2 GREASE 同左 一致
trust_anchors 28 个 ID(集合固定、顺序随启动变) 同一集合 一致
HEADERS 的 PRIORITY 前缀 带(u=0→256 / u=1→220 / u=3→147) 同左 一致
请求头顺序/取值 导航与子资源各一套 同左 一致(逐条比对)

实网站点冒烟(tools/live_sites.py,该脚本已丢失,结论保留):

example.com / taobao  h2 + TLS1.3
baidu / qq / sohu     TLS1.2 回落 + HTTP/1.1        5/5 OK

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 才打开(153/154 都不发 PRIORITY 帧)
    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:不发 X25519MLKEM768
  • grease_parts=():完全关闭 GREASE
  • permute_extensions=False:关闭扩展随机置换(调试用)

协议实现覆盖

TLS

能力 状态
TLS 1.3 / 1.2(同一 ClientHello 自动分派) ✅
密码套件:AES-GCM / ChaCha20-Poly1305 / AES-CBC-SHA ✅
RSA 密钥交换(009c/009d/002f/0035) ✅
X25519MLKEM768 混合密钥共享(纯 Python ML-KEM-768) ✅
HelloRetryRequest(换 key_share 组 / cookie) ✅
证书链校验(交叉签名根、EKU 缺失放行、通配符) ✅
CertificateVerify 验签、压缩证书(brotli)、ALPS ✅
客户端证书 mTLS(cert=,TLS 1.3 与 1.2) ✅
KeyUpdate / NewSessionTicket 容忍 ✅
SSLKEYLOGFILE 导出(Wireshark 可解密本库流量) ✅
会话恢复(PSK / session ticket)、0-RTT、ECH 真加密 ❌ 见下

cert= 的用法和 requests 一致:

s.get(url, cert="client.pem")                  # 证书+私钥在同一个 PEM
s.get(url, cert=("client.pem", "client.key"))  # 分开两个文件
Session(cert=...)                              # 会话级

HTTP/2

能力 状态
Chrome 帧序(preface/SETTINGS/WINDOW_UPDATE/HEADERS,153/154 一致) ✅
HEADERS 的 PRIORITY 前缀(0x25 + E=1/dep=0/weight=urgency 查表,153/154 都带) ✅
TLS 记录分帧(preface+SETTINGS+WINDOW_UPDATE 合成一条 record) ✅
HPACK 编码选择与 Chromium 一致(见下) ✅
请求体流控(连接窗口 + 流窗口,等 WINDOW_UPDATE) ✅
尊重对端 SETTINGS_MAX_FRAME_SIZE / INITIAL_WINDOW_SIZE ✅
PUSH_PROMISE(回 RST_STREAM(CANCEL)) ✅
CONTINUATION / 1xx / trailer / GOAWAY / RST_STREAM ✅
流式响应(stream=True)与流式请求体(生成器) ✅

HPACK 为什么重要:头解码后一样 ≠ 编码后一样。JA3/JA4/Akamai 都不看 HPACK, 但服务器/中间盒 dump 一下 HEADERS 帧的原始载荷,就能用编码方式把客户端区分开。 hpack_chromium.py 复刻了 Chromium 的选择规则,规则是从真机抓包里统计出来的 (当时用 tools/diff_wire.py 复核,该脚本已丢失;本轮的探针 tools/tap_probe.py 能把每条连接的 HEADERS 原始载荷按序留档,可用来重做同样的比对):

规则 证据(真 Chrome 153 抓包)
静态表精确命中 → 索引表示 :method: GET、:scheme: https
:method / :path 命中不到精确项 → 字面量+不索引(0x04) OPTIONS 的 :method、所有 :path
其余头 → 字面量+增量索引(0x40) cookie / referer 实测都是 0x44(不是"从不索引")
Huffman 择优,平局取原始字节 /?n=0 平局→raw;/style.css 更短→Huffman

验证方式是把抓包里每条连接上的请求按原顺序重放再逐块比字节(HPACK 动态表是连接级 状态,不按顺序重放比出来的差异是噪声):真 Chrome 抓包 32/32、本库抓包 11/11 全部逐字节复现。

Client Hints(Accept-CH)

网站用 Accept-CH 点单后,真 Chrome 会在后续请求里带上高熵 client hints。完全不发的话, 凡是下发过 Accept-CH 的站(不少 CDN 都发)一眼就能看出不是浏览器 —— 这比 JA3 更硬。 本库按规范跟踪每个源的 Accept-CH,命中后按 Chrome 的内部固定顺序回发 (顺序与取值来自探针抓包;本轮重写的探针 tools/tap_probe.py 会把每个请求的头顺序 原样记进 probe.json,可直接复核)。

Session(client_hints=True)                            # 默认开
Session(client_hints=False)                           # 完全不发
Session(client_hint_values={"device-memory": "4"})    # 覆盖取值

⚠️ 如实说明:探针抓包里 UA 派生那几个(full-version / full-version-list / arch / bitness / platform-version)是空串 —— 因为抓包时为让 UA 逐字节一致加了 --user-agent,Chrome 会把 UA 派生的高熵 hint 置空。正常 UA 下它们有真实值,本库按 Chrome 153 / Windows x86_64 填。这几个取值没有抓包直接验证,需要精确对齐时逐项覆盖。

HTTP/1.1

能力 状态
定长 / chunked 响应、gzip/deflate/br/zstd ✅
chunked 请求体(生成器/无长度文件对象) ✅
真流式响应(stream=True + iter_content,增量解压) ✅
连接复用(响应头标题大小写按 Chrome 习惯) ✅
Expect: 100-continue ❌ 未实现(极少见)

连接层

DNS 返回多个地址(尤其 IPv6 排在前面)时会自己遍历并 IPv4 优先,每个地址限时 5 秒、整体不超过 timeout。踩过的坑:socket.create_connection 按 DNS 顺序逐个用 完整 timeout 尝试,两个黑洞 IPv6 地址就能烧光整个预算 —— 实测 example.com 要 12 秒 才连上、timeout 小一点直接失败,而同一时刻 requests 只要 0.6 秒。修好后 1.1 秒。

还能被检测出来吗(现状)

分三层看,别混为一谈:

层 现状
TLS/HTTP2 内容指纹(JA3/JA4/Akamai/扩展集合/头顺序) 对齐(见上面各表)
线上字节细节(TLS 记录分帧、HPACK 编码) 已对齐(当时用 tools/diff_wire.py 复核 32/32、11/11;该脚本已丢失)
Client Hints(Accept-CH) 机制已实现,UA 派生取值未直接抓包验证
行为 / JS / 跨层一致性 做不到,见下

具体来说,下面这些仍然是"是不是浏览器"的硬伤,不是指纹库能解决的:

  1. 不执行 JS。Cloudflare / Akamai / DataDome / PerimeterX 最终都靠主动 JS 挑战 + 行为判定;TLS 指纹只决定"别在门口就被拦"。
  2. 行为不像人。单发请求、无子资源、无缓存/条件请求、请求节奏固定、长期零会话恢复。
  3. 跨层矛盾:库声明 Windows 平台,但 TCP/IP 栈用的是宿主机 —— 在 Linux 上跑必然 出现"Windows 浏览器 + Linux TCP 指纹",这一条比任何指纹都硬。(Windows 上自洽)
  4. Cookie 语义仍未实现 Secure / SameSite / partitioned / __Host- 前缀与排序规则, 服务器可以用一个 Secure cookie 测出来。
  5. 无 HTTP 缓存,从不发 If-None-Match / If-Modified-Since。
  6. sec-fetch-site / referer / origin 依赖调用方给上下文,给错就与真实页面矛盾。

故意不实现的部分(附原因)

  • TLS 1.3 会话恢复(PSK / session ticket):恢复用的 ClientHello 会多出 pre_shared_key 扩展,JA4 与首次连接不同。本库的真值来自 Chrome 全新 profile 的首次连接(17 条真实扩展 + 2 GREASE、无 PSK),没有抓到 Chrome 的恢复握手样本。 在没有真值的情况下实现,只会让指纹从"确定对"变成"可能错",所以宁可不做。 要拿真值需要在同一个 profile 里连打两次 —— 本轮的 tools/run_capture.py 每次都用 全新临时 profile,所以抓不到 pre_shared_key,需要改造后才有样本。
  • 0-RTT / early_data:本库的 ClientHello 与真 Chrome 一样不带 early_data(0x002a), 发早期数据会直接改变指纹。
  • ECH 真加密:需要 DNS HTTPS 记录里的 ECHConfigList + HPKE。Chrome 只在站点发布 ECH 时才真加密,本库一律发 ECH GREASE(与抓包一致)。
  • QUIC / HTTP/3:要另写一整套 QUIC 传输(丢包恢复、拥塞控制、QPACK…), 是另一个量级的工程;Chrome 走 TCP 时就是本库复刻的这套指纹。
  • Expect: 100-continue:极少见,收益低。

测试

python -m unittest discover -s tests -v      # 97 个用例(80 通过 / 17 跳过)
python -m pytest tests -q                    # 同上, 输出更紧凑

需要抓包数据的用例(test_chrome153_fingerprint.py)在本机找不到 capture/chrome153/ 时会自动跳过 —— 就是那 17 个 skip。端到端 / 线级校验现在走 「抓包与验证工具链」一节的 tools/run_capture.py + tools/verify_against_probe.py。

测试文件 覆盖
test_profiles.py 多版本 profile:153/154 注册与切换、旧常量兼容、JA4 前缀、HEADERS 优先级前缀
test_chrome153_fingerprint.py 把 ClientHello / 请求头钉死在真机抓包上(含 key_share 私钥配对回归;无抓包数据时跳过)
test_hpack_chromium.py HPACK 编码逐字节复现真 Chrome(含动态表连续状态)
test_client_hints.py Accept-CH 跟踪、高熵 hint 顺序/取值、cookie 位置
test_requests_api.py requests 语法、cookie jar、重定向、多地址连接
test_tls_features.py HRR、TLS1.2 CBC、mTLS(各自带本地 TLS 服务器)
test_http2_features.py 请求体流控、MAX_FRAME_SIZE、PUSH_PROMISE(h2 库的流控检查当裁判)
test_streaming.py chunked 上传、stream=True 真流式、增量解压

构建与发布

自动发版(GitHub Actions,推荐)

.github/workflows/release.yml —— push 一个 v* tag 就自动发版:

push tag v0.6.0
      └─ test    跑 unittest(97 个用例)
      └─ build   python -m build + twine check
                 tools/check_release.py --tag $TAG --dist dist
                 (tag / pyproject.toml / __init__.py 三者版本必须一致 + 产物内容检查)
                 sha256sum > dist/SHA256SUMS
      └─ release tools/make_release_notes.py 生成正文(含每个产物的 SHA256)
                 gh release create —— 上传 *.whl / *.tar.gz / SHA256SUMS
      └─ pypi    可选,见下

本机一条命令:

python tools/release.py patch        # 0.6.0 -> 0.6.1: 改版本号 + 跑测试 + 提交 + 打 tag + push
python tools/release.py minor        # 0.6.0 -> 0.7.0
python tools/release.py 0.7.0        # 指定版本
python tools/release.py patch --dry-run     # 只打印要做什么

tools/release.py 会先跑测试,测试不过就把版本号改回来并中止;之后 Actions 接管构建与上传, 本机不需要 twine。手动触发 workflow_dispatch 只跑测试+构建(产物留在 Actions artifact)。

.github/workflows/ci.yml:push 到 main / PR 时跑测试 + 构建校验(目前只跑 Python 3.12, 即本机验过的版本;要扩矩阵就改 python-version 列表)。

发 PyPI(可选,默认关):把仓库变量 ENABLE_PYPI 设为 true,并在 PyPI 配好 Trusted Publisher(owner=DreamXiaoJing、repo=chrome-fp、workflow=release.yml、 environment=pypi),release 流程里的 pypi job 就会用 OIDC 发布 —— 不需要任何长期 token。

手动构建(备用)

python -m pip install build wheel twine
python -m build                    # 产出 dist/chrome_fp-0.6.0-py3-none-any.whl 和 .tar.gz
python -m twine check dist/*       # README 渲染与元数据检查
python -m twine upload dist/*      # -u __token__ -p pypi-xxxx (建议用 TWINE_PASSWORD 环境变量)
python tools/check_release.py --tag v0.6.0 --dist dist   # 版本 + 产物自检
  • 当前仓库版本:0.6.0(多版本 profile:新增 Chrome 154、153→154 A/B 实抓校对、 修正 HEADERS 的 PRIORITY 前缀)—— 已提交,尚未发 PyPI
  • 已发布:chrome-fp 0.5.0(https://pypi.org/project/chrome-fp/0.5.0/) —— Chromium 一致的 HPACK 编码 / TLS 记录分帧 / Accept-CH 高熵 hints
  • 历史版本:0.4.0(HRR / TLS1.2 CBC / 客户端证书 / HTTP2 请求体流控 / 真流式)、 0.3.0(Chrome 153 指纹对齐)、0.2.x(Chrome 152)
pip install chrome-fp             # 或者 pip install "chrome-fp[encoding]"

产物内容(0.6.0 实测):

  • wheel:chrome_fp 包 17 个模块 + LICENSE + METADATA
  • sdist:额外带 tests/(8 个测试文件),不含 tools/ —— 旧 MANIFEST.in 在目录 清理中丢失,需要把工具链也打进 sdist 的话得补回 MANIFEST.in(或写进 [tool.setuptools] 的 sdist 配置)
  • capture/ 抓包数据不进任何产物

0.5.0 之前的构建/发布校验脚本(tools/verify_build.py、tools/verify_pypi.py) 已随目录清理丢失,本轮没有重建;上面列的是当前能跑的命令。

目录结构

chrome_fp/
  spec.py         多版本 profile(Profile 数据类 + PROFILES 注册表 + 153/154 常量 +
                  每种资源类型的头顺序,全部标注出处;旧的大写常量名继续可用)
  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/**CBC**, EMS, 客户端证书)
  clientcert.py   客户端证书加载 + 签名算法选择 + TLS1.2/1.3 证书消息构造
  client.py       发同一个 ClientHello, 按 ServerHello 自动分派 1.3 / 1.2
  mlkem.py        ML-KEM-768 (FIPS 203) 纯 Python
  http2.py        HTTP/2 客户端(帧序/SETTINGS/头顺序/请求体流控/推送/流式)
  hpack_chromium.py  HPACK 编码选择与 Chromium 一致(编码字节可逐字节复现)
  _huffman_table.py  RFC 7541 附录 B 的 Huffman 表(生成物, 不依赖 hpack 内部实现)
  http1.py        HTTP/1.1 客户端(Title-Case 头名 / chunked / 流式)
  session.py      requests 风格的 Session / Response / PreparedRequest
  api.py          模块级 get/post/...(与 requests 同名同签名)
  structures.py   CaseInsensitiveDict / RequestsCookieJar
  exceptions.py   与 requests.exceptions 对应的异常层次
tools/            真机抓包探针(tap_probe/run_capture/launch_chrome) + 指纹解析分析
                  (analyze_hello) + 本库与真机对比(verify_against_probe)
                  + 发版工具(release.py 改版本号打 tag / check_release.py 一致性校验 /
                  make_release_notes.py 生成 Release 正文)
.github/workflows/  release.yml(tag 自动发版)+ ci.yml(push/PR 跑测试与构建)
tests/            单元 + 与真机抓包的回归测试(97 个用例, 其中 17 个需要抓包数据)
capture/          真机抓包证据(chrome154* / chrome153cft / library154;.gitignore 未提交)

已知边界

  • www.google.com / www.googleapis.com:握手能完成(JA4 一致),但 GFE 随后用 unexpected_message 断链。本机到 Google 的 IP 被黑洞/污染(tcp 都连不上), 暂时无法进一步定位。其余实测站点正常。
  • JA3 每次连接都不同,这是真 Chrome 的行为(扩展顺序随机置换),不是 bug; 稳定的标识是 JA4。
  • 扩展顺序、GREASE 取值、ECH GREASE 长度每次连接都重新随机(与 Chrome 相同分布)。
  • trust_anchors 的顺序是在启动之间变的(CfT 153 两次独立启动给出两个不同顺序), 同一次启动内固定;集合恒定。本库发的是其中一个合法顺序(与 Stable 154 实测一致), 所以集合一定对、顺序可能落在另一个排列上。
  • 会话恢复 / 0-RTT / ECH 真加密 / QUIC-HTTP3 故意未实现,原因见上面「故意不实现的部分」。
  • 连接策略是 IPv4 优先(Chrome 用 happy eyeballs 并发尝试)。对黑白洞 IPv6 的环境 这是必要的取舍,但严格来说与 Chrome 的连接时序不同 —— 这不影响 TLS/HTTP2 指纹。

真值来源

  1. 本机真 Chrome 的抓包:
    • 154.0.8037.98(Windows x64, Stable):22 条 ClientHello + 20 条 HTTP/2 连接, 另有第二次启动复核 UA 品牌与 trust_anchors 顺序(capture/chrome154*)
    • 153.0.8010.48(Windows x64, Stable):22 条 ClientHello + 10 条 HTTP/2 连接, JA4 由 tshark 4.6.4 判定 —— 这批数据(capture/chrome153/)已在目录清理中丢失, 只剩结论;好在 153 的线格式与 154 完全一致(见上面的 A/B 表)
    • A/B 对照用的 Chrome for Testing 153.0.8010.47(capture/chrome153cft/); 注意它是 Chromium 分支(sec-ch-ua 无 Google Chrome 品牌),且多一个构建特有的 扩展 0x12e0,所以只用于对照 TLS/H2 线格式,不当作 Stable 153 的品牌基准
  2. 抓包工具(本轮新增/重写,替代已删除的旧 tools):
    • tools/tap_probe.py —— 本地 TLS/HTTP2 探针,记录 ClientHello 原字节 + 解密后的 H2 帧与请求头(MemoryBIO 驱动,握手期的原始字节逐字节留档)
    • tools/run_capture.py —— 起探针 + 拉真 Chrome + 收日志的一条龙入口
    • tools/analyze_hello.py —— 解析 ClientHello/JA4/trust_anchors 并与 profile 逐项 diff
    • tools/verify_against_probe.py —— 把本库指向同一探针,和真 Chrome 对比字节
  3. 本地 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.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for chrome-fp 0.6.0
File Size Uploaded
chrome_fp-0.6.0.tar.gz 141.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for chrome-fp 0.6.0
File Interpreter ABI Platform
chrome_fp-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 239.8 kB

Release files / chrome_fp-0.6.0.tar.gz

Download URL chrome_fp-0.6.0.tar.gz
Size 141.4 kB
Tags Source
SHA-256 checksum
How to use checksums
65b10e70bd6206867418a3676ebc148c076e962e413430e96046073d7edc328e
BLAKE2b-256 checksum
How to use checksums
0d2374a64549413343ef7b0562f16d468045ce9baf13324046cb544ce247ada9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release files / chrome_fp-0.6.0-py3-none-any.whl

Download URL chrome_fp-0.6.0-py3-none-any.whl
Size 98.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
968983497037681a11bbd38237bf64bbf97eb52153eef834303b86fec962b6d0
BLAKE2b-256 checksum
How to use checksums
ecbe0bf7a25aede9f1a361d655f4744677a9624c14d69cc24249c702991d5327
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release history Release notifications | RSS feed

0.7.0

2 release files

This release

0.6.0 This release

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.3

1 release file

0.2.2

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page