{"id":50887558,"url":"https://github.com/shuque/decode_nsec_response","last_synced_at":"2026-06-15T18:01:38.262Z","repository":{"id":354678971,"uuid":"1224580780","full_name":"shuque/decode_nsec_response","owner":"shuque","description":"Decode NSEC/NSEC3 responses","archived":false,"fork":false,"pushed_at":"2026-04-29T15:21:03.000Z","size":17,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-29T16:31:05.465Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shuque.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-29T12:25:34.000Z","updated_at":"2026-04-29T15:23:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/shuque/decode_nsec_response","commit_stats":null,"previous_names":["shuque/decode_nsec_response"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/shuque/decode_nsec_response","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shuque%2Fdecode_nsec_response","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shuque%2Fdecode_nsec_response/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shuque%2Fdecode_nsec_response/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shuque%2Fdecode_nsec_response/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shuque","download_url":"https://codeload.github.com/shuque/decode_nsec_response/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shuque%2Fdecode_nsec_response/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34374146,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-15T18:01:37.355Z","updated_at":"2026-06-15T18:01:38.245Z","avatar_url":"https://github.com/shuque.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NSEC/NSEC3 Response Decoder\n\n## Description\n\n[decode\\_nsec\\_response.py](decode_nsec_response.py) — Queries a given\nname and type, using a validating DNS resolver, then decodes and explains\nthe NSEC or NSEC3 records in the authority section of the response.\nIdentifies the role of each record in the authenticated denial proof:\n\n- **NXDOMAIN (NSEC)**: which NSEC covers the queried name and which\n  covers the wildcard at the closest encloser.\n- **NXDOMAIN (NSEC3)**: computes NSEC3 hashes and identifies the\n  closest encloser match, next closer name cover, and wildcard cover.\n- **NODATA**: explains how the type bitmap proves the queried type\n  does not exist.\n- **Wildcard synthesis**: identifies the NSEC/NSEC3 proving no closer\n  match than the wildcard exists, validating the wildcard match. Handles\n  wildcard NODATA (wildcard exists but lacks the queried type).\n- **Compact Denial of Existence**: detects both NSEC (RFC 9824) and\n  NSEC3 (RFC 9824 Section 4) CDoE patterns, with or without NXNAME.\n- **NSEC3 opt-out**: flags opt-out NSEC3 records and notes that\n  unsigned delegations may exist within the covered range.\n\n```\n./decode_nsec_response.py [--doh] [--doh-server URL] QNAME QTYPE\n```\n\n## Dependencies\n\n- Python 3.9+\n- [dnspython](https://www.dnspython.org/) (`pip install dnspython`)\n- For DoH support: `pip install dnspython[doh]`\n\n\n## Installation\n\n```\npip install .\n```\n\nOr directly from the GitHub repository:\n\n```\npip install git+https://github.com/shuque/decode_nsec_response.git\n```\n\nThis installs the `decode_nsec_response.py` script and its dependencies.\n\nFor a system-wide install:\n\n```\nsudo pip install .\n```\n\nOn systems where pip is restricted from modifying the system Python\nenvironment (Debian/Ubuntu with PEP 668), use one of:\n\n```\nsudo pip install --break-system-packages .\nsudo pip install --prefix=/usr/local .\n```\n\n\n## Testing\n\nThe test suite uses canned DNS responses in wire format, so tests run\ninstantly and are not affected by live DNS changes.\n\n**Capture test data** (only needed when adding or refreshing test cases):\n\n```\npython3 capture_test_data.py\n```\n\nThis queries all 14 test cases via DoH to Cloudflare and saves the raw\nwire bytes to `testdata/`.\n\n**Run the test suite:**\n\n```\npython3 -m unittest test_decode -v\n```\n\nOr with pytest if installed:\n\n```\npython3 -m pytest test_decode.py -v\n```\n\n\n## Sample Output\n\nNSEC3 NODATA:\n\n```\n$ ./decode_nsec_response.py --doh salesforce.com. TLSA\n\nQuery: salesforce.com. TLSA\nResponse: NOERROR [AD]\n======================================================================\nZone: salesforce.com.\nNSEC3 params: algorithm 1, iterations 0, salt 7FEA7B83\n\n  H(salesforce.com.) = 49STKNJU01HOVPN0L8N7MMD35E9VD3VD\n\nNODATA: salesforce.com. exists but has no TLSA record.\n\nAuthority section:\n\n  NSEC3: 49STKNJU01HOVPN0L8N7MMD35E9VD3VD -\u003e 49T2A4TT2OHA06O3HB89B4PCF7U0824L\n    Type bitmap: [A NS SOA MX TXT RRSIG DNSKEY NSEC3PARAM TYPE65534]\n\n    Role: Matches H(salesforce.com.)\n    The type bitmap does not include TLSA, proving no TLSA record exists at this name.\n```\n\nNSEC3 NXDOMAIN:\n\n```\n$ ./decode_nsec_response.py --doh foo.nxd123.salesforce.com. A\n\nQuery: foo.nxd123.salesforce.com. A\nResponse: NXDOMAIN [AD]\n======================================================================\nZone: salesforce.com.\nNSEC3 params: algorithm 1, iterations 0, salt 7FEA7B83\n\nNXDOMAIN: foo.nxd123.salesforce.com. does not exist.\n\nAuthority section:\n\n  Closest encloser: salesforce.com.\n  Next closer name: nxd123.salesforce.com.\n  Wildcard at CE:   *.salesforce.com.\n  H(salesforce.com.) = 49STKNJU01HOVPN0L8N7MMD35E9VD3VD\n  H(nxd123.salesforce.com.) = JP5FLA1OE214J8NI0E55A3GVP96NGINB\n  H(*.salesforce.com.) = 09UJ9K6OKDGIKMN908E3ULJRDMKM277V\n\n  NSEC3: 49STKNJU01HOVPN0L8N7MMD35E9VD3VD -\u003e 49T2A4TT2OHA06O3HB89B4PCF7U0824L\n    Type bitmap: [A NS SOA MX TXT RRSIG DNSKEY NSEC3PARAM TYPE65534]\n\n    Role: Matches H(salesforce.com.) — closest encloser proof\n    Proves salesforce.com. exists in the zone.\n\n  NSEC3: JP1PCI1BBC6Q7F8136EPU4LT4CUEPNTM -\u003e JP6FI3JBGQTR23BALRE30LG9UFU3FJHJ\n    Type bitmap: [A RRSIG]\n\n    Role: Covers H(nxd123.salesforce.com.) — next closer name cover\n    Proves nxd123.salesforce.com. does not exist.\n\n  NSEC3: 09TD20B1LCISV1SUHEMNIUCF1FGB5K26 -\u003e 09UJ9OKA6O2IRL1I3Q0D193ERNT3P0I6\n    Type bitmap: [A RRSIG]\n\n    Role: Covers H(*.salesforce.com.) — wildcard cover\n    Proves no wildcard exists at the closest encloser (salesforce.com.),\n    so no wildcard synthesis can produce an answer.\n```\n\nNSEC3 Wildcard Match:\n\n```\n$ ./decode_nsec_response.py --doh foo.wild.dnskensa.com. A\n\nQuery: foo.wild.dnskensa.com. A\nResponse: NOERROR [AD]\n======================================================================\nZone: dnskensa.com.\nNSEC3 params: algorithm 1, iterations 10, salt 73B2182A738FCBC4\n\nWildcard-synthesized answer for foo.wild.dnskensa.com..\n\nAnswer section:\n  foo.wild.dnskensa.com. 86400 A 10.1.1.1\n\nAuthority section:\n\n  Closest encloser: wild.dnskensa.com.\n  Next closer name: foo.wild.dnskensa.com.\n  Wildcard:         *.wild.dnskensa.com.\n  H(foo.wild.dnskensa.com.) = 3BPL37FUV6JQLG6BLVIRV23T5JVP1H4L\n\n  NSEC3: 33OE6CIJFV452QC67M4A72F474I3M2E5 -\u003e 3F3DEH8FT59Q4S2MNVN446MFALKSAFSU\n    Type bitmap: [A AAAA RRSIG]\n\n    Role: Covers H(foo.wild.dnskensa.com.) — next closer name cover\n    Proves no closer match than wild.dnskensa.com. exists for foo.wild.dnskensa.com.,\n    validating that the answer was synthesized from a wildcard.\n```\n\nNSEC NXDOMAIN:\n\n```\n$ ./decode_nsec_response.py --doh foobar. A\n\nQuery: foobar. A\nResponse: NXDOMAIN [AD]\n======================================================================\nZone: .\n\nNXDOMAIN: foobar. does not exist.\n\nAuthority section:\n\n  NSEC: foo. -\u003e food.\n    Type bitmap: [NS DS RRSIG NSEC]\n\n    Role: Covers the queried name (foobar.)\n    Owner sorts before qname, next sorts after qname\n    in canonical order, proving foobar. does not exist.\n\n  NSEC: . -\u003e aaa.\n    Type bitmap: [NS SOA RRSIG NSEC DNSKEY ZONEMD]\n\n    Role: Covers the wildcard (*.)\n    Proves no wildcard exists at the closest encloser (.),\n    so no wildcard synthesis can produce an answer.\n```\n\nWildcard CNAME NODATA (cross-zone; NSEC3 wildcard proof + NSEC target NODATA):\n\n```\n$ ./decode_nsec_response.py --doh 12345asdfasfadf.horoscope-divination.com. AFSDB\n\nQuery: 12345asdfasfadf.horoscope-divination.com. AFSDB\nResponse: NOERROR [AD]\n======================================================================\n\nWildcard CNAME NODATA: 12345asdfasfadf.horoscope-divination.com. matched wildcard *.horoscope-divination.com.,\nwhich targets general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com.. The target has no AFSDB record.\n\nAnswer section:\n  12345asdfasfadf.horoscope-divination.com. 600 CNAME general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com.\n\nAuthority section:\n\n  --- Wildcard proof (zone: horoscope-divination.com.) ---\nNSEC3 params: algorithm 1, iterations 0, salt D54DF1360676F4B8\n\n  Closest encloser: horoscope-divination.com.\n  Next closer name: 12345asdfasfadf.horoscope-divination.com.\n  Wildcard:         *.horoscope-divination.com.\n  H(12345asdfasfadf.horoscope-divination.com.) = V24NHP56RH0DS80NDKCVTMVU9BC4IR0M\n\n  NSEC3: UOLUGA2L16M65IELFLBNLEM2V8COSCI6 -\u003e 28DNC0LB8B15CTN3GTIRT1RJDR0P16R7\n    Type bitmap: [A RRSIG]\n\n    Role: Covers H(12345asdfasfadf.horoscope-divination.com.) — next closer name cover (wrap-around)\n    Proves no closer match than horoscope-divination.com. exists for 12345asdfasfadf.horoscope-divination.com.,\n    validating that the CNAME was synthesized from *.horoscope-divination.com..\n\n  --- NODATA proof (zone: herokudns.com.) ---\n\n  NSEC: general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com. -\u003e \\000.general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com.\n    Type bitmap: [A AAAA RRSIG NSEC]\n\n    Role: Matches the CNAME target (general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com.)\n    The type bitmap does not include AFSDB, proving no AFSDB record exists at general-beetle-fec22eecz21z3tnuxbx8mde3.herokudns.com..\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshuque%2Fdecode_nsec_response","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshuque%2Fdecode_nsec_response","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshuque%2Fdecode_nsec_response/lists"}