Sign in

    moon-dnssec

    Bounded offline DNSSEC authentication and denial proofs for supplied RRsets

    dnssec
    rrsig
    nsec
    nsec3
    validation
    Download zip
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    9 days ago
    Downloads
    21

    #MoonDNSSEC

    面向调用方提供的、未压缩 IN 类资源记录集合(RRset) 的有界 DNSSEC 离线认证库。

    • 模块:Han-Wentao/moon-dnssec@0.1.1
    • 许可证:Apache-2.0
    • 0.1.1 为中文文档补丁版本,验证引擎与公共 API 相对 0.1.0 不变。

    本项目是验证库,不是递归解析器、DNS 报文解析器、区域文件解析器或 DNSSEC 部署规划器。解析出 DNSKEY 或找到 DS 记录并不意味着建立了信任。本项目未经独立安全审计,也不宣称已经达到生产安全应用标准。

    #安装

    在使用方 MoonBit 模块中执行:

    moon add Han-Wentao/moon-dnssec@0.1.1

    在使用方包的 moon.pkg 中加入:

    import {
    "Han-Wentao/moon-dnssec" @dns,
    }

    模块依赖 MoonCrypt 0.3.1 和 MoonZoneGuard 0.3.0。请使用兼容的 MoonBit 工具链;本地实测版本与 CI 平台差异见 验证说明。

    #运行三个使用流程

    在本仓库根目录执行:

    moon test --target js --deny-warn moon run --target js examples/offline

    预期输出如下;保留英文输出,便于与可执行示例逐行核对:

    resolver: signed child answer; cache TTL=40 authoritative: canonical signing bytes=66 audit: NSEC NXDOMAIN=Secure audit: NSEC3 NXDOMAIN=Secure

    示例使用通过 Node.js OpenSSL 独立签名的公开合成快照,不包含私有签名材料。测试时间明确设为 150/160;如果改用当前真实时间,已经过期的 RRSIG 必须被拒绝,不能将该示例当成实时 DNS 回答。

    1. 解析器集成:输入可信锚、完整 DNSKEY 集合、已签名的父区 DS 与子区 DNSKEY 集合,认证回答并取得剩余缓存 TTL。库不会主动获取记录,也不会将“网络上存在某记录”当成可信依据。
    2. 权威 DNS 工具:输入签名结果,重建规范化签名输入,检查签名、密钥标识、名称标签重建和时间窗口。返回字节可与独立生成的签名数据比对。
    3. 离线安全审计:重放已签名的 NSEC/NSEC3 证明快照与查询,取得受限的 NODATA、NXDOMAIN、委派分类或明确失败结果。仅解析出的 NSEC 结构不能冒充已经认证的证据。

    #API 调用流程

    相关操作通过 Result 显式返回错误。处理不可信输入时应使用模式匹配,而不是直接 unwrap;示例中的 unwrap 只针对固定且预期成功的测试数据。

    • Name::parse 接受绝对 ASCII 文本域名;Name::from_wire 接受不使用压缩、可以规范化的二进制标签。根域名编码为单个零字节。
    • Record::new(owner, type, ttl, rdata) 检查线格式字段边界;记录类别固定为 IN。
    • Dnskey::parse、Ds::parse、Rrsig::parse 解析受大小约束的记录数据。
    • anchor_keys 使用带外可信密钥认证完整的 DNSKEY RRset。
    • delegate_keys 验证已签名的父区 DS、关联子区密钥、认证完整的子区 DNSKEY 集合,并限制链深度和继承的有效期。
    • authenticate_rrset 接收 TrustedKeys;较底层的 authenticate_with_key 要求调用方明确承担该密钥可信的前提。两者都不能单独证明完整 DNS 响应安全。
    • signing_input 对规范化 RDATA 排序、去重,使用原始 TTL,并根据 RRSIG 标签数重建通配符所有者名称。
    • nsec_nodata / nsec3_nodata 要求精确名称匹配、已经认证的证明。
    • nsec_nxdomain / nsec3_nxdomain 要求最近包围者(closest encloser)、更近一级名称(next closer)和通配符不存在的完整证明。缺少闭环时返回 Indeterminate,不会凭空报告成功。
    • AuthenticatedSet::cache_records 返回复制后的记录数组,TTL 限制为剩余有效期。

    公共接口记录在 pkg.generated.mbti。完整可执行用法见 examples/offline/main.mbt;integration/proof_test.mbt 仅通过公共 API 验证真实签名的 NSEC/NSEC3 证明闭环。

    #支持范围与明确限制

    领域已支持不支持或受限
    签名RSA/SHA256(8)、RSA/SHA512(10)、P256/SHA256(13)、P384/SHA384(14)、Ed25519(15)RSA/SHA1、Ed448、算法协商与网络策略
    RSA 参数1024–4096 位完整长度的奇数模数、有界指数、PKCS1 v1.5更大 RSA 密钥、PSS DNSSEC 变体
    DS 关联摘要类型 2(SHA256),直接复用 MoonZoneGuardSHA1/SHA384 DS 接受、非 ASCII 的 DS 所有者名称
    含名称的 RDATANS、CNAME、PTR、DNAME、MX、SRV、SOA、NSEC对已知但尚未实现的含名称类型按安全失败处理
    其他 RDATAA/AAAA 长度检查、TXT 字符串检查、不透明 RFC3597 数据全部 IANA RR 类型的完整语义解析
    不存在证明精确名称 NODATA、显式最近包围者 NXDOMAIN、精确不安全委派证明通配符 NODATA、推断 NSEC 空非终结点、opt-out 委派发现
    NSEC3SHA1、盐与迭代次数限制、base32hex、单一参数集合opt-out 一般 NXDOMAIN 返回 Indeterminate
    信任显式可信锚、调用方提供的已签名委派RFC5011 密钥轮换、根锚自动获取、在线发现信任链

    虽然名称解析器可以处理二进制标签,但复用的 DS / key-tag 关联 API 只接受 ASCII 主机标签。这是实际限制,不代表完整支持国际化域名(IDN)。

    对由通配符展开的正向 RRset,仅完成签名认证。 解析器应检查 is_wildcard_expansion,并补齐响应级通配符与名称存在性证明。本版本没有实现这一完整正向响应策略。

    不存在证明的寿命只表示证明与信任的剩余有效期,不能代替 RFC2308 中基于 SOA MINIMUM 的负缓存处理。报文组装、区域权限边界(bailiwick)、查询匹配、传输、时钟选择、可信锚真实性和负缓存策略均由调用方负责。

    #资源预算与有效期策略

    默认上限:256 条记录、1 MiB 签名输入、32 个签名/密钥候选、64 个不存在证明、16 层委派/祖先深度、100 次 NSEC3 迭代。validate 会执行硬性上限检查。

    签名有效期使用 RFC1982 序列算术;跨越序列空间一半的时间区间、超过一小时的时钟偏差均被拒绝。TTL 不会延长签名或上级信任的寿命;过期与时钟倒退按安全失败处理。

    携带信任状态的类型使用私有字段,使用方不能自行构造。导出的数组均为副本;外部编译拒绝测试验证了构造与字段修改攻击会失败。库不使用进程级可变全局信任存储。

    #测试、查重与生态边界

    36 项命名测试覆盖核心策略、规范化数据、五组独立 OpenSSL 密码向量、DNSKEY 可信锚、签名委派、已签名 NSEC/NSEC3 公共 API 集成、截断输入、别名/委派边界及缓存;另有 4 项编译拒绝探针验证封装。

    CI 对 JS、wasm-gc、wasm、native 执行严格检查与构建,并在 JS、wasm-gc、Linux native 运行测试和示例。普通 wasm 仅验证编译,不宣称执行过运行时测试,也不宣称 Windows 本地 native 验证。命令与工具链说明见 验证说明。

    MoonZoneGuard 已提供 DS 关联和部署诊断;本库直接复用其关联能力,再提供离线密码认证和证明闭环,而不是重复实现并宣称原创。MoonCrypt 提供密码原语;本项目不实现新的密码运算算法。

    #许可、参考与 AI 辅助说明

    项目采用 Apache-2.0,见 LICENSE 与 NOTICE。原创 DNSSEC 验证组合逻辑、数据模型、测试和文档的实现使用了 AI 辅助;参赛者仍对正确性、来源与后续维护负责。

    参考规范包括 RFC4034、RFC4035、RFC5155、RFC1982、RFC2181 与 RFC3597;未复制规范正文或上游实现。依赖及许可证边界见 来源与归属说明。

    AuthenticatedSet

    pub struct AuthenticatedSet {
    // private fields
    } derive(
    Debug
    )

    Cryptographically authenticated under an explicitly trusted supplied key. Private fields prevent constructing an authenticated proof without verification.

    AuthenticatedSet::cache_records

    fn AuthenticatedSet::cache_records(self : AuthenticatedSet, now : UInt) -> Result[Array[Record], DnsError]

    Return a copy suitable for a resolver cache, with the residual trust TTL. This does not create a new trust assertion or extend signature lifetime.

    AuthenticatedSet::denial_record

    fn AuthenticatedSet::denial_record(self : AuthenticatedSet, zone : Name, rrtype : Int, now : UInt) -> Result[Record, DnsError]

    Expose proof preconditions to downstream tools without exposing trust constructors.

    AuthenticatedSet::is_wildcard_expansion

    fn AuthenticatedSet::is_wildcard_expansion(self : AuthenticatedSet) -> Bool

    AuthenticatedSet::lifetime

    fn AuthenticatedSet::lifetime(self : AuthenticatedSet) -> UInt

    AuthenticatedSet::owner

    AuthenticatedSet::records

    AuthenticatedSet::remaining

    fn AuthenticatedSet::remaining(self : AuthenticatedSet, now : UInt) -> Result[UInt, DnsError]

    TTL cache freshness uses serial elapsed time, rejecting backwards clock changes.

    AuthenticatedSet::rrtype

    fn AuthenticatedSet::rrtype(self : AuthenticatedSet) -> Int

    AuthenticatedSet::zone

    Denial

    pub struct Denial {
    // private fields
    } derive(
    Debug
    )

    Denial::kind

    fn Denial::kind(self : Denial) -> DenialKind

    Denial::lifetime

    fn Denial::lifetime(self : Denial) -> UInt

    Denial::to_repr

    Denial::verdict

    fn Denial::verdict(self : Denial) -> Verdict

    DenialKind

    pub(all) enum DenialKind {
    NoData
    NxDomain
    InsecureDelegation
    } derive(Eq,
    Debug
    )

    DenialKind::equal

    fn DenialKind::equal(DenialKind, DenialKind) -> Bool

    DenialKind::not_equal

    fn DenialKind::not_equal(x : DenialKind, y : DenialKind) -> Bool

    DnsError

    pub(all) enum DnsError {
    Malformed(String)
    ResourceLimit(String)
    Unsupported(String)
    Bogus(String)
    Indeterminate(String)
    Stale(String)
    } derive(Eq,
    Debug
    )

    Failure is distinct from a cryptographically authenticated absence.

    DnsError::equal

    fn DnsError::equal(DnsError, DnsError) -> Bool

    DnsError::not_equal

    fn DnsError::not_equal(x : DnsError, y : DnsError) -> Bool

    DnsError::to_repr

    DnsError::verdict

    fn DnsError::verdict(self : DnsError) -> Verdict

    Dnskey

    pub struct Dnskey {
    // private fields
    } derive(Eq,
    Debug
    )

    Dnskey::algorithm

    fn Dnskey::algorithm(self : Dnskey) -> Int

    Dnskey::equal

    fn Dnskey::equal(Dnskey, Dnskey) -> Bool

    Dnskey::is_sep

    fn Dnskey::is_sep(self : Dnskey) -> Bool

    Dnskey::key_tag

    fn Dnskey::key_tag(self : Dnskey, owner : Name) -> Result[Int, DnsError]

    Dnskey::not_equal

    fn Dnskey::not_equal(x : Dnskey, y : Dnskey) -> Bool

    Dnskey::parse

    fn Dnskey::parse(data : Bytes) -> Result[Dnskey, DnsError]

    Dnskey::rdata

    fn Dnskey::rdata(self : Dnskey) -> Bytes

    Dnskey::to_repr

    pub struct Ds {
    // private fields
    } derive(Eq,
    Debug
    )

    Ds::digest_type

    fn Ds::digest_type(self : Ds) -> Int

    Ds::equal

    fn Ds::equal(Ds, Ds) -> Bool

    Ds::matches

    fn Ds::matches(self : Ds, owner : Name, key : Dnskey) -> Result[Bool, DnsError]

    SHA256 DS is delegated to the established ecosystem implementation. Other digest types are reported unsupported, never silently downgraded.

    Ds::not_equal

    fn Ds::not_equal(x : Ds, y : Ds) -> Bool

    Ds::parse

    fn Ds::parse(data : Bytes) -> Result[Ds, DnsError]

    Ds::rdata

    fn Ds::rdata(self : Ds) -> Bytes

    Ds::to_repr

    Limits

    pub(all) struct Limits {
    max_records : Int
    max_signed_bytes : Int
    max_signatures : Int
    max_proofs : Int
    max_chain : Int
    max_nsec3_iterations : Int
    } derive(Eq,
    Debug
    )

    Limits are checked before allocation, quadratic sorting or signature work.

    Limits::default

    fn Limits::default() -> Limits

    Limits::equal

    fn Limits::equal(Limits, Limits) -> Bool

    Limits::not_equal

    fn Limits::not_equal(x : Limits, y : Limits) -> Bool

    Limits::to_repr

    Limits::validate

    fn Limits::validate(self : Limits) -> Result[Unit, DnsError]

    Name

    pub struct Name {
    // private fields
    } derive(Eq,
    Debug
    )

    Canonical uncompressed name. Construction checks wire size and label size.

    Name::canonical_compare

    fn Name::canonical_compare(self : Name, other : Name) -> Int

    RFC4034 rightmost-label order, not presentation string order.

    Name::equal

    fn Name::equal(Name, Name) -> Bool

    Name::from_wire

    fn Name::from_wire(data : Bytes) -> Result[Name, DnsError]

    Name::host_text

    fn Name::host_text(self : Name) -> Result[String, DnsError]

    ASCII host presentation required by the reused MoonZoneGuard association API.

    Name::is_subdomain

    fn Name::is_subdomain(self : Name, zone : Name) -> Bool

    Name::label_count

    fn Name::label_count(self : Name) -> Int

    Name::not_equal

    fn Name::not_equal(x : Name, y : Name) -> Bool

    Name::parent

    fn Name::parent(self : Name) -> Name?

    Name::parse

    fn Name::parse(text : String) -> Result[Name, DnsError]

    Name::signed_owner

    fn Name::signed_owner(self : Name, labels : Int) -> Result[Name, DnsError]

    Name::to_repr

    Name::wire

    fn Name::wire(self : Name) -> Bytes

    Nsec

    pub struct Nsec {
    // private fields
    } derive(
    Debug
    )

    Nsec::covers

    fn Nsec::covers(self : Nsec, name : Name) -> Bool

    Nsec::parse

    fn Nsec::parse(record : Record) -> Result[Nsec, DnsError]

    Parsing alone does not authenticate this record.

    Nsec::to_repr

    Nsec3

    pub struct Nsec3 {
    // private fields
    } derive(
    Debug
    )

    Nsec3::covers_hash

    fn Nsec3::covers_hash(self : Nsec3, hash : Bytes) -> Bool

    Nsec3::parse

    fn Nsec3::parse(record : Record, zone : Name, limits : Limits) -> Result[Nsec3, DnsError]

    Decode bounded SHA1 NSEC3 and enforce its immediate signer-zone owner.

    Nsec3::to_repr

    Record

    pub struct Record {
    // private fields
    } derive(Eq,
    Debug
    )

    A caller-supplied resource record, never a compressed DNS message fragment.

    Record::canonical_rdata

    fn Record::canonical_rdata(self : Record) -> Result[Bytes, DnsError]

    Canonical RDATA for enabled types; known unimplemented name-bearing types fail closed.

    Record::equal

    fn Record::equal(Record, Record) -> Bool

    Record::new

    fn Record::new(owner : Name, rrtype : Int, ttl : UInt, data : Bytes) -> Result[Record, DnsError]

    Record::not_equal

    fn Record::not_equal(x : Record, y : Record) -> Bool

    Record::owner

    fn Record::owner(self : Record) -> Name

    Record::rdata

    fn Record::rdata(self : Record) -> Bytes

    Record::rrtype

    fn Record::rrtype(self : Record) -> Int

    Record::to_repr

    Record::ttl

    fn Record::ttl(self : Record) -> UInt

    Rrsig

    pub struct Rrsig {
    // private fields
    } derive(Eq,
    Debug
    )

    Rrsig::covered_type

    fn Rrsig::covered_type(self : Rrsig) -> Int

    Rrsig::equal

    fn Rrsig::equal(Rrsig, Rrsig) -> Bool

    Rrsig::not_equal

    fn Rrsig::not_equal(x : Rrsig, y : Rrsig) -> Bool

    Rrsig::parse

    fn Rrsig::parse(data : Bytes) -> Result[Rrsig, DnsError]

    Rrsig::rdata

    fn Rrsig::rdata(self : Rrsig) -> Bytes

    Rrsig::remaining

    fn Rrsig::remaining(self : Rrsig, time : TimePolicy) -> Result[UInt, DnsError]

    RFC1982 arithmetic: validity intervals must be less than half the serial space.

    Rrsig::signer

    fn Rrsig::signer(self : Rrsig) -> Name

    Rrsig::to_repr

    TimePolicy

    pub(all) struct TimePolicy {
    now : UInt
    skew : UInt
    } derive(Eq,
    Debug
    )

    TimePolicy::at

    fn TimePolicy::at(now : UInt) -> TimePolicy

    TimePolicy::equal

    fn TimePolicy::equal(TimePolicy, TimePolicy) -> Bool

    TimePolicy::not_equal

    fn TimePolicy::not_equal(x : TimePolicy, y : TimePolicy) -> Bool

    TimePolicy::validate

    fn TimePolicy::validate(self : TimePolicy) -> Result[Unit, DnsError]

    TrustedKeys

    pub struct TrustedKeys {
    // private fields
    } derive(
    Debug
    )

    DNSKEY set authenticated from an out-of-band key or signed parent DS.

    TrustedKeys::depth

    fn TrustedKeys::depth(self : TrustedKeys) -> Int

    TrustedKeys::remaining

    fn TrustedKeys::remaining(self : TrustedKeys, now : UInt) -> Result[UInt, DnsError]

    TrustedKeys::zone

    fn TrustedKeys::zone(self : TrustedKeys) -> Name

    TypeBitmap

    pub struct TypeBitmap {
    // private fields
    } derive(Eq,
    Debug
    )

    Strict RFC4034 window bitmap. Empty is allowed for NSEC3 empty nonterminals.

    TypeBitmap::contains

    fn TypeBitmap::contains(self : TypeBitmap, rrtype : Int) -> Bool

    TypeBitmap::encode

    fn TypeBitmap::encode(self : TypeBitmap) -> Bytes

    TypeBitmap::equal

    fn TypeBitmap::equal(TypeBitmap, TypeBitmap) -> Bool

    TypeBitmap::not_equal

    fn TypeBitmap::not_equal(x : TypeBitmap, y : TypeBitmap) -> Bool

    TypeBitmap::parse

    fn TypeBitmap::parse(data : Bytes) -> Result[TypeBitmap, DnsError]

    TypeBitmap::types

    fn TypeBitmap::types(self : TypeBitmap) -> Array[Int]

    Verdict

    pub(all) enum Verdict {
    Secure
    Insecure
    Bogus
    Indeterminate
    } derive(Eq,
    Debug
    )

    Verdict::equal

    fn Verdict::equal(Verdict, Verdict) -> Bool

    Verdict::not_equal

    fn Verdict::not_equal(x : Verdict, y : Verdict) -> Bool

    Verdict::to_repr

    anchor_keys

    fn anchor_keys(zone : Name, anchor : Dnskey, records : Array[Record], signatures : Array[Rrsig], time : TimePolicy, limits : Limits) -> Result[TrustedKeys, DnsError]

    Authenticate the complete DNSKEY RRset, not merely an anchor's presence.

    authenticate_rrset

    fn authenticate_rrset(records : Array[Record], signatures : Array[Rrsig], trusted : TrustedKeys, time : TimePolicy, limits : Limits) -> Result[AuthenticatedSet, DnsError]

    authenticate_with_key

    fn authenticate_with_key(records : Array[Record], signatures : Array[Rrsig], zone : Name, key : Dnskey, time : TimePolicy, limits : Limits) -> Result[AuthenticatedSet, DnsError]

    A supplied key is a trust assertion by the caller; DNS presence is not trust.

    decode_nsec3_label

    fn decode_nsec3_label(label : Bytes) -> Result[Bytes, DnsError]

    delegate_keys

    fn delegate_keys(parent : TrustedKeys, child : Name, ds_records : Array[Record], ds_signatures : Array[Rrsig], key_records : Array[Record], key_signatures : Array[Rrsig], time : TimePolicy, limits : Limits) -> Result[TrustedKeys, DnsError]

    Advance exactly one authenticated delegation. No network lookup or implicit trust.

    nsec3_hash

    fn nsec3_hash(name : Name, salt : Bytes, iterations : Int, limits : Limits) -> Result[Bytes, DnsError]

    RFC5155 SHA1 iteration with policy checked before any hashing.

    nsec3_label

    fn nsec3_label(hash : Bytes) -> Result[String, DnsError]

    nsec3_nodata

    fn nsec3_nodata(proof : AuthenticatedSet, zone : Name, name : Name, rrtype : Int, now : UInt, limits : Limits) -> Result[Denial, DnsError]

    nsec3_nxdomain

    fn nsec3_nxdomain(proofs : Array[AuthenticatedSet], zone : Name, name : Name, now : UInt, limits : Limits) -> Result[Denial, DnsError]

    Authenticate NSEC3 NXDOMAIN with one parameter set and complete proof closure. Opt-out coverage is deliberately Indeterminate, never Secure NXDOMAIN.

    nsec_nodata

    fn nsec_nodata(proof : AuthenticatedSet, zone : Name, name : Name, rrtype : Int, now : UInt) -> Result[Denial, DnsError]

    nsec_nxdomain

    fn nsec_nxdomain(proofs : Array[AuthenticatedSet], zone : Name, name : Name, now : UInt, limits : Limits) -> Result[Denial, DnsError]

    Complete bounded NSEC closest-encloser and wildcard denial. Requires an explicit exact-owner closest-encloser proof (no inferred empty nonterminals).

    signing_input

    fn signing_input(records : Array[Record], signature : Rrsig, limits : Limits) -> Result[Bytes, DnsError]

    Build RFC4034 signing input. IN class only; RRsets must be homogeneous.

    supported_algorithm

    fn supported_algorithm(algorithm : Int) -> Bool

    Explicitly supported signing algorithms; legacy RSA/SHA1 is not enabled.

    verify_signature

    fn verify_signature(key : Dnskey, message : Bytes, signature : Bytes) -> Result[Bool, DnsError]

    Verify DNSSEC wire key/signature shapes using MoonCrypt, not mock digests.