Coverage for node / src / stigmem_node / federation / dnssec / anchor.py: 50%
24 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-18 05:34 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-18 05:34 +0000
1"""IANA DNS root trust anchor for the in-process DNSSEC chain validator.
3Rev 6 I2: the validator walks the chain to the root and validates each cut
4*cryptographically* — it never trusts a resolver's AD bit. The root DNSKEY
5RRset is the only key the validator trusts a priori; everything below it is
6proven by signatures chaining up to a root KSK whose DS digest is published by
7IANA out-of-band (https://www.iana.org/dnssec/files).
9What is embedded
10----------------
11``ROOT_TRUST_ANCHORS`` is the set of root-zone KSK DS records (the same content
12as IANA's ``root-anchors.xml``):
14 * **KSK-2017** (key tag 20326, algorithm 8 / RSASHA256, SHA-256 digest) — the
15 currently-active root KSK.
16 * **KSK-2024** (key tag 38696, algorithm 8 / RSASHA256, SHA-256 digest) — the
17 successor KSK published by IANA for the next rollover.
19A root DNSKEY RRset validates against this anchor iff one of its KSKs (a DNSKEY
20with the SEP/flags-257 bit) produces a DS digest equal to one of these records
21(RFC 4509). Holding *both* the active and successor anchors means a root KSK
22rollover does not strand the validator between ceremonies.
24Rotation note (operator-facing)
25--------------------------------
26Root KSK rollovers are rare (the 2017 rollover was the first since the root was
27signed in 2010) and pre-announced by IANA years ahead. When IANA publishes a
28new KSK, add its DS record here and ship it in a release *before* the old anchor
29is retired. This module is the single source of truth in production; the
30DNSSEC test harness monkeypatches ``ROOT_TRUST_ANCHORS`` to a fake root so the
31offline fixture chain validates without touching the live root.
33This module has **no top-level dnspython import** (Rev 6 I11): the anchor is
34stored as plain text + ints and only materialised into ``dns.*`` rdata inside
35``root_ds_rdataset()``, which the validator calls at runtime.
36"""
38from __future__ import annotations
40from dataclasses import dataclass
41from typing import TYPE_CHECKING
43if TYPE_CHECKING: # import for type-checkers only; never at runtime (I11).
44 import dns.rdataset
47@dataclass(frozen=True)
48class RootTrustAnchor:
49 """One root-zone KSK DS record (RFC 4034 §5.1 / RFC 4509).
51 ``key_tag``/``algorithm``/``digest_type`` and the hex ``digest`` are exactly
52 the fields of a DS RR; ``label`` is a human tag for logs/operators.
53 """
55 label: str
56 key_tag: int
57 algorithm: int
58 digest_type: int # 2 == SHA-256
59 digest: str # hex, uppercase
61 def to_ds_text(self) -> str:
62 """Render the canonical DS presentation form (``tag alg digtype hex``)."""
63 return f"{self.key_tag} {self.algorithm} {self.digest_type} {self.digest}"
66# IANA root-zone KSK DS records (root-anchors.xml). SHA-256 digests, RSASHA256.
67ROOT_TRUST_ANCHORS: tuple[RootTrustAnchor, ...] = (
68 RootTrustAnchor(
69 label="KSK-2017",
70 key_tag=20326,
71 algorithm=8,
72 digest_type=2,
73 digest="E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D",
74 ),
75 RootTrustAnchor(
76 label="KSK-2024",
77 key_tag=38696,
78 algorithm=8,
79 digest_type=2,
80 digest="683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16",
81 ),
82)
85def root_ds_rdataset() -> dns.rdataset.Rdataset:
86 """Materialise ``ROOT_TRUST_ANCHORS`` as a dnspython DS rdataset at the root.
88 dnspython is imported here (function-local, Rev 6 I11) so importing this
89 module never pulls in the optional ``[federation-dnssec]`` extra.
90 """
91 import dns.name
92 import dns.rdata
93 import dns.rdataclass
94 import dns.rdataset
95 import dns.rdatatype
97 rds = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.DS)
98 rds.ttl = 3600
99 for anchor in ROOT_TRUST_ANCHORS:
100 rds.add(
101 dns.rdata.from_text(
102 dns.rdataclass.IN,
103 dns.rdatatype.DS,
104 anchor.to_ds_text(),
105 )
106 )
107 return rds