TLS 1.3 仍以明文发送服务器名称
TLS 1.3 加密握手,但不加密 SNI。读取 ClientHello,确认真正的 ECH 接受,并记录观察者仍能看到什么。
将监听器升级到 TLS 1.3 会改变观察者能读到什么,但变化并不像版本号暗示的那么大。ClientHello 仍然在任何加密协商之前发出,而在普通连接里,所请求的服务器名就以明文待在那里。Encrypted Client Hello 是把它藏起来的机制,而这是一套独立部署,有自己的前提条件。在写一条假定其中任一行为的策略之前,请先跑完这四步。
握手究竟暴露了什么
TLS 1.3 会加密服务器的证书以及 ServerHello 之后的大部分握手内容。客户端最先发出的东西都在那层保护之外,因为密钥尚不存在。服务器名称指示就属于第一趟报文,所以被动观察者仍能直接读到它,而不必破坏任何东西。
ECH 规范(RFC 9849)改变了这一点:它为协作服务器加密内部 ClientHello。外层握手仍然保留,并携带一个网络可见的公开名称,因此部署仍然需要一个对中间设备有意义的对外身份。那个外层名字正是人们期待连接变得匿名时容易忽略的部分。它是掩护名,不是被隐藏的名字,而且它必须像其他名字一样被解析和认证。
这些都不能隐藏投递路径。 源地址、目的地址、端口、分组大小和时序仍然可观察,而这些往往比名字更能说明是谁在说话。名称解析和应用层关系又是另一层,每层各有自己的可见性。
读一个 ClientHello,而不是想当然
ServerHello 里的版本并不能告诉你客户端发了什么。请直接从代表你所关心观察者的位置查看第一趟报文。
openssl s_client -connect app.plant.example:443 -servername app.plant.example -tls1_3 </dev/null> 2>/dev/null | head -20 sudo tcpdump -ni eth0 -c 40 'tcp port 443' -w /tmp/tls.pcap tshark -r /tmp/tls.pcap -Y 'tls.handshake.extensions_server_name' -T fields -e tls.handshake.extensions_server_name
第一条命令执行一次握手并显示协商出的参数,包括协议版本和你请求的名字。第二条捕获真实连接的最初几个分组,第三条从该捕获中提取 SNI 字段。如果该字段有值,你所模拟的观察者就能读到它。
当两侧不一致时,分别在客户端一侧和链路上抓包,因为本地抓包可能显示一个后来被中间设备改写或丢弃的名字。也要从一台不自行终止 TLS 的主机上抓包,因为终止型代理按设计就能看到明文,对于网络实际看到什么毫无证明力。
把 ECH 扩展当作一种声称,而不是证据
在 ClientHello 里看到 ECH 扩展并不意味着连接真的用了 ECH。客户端在没有可用配置时会发送 GREASE ECH,正是为了让它们的握手看起来一致。扩展在场,内部 hello 是诱饵,而真实名字仍然可读。
dig +short HTTPS app.plant.example openssl s_client -connect app.plant.example:443 -tls1_3 -status </dev/null> 2>/dev/null | grep -i -E 'ech|status_request'
第一条命令显示源站是否发布了带 ECH 配置参数的 HTTPS 资源记录,客户端正是这样学到密钥的。第二条查找握手状态信息,不过客户端的成功消息是更强的证据。关键在于接受:没有匹配密钥的服务器无法解密内部 hello,连接就会回退。
也要检查客户端,因为浏览器支持和配置随版本与平台而不同。建立在“现代浏览器都使用 ECH”之上的策略,关于的是一个机群的猜测,而不是你自己机群的事实。盘点实际会连接的版本,然后每类各测一个。同一个页面在当前的桌面浏览器上能用,未必能在同一网络里较旧的客户端上藏住任何东西。
把观察者的位置写进策略
在决定某项控制能依赖什么之前,先说明观察者坐在哪里、在那个位置能看到什么。浏览器到网站的连接、外层 VPN 传输以及终止 TLS 的代理各自暴露不同的事实集,而为某个位置写的规则很少能干净地迁移到另一个位置。
如果某项控制依赖读取 SNI,就测试已部署客户端在 ECH 可用与不可用时的行为。然后记录兼容性后果,因为一个悄悄停止匹配的过滤器,比一个从未部署过的过滤器更糟。
最后,把不确定性留在记录里。用一个更新的说法替换旧的普适说法,只会把同一个空白留在另一个协议名下,而下一任评审者会同样坚定地相信它。