密码学 22:DNSSEC 与 DoH、DoT 各保护哪一段
浏览器打算向 orders.test 发一笔合成 HTTPS 订单。它先要把域名解析为地址。有人建议“启用加密 DNS”;有人建议“启用 DNSSEC”。如果客户端到解析服务的连接虽有 TLS,但解析服务给出了攻击者操纵的答案,该相信谁?反过来,如果能验证 DNSSEC 签名,路边的监听者就看不见域名查询了吗?两种建议回答的根本不是同一道题。
被保护的不是同一段
普通语言说,DoH/DoT 是对客户端与它选择的解析服务之间的传输建立加密、认证通道。DNSSEC 是让有能力验证的解析方核对某个域所有者签出的 DNS 数据来源和完整性。边界如图:
1 | |
只有客户端和解析服务之间这一跳的流量受 DoH/DoT 当前 TLS 会话保护;解析服务知道它查了什么。解析服务再问权威服务器、域名出现在别处的连接元数据或访问后续服务的路径,要另行分析。DNSSEC 对查询不加密,也不因为验证通过就能保证最终网站安全。HTTPS 订单是否提交到正确服务,要看后续 TLS 服务身份验证;合成订单是否被接受或交付另属业务规则。
DNSSEC 为什么需要预先可信的根
一个 DNSSEC 验证器拿到 order.example. A 192.0.2.10 时,要确认的是整个同名同类型 RRset 的签名 RRSIG,而不是把单个 IP 地址当作公钥、也不是只比较一个自己现算的哈希。签名者有一把 DNSKEY 公钥;验证器如果只从攻击者回答里同时收了 DNSKEY 与 RRSIG,就只能证明“这把刚收到的钥匙能验证它自己的签名”。必须先有无法被攻击者一起替换的信任锚(预置的可信 DNSKEY 或相应 DS),再经父区的 DS 摘要指向子区 DNSKEY、核对 DNSKEY 与 RRset 上的签名,才谈得上可信委派。
1 | |
日期是后面实验选取的合成签名有效期,不是现实 example. 的密钥轮换安排。签名的有效期要检查;“名称不存在”也不是让权威服务器回一个口头的“查不到”,需专门认证的否定回答机制(如 NSEC/NSEC3),本章实验没有覆盖。DNSSEC 验过只说明这个记录集在可信链及有效期条件下来自相应签名者,并不说明签名者填写的 IP 必然安全、网页真的提供某商品或测试交易已经确认。
加密查询到底输入输出什么
DoH(RFC 8484)把 DNS 消息放在 HTTPS 中,常用 application/dns-message 的二进制格式;规范允许 GET 或 POST,并要求服务器两种都能处理。下面回环演示只实现 POST 子集:客户端构造 order.example. 的 A 查询 DNS 字节,按 HTTPS 请求体发送,服务端回复 DNS 字节。成功 TLS 握手检查的是 orders.test 的证书、服务名和本次指定的测试 CA,不是查询名 order.example. 的 DNSSEC 公钥。
DoT(RFC 7858)直接在 TLS 连接中发送 DNS 报文,报文前面有 2 字节长度;回环演示同样只测试一条请求和回复。客户端也核验解析服务的 TLS 服务名。服务端读到 TLS 解密后的 DNS 问题,符合“加密的是网络传输而非对服务端隐藏”这一边界。实际部署有标准端口、身份验证模式、连接管理和递归解析器运营约束,不能从一条本地回环响应推断公网隐私。
两条路径都可以返回同一个已经签名的合成 A 回答。客户端先验 TLS 拒绝错服务,再另用预置 DNSKEY 验证 RRSIG。若解析服务持合法 TLS 证书却篡改 A 值,能通过 TLS 传输,但在客户端自己的 DNSSEC 验证处会失败;若客户端只信解析服务的响应而不独立做 DNSSEC,安全性就转而依赖解析服务本身的可信度与其验证策略。两个结论不可混写为“有 DoH 所以 DNSSEC 已通过”。
本地可复现的正反证据
22_dnssec_answer.py 用 dnspython==2.7.0 与成熟密码库生成教学 Ed25519 DNSKEY 和真实 RRSIG,指定 2026-10-06 00:00 UTC 为校验时间。公开测试钥匙是固定常量,只用于演示;受信 DNSKEY 在验证器代码里独立给出。正例 RRset 通过;A 答案改成 192.0.2.11 后在同一签名下失败;将受信 DNSKEY 换为另一把钥匙也失败。DS 摘要虽有真实计算,未从真实 DNS 根走到此教学 example. 区。
22_dns_transports.py 在 127.0.0.1 随机端口建临时 DoH POST/DoT 服务,复用 14 的一次性 CA/证书,仅把 CA 配给本次客户端;收到同一签名回答后显式调用 DNSSEC 验签。两条 TLS 通道均接受正确服务名;把 DoT 客户端预期服务名设为 wrong.test,同一 CA 下 TLS 身份检查失败。
先按仓库内 examples/cryptography/README.md 完成环境准备;以下命令从仓库根目录执行。
1 | |
2026-10-06 UTC 上述脚本/测试退出码 0,共 2 项测试通过;以上临时 venv 路径可换为自己环境中安装相同版本的路径。这里没做完整 DNS 根信任链、NSEC/NSEC3、DoH GET/HTTP/2、真实递归解析,也没抓包独立核算 TLS 密文;不要把子集实验改称生产 DNS 部署验收。测试 CA 绝不装进系统信任库,使用后私钥随临时目录删除。
两道带答案的练习
画图题: 将 orders.test 解析、随后发合成 HTTPS 订单、钱包经另一个 HTTPS RPC 发交易画为三段。解析服务如果给出错误 A 值,但它自己的 TLS 证书仍正确,分别写出 DoH/DoT 和拥有可信锚的 DNSSEC 谁能发现哪一种错误。
可核对答案: 两种加密查询通道仍可能正常:客户端确实连接到它期望的解析服务,但它的内容可能是被该服务给出的错误地址。客户端独立验证 DNSSEC 且记录受可信委派签名时,错误的 A 值在 RRset/RRSIG 核对处失败。若该域本身不在可验证签名链或客户端把对方返回的 DNSKEY 当锚,不能套用这一结论。后续 HTTPS 仍要验证订单服务名,钱包还要独立做交易授权。
变更题: 把 22_dnssec_answer.py 中仅用于篡改对照的 changed_answer 从 192.0.2.11 改成与已签记录相同的 192.0.2.10,不改变签名;再把 22_dns_transports.py 中正常客户端 tls_socket 的预期名从 orders.test 改成 wrong.test。两个原有断言分别在哪里失败?改答案和值与签名一同重算,就自动可信了吗?
可核对答案: 将篡改对照改回原记录,它也能通过验签,changed_rrset_rejected 断言失败,说明必须真正变更签名覆盖的字节才能形成负例。改 TLS 预期名,TLS 握手因证书服务名不匹配而在读 DNS 内容之前失败。即便攻击者能为自己的钥匙重签它自己选的内容,客户端没有把那把钥匙可信地绑定到 example. 区,就不能把这次签名当作可信 DNSSEC 结果。
资料与导航
- RFC 4033 §3–§4、RFC 4034、RFC 4035 §5:DNSSEC 的信任锚、签名与验证边界。
- RFC 8080、RFC 8484 §4、§8、RFC 7858:Ed25519 DNSSEC、DoH 与 DoT。
- 14 证书为何绑定服务名与公钥 · 21 VPN 的密钥怎样建立。






