TLS 指纹与 SNI 详解
每一条 HTTPS 连接都以一段明文进行的对话开始。这段对话中说出的两件事——你想要的站点名,以及你的软件打招呼的确切方式——是过滤系统所拥有的最有价值的信号。
HTTPS 中未被加密的那一部分
TLS 无法以加密状态开始,因为双方尚未就密钥达成一致。因此,建立该密钥的握手是以明文发送的,而且内容相当丰富:客户端提出版本、密码套件、椭圆曲线、签名算法和一份扩展列表,服务器则从它收到的选项中挑选。
握手之后的一切都受保护。握手期间的一切对路径上的任何人都可读。其中有两个要素承载了几乎全部的识别价值。
Server Name Indication:明文的主机名
一个 IP 地址通常服务着成千上万个站点。服务器必须在完成握手之前就知道该出示哪份证书,因此客户端会在一个名为 Server Name Indication 的扩展里声明它想要的主机名。它是未加密的,就在第一条消息里,由几乎所有主流客户端发送。
对过滤系统来说这近乎理想。它以域名形式、在固定且可预测的位置给出了目标目的地,无须继续跟踪连接,也无须与 DNS 做关联。与一份列表做匹配几乎不花任何代价。
- 它比按地址封锁更可靠,因为一个地址上可能同时托管着想放行和不想放行的站点。
- 它能挺过改用其他 DNS 解析器的做法,包括加密 DNS——域名会在连接本身中再一次以明文被声明。
- 它同样适用于白名单,而白名单是更严苛的配置:丢弃任何名称不在许可列表中的连接。
Encrypted Client Hello
Encrypted Client Hello 就是为弥合这一缺口而设计的机制。客户端通过 DNS 取得服务器的公钥,加密其问候中的敏感部分——包括真实主机名——转而发送一个携带通用公开名称的外层问候。观察者只能看到那个公开名称。
它确实有效,也是一项实实在在的改进。它的局限是实践层面而非理论层面的:需要两端都支持,依赖一条恶意解析器可以拒绝提供的 DNS 记录,而在几乎无人使用它的网络里,你在使用它这件事本身就很显眼。网络也可以干脆拒绝任何携带它的握手。
TLS 指纹:识别的是软件,不是站点
第二个信号更微妙。TLS 中没有任何规定要求所有客户端以相同顺序提出相同的东西,而实践中也没有哪两个是一样的。Chrome 会以特定顺序提供一组特定的密码套件,配以特定的一组扩展,其中还包括刻意的诱饵值。Firefox 不同。Go 的标准库与两者都差别明显。OpenSSL 又不一样,而且不同版本之间也有差别。
把这个有序集合哈希一下,就得到客户端软件的一个简短而稳定的标识符。JA3 是最早被广泛使用的方案;JA4 是它的后继者,设计上对某些客户端如今刻意进行的乱序更有韧性。无论哪种,结果都一样:观察者无须解密任何东西,就能判断出是什么类型的程序开启了这条连接。
| 问题 | 答案 |
|---|---|
| 是哪个浏览器或库建立了这条连接? | 通常可以,精确到一个版本区间。 |
| 请求的是哪个站点? | 指纹给不出——那是 SNI 的职责。 |
| 发送或接收了什么? | 不能。 |
| 客户端与它自称的身份相符吗? | 能,而这正是关键。 |
正是最后这一行,让指纹对隧道技术至关重要。一条 SNI 指向普通网站、指纹却属于 Go 程序而非浏览器的连接,自相矛盾了。两个信号单独看都不可疑;可疑的是它们之间的矛盾。
为什么模仿比听起来更难
显而易见的对策是让隧道产生浏览器的指纹。正是为此而生的库是存在的,而且确实管用。真正棘手的地方在于,这不是解决一次就完事的问题。
- 随着密码套件被淘汰、后量子密钥交换逐步铺开,浏览器会定期改变自己的握手。一年前与 Chrome 匹配的指纹如今谁都不匹配,而这比完全不模仿更糟。
- 有些浏览器会刻意随机化扩展顺序,因此对某一次抓包的静态模仿本身就是一种异常。
- 指纹必须与其他一切保持一致:协商出的版本、ALPN 值、随后的记录大小、首个请求的时序。
- 罕见的指纹即便未被识别为隧道,本身也是一个信号。仅仅「不寻常」就已经足够。
值得记取的通则是:隐藏并不在于是否加密,也不在于是否不寻常。它在于一致——每一层都讲述同一个故事,而那个故事是平淡无奇的那一个。
常见问题
我的网络能看到我通过 HTTPS 访问了哪些网站吗?
在几乎所有情况下,它都能看到主机名,因为主机名是在 TLS 握手中以明文发送的。它看不到具体页面、内容,或你提交的任何东西。在两端都支持的情况下,Encrypted Client Hello 也会把主机名隐藏起来。
什么是 JA3 指纹?
它是把客户端在 TLS Client Hello 中提供的有序字段——版本、密码套件、扩展、曲线和点格式——做哈希得到的值。它识别的是客户端软件而非用户,在安全监控和过滤中都被广泛使用。JA4 是目的相同的较新方案。
VPN 会隐藏 SNI 吗?
它把 SNI 挪了个位置。你通过隧道访问的那些站点,其握手发生在加密通道内部,本地网络读不到。而通往隧道本身的连接是一个独立的 TLS 会话,有它自己的握手,它在那个字段里填什么,网络看到的就是什么。