Sign in

    moon-dnssec

    Bounded offline DNSSEC authentication and denial proofs for supplied RRsets

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

    #MoonDNSSEC

    Bounded offline DNSSEC authentication for caller-supplied, uncompressed IN-class resource-record sets. Module: Han-Wentao/moon-dnssec@0.1.0. Apache-2.0.

    This is a verification library, not a recursive resolver, packet parser, zone-file parser or DNSSEC deployment planner. Parsing DNSKEY or finding a DS record does not establish trust. No audited-security or production-readiness claim is made.

    #Install

    In a MoonBit module run:

    moon add Han-Wentao/moon-dnssec@0.1.0

    Add this to the consuming package's moon.pkg:

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

    The release also requires MoonCrypt 0.3.1 and MoonZoneGuard 0.3.0. Use a current MoonBit toolchain; the locally tested toolchain is recorded in docs/ci.md.

    #Run the three workflows

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

    Expected output:

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

    The example uses public, synthetic snapshots independently signed by Node.js OpenSSL. No private signing material is included. Time is explicitly 150/160 in these fixtures; substituting today's wall clock must reject their expired RRSIGs.

    1. Resolver integrators supply a trusted anchor, complete DNSKEY sets, signed parent DS and child DNSKEY sets, then authenticate an answer and obtain residual cache TTLs. The library never fetches records or treats network presence as trust.
    2. Authoritative DNS tooling supplies signed output and rebuilds canonical signing input to check signatures, key tags, label reconstruction and time windows. Returned bytes can be compared with independently produced signing data.
    3. Offline security audits replay signed NSEC/NSEC3 proof snapshots and a query, receiving bounded NODATA/NXDOMAIN/delegation classifications or explicit failures. A parsed NSEC structure alone cannot be supplied as authenticated evidence.

    #API flow

    All operations return Result values. Use matching rather than unwrap on untrusted production input (unwrap in the executable is for fixed successful fixtures only).

    • Name::parse accepts absolute ASCII presentation names; Name::from_wire accepts canonicalizable binary labels without compression. Root is a single zero byte.
    • Record::new(owner, type, ttl, rdata) checks wire-field bounds. Records are IN class.
    • Dnskey::parse, Ds::parse and Rrsig::parse decode bounded record data.
    • anchor_keys authenticates a complete DNSKEY RRset under an out-of-band key.
    • delegate_keys verifies signed parent DS, associates child keys, authenticates the complete child DNSKEY set, and bounds depth and inherited freshness.
    • authenticate_rrset accepts TrustedKeys; authenticate_with_key is the lower-level explicit caller-trust assertion API. Neither makes a complete DNS response secure.
    • signing_input sorts/deduplicates canonical RDATA, substitutes original TTL and reconstructs wildcard owners from RRSIG labels.
    • nsec_nodata / nsec3_nodata require exact-name authenticated proofs.
    • nsec_nxdomain / nsec3_nxdomain require closest-encloser, next-closer and wildcard denial. Missing proof closure returns Indeterminate, not an invented success.
    • AuthenticatedSet::cache_records returns a copied array with residual TTL.

    Public interfaces are checked into pkg.generated.mbti. Executable source in examples/offline/main.mbt is a complete usage reference; integration/proof_test.mbt uses only public APIs for real signed NSEC/NSEC3 proof closure.

    #Supported protocols and deliberate limits

    AreaEnabledNot enabled
    SignaturesRSA/SHA256 (8), RSA/SHA512 (10), P256/SHA256 (13), P384/SHA384 (14), Ed25519 (15)RSA/SHA1, Ed448, algorithm negotiation/network policy
    RSA shape1024..4096-bit full-length odd modulus; bounded exponent; PKCS1 v1.5larger RSA keys, PSS DNSSEC variants
    DS associationdigest type 2 SHA256, reused from MoonZoneGuardSHA1/SHA384 DS acceptance; non-ASCII DS owners
    Name-bearing RDATANS, CNAME, PTR, DNAME, MX, SRV, SOA, NSECknown unimplemented name-bearing types fail closed
    Other RDATAA/AAAA length checks, TXT character-string checks, opaque RFC3597 datafull semantic parsing of every IANA RR type
    Denialexact-name NODATA; explicit closest-encloser NXDOMAIN; exact insecure delegationwildcard NODATA, inferred NSEC empty nonterminals, opt-out delegation discovery
    NSEC3SHA1, salt/iterations limits, base32hex, one parameter setopt-out general NXDOMAIN is Indeterminate
    Trustexplicit anchor and supplied signed delegationsRFC5011 rollover, root bootstrap, online chain discovery

    Binary names can be parsed, but the reused DS/key-tag association API accepts only ASCII host labels. This is a real limitation, not an assertion of full IDN support.

    Wildcard-expanded positive RRsets are signature-authenticated only. Consult is_wildcard_expansion and supply response-level wildcard/existence proof closure in the resolver. This release does not implement that complete positive-response policy. Denial lifetime is proof/trust freshness, not a substitute for negative-cache SOA MINIMUM handling under RFC2308. The caller owns message assembly, bailiwick, query matching, transport, clock selection, anchor authenticity and negative caching.

    #Resource and freshness policies

    Defaults: 256 records, 1 MiB signing input, 32 signature/key candidates, 64 denial proofs, 16 delegation/ancestor depth, 100 NSEC3 iterations. validate applies hard upper bounds. Signature validity uses RFC1982 serial arithmetic; intervals spanning half the serial space and clock skew above one hour are rejected. TTLs never extend signature or ancestor trust lifetime; expiry and backwards clock changes fail closed.

    Types carrying trust have private fields and cannot be constructed by consumers. Exported arrays are copies; negative external compiler checks verify construction and field mutation rejection. No process-global mutable trust store is used.

    #Verification and project boundaries

    36 named tests cover core policies, canonical data, five independent OpenSSL crypto vectors, DNSKEY anchors, signed delegation, signed NSEC/NSEC3 public-API integration, truncation, alias/delegation boundaries and caching. Four negative compiler probes verify encapsulation. See docs/ci.md for commands and platform distinctions.

    See SECURITY.md for threat model and reporting; docs/architecture.md for invariants; docs/competition/duplicate-check.md for bounded semantic research and adjacent projects. MoonZoneGuard already provides DS association/deployment diagnostics: this library reuses, rather than duplicates, that association capability and adds offline cryptographic proof authentication. MoonCrypt provides primitives; this project does not implement new cryptographic arithmetic.

    #License, references and AI assistance

    Apache-2.0; see LICENSE and NOTICE. Original DNSSEC validation glue/data models were implemented with AI assistance, including tests and documentation. The participant remains responsible for correctness, provenance and maintenance. Specifications are RFC4034, RFC4035, RFC5155, RFC1982, RFC2181 and RFC3597; normative RFC text and upstream implementations are not copied. Dependencies and their license boundaries are listed in docs/provenance.md.

    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.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io