https://github.com/shuque/decode_nsec_response
Decode NSEC/NSEC3 responses
https://github.com/shuque/decode_nsec_response
Last synced: about 2 months ago
JSON representation
Decode NSEC/NSEC3 responses
- Host: GitHub
- URL: https://github.com/shuque/decode_nsec_response
- Owner: shuque
- License: mit
- Created: 2026-04-29T12:25:34.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-04-29T15:21:03.000Z (3 months ago)
- Last Synced: 2026-04-29T16:31:05.465Z (3 months ago)
- Language: Python
- Size: 16.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# NSEC/NSEC3 Response Decoder
## Description
[decode\_nsec\_response.py](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](https://www.dnspython.org/) (`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..
```