Last active
March 11, 2026 08:18
-
-
Save kjd/4ab344767ecda2ebf07b94b1008ad001 to your computer and use it in GitHub Desktop.
Fetch the root zone, validating it with DNSSEC and ZONEMD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # /// script | |
| # dependencies = ['dnspython', 'cryptography', 'certifi'] | |
| # /// | |
| """Fetch, validate, and save the current DNS root zone. | |
| Downloads the root zone from IANA, validates DNSSEC signatures on the | |
| ZONEMD record against IANA trust anchors, verifies the zone digest, and | |
| writes the result to root.zone in the current directory. | |
| Invocation with standard Python: | |
| pip install dnspython cryptography certifi | |
| python3 fetch_root_zone.py | |
| Invocation using uv: | |
| uv run fetch_root_zone.py | |
| """ | |
| import ssl | |
| import time | |
| import urllib.request | |
| import xml.etree.ElementTree as ET | |
| from datetime import datetime, timezone | |
| import certifi | |
| import dns.dnssec | |
| import dns.name | |
| import dns.rdata | |
| import dns.rdataclass | |
| import dns.rdatatype | |
| import dns.zone | |
| ZONE_URL = "https://www.internic.net/domain/root.zone" | |
| TRUST_ANCHOR_URL = "https://data.iana.org/root-anchors/root-anchors.xml" | |
| _ssl_ctx = ssl.create_default_context(cafile=certifi.where()) | |
| def _fetch(url, retries=5): | |
| for attempt in range(retries): | |
| try: | |
| with urllib.request.urlopen(url, timeout=30, context=_ssl_ctx) as r: | |
| return r.read() | |
| except Exception: | |
| if attempt == retries - 1: | |
| raise | |
| time.sleep(2 ** (attempt + 1)) | |
| def _parse_trust_anchors(xml_data): | |
| now = datetime.now(timezone.utc) | |
| ds_records = [] | |
| for kd in ET.fromstring(xml_data).findall("KeyDigest"): | |
| valid_from = datetime.fromisoformat(kd.get("validFrom")) | |
| valid_until = kd.get("validUntil") | |
| if valid_until and now > datetime.fromisoformat(valid_until): | |
| continue | |
| if now < valid_from: | |
| continue | |
| ds_text = ( | |
| f"{kd.findtext('KeyTag')} {kd.findtext('Algorithm')} " | |
| f"{kd.findtext('DigestType')} {kd.findtext('Digest')}" | |
| ) | |
| ds_records.append( | |
| dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, ds_text) | |
| ) | |
| assert ds_records, "No valid trust anchors found" | |
| return ds_records | |
| def _validate_dnssec(zone, trust_anchors): | |
| origin = dns.name.root | |
| node = zone.find_node(origin) | |
| get = lambda rdtype, covers=dns.rdatatype.NONE: node.get_rdataset( | |
| dns.rdataclass.IN, rdtype, covers | |
| ) | |
| dnskeys = get(dns.rdatatype.DNSKEY) | |
| assert any( | |
| dns.dnssec.make_ds(origin, k, ds.digest_type) == ds | |
| for k in dnskeys | |
| for ds in trust_anchors | |
| ), "No DNSKEY matches any trust-anchor DS record" | |
| dns.dnssec.validate( | |
| (origin, get(dns.rdatatype.ZONEMD)), | |
| (origin, get(dns.rdatatype.RRSIG, dns.rdatatype.ZONEMD)), | |
| {origin: dnskeys}, | |
| origin=origin, | |
| ) | |
| if __name__ == "__main__": | |
| zone_data = _fetch(ZONE_URL) | |
| zone = dns.zone.from_text( | |
| zone_data.decode("ascii"), | |
| origin=dns.name.root, | |
| relativize=False, | |
| check_origin=True, | |
| ) | |
| _validate_dnssec(zone, _parse_trust_anchors(_fetch(TRUST_ANCHOR_URL))) | |
| zone.verify_digest() | |
| with open("root.zone", "wb") as f: | |
| f.write(zone_data) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment