DNSSEC validation

Local DNSSEC signature verification (RFC 4033–4035 / 6605 / 8080) — the cryptographic core that lets a resolver client verify a DNS answer itself instead of trusting the upstream resolver's AD bit. b.network.dns.resolver checks the AD flag; this module verifies the actual RRSIG signature over the canonicalised RRset, defending against a compromised or on-path resolver.

verifyRrset reconstructs the RFC 4034 §3.1.8.1 signed data (the RRSIG RDATA without the signature, followed by the RRset in canonical form — owner names lowercased, RRs ordered by canonical RDATA, the RRSIG's Original TTL) and verifies it with the DNSKEY, enforcing the signature's inception / expiration window. The DNSKEY algorithms are RSA/SHA-256 (8), ECDSA P-256/SHA-256 (13), ECDSA P-384/SHA-384 (14), and Ed25519 (15) — the modern, deployed set. verifyDs checks a delegation-signer digest against a DNSKEY (SHA-256 / SHA-384), and keyTag computes the RFC 4034 Appendix B key tag.

Scope. This is the verification core. RR types that carry domain names in their RDATA (NS, CNAME, SOA, MX, SRV, …) need name-lowercasing inside the RDATA (RFC 4034 §6.2) that this version does not perform, so they are refused with dnssec/uncanonicalizable-type rather than mis-validated — the security-critical DNSKEY / DS and the name-free address / text types (A, AAAA, TXT, …) are fully supported. The recursive chain-walk (root → TLD → zone via verifyChain against the bundled IANA root trust anchors) and NSEC / NSEC3 denial-of-existence (verifyDenial / nsec3Hash) ship alongside the per-RRset verification core.

b.network.dns.dnssec.keyTag(dnskeyRdata) #

stable0.12.48

Compute the RFC 4034 Appendix B key tag of a DNSKEY from its full RDATA (flags || protocol || algorithm || public key) — the 16-bit identifier an RRSIG / DS references to select the signing key.

var tag = b.network.dns.dnssec.keyTag(dnskeyRdata);

b.network.dns.dnssec.verifyDs(opts) #

stable0.12.48
{
  {
    ownerName:    string,   // the child zone name (the DNSKEY owner)
    dnskeyRdata:  Buffer,   // full DNSKEY RDATA (flags||protocol||alg||publicKey)
    ds: { keyTag, algorithm, digestType, digest: Buffer },  // the parent DS
  }
}

Verify a DS (Delegation Signer) record against a child DNSKEY — the link that lets a parent zone vouch for a child's key. The DS digest (SHA-256 / SHA-384) is recomputed over the owner name plus the DNSKEY RDATA and compared to the DS, with the key tag and algorithm checked.

b.network.dns.dnssec.verifyDs({ ownerName: "example.com", dnskeyRdata: ksk, ds: parentDs });

b.network.dns.dnssec.verifyRrset(opts) #

stable0.12.48soc2
{
  {
    name:    string,    // owner name of the RRset
    type:    string|number, // RR type (e.g. "DNSKEY", "A")
    class?:  number,    // default 1 (IN)
    rdatas:  Buffer[],  // each record's wire-format RDATA
    rrsig: {            // the RRSIG covering the RRset
      algorithm, labels, originalTtl, expiration, inception, keyTag,
      signerName: string, signature: Buffer,
    },
    dnskey: { algorithm, publicKey: Buffer },  // the signing DNSKEY (publicKey = bytes after flags/proto/alg)
    at?:     Date,      // validity instant (default now); must be a valid Date
  }
}

Verify an RRSIG over an RRset against a DNSKEY (RFC 4035 §5.3). The signed data is reconstructed in canonical form — the RRSIG RDATA without the signature, then the RRset's records ordered by canonical RDATA with the RRSIG Original TTL — and the signature is verified with the DNSKEY (RSA/SHA-256, ECDSA P-256/384, Ed25519). The signature's inception / expiration window is enforced against opts.at. RR types carrying embedded domain names are refused (dnssec/uncanonicalizable-type) rather than mis-validated.

b.network.dns.dnssec.verifyRrset({ name: "example.com", type: "DNSKEY", rdatas: keys, rrsig: sig, dnskey: ksk });

b.network.dns.dnssec.nsec3Hash(name, opts) #

stable0.12.49
{
  {
    salt:        Buffer,  // zone NSEC3 salt (may be empty)
    iterations:  number,  // additional hash iterations (>= 0)
  }
}

Compute the RFC 5155 §5 NSEC3 hash of a name — iterated SHA-1 over the canonical (lowercased, root-terminated) wire form with the zone's salt. The result is the unencoded hash; the NSEC3 owner label is its base32hex encoding. SHA-1 is the only hash IANA registers for NSEC3, so this is a wire-protocol constant, not a cryptographic default.

var h = b.network.dns.dnssec.nsec3Hash("a.example.com", { salt: salt, iterations: 0 });

b.network.dns.dnssec.verifyDenial(opts) #

stable0.12.49soc2
{
  {
    qname:   string,        // the queried name
    qtype:   string|number, // queried type (required for proof "nodata")
    proof:   string,        // "nxdomain" | "nodata"
    zone:    string,        // the signer zone apex (a suffix of qname)
    nsec3?:  [ { owner: string, rdata: Buffer } ],  // NSEC3 records (owner = base32hex-label.zone)
    nsec?:   [ { owner: string, rdata: Buffer } ],  // NSEC records
    maxIterations?: number, // NSEC3 iteration cap (default 500)
    allowOptOut?:   boolean, // accept an Opt-Out NXDOMAIN proof (default false)
  }
}

Prove that a name does not exist (NXDOMAIN) or that a name has no records of a given type (NODATA) from the signed NSEC (RFC 4034 §4) or NSEC3 (RFC 5155) records in a response's Authority section. This is the other half of "verify the answer yourself": verifyRrset proves a positive answer, verifyDenial proves a negative.

The records MUST already be verified with verifyRrset — this checks the denial RELATION (closest-encloser, covering ranges, type-bitmap absence), not the signatures. For NSEC3, the iterated-hash count is capped (opts.maxIterations, default 500) to bound the SHA-1 work an attacker can force. An NXDOMAIN proof that relies on an Opt-Out NSEC3 (RFC 5155 §6) is refused unless opts.allowOptOut — opt-out only proves "no signed records", not non-existence.

b.network.dns.dnssec.verifyDenial({
  qname: "nope.example.com", proof: "nxdomain", zone: "example.com", nsec3: records,
});

b.network.dns.dnssec.verifyChain(opts) #

stable0.12.50soc2
{
  {
    links: [ {                        // ordered root-first
      zone:        string,
      dnskeys:     Buffer[],          // the zone's DNSKEY RRset RDATAs
      dnskeyRrsig: { algorithm, labels, originalTtl, expiration, inception, keyTag, signerName, signature },
      dsRdatas?:   Buffer[],          // DS RRset for this zone (served by parent; omit for root)
      dsRrsig?:    { ... },           // RRSIG over the DS RRset (signed by parent; omit for root)
    } ],
    trustAnchors?: [ { keyTag, algorithm, digestType, digest: Buffer } ],  // default IANA root
    at?:           Date,             // validity instant (default now)
  }
}

Validate a DNSSEC delegation chain from the root down to a zone, against a pinned trust anchor (RFC 4035 §5). For each link, the zone's DNSKEY RRset must be self-signed by one of its keys; that signing key must be vouched for either by a pinned anchor (root) or by a DS record served by the already-trusted parent. The DS RRset itself is verified against the parent's keys, so trust flows root → TLD → zone with no gap. The default anchors are the IANA root KSKs; override with opts.trustAnchors.

This composes verifyRrset + verifyDs + the key tag; it returns the leaf zone's trusted DNSKEY set, which the caller then passes to verifyRrset / verifyDenial for the actual answer.

KeyTrap (CVE-2023-50387) amplification is bounded with non-configurable caps: at most 4 same-tag DNSKEY candidates are tried per RRSIG, at most 64 DNSKEYs per zone link and 16 DS records per delegation are accepted, the chain is at most 128 links deep, and the whole response is held to a signature-validation budget that scales with chain depth (so a legitimate deep delegation always fits while bounded collisions stay bounded). A hostile zone publishing many colliding keys / signatures is refused with dnssec/too-many-colliding-keys / dnssec/too-many-dnskeys / dnssec/too-many-ds / dnssec/too-many-links / dnssec/validation-budget-exceeded rather than driving O(keys x sigs) verifications. (NSEC3 iteration counts are separately capped at 150 per RFC 9276 / the CVE-2023-50868 fix.)

var trusted = b.network.dns.dnssec.verifyChain({ links: [rootLink, orgLink] });
// → { ok: true, zone: "org.", keys: [ ...trusted org DNSKEY rdatas ] }

Last updated 2026-08-08T16:39:49.652Z by seeder.