4. JA3与JA3S指纹原理与实现
JA3 与 JA3S 是 TLS 指纹识别中最经典、工程落地最广的一对方法。它们并不解密流量,也不依赖证书内容,而是把 TLS 握手阶段客户端与服务端各自“主动暴露”的协商参数,按固定规则拼接成字符串,再做一次 MD5 哈希,得到一个 32 位十六进制指纹。理解 JA3/JA3S 的关键,不是记住哈希值,而是理解“哪些字段被采集、按什么顺序拼接、为什么客户端和服务端要分开算”。
一、JA3 的字段拼接规则
JA3 针对的是 ClientHello 报文,采集五个字段,按固定顺序用逗号分隔,字段内部再用短横线连接:
- TLS Version:ClientHello 中客户端声明的最高支持版本,取十进制值,例如 TLS 1.2 记为 771,TLS 1.3 记为 772。
- Cipher Suites:客户端支持的密码套件列表,按报文中的原始顺序,取每个套件的十进制值,用“-”连接。GREASE 值需要剔除。
- Extensions:扩展类型列表,同样按原始顺序取十进制值,用“-”连接,剔除 GREASE。
- Elliptic Curves:supported_groups 扩展中的椭圆曲线列表,按顺序取十进制值。
- EC Point Formats:ec_point_formats 扩展中的点格式列表,按顺序取十进制值。
拼接格式为:Version,Ciphers,Extensions,Curves,PointFormats。例如一个简化的示例字符串可能是:
771,4865-4866-4867-49195-49199,0-11-10-35-22-23-13-43-45-51,29-23-24,0
对这个字符串做 MD5,得到的就是 JA3 指纹。需要注意:字段顺序不能变,列表内部顺序也不能重排,因为顺序本身携带了客户端实现特征。不同浏览器、不同 TLS 库、甚至同一库的不同版本,其扩展顺序和套件顺序都可能不同,这正是 JA3 能区分客户端栈的原因。
二、JA3S 的字段拼接规则
JA3S 针对的是 ServerHello 报文,字段更少,只有三个:
- TLS Version:服务端最终选定的版本,取十进制值。
- Cipher Suite:服务端选定的单个密码套件,取十进制值。
- Extensions:ServerHello 中出现的扩展类型列表,按顺序取十进制值,用“-”连接。
拼接格式为:Version,Cipher,Extensions。例如:
771,49195,65281-0-11-35-23
同样做 MD5 得到 JA3S 指纹。JA3S 的字段少,区分度天然低于 JA3,但它反映的是服务端的“选择行为”,在识别服务端软件、CDN、负载均衡或中间盒时非常有用。
三、客户端与服务端指纹的差异
JA3 和 JA3S 虽然算法同源,但语义完全不同。JA3 描述的是“客户端想谈什么”,字段多、组合空间大,因此区分度高,适合做客户端栈识别、恶意工具识别和资产画像。JA3S 描述的是“服务端最终选了什么”,字段少、变化空间小,区分度低,但它能反映服务端的配置策略和实现特征。
工程上常见的用法是把 JA3 与 JA3S 组合成二元组。同一个 JA3 客户端与不同 JA3S 服务端交互,会形成不同的组合;反过来,同一个 JA3S 也可能被大量不同 JA3 客户端命中。组合使用可以显著降低误报,例如某些扫描器 JA3 很特殊,但 JA3S 与正常浏览器一致,就需要结合两者判断。
四、代码实现与验证方法
下面用 Python 演示 JA3 的核心拼接与哈希逻辑。实际解析 ClientHello 时,需要从 TLS 记录层逐层解析到握手层,再按字段读取。这里用结构化数据模拟已解析出的字段,重点展示拼接规则:
import hashlib
def ja3_fingerprint(version, ciphers, extensions, curves, point_formats):
# 剔除 GREASE 值(0x?a?a 形式,十进制如 2570, 6682 等)
def clean(lst):
return [x for x in lst if not (x & 0x0f0f == 0x0a0a and (x >> 8) == (x & 0xff))]
parts = [
str(version),
"-".join(map(str, clean(ciphers))),
"-".join(map(str, clean(extensions))),
"-".join(map(str, clean(curves))),
"-".join(map(str, clean(point_formats))),
]
raw = ",".join(parts)
return hashlib.md5(raw.encode()).hexdigest(), raw
# 示例参数(模拟解析结果,非实测)
fp, raw = ja3_fingerprint(
771,
[4865, 4866, 4867, 49195, 49199],
[0, 11, 10, 35, 22, 23, 13, 43, 45, 51],
[29, 23, 24],
[0]
)
print(raw)
print(fp)
运行后会输出拼接串和 32 位 MD5 值。验证时建议分三步:第一步,用 Wireshark 抓一次真实 TLS 握手,导出 ClientHello 的字段,手工按规则拼一遍,与代码输出比对;第二步,用同一客户端访问不同服务端,观察 JA3 是否稳定、JA3S 是否随服务端变化;第三步,用不同客户端(如 Chrome、curl、Python requests)访问同一服务端,观察 JA3 是否出现明显差异。
掌握 JA3/JA3S 后,下一步就是把它接入检测流水线:从流量中提取握手字段、计算指纹、建立基线、对异常指纹告警。本节的重点是先把“字段怎么取、字符串怎么拼、哈希怎么算、结果怎么验”这条链路走通,后续的检测策略才有可靠的数据基础。