麻烦的 ECH 是什么?

创建于 7060 / 约需 33 分钟

如果只是想在浏览器上启用 ECH 的话,按照 Firefox 的教程Chrome 的文档进行配置就可以了。在 tls-ech.dev 可以进行测试。

说在前面

ECH 的几个设计上的特性:

  • ECH 只运行在 TLS 1.3。

    It MUST NOT offer to negotiate TLS 1.2 or below.

  • ECH 要求 DNS over HTTPS(或者其它的加密 DNS 方案)。并不是出于安全性原因,而更多地是出于隐私保护的原因。毕竟如果 DNS 可以被监听的话,SNI 加密就没意义了。目前实现了 ECH 客户端的 FirefoxChrome 也都要求 DNS over HTTPS 有启用才能使用 ECH。

    ECH requires encrypted DNS to be an effective privacy protection mechanism. [§10.2]

    Thus, allowing the ECH records in the clear does not make the situation significantly worse. [§10.6]

  • 受逻辑思维能力限制,笔者其实没有特别看懂这条草案。下面的内容可能有错漏,如果有遇到什么问题或者发现什么错误,还请读者告知和斧正。

草案版本

因为 ECH(以及相关的一系列)文档现在还处于草案状态,没有成为标准,所以这篇文章是根据目前最新的草案版本而合成的。本文涉及的 IETF 草案版本:

然后我们就进入正题,来聊聊这个反正早晚会被封(哈?),所以其实对中国大陆用户没什么用,但有总比没有好的 SNI 新玩法。

由于笔者太懒,在本文开始写到写完期间,版本 17也已经发布。不过它和版本 16 只有行文上的微小区别(例如把 HPKE 草案的引用更新到了 RFC9180),所以本文的内容基本不受影响。


SNI 是什么?

SNI 是 Server Name Indication 的简称。它的主要作用是让服务端了解客户端想要访问的是哪个域名,对于同一个服务器上托管多个站点的场景尤其有用(现在这是很常见的情况了)。这是一个从 TLS 1.0 时代,2003 年的 RFC 3546 就有的特性。后来它的身影又出现在 RFC 4366RFC 6066 中。虽然 TLS 数据流的主体是加密的,但由于包含 SNI 信息的 clientHello 是明文的,中间人很容易知道一个正在建立中的 TLS 连接在访问哪个域名。

为什么要有 ESNI/ECH?

SNI:「我从来没觉得 TLS 握手开心过」

现在是 2023 年。刚才我们提到过,TLS 是从 2003 年开始就有的东西;从第二个握手开始,所有的内容就都是加密的了。而 clientHello 之前的部分,也就是 DNS 请求,也早在 2016 年的 DNS over TLS (RFC7858) 和 2018 年的 DNS over HTTPS (RFC8484) 就可以被加密。serverHello 的证书在之前是明文传输的,但 TLS 1.3 已经把服务端证书部分加密,只留下临时密钥交换的部分了。剩下的就只有缺少关爱的迷子 clientHello 了。其中的 SNI 又因其敏感性首当其冲,成为了中间人们的首要目标。

所以就像笔者之前说的一样,ECH 是 TLS 全流程加密上的最后一块拼图;SNI 的迷路日々也终于来到了尾声。

从 ESNI 到 ECH

为什么还要叫 esni

这个草案之前叫 ESNI (Encrypted Server Name Indication)。不过现在虽然草案名还叫 draft-ietf-tls-esni,但草案的核心从版本 6 开始就从加密 SNI 转移到了加密整个 clientHello;标题也从版本 7 开始就从 Encrypted Server Name Indication (ESNI) 变成了 Encrypted Client Hello (ECH)。这期间草案对 TLS 握手过程设计的变化还挺多的

TLS 和 ECH 的价值

我们列出一个表格来对比不同条件下中间人(不控制 CA 时)可以知道的信息:

HTTPHTTPS (+TLS)HTTPS + ECH
IP/运营商IP/运营商IP/运营商
站点域名站点域名
访问路径

举一个实际的例子的话:

  • HTTP:家长知道你在 YouTube 看《BanG Dream! It's MyGO!!!!!》的第七集,而且直接跳转到「なんで春日影やったの?!!」的场面。
  • HTTPS (+TLS):家长知道你在看 YouTube。
  • HTTPS + ECH:家长知道你在访问 Google 旗下网站。

妳当然可以在 YouTube 看《BanG Dream! It's MyGO!!!!!》。不过因为 YouTube 有开 HSTS 和跳转 HTTP 到 HTTPS,家长不会知道你在看《春日影》;安装了监控用 CA 的情况下就另当别论了。)

麻烦的 ECH 部署:托管在 DNS 上的 ECHConfig

和 TLS 1.3 或者 HTTP/2 这种技术相比,ECH 的部署要麻烦得多。除了需要服务端支持,还需要 DNS 记录的维护。个中的原因,是因为需要的一部分信息不能通过原来的信道传输了。这个麻烦的信息就是 ECHConfig,里面最重要的内容是用来给服务端加密 SNI 等的公钥。

来聊聊 ECHConfig

客户端在发送「表 clientHello」的时候,就需要给服务端传递加密的「里 clientHello」载荷。但是,客户端怎么拿到加密所需的信息(例如公钥)呢?这就是 ECHConfig 要解决的问题。

目前的实现中,ECHConfig 放在一个 HTTPS (type 65) 类型记录中。这种记录是 SVCB 类 DNS 记录范畴中的一种,其中 SVCB 是 Service Binding 的略称。ECH 本身其实并没有规定 ECHConfig 的具体发布方式,不过 HTTPS RR 大概是目前这几个实现者共同同意的一种方式。SVCB RR 这边(或者说 HTTPS RR 这边)也有给它预留好ech 这个 SvcParam 名。

站点的 ECHConfig 可以通过 DNS 请求拿到:

$ dig https tls-ech.dev +short
1 . ech=AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
sh

根据 draft-ietf-dnsop-svcb-https 的第 12 版,这三个部分分别是:

1 -- SvcPriority
. -- TargetName
ech=AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA -- SvcParams
text

SvcParams 又是一堆 SvcParam 用空格分开的集合。这里的 SvcParams 里只有 ech 这一个 SvcParam,但实际上 SVCB RR 也有定义其它的 SvcParam 类型,例如在 HTTP/3 中有用到的 alpn 等。举个例子,crypto.cloudflare.com

$ dig https crypto.cloudflare.com +short
1 . alpn="http/1.1,h2" ipv4hint=162.159.137.85,162.159.138.85 ech=AEX+DQBBWwAgACDIkV1OW4BR/vVBAPUGVsfm082AtXUXUnXCLl7LkT6vLAAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA= ipv6hint=2606:4700:7::a29f:8955,2606:4700:7::a29f:8a55
sh

回到我们拿到的 tls-ech.dev 的这个 echSvcParam,它的值是:

AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
text

这是一个 base64 编码过的二进制,原 binary 为:

00000000  00 49 fe 0d 00 45 2b 00  20 00 20 01 58 81 d4 1a  |.I...E+. . .X...|
00000010  3e 2e f8 f2 20 81 85 dc  47 92 45 d2 06 24 dd d0  |>... ...G.E..$..|
00000020  91 8a 80 56 f2 e2 6a f4  7e 26 28 00 08 00 01 00  |...V..j.~&(.....|
00000030  01 00 01 00 03 40 12 70  75 62 6c 69 63 2e 74 6c  |.....@.public.tl|
00000040  73 2d 65 63 68 2e 64 65  76 00 00                 |s-ech.dev..|
text

这是一个 ECHConfigList,里面可能会有一个或多个 ECHConfigList。按照 ECHConfigList 的格式,我们来解析一下:

  • 00 49 - 条目长度,0x49 = 73
  • fe 0d - 版本 (version),和 encrypted_client_hello 这个 TLS Extension 的编码值(0xfe0d)一致
  • 00 45 - 记录长度 (length),0x45 = 69
  • HpkeKeyConfig 部分。这里参考 draft-irtf-cfrg-hpke 版本 12,也就是 RFC 9180 的草稿。
    • 2b - config_id
    • 00 20 - kem_id,根据 HPKE 草稿,也就是 DHKEM(X25519, HKDF-SHA256)
    • public_key 部分
      • 00 20 - 长度,0x20 = 32
      • 01 58 81 d4 1a 3e 2e f8 f2 20 81 85 dc 47 92 45 d2 06 24 dd d0 91 8a 80 56 f2 e2 6a f4 7e 26 28 - X25519 公钥
    • cipher_suites 部分,可用的 HPKF KDF(密钥派生函数)/AEAD 组合;KDF 算法的 ID 在这张表,AEAD 算法的 ID 则在这张表
      • 00 08 - 长度,0x08 = 8
      • 00 01 00 01 - HKDF-SHA256 (0x0001) + AES-128-GCM (0x0001)
      • 00 01 00 03 - HKDF-SHA256 (0x0001) + ChaCha20Poly1305 (0x0003)
    • 40 - maximum_name_length,这个值在后面会用到。
    • public_name 部分,服务端域名。
      • 12 - 长度,0x12 = 18
      • 70 75 62 6c 69 63 2e 74 6c 73 2d 65 63 68 2e 64 65 76 - 值,这里是 public.tls-ech.dev
    • extensions 部分,追加的 TLS 扩展。
      • 00 00 - 长度,0x0 = 0
      • 没有数据。

以上就是一个只有一个 ECHConfigECHConfigList,刚好这个就是我们要用的 ECHConfig

如果觉得这样不够直观的话,也可以参考 Mozilla 家 NSS 的实现

这样,客户端就拿到了用来加密「里 clientHello」的信息。该到了发送 clientHello 的时候了。

不部署 DNS 也不是不可以:GREASE ECH

草案中还提到了客户端的另一种 behavior:如果客户端没有 ECHConfig 信息的话,可以随便随机生成一个 encrypt_client_hello 扩展的 payload,然后让服务端回复可用的 retry_configs

不过,GREASE ECH 和正牌的 ECH 当然不一样。GREASE ECH 并不能达到 ECH 的保护效果,就像 MyGO!!!!! 无法重现 CRYCHIC 的《春日影》。GREASE ECH 中服务端发送的 retry_configs 缺少基于安全 DNS 等的权威性背书,所以可以被 MitM 替换;它存在的主要原因是为了兼容性。前文提到过 ECH 部署复杂,而部署 GREASE ECH 就简单得多,不需要 DNS 服务的参与。又因为 GREASE ECH 和一般 ECH 的流程及包装都很像,它可以作为测试网络中间件和 ECH 是否存在兼容性问题的方便方案。

麻烦的 TLS 握手:clientHello 的「表」与「里」

在客户端根据 ECH 发送的 clientHello 中,其实还藏了另一个 clientHello。他们就是草案中的 ClientHelloOuterClientHelloInnerClientHelloOuter 是明文的(就像一般的 clientHello 一样),里面包含着各种一般信息、被加密的 ClientHelloInner——这个放在encrypted_client_hello 扩展里,以及在 HPKE 中 ClientHelloInnerAAD 信息 ClientHelloOuterAAD。如果读者有了解过一些 AEAD 算法,对 AAD(Associated Data)这个词可能有印象。

本文不对两个 clientHello 的生成过程进行详细讨论(因为很麻烦……不过当然是比长崎素世要轻松得多了)。总之,服务端如果接受了 ECH,就用 ClientHelloInner 作为 clientHello;反之,就用 ClientHelloOuter 作为 clientHello

因为笔者在写文章的时候差点忘了,所以虽然之前有说过,但也在这里再提一句:TLS 1.3 的服务端证书已经加密了,不在明文的 serverHello 部分;它不会在这儿泄露 SNI。

那么,ECH 的这些麻烦又带来了什么?

隐私性

对于中间人,ECH 的隐私性要求获取 ECHConfig 的过程是加密的。ECH 的主要设计目的是加密 SNI。如果在它之前的 DNS 请求已经把 hostname 暴露了的话,加密 SNI 也就没有意义了,无论查询的是服务器地址的 A/AAAA/CNAME 记录,还是查询 ECHConfig 的 HTTPS RR 记录。

对于服务端,ECH 的隐私性问题也值得考虑。由于 ECHConfig 的内容和域名并不是一对一的关系,服务端可以通过客户端使用的 ECHConfig 内容追踪到进行 HTTPS RR 请求的用户。这一点的解决方案也并不复杂:考虑到服务端本来就可以利用 IP 地址追踪到用户,如果用户每次变换 IP 地址时都重新请求 ECHConfig,这个问题就可以解决。

安全性

ECH 本身并没有完整性 (integrity) 验证手段。它依赖 DNSSEC 或(哪怕是)加密 DNS 保证 ECH 信息的可信度。

通过 DNS 得到的 ECHConfig 本身并没有验证方式。因此,不安全的 DNS 信道可以很容易地修改或移除 ECH 信息。因此,ECH 会要求加密 DNS 方案(例如 DNS over HTTPS 等)。如果能通过 DNSSEC 验证 ECH 信息的真实性,ECH 的安全性就可以更加提升。

兼容性

上文提到过 GREASE ECH 的兼容性方案。除此之外,草案里还有一条 GREASE PSK 的内容。这个在草案文章写得比较清楚,所以也不展开讲了;猫猫还等着我喂芭菲呢。

隐蔽性

ECH 的草案设计并没有考虑隐蔽性 (obscurity)。ECH 的前身(其实是旧版草案啦)ESNI 在中国大陆是被封锁的状态。看起来目前的 16 版本草案和之前的草案变换算不上大(顶多是 encrypted_client_hello 这个 TLS extention 的 ID 变了之类的),所以虽然现在还没有受到影响,想必将来早晚会被中国大陆封锁吧。

不知道放在哪的一句话

Staff A 提醒您,0-RTT 的 early data 在 ECH 中还是可以工作的!


LIKE 本文

Webmention 回应

本文暂无回应。