系统设计 / DNS · 层级、委任、缓存,以及不存在的「浸透」 / 应答的可验证性:cache poisoning 与 DNSSEC 待审核 7 / 11
DS · RRSIG · NSEC

应答的可验证性:cache poisoning 与 DNSSEC

原始 DNS 的应答没有任何可验证性。递归服务器发出一个 UDP 查询,然后接受第一个「看起来对得上」的应答——对得上的判据只有源地址、源端口、目的端口与报文里那个 16 位 ID。这几项全部可以伪造或猜中。

1 · cache poisoning

如果攻击者能在真正的权威服务器之前送到一个伪造应答,递归服务器就会缓存它,并在整个 TTL 期间把它交给所有用户。这叫 cache poisoning(缓存投毒)。

伪造需要猜中的东西是那个 16 位 ID,即 65536 种可能。2008 年 Dan Kaminsky 展示的手法把这件事从「不太可能」变成了「几分钟内可行」:不去攻击目标名字本身,而是查询大量不存在的随机子名字,每次都伴随一批伪造应答;伪造应答里带的不是最终答案,而是委任——把目标 zone 的 NS 指向攻击者的服务器。只要有一次命中,整个 zone 的控制权就被劫持,而且不需要等任何 TTL 到期。

当时的应急缓解是源端口随机化:把可猜空间从 2162^{16} 扩到约 2322^{32}。这是缓解不是修复——它只是把猜中的概率压低,并没有让应答变得可验证。

2 · DNSSEC 的保证范围

真正的修复要让应答本身可验证。DNSSEC 的做法是给每个 RRset 附一个数字签名,并把验证公钥的责任沿委任链向上传递。

警示 · DNSSEC 不提供机密性。RFC 4033 §4 明确列出它不解决的问题:查询与应答仍然是明文,任何路径上的观察者都看得见查的是什么、得到了什么。「上了 DNSSEC 就不用 DoH 了」这个推论是错的——两者解决的是不相干的两个问题(后者见第 8 讲)。DNSSEC 同样不提供 DoS 抗性,也不管谁有权拉走整个 zone。

它提供的是两件事:应答数据的权威认证(这条记录确实出自该 zone 的持有者)与完整性(内容未被改动),且完整性包含不存在的证明——「这个名字没有记录」这句话也必须可验证,否则攻击者只要把应答改成 NXDOMAIN 就能实现拒绝服务。

3 · 与委任同形的信任链

DNSSEC 的结构是委任结构的复制:父 zone 用一条 DS 记录存子 zone 密钥的摘要,就像它用 NS 记录指出子 zone 的服务器。

四种记录各就各位。DNSKEY 是 zone 的公钥,只存在于 zone 顶点。RRSIG 是某个 RRset 的签名。DS 是子 zone KSK 公钥的摘要,只存在于父 zone——它是整套设计里唯一「关于子 zone 的数据由父 zone 权威应答」的类型。NSEC / NSEC3 用于不存在证明。

密钥按角色分两把:KSK(key signing key)只签 DNSKEY RRset,ZSK(zone signing key)签其余所有 RRset。这个划分不是协议强制的——协议层只有 DNSKEY flags 里一个 SEP 位(flags 257 表示置起,256 表示未置),而它不改变密钥能签什么。分两把的理由纯粹是运维:ZSK 可以频繁更换而不惊动父 zone,KSK 更换才需要与父 zone 协调更新 DS。

图 3-1 · 根到 lab.vega.dev 的四级信任链,密钥标签按 RFC 4034 附录 B 真算。可选择弄坏某一环,观察断链的位置与后果。

链的起点无法由链本身建立:根的 KSK 不由任何父 zone 担保,而是作为信任锚由带外途径分发(操作系统包、IANA 网页),需人工核对。这是整条链上唯一必须靠链外手段建立的信任。

注 · 密钥标签(key tag)不是密钥的哈希,而是公钥 RDATA 的一个 16 位校验和。它会碰撞——把 RDATA 里同一奇偶位置上的字节值互相挪一挪,校验和分毫不动(本页的测试里就构造了这样一对)。所以「按标签找到了对应密钥」不构成任何安全结论,验证方按标签筛出候选后仍必须逐个试签名。

断链的后果值得单独强调:做验证的解析器对断链 zone 及其以下的一切返回 SERVFAIL,不是「降级为不可信」。从这些用户的视角看整个域名消失了,而且没有任何「继续访问」的选项——比 TLS 证书过期严重得多,后者至少给一个可以点过去的警告页。

这也决定了 DNSSEC 运维的重心:难在时序协调,不在密码学。信任链的每一环都跨两个 zone、两拨管理员,换 KSK 时子 zone 要等父 zone 的 DS 更新并等旧 DS 的 TTL 过完;签名有效期通常两到四周,而重签往往每天跑,因为过期的后果是整体消失。

4 · 不存在的证明

「这个名字没有记录」也必须可验证,可 zone 里不存在的名字有无穷多个,不可能逐个预先签名。

NSEC 的解法是把无穷多个空隙压成有限条记录:按 canonical order 排序所有存在的名字,每个名字上放一条 NSEC,内容是「排序后的下一个名字」加「本名字上存在哪些类型」。链首尾相接成环,于是任意一个不存在的名字都恰好落在某一条 NSEC 的区间内。

图 4-1 · NSEC 链与五种查询的不存在证明。可切换查询观察哪几条 NSEC 被选中;「沿链走一遍」按钮演示 zone walking。

排序规则是 RFC 4034 §6.1 的 canonical order:从最右 label 起逐级比较,同级按折算小写后的字节序。它不是字符串序,有两处特别容易记错:_dmarc 排在 api 之前(_ 是 0x5F,小于 a 的 0x61);而 *.cdn 排在 api 之后——先比第三个 label(cdn > api),* 那一级根本没轮到。写本页测试时这两条都断言反了,是实测纠正的。

NXDOMAIN 的证明需要两件,第二件常被忽略:既要证明被问的名字落在空隙里,也要证明「本该匹配的通配同样不存在」。少了后者,攻击者就能把一次通配合成出来的正常应答改写成 NXDOMAIN。两件证明有时由同一条 NSEC 兼任,于是应答里只出现一条——这也是它容易被漏掉的原因。

5 · NSEC3 与它的代价

NSEC 有一个固有后果:它公开了排序后的相邻关系,于是从顶点出发反复查询就能枚举整个 zone 的全部名字。这叫 zone walking,不是漏洞而是设计的直接推论——要让「不存在」可以被预先签名,就必须公开某种全序信息。

NSEC3(RFC 5155)把名字换成加盐的迭代哈希再排序。这堵住了直接枚举,但代价有三层:验证方要做哈希;离线字典攻击仍可行(名字空间的熵通常很低,wwwmailvpn 一猜就中);以及迭代次数本身成了 DoS 面。RFC 9276 因此规定迭代次数必须取 0(§3.1 的 MUST),盐则推荐取空——原话是「MUST be used to alleviate computational burdens」,依据是它给出的实测:迭代 100 次时查询吞吐降到基线的 47%。

这是个值得留意的结论:一项设计参数从「越大越安全」被重新评估成「必须取零」,理由是它防的那件事本来就防不住,而它带来的开销是实在的。

真正压住 zone 内容泄露的是另一条路:最小覆盖 NSEC(RFC 4470,社区里常叫 white lies)。权威服务器在应答时按需即时合成一条区间尽可能窄的 NSEC,刚好覆盖被问的那个名字,于是不泄露任何真实的相邻关系。代价写在 RFC 4470 §5 里:它要求私钥在线,而「预先签好、私钥不上线」正是 DNSSEC 相对 TLS 的一项原始卖点。两条路都要付代价,没有免费的选项。

6 · 参考文献

  1. Arends, R., Austein, R., Larson, M., Massey, D., & Rose, S. (2005). DNS Security Introduction and Requirements (RFC 4033). IETF. §4 列出 DNSSEC 不解决的问题。
  2. Arends, R., et al. (2005). Resource Records for the DNS Security Extensions (RFC 4034). IETF. §6.1 是 canonical order,附录 B 是密钥标签算法。
  3. Arends, R., et al. (2005). Protocol Modifications for the DNS Security Extensions (RFC 4035). IETF.
  4. Laurie, B., Sisson, G., Arends, R., & Blacka, D. (2008). DNS Security (DNSSEC) Hashed Authenticated Denial of Existence (RFC 5155). IETF.
  5. Hardaker, W., & Dukhovni, V. (2022). Guidance for NSEC3 Parameter Settings (RFC 9276). IETF.(迭代次数必须取 0、盐推荐取空的论证与实测数据。)
  6. Weiler, S., & Ihren, J. (2006). Minimally Covering NSEC Records and DNSSEC On-line Signing (RFC 4470). IETF. §5 讨论私钥在线的风险。
  7. Kaminsky, D. (2008). Black ops 2008: It's the end of the cache as we know it. Black Hat USA.
  8. Hoffman, P. (2022). DNS Security Extensions (DNSSEC) (RFC 9364, BCP 237). IETF.