Skip to content

shuque/decode_nsec_response

Repository files navigation

NSEC/NSEC3 Response Decoder

Description

decode_nsec_response.py — Queries a given name and type, using a validating DNS resolver, then decodes and explains the NSEC or NSEC3 records in the authority section of the response. Identifies the role of each record in the authenticated denial proof:

  • NXDOMAIN (NSEC): which NSEC covers the queried name and which covers the wildcard at the closest encloser.
  • NXDOMAIN (NSEC3): computes NSEC3 hashes and identifies the closest encloser match, next closer name cover, and wildcard cover.
  • NODATA: explains how the type bitmap proves the queried type does not exist.
  • Wildcard synthesis: identifies the NSEC/NSEC3 proving no closer match than the wildcard exists, validating the wildcard match. Handles wildcard NODATA (wildcard exists but lacks the queried type).
  • Compact Denial of Existence: detects both NSEC (RFC 9824) and NSEC3 (RFC 9824 Section 4) CDoE patterns, with or without NXNAME.
  • NSEC3 opt-out: flags opt-out NSEC3 records and notes that unsigned delegations may exist within the covered range.
./decode_nsec_response.py [--doh] [--doh-server URL] QNAME QTYPE

Dependencies

  • Python 3.9+
  • dnspython (pip install dnspython)
  • For DoH support: pip install dnspython[doh]

Installation

pip install .

Or directly from the GitHub repository:

pip install git+https://github.com/shuque/decode_nsec_response.git

This installs the decode_nsec_response.py script and its dependencies.

For a system-wide install:

sudo pip install .

On systems where pip is restricted from modifying the system Python environment (Debian/Ubuntu with PEP 668), use one of:

sudo pip install --break-system-packages .
sudo pip install --prefix=/usr/local .

Testing

The test suite uses canned DNS responses in wire format, so tests run instantly and are not affected by live DNS changes.

Capture test data (only needed when adding or refreshing test cases):

python3 capture_test_data.py

This queries all 14 test cases via DoH to Cloudflare and saves the raw wire bytes to testdata/.

Run the test suite:

python3 -m unittest test_decode -v

Or with pytest if installed:

python3 -m pytest test_decode.py -v

Sample Output

NSEC3 NODATA:

$ ./decode_nsec_response.py --doh salesforce.com. TLSA

Query: salesforce.com. TLSA
Response: NOERROR [AD]
======================================================================
Zone: salesforce.com.
NSEC3 params: algorithm 1, iterations 0, salt 7FEA7B83

  H(salesforce.com.) = 49STKNJU01HOVPN0L8N7MMD35E9VD3VD

NODATA: salesforce.com. exists but has no TLSA record.

Authority section:

  NSEC3: 49STKNJU01HOVPN0L8N7MMD35E9VD3VD -> 49T2A4TT2OHA06O3HB89B4PCF7U0824L
    Type bitmap: [A NS SOA MX TXT RRSIG DNSKEY NSEC3PARAM TYPE65534]

    Role: Matches H(salesforce.com.)
    The type bitmap does not include TLSA, proving no TLSA record exists at this name.

NSEC3 NXDOMAIN:

$ ./decode_nsec_response.py --doh foo.nxd123.salesforce.com. A

Query: foo.nxd123.salesforce.com. A
Response: NXDOMAIN [AD]
======================================================================
Zone: salesforce.com.
NSEC3 params: algorithm 1, iterations 0, salt 7FEA7B83

NXDOMAIN: foo.nxd123.salesforce.com. does not exist.

Authority section:

  Closest encloser: salesforce.com.
  Next closer name: nxd123.salesforce.com.
  Wildcard at CE:   *.salesforce.com.
  H(salesforce.com.) = 49STKNJU01HOVPN0L8N7MMD35E9VD3VD
  H(nxd123.salesforce.com.) = JP5FLA1OE214J8NI0E55A3GVP96NGINB
  H(*.salesforce.com.) = 09UJ9K6OKDGIKMN908E3ULJRDMKM277V

  NSEC3: 49STKNJU01HOVPN0L8N7MMD35E9VD3VD -> 49T2A4TT2OHA06O3HB89B4PCF7U0824L
    Type bitmap: [A NS SOA MX TXT RRSIG DNSKEY NSEC3PARAM TYPE65534]

    Role: Matches H(salesforce.com.) — closest encloser proof
    Proves salesforce.com. exists in the zone.

  NSEC3: JP1PCI1BBC6Q7F8136EPU4LT4CUEPNTM -> JP6FI3JBGQTR23BALRE30LG9UFU3FJHJ
    Type bitmap: [A RRSIG]

    Role: Covers H(nxd123.salesforce.com.) — next closer name cover
    Proves nxd123.salesforce.com. does not exist.

  NSEC3: 09TD20B1LCISV1SUHEMNIUCF1FGB5K26 -> 09UJ9OKA6O2IRL1I3Q0D193ERNT3P0I6
    Type bitmap: [A RRSIG]

    Role: Covers H(*.salesforce.com.) — wildcard cover
    Proves no wildcard exists at the closest encloser (salesforce.com.),
    so no wildcard synthesis can produce an answer.

NSEC3 Wildcard Match:

$ ./decode_nsec_response.py --doh foo.wild.dnskensa.com. A

Query: foo.wild.dnskensa.com. A
Response: NOERROR [AD]
======================================================================
Zone: dnskensa.com.
NSEC3 params: algorithm 1, iterations 10, salt 73B2182A738FCBC4

Wildcard-synthesized answer for foo.wild.dnskensa.com..

Answer section:
  foo.wild.dnskensa.com. 86400 A 10.1.1.1

Authority section:

  Closest encloser: wild.dnskensa.com.
  Next closer name: foo.wild.dnskensa.com.
  Wildcard:         *.wild.dnskensa.com.
  H(foo.wild.dnskensa.com.) = 3BPL37FUV6JQLG6BLVIRV23T5JVP1H4L

  NSEC3: 33OE6CIJFV452QC67M4A72F474I3M2E5 -> 3F3DEH8FT59Q4S2MNVN446MFALKSAFSU
    Type bitmap: [A AAAA RRSIG]

    Role: Covers H(foo.wild.dnskensa.com.) — next closer name cover
    Proves no closer match than wild.dnskensa.com. exists for foo.wild.dnskensa.com.,
    validating that the answer was synthesized from a wildcard.

NSEC NXDOMAIN:

$ ./decode_nsec_response.py --doh foobar. A

Query: foobar. A
Response: NXDOMAIN [AD]
======================================================================
Zone: .

NXDOMAIN: foobar. does not exist.

Authority section:

  NSEC: foo. -> food.
    Type bitmap: [NS DS RRSIG NSEC]

    Role: Covers the queried name (foobar.)
    Owner sorts before qname, next sorts after qname
    in canonical order, proving foobar. does not exist.

  NSEC: . -> aaa.
    Type bitmap: [NS SOA RRSIG NSEC DNSKEY ZONEMD]

    Role: Covers the wildcard (*.)
    Proves no wildcard exists at the closest encloser (.),
    so no wildcard synthesis can produce an answer.

Wildcard CNAME NODATA (cross-zone; NSEC3 wildcard proof + NSEC target NODATA):

$ ./decode_nsec_response.py --doh 12345asdfasfadf.horoscope-divination.com. AFSDB

Query: 12345asdfasfadf.horoscope-divination.com. AFSDB
Response: NOERROR [AD]
======================================================================

Wildcard CNAME NODATA: 12345asdfasfadf.horoscope-divination.com. matched wildcard *.horoscope-divination.com.,
which targets general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com.. The target has no AFSDB record.

Answer section:
  12345asdfasfadf.horoscope-divination.com. 600 CNAME general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com.

Authority section:

  --- Wildcard proof (zone: horoscope-divination.com.) ---
NSEC3 params: algorithm 1, iterations 0, salt D54DF1360676F4B8

  Closest encloser: horoscope-divination.com.
  Next closer name: 12345asdfasfadf.horoscope-divination.com.
  Wildcard:         *.horoscope-divination.com.
  H(12345asdfasfadf.horoscope-divination.com.) = V24NHP56RH0DS80NDKCVTMVU9BC4IR0M

  NSEC3: UOLUGA2L16M65IELFLBNLEM2V8COSCI6 -> 28DNC0LB8B15CTN3GTIRT1RJDR0P16R7
    Type bitmap: [A RRSIG]

    Role: Covers H(12345asdfasfadf.horoscope-divination.com.) — next closer name cover (wrap-around)
    Proves no closer match than horoscope-divination.com. exists for 12345asdfasfadf.horoscope-divination.com.,
    validating that the CNAME was synthesized from *.horoscope-divination.com..

  --- NODATA proof (zone: herokudns.com.) ---

  NSEC: general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com. -> \000.general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com.
    Type bitmap: [A AAAA RRSIG NSEC]

    Role: Matches the CNAME target (general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com.)
    The type bitmap does not include AFSDB, proving no AFSDB record exists at general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com..

About

Decode NSEC/NSEC3 responses

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages