浏览器打算向 orders.test 发一笔合成 HTTPS 订单。它先要把域名解析为地址。有人建议“启用加密 DNS”;有人建议“启用 DNSSEC”。如果客户端到解析服务的连接虽有 TLS,但解析服务给出了攻击者操纵的答案,该相信谁?反过来,如果能验证 DNSSEC 签名,路边的监听者就看不见域名查询了吗?两种建议回答的根本不是同一道题。

被保护的不是同一段

普通语言说,DoH/DoT 是对客户端与它选择的解析服务之间的传输建立加密、认证通道。DNSSEC 是让有能力验证的解析方核对某个域所有者签出的 DNS 数据来源和完整性。边界如图:

1
2
3
4
5
6
7
8
客户端 stub -- DoH: HTTPS / DoT: TLS --> 选定的解析服务 ----> 权威 DNS
│ 验证配置的 TLS 服务名 │ 解析器在 TLS 终点能看到问题
│ │ 可能自己做 DNSSEC 验证
└── 若自己验证 DNSSEC:从预置信任锚
经 DS / DNSKEY / RRSIG 检查回答 RRset

订单客户端 -- HTTPS/TLS --> orders.test:另一次服务证书与会话认证
钱包 -- HTTPS/TLS --> 测试 RPC:另一次连接;交易仍须独立签名

只有客户端和解析服务之间这一跳的流量受 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
2
3
4
5
可信锚(不能取自同一不可信答案)
└─ 父区已验的 DS 摘要 ─匹配─> 子区 DNSKEY
└─ 公钥验 RRSIG ─> 指定名称/类型的 RRset
↓
2026-10-01 至 10-20 的有效期

日期是后面实验选取的合成签名有效期,不是现实 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
2
3
python examples/cryptography/22_dnssec_answer.py
python examples/cryptography/22_dns_transports.py
python -m unittest discover -s examples/cryptography -p 'test_22*.py' -v

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 结果。

资料与导航