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

1"""IANA DNS root trust anchor for the in-process DNSSEC chain validator. 

2 

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). 

8 

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``): 

13 

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. 

18 

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. 

23 

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. 

32 

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""" 

37 

38from __future__ import annotations 

39 

40from dataclasses import dataclass 

41from typing import TYPE_CHECKING 

42 

43if TYPE_CHECKING: # import for type-checkers only; never at runtime (I11). 

44 import dns.rdataset 

45 

46 

47@dataclass(frozen=True) 

48class RootTrustAnchor: 

49 """One root-zone KSK DS record (RFC 4034 §5.1 / RFC 4509). 

50 

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 """ 

54 

55 label: str 

56 key_tag: int 

57 algorithm: int 

58 digest_type: int # 2 == SHA-256 

59 digest: str # hex, uppercase 

60 

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}" 

64 

65 

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) 

83 

84 

85def root_ds_rdataset() -> dns.rdataset.Rdataset: 

86 """Materialise ``ROOT_TRUST_ANCHORS`` as a dnspython DS rdataset at the root. 

87 

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 

96 

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