资讯动态

libwebsockets 异步 DNS 的 DNSSEC 信任链验证:用自签名私有层次与伪 nameserver 跑通从根 KSK 到任意 zone 的完整认证

发布时间:2026/10/13 7:44:14 来源:尧图企业网站定制
后端网络通信【免费下载链接】libwebsocketscanonical libwebsockets.org networking library项目地址https://gitcode.com/gh_mirrors/li/libwebsockets点击查看免费下载本篇指南基于 libwebsockets 仓库中的自测程序 api-test-dnssec-chain 展开讲解 async-dns 子系统的 DNSSEC 校验器如何从信任锚点trust anchor出发沿着 DS 记录逐级向上认证每一层 zone 的签名密钥并在信任链未走通之前拒绝相信任何签名数据。读完后你将掌握如何用 ECDSA 密钥现场铸造一套带签名的私有 DNS 层次、如何用 loopback UDP 伪 nameserver 在完全离线的环境中驱动真实解析流程以及lws_async_dns_dnssec_set_root_anchors()、lws_async_dns_dnssec_set_mode()、LWS_ADNS_WANT_DNSSEC等公开 API 的精确语义并可深入 lib/system/async-dns/dnssec.c 的 zone 状态机理解信任链走查chain walk的实现细节。1. async-dns 的 DNSSEC 验证与这个自测程序libwebsockets 的异步 DNS 解析器lib/system/async-dns/在开启LWS_WITH_SYS_ASYNC_DNS_DNSSEC编译选项后内置了一个 DNSSEC 校验器它不直接相信任何 RRset 的 RRSIG而是先确认签名密钥本身是否可信——即从被解析 zone 向上经过一层层父 zone 签发的 DSDelegation Signer记录直到与根区的一组信任锚点DS 形式的根 KSK 摘要匹配为止。api-test-dnssec-chain 就是为这套机制编写的端到端自测selftest。它做三件事在启动时用 ECDSA 算法铸造一套完全私有的签名 DNS 层次用一个本机 loopback UDP socket 上的伪 nameserver提供该层次的带签名应答把解析器的 trust anchor 指向自己的根 KSK逐条验证信任链完整则LWS_ADNS_DNSSEC_VALID任何一环被破坏则拒绝的行为。测试全程没有任何流量离开本机。它的构建条件见 CMakeLists.txt需要LWS_WITH_SYS_ASYNC_DNS、LWS_WITH_SYS_ASYNC_DNS_DNSSEC、LWS_WITH_GENCRYPTO、LWS_WITH_IPV4四个配置项均为 1且在WIN32平台上直接不构建伪 nameserver 是一个裸的 BSD socketWindows 下不可用。2. 私有签名层次11 把 ECDSA 密钥与 13 个 RRset2.1 层次结构与密钥布局测试按 main.c#L96-L105 定义 11 把密钥覆盖 5 个 zonezone密钥索引说明.根K_ROOT_KSK、K_ROOT_ZSK根 KSK 的 DS 摘要被设为唯一的信任锚点tld.K_TLD_KSK、K_TLD_ZSK其 DS 由根 ZSK 签名zone.tld.K_ZONE_KSKP-256、K_ZONE_ZSKP-384KSK/ZSK算法不同、长度不同用于校验 RRset 的规范排序bad.tld.K_BAD_KSK、K_BAD_ZSK结构与 zone.tld 相同但父区tld对它 DS 的签名被人为损坏rogue.tld.K_ROGUE_DS_KSK、K_ROGUE_KSK、K_ROGUE_ZSK它的 DNSKEY RRset 是由 K_ROGUE_KSK 签的而它的 DS 却为另一把 K_ROGUE_DS_KSK 背书所有密钥通过 libwebsockets 的 genec 接口铸造main.c#L239-L263lws_genecdsa_create(k-ctx, cx, NULL) lws_genecdsa_new_keypair(k-ctx, P-256 /* 或 P-384 */, k-el)DNSKEY 的 RDATA 按 RFC 4034 布局flagsKSK257 / ZSK256、protocol3、algorithmP-256 为 13、P-384 为 14随后是未压缩的 X、Y 坐标key tag 按 RFC 4034 6.1 的校验和算法计算main.c#L226-L236。2.2 DS 记录与 RRSIG 的现场铸造DS 记录main.c#L268-L291为key tag algorithm digest type(2, SHA-256) SHA-256(owner 线格式名 || DNSKEY RDATA)digest 由lws_genhash_*系列接口计算。RRSIG 的签名过程sign_rrset()main.c#L309-L376值得细看因为它精确复刻了 RFC 4034 的数据规范化要求RRSIG RDATA 头covered type、algorithm、labels count、original TTL、expirationnow 86400、inceptionnow - 3600、signer key tag、signer 名称RRset 内的 RR 按 RDATA 的字节序排序后再逐个进入哈希——源码注释明确指向 RFC 4034 6.3: RRs of the set ordered by their RDATA alonerd_cmp()main.c#L293-L306。zone.tld的 DNSKEY RRset 恰好是 P-25672 字节 RDATA与 P-384100 字节 RDATA混排使这一步真正被压测到哈希输入为 RRSIG 头 规范化 signer 名然后对每条 RR 依次追加 owner 线格式名 type/class/original TTL/RDATA length RDATA签名本身走lws_genecdsa_hash_sign_jws()JWS 风格的 r||s 紧凑格式bad.tld的 DS 在签名完成后执行p[18 sl 5] ^ 0x55破坏签名字节main.c#L370-L371。2.3 13 个被提供的 RRset层次数据全部由rrsets[]表描述main.c#L147-L183包含 4 类记录owner类型签名者备注.DNSKEY2 条 RDATA根 KSK根自身的 DNSKEY RRsettldDS → K_TLD_KSK根 ZSK父区签发tldDNSKEYtld KSKzone.tldDS → K_ZONE_KSKtld ZSKzone.tldDNSKEYP-256 P-384zone KSK混长密钥www.zone.tldA 127.0.0.7zone ZSK正常数据记录bad.tldDSRRSIG 损坏tld ZSK应被拒绝bad.tldDNSKEY、www.bad.tldA各自密钥rogue.tldDS → K_ROGUE_DS_KSKtld ZSKDS 与 DNSKEY 不匹配rogue.tldDNSKEY、www.rogue.tldAalias.zone.tldCNAME → www.zone.tldzone ZSK带签且应答同时附上目标记录的递归应答风格plain.zone.tldCNAME → www.zone.tld未签指向的 target 本身是合法的3. loopback 伪 nameserver 与解析器接线3.1 伪 nameserver 的实现ns_socket()绑定127.0.0.1的随机 UDP 端口main.c#L450-L473ns_answer()解析问题的 qname转小写、比对 owner 线格式命中后把该 RRset 及其 RRSIG 写入应答Rcode 为 NOERROR未知名称返回 NXDOMAIN0x83main.c#L522-L585。两个细节值得注意对 CNAME 查询若该 RRset 标记了with_target仿照递归解析器顺带附上目标名称的被查类型 RRsetns_answer()中find_rrset(s-target, qtype)每个 RRset 都带asked计数器后续用它断言每个 zone 的密钥只取过一次缓存命中没有再发网络请求等事实。3.2 把解析器指到伪 nameservermain 函数main.c#L944-L1015的接线顺序static const char *servers[] { 127.0.0.1, NULL }; ... lws_snprintf(portstr, sizeof(portstr), %u, port); setenv(LWS_ASYNCDNS_PORT, portstr, 1); /* 让解析器走伪 nameserver 的端口 */ info.port CONTEXT_PORT_NO_LISTEN; info.options LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; info.async_dns_servers servers; cx lws_create_context(info);随后用lws_plat_asyncdns_get_server()遍历出系统配置的 nameserver 并逐个lws_async_dns_server_remove()掉保证唯一的 DNS 服务器就是伪 nameserver。最后把自己的根 KSK 铸成锚点并装入make_ds(, keys[K_ROOT_KSK], anchor_rd, anchor_rdlen); an.key_tag keys[K_ROOT_KSK].tag; an.algorithm ALG_P256; an.digest_type 2; /* SHA-256 */ an.digest anchor_rd 4; an.digest_len 32; lws_async_dns_dnssec_set_root_anchors(cx, an, 1);整个测试循环由 5ms 的 SULsorted usec listtick 驱动20 秒总超时每一步查询在query_cb回调里落定后由step_check()断言。4. 11 步查询矩阵每一步在验证什么步骤表steps[]main.c#L624-L665按序执行除 REQUIRE 一步外每条查询都显式携带LWS_ADNS_WANT_DNSSEC这正是 DHT DNSSEC 插件查 DS 的用法步骤名称 / 类型附加条件预期0alias.zone.tldA—validatedCNAME 带签、target 带签target 记录虽随 CNAME 应答带来但仍被单独重新查询check_a同时校验返回的 A 是 127.0.0.71zone.tldDS—validated父区签发 DS 的典型路径2www.zone.tldA—validated地址为 127.0.0.73zone.tldDNSKEY—validated混长 DNSKEY RRset顺带验证规范排序4zone.tldDSfrom_cachevalidated 且必须命中缓存断言同步完成LADNS_RET_CONTINUING不允许且伪 nameserver 的asked计数未增加5www.bad.tldA—refused父区对 DS 的签名被破坏6www.rogue.tldA—refusedDNSKEY RRset 的签名密钥不是 DS 背书的那把7plain.zone.tldA—refusedCNAME 本身无签尽管 target 可验证8www.zone.tldALWS_ADNS_NOCACHE先set_mode(REQUIRE)server_reload()查询不带LWS_ADNS_WANT_DNSSECvalidatedREQUIRE 模式下即使查询者不索取验证也会被验证且该模式在 nameserver 重载后依然保持9www.zone.tldALWS_ADNS_NOCACHE先把信任锚点换成与任何根密钥都不匹配的假锚点refused换锚点触发从根重新走链10zone.tldDS—refused旧锚点下缓存为 validated 的 DS在新锚点下不再可信步骤 0 的 CNAME 场景还有一个断言main.c#L746-L756www.zone.tldA 的asked计数恰好比步骤开始前多 1——说明 CNAME 应答里顺带带来的记录不会被直接采信必须等 target 自己的应答走通验证。4.1 步骤 9 之前的取数次数断言在切换到坏锚点之前step_start()main.c#L796-L812先核对每个 zone 的密钥材料只取过一次证明走链有复用、没有重复抓取. DNSKEY 被问 1 次 tld DS 被问 1 次 tld DNSKEY 被问 1 次 zone.tld DS 被问 2 次步骤 1 与步骤 4其中 4 是缓存命中 zone.tld DNSKEY 被问 2 次4.2 最后一段在飞行中的链路上销毁 contextteardown_start()main.c#L854-L881断言换锚点后根 DNSKEY 又被取过累计 2 次——信任链确实从根重新走了调lws_async_dns_dnssec_set_root_anchors(cx, NULL, 0)恢复内置锚点。这一步同时忘记刚刚走链失败的 zone使它们被重新走链而不是直接从存储里读失败置ns_drop_root 1让伪 nameserver 对根 DNSKEY 查询永不应答然后发起一条www.zone.tld查询等 20 个 tick 后断言查询确实停在parked等待根 DNSKEY这一步根 DNSKEY 被问了 3 次且查询未完成随即lws_default_loop_exit(cx)销毁 context。也就是说zone.tld的查询挂在等tld.tld.挂在等根根的 DNSKEY 抓取永远悬空——在这个链路过半、多方等待的瞬间销毁整个 context验证拆除代码不会回调进已释放内存在 ASan 下运行即是对 teardown 路径的内存安全检验。5. 公开 API 语义结合头文件与库内实现以下 API 均声明在 include/libwebsockets/lws-async-dns.h5.1lws_async_dns_dnssec_set_root_anchors()typedef struct lws_adns_ds_anchor { const uint8_t *digest; size_t digest_len; uint16_t key_tag; uint8_t algorithm; /* eg, 8 RSASHA256 */ uint8_t digest_type; /* 2 SHA-256, 4 SHA-384 */ } lws_adns_ds_anchor_t; int lws_async_dns_dnssec_set_root_anchors(struct lws_context *context, const lws_adns_ds_anchor_t *anchors, size_t count);传入的是根区 KSK 的 DS 记录数组即信任锚点传NULL, 0恢复内置集头文件注释与实现一致说明替换锚点后旧锚点下已认证的密钥一律被遗忘这正是步骤 9/10 被拒绝的机制返回 0 成功锚点畸形或 OOM 时返回非零且保留旧锚点。内置锚点见 dnssec.c#L885-L899ICANN 根区两把 KSK 的 DS 记录key tag 20326KSK-2017与 38696KSK-2024均为 RSASHA256algorithm 8 SHA-256 摘要。也就是说不显式设置锚点时libwebsockets 信任的是真实互联网根区的 KSK私有层次或测试场景必须用自己的锚点覆盖。5.2lws_async_dns_dnssec_set_mode()与查询级标志上下文级模式只有三档lws-async-dns.h#L63-L67LWS_ADNS_DNSSEC_OFF 0, LWS_ADNS_DNSSEC_TOLERATE, LWS_ADNS_DNSSEC_REQUIRE,头文件强调set_mode()取代 context 创建时从lws_system_ops_t.async_dns_dnssec_mode取到的默认值且在 context 生命周期内保持——nameserver 重载例如平台 DNS 配置变化不会重置它。步骤 8 正是靠这一点通过先set_mode(REQUIRE)再lws_async_dns_server_reload(cx)该 API 强制重新向 OS 查询 DNS 服务器配置lws-async-dns.h#L326-L334随后一条不带LWS_ADNS_WANT_DNSSEC的查询仍被验证。查询级标志位可与 RR 类型 OR 组合标志值含义LWS_ADNS_WANT_DNSSEC0x80000在 EDNS0 OPT 中显式置 DO 位请求该条查询走验证LWS_ADNS_NOCACHE0x40000绕过缓存强制走网络LWS_ADNS_INDICATE_LACKS_DNSSEC0x20000本条查询容忍缺失 DNSSECLWS_ADNS_IGNORE_HOSTS_FILE0x100000绕过/etc/hosts强制走网络验证结果通过回调返回码的两个高位表达LWS_ADNS_DNSSEC_VALID (1 8)与LWS_ADNS_DNSSEC_INVALID (1 9)。测试的判定逻辑即(step_result ~LWS_ADNS_DNSSEC_VALID) LADNS_RET_FOUND且带 VALID 位才算通过main.c#L715-L717。一条查询是否需要验证的决策在 async-dns.c#L1895-L1900int validates (dns-dnssec_mode LWS_ADNS_DNSSEC_REQUIRE || want_dnssec) !(qtype LWS_ADNS_INDICATE_LACKS_DNSSEC);5.3 缓存检查辅助lws_async_dns_get_rr_cache()测试用它从缓存里取出 DS、DNSKEY、RRSIG 等通常不产出 addrinfo的原始记录做字节级断言步骤 4 取出缓存的 DS 与伪 nameserver 实际下发的 RDATA 逐字节比对步骤 2 则反向验证一个已验证 A 应答的缓存条目里没有RRSIG 记录main.c#L733-L744——即缓存只保留 RRSIG 所覆盖的那个 RRset随应答捎带的其余记录不入库。6. 库内实现zone 状态机与信任链走查以上行为的实现集中在 lib/system/async-dns/dnssec.c。核心是每个 zone 一个状态机lws_dnssec_zone_sul_cb()dnssec.c#L1434-L1467LDZ_IDLE └─ 取材料非根区取自己的 DS RRset连同父区发的 RRSIG根区取 DNSKEY LDZ_FETCH_DS └─ 从 DS 的 RRSIG 中读出应该为它作保的父区signer 必须是本名的真祖先 加入父区的 waiters状态 LDZ_WAIT_PARENT LDZ_WAIT_PARENT └─ 父区 settle 时回调 lws_dnssec_zone_parent_settled() 用父区已认证的 keys 验证本区 DS RRset 的签名通过则取自己的 DNSKEY LDZ_FETCH_DNSKEY └─ lws_dnssec_zone_take_keys()DNSKEY RRset 必须能由其中一把 被本区 DS或信任锚点背书的密钥验签通过 → LDZ_TRUSTED 失败则 → LDZ_FAILED带 LWS_DNSSEC_FAIL_HOLD_SECS 的失败保持期几个关键函数与测试现象一一对应lws_dnssec_zone_fetch()dnssec.c#L1298-L1325走链时取 DS/DNSKEY 用的查询自带LWS_ADNS_WANT_DNSSEC | LWS_ADNS_INDICATE_LACKS_DNSSEC | LWS_ADNS_NOCACHE——注释解释了原因这次抓取本身不做验证取回的材料正是待验证对象且必须跳过缓存缓存里的可能来自一条没要 RRSIG 的旧查询。lws_dnssec_zone_take_keys()dnssec.c#L1411-L1432失败日志 DNSKEY RRset of %s not signed by a key its DS vouches for 正是rogue.tld被拒的路径DS 匹配本身由lws_dnssec_zone_ds_matches()完成dnssec.c#L942-L969按 key tag algorithm 找到候选 DS再重算 owner||key 的 SHA-256 摘要比对。lws_dnssec_rrs_verify()dnssec.c#L1026-L1123RRSIG 校验。注意两处与测试直接相关的约束RRSIG 的 labels 数必须等于 owner 的标签数no wildcard expansion of the records we walk with时间窗口按 RFC 1982 序列算术比较inception 未生效或 expiration 已过都跳过。有效期取 original TTL 与签名到期时间的较早者。lws_dnssec_zone_settle()中的stale标记dnssec.c#L1229-L1251the anchors it was walked from are gone——替换锚点后旧结果被强制降级为不可信失败区失败保持期清零。这解释了步骤 10缓存里那份曾经 validated 的 DS在步骤 9 的坏锚点下重新被拒。lws_dnssec_zone_destroy()dnssec.c#L1127-L1145拆除时先lws_async_dns_cancel_by_opaque()取消在飞的取数查询、清 waiters、释放所有 rrs 缓冲——teardown 用例检验的就是这条路径在无应答悬空时依然干净。缓存命中仍保持 VALIDasync-dns.c#L1951-L1960 的注释指出若不在命中缓存时补LWS_ADNS_DNSSEC_VALID位同一 TTL 内的第二个查询者未检查该位的调用方会 fail-closed会拒绝第一个查询者已接受的结果。步骤 4 断言的正是这条路径。7. 构建与运行前置条件LinuxWindows 不构建、构建 libwebsockets 时启用LWS_WITH_SYS_ASYNC_DNS_DNSSEC及LWS_WITH_GENCRYPTO、LWS_WITH_IPV4。该目录是一个独立 CMake 工程两种方式运行方式一随主工程构建后 ctest$ cmake . makeCMake 注册了 60 秒超时的 ctest 用例CMakeLists.txt#L25-L29可用ctest -R api-test-dnssec-chain定向执行。方式二独立构建$ cmake -S minimal-examples-lowlevel/api-tests/api-test-dnssec-chain -B build $ cmake --build build $ ./build/lws-api-test-dnssec-chain预期输出与 README.md 中的示例一致$ ./lws-api-test-dnssec-chain [2026/09/26 18:00:00:0000] U: LWS API selftest: DNSSEC chain of trust [2026/09/26 18:00:00:0100] U: step 0 (zone.tld type 43): validated as expected ... [2026/09/26 18:00:00:0400] U: Completed: PASS每一步输出step N (name type T): validated/refused as expected末尾Completed: PASS且进程退出码为 0 即全部通过任何一步的取数计数、缓存内容或验证结论偏离预期都会打印错误并以非零码退出。需要说明的适用前提整个测试是自洽的生产者消费者伪 nameserver 与解析器跑在同一进程里它验证的是库内 DNSSEC 校验逻辑本身不涉及真实网络、ICANN 根区的实际状态或任何外部基础设施。8. 要点回顾DNSSEC 验证在 async-dns 中是**失败关闭fail-closed**的只有信任链从 zone 一路走到与信任锚点匹配时结果才带LWS_ADNS_DNSSEC_VALID父区签名损坏bad.tld与 DS-DNSKEY 不匹配rogue.tld都必须被拒绝。CNAME 的安全边界带签 CNAME 要等目标 zone 认证完成后才能跟进且目标记录即使随 CNAME 应答捎带回来也必须被单独验证无签 CNAME 一律不信。缓存语义validated 结果命中缓存仍保持 VALID已验证应答的缓存条目只保留 RRSIG 覆盖的 RRset替换信任锚点会使旧锚点下的一切认证结果失效并重走链。LWS_ADNS_DNSSEC_REQUIRE是上下文级强制优先于查询者是否携带LWS_ADNS_WANT_DNSSEC且在 nameserver 重载后保持。私有层次/测试场景用lws_async_dns_dnssec_set_root_anchors()覆盖内置 ICANN 根 KSK 锚点NULL, 0恢复内置集。信任链走查是异步、可并行的状态机多个查询可以挂在同一个尚未 settle 的 zone 上等待waiterscontext 即使在链路中途被销毁拆除路径也必须安全——这正是最后一个 teardown 用例在 ASan 下检验的内容。涉及的核心源码与文档索引自测 README、测试实现、构建脚本、API 头文件、DNSSEC 校验器实现、async-dns 主逻辑、async-dns 设计文档。赞分享后端网络通信【免费下载链接】libwebsocketscanonical libwebsockets.org networking library项目地址https://gitcode.com/gh_mirrors/li/libwebsockets点击查看免费下载相关推荐LunaTranslator 完整指南HOOK、OCR、双引擎翻译5 分钟搞定任意语言游戏LunaTranslator 完整指南HOOK、OCR、双引擎翻译5 分钟搞定任意语言游戏 当游戏弹出一整段日文对话框、你连「确认」按钮在哪都找不到的时候桌面应用OCR人工智能DNSSEC在miekg/dns中的终极应用指南从签名到验证的完整教程DNSSEC在miekg/dns中的终极应用指南从签名到验证的完整教程 DNSSECDNS安全扩展为域名系统添加了一层安全保障通过公钥加密技术对资源记录网络后端BentoPDF 数字签名验证实战在浏览器中校验 PDF 签名完整性、证书有效期与信任链BentoPDF 数字签名验证实战在浏览器中校验 PDF 签名完整性、证书有效期与信任链 本文以 BentoPDFPrivacy First PDF Too前端上一篇MerkleTree.js高级应用增量默克尔树与默克尔山脉范围详解下一篇portfolio-template高级技巧自定义配色方案与动画效果的秘密创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑