Coverage for node / src / stigmem_node / federation / dnssec / resolve.py: 88%

43 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-06-18 05:34 +0000

1"""Off-path composition entry point for the DNSSEC first-trust tier (Rev 6 §7/I2/I3). 

2 

3``resolve_dnssec_binding`` is the single seam the 3b first-trust ladder consumes. 

4It composes the three self-contained 3a pieces: 

5 

6 1. ``host_from_entity_uri`` (3a.2 / I3) — derive the canonical query host from 

7 the *signed wire* ``entity_uri``. A non-DNSSEC-capable URI (non-HTTP 

8 scheme, IP-literal, userinfo, explicit port) yields ``None`` and the binding 

9 is ``NOT_APPLICABLE`` — the caller routes to operator-confirm (I3). 

10 2. ``validate_binding`` (3a.4/5/6 / I2) — walk the chain to the IANA root, 

11 re-deriving trust from signatures (never the AD bit), and validate the 

12 binding TXT, its authenticated absence, or its insecure delegation. 

13 3. The record parse already happened inside the validator (3a.3); its parsed 

14 ``BindingRecord`` rides on ``SECURE``. 

15 

16This module is OFF-PATH (Rev 6, build-phase 3a): no resolver is wired into the 

17relay terminal yet — that is build-phase 3c. Its only contract is total: every 

18input, including a malformed ``entity_uri`` or any validator outcome, maps to 

19exactly one ``DnssecResult``. **No exception escapes.** 

20 

21The ``Validation`` -> ``DnssecResult.Outcome`` mapping is fail-closed (I10): 

22 

23 ====================== ============================ ========================== 

24 validator outcome record DnssecResult.Outcome 

25 ====================== ============================ ========================== 

26 SECURE active ACTIVE (+ record) 

27 SECURE revoked (``status=revoked``) REVOKED (+ record) 

28 INSECURE — INSECURE 

29 ABSENT_AUTHENTICATED — ABSENT_AUTHENTICATED 

30 UNVALIDATABLE — UNVALIDATABLE 

31 BOGUS — BOGUS 

32 ====================== ============================ ========================== 

33 

34dnspython stays function-local (it is only reached through ``validate_binding``, 

35which imports it lazily), so importing this module never loads the 

36``[federation-dnssec]`` extra (Rev 6 I11). 

37""" 

38 

39from __future__ import annotations 

40 

41import enum 

42from dataclasses import dataclass 

43from typing import TYPE_CHECKING 

44 

45from .host import host_from_entity_uri 

46from .record import BindingRecord 

47from .validator import Validation, validate_binding 

48 

49if TYPE_CHECKING: # type-checkers only; never imported at runtime (I11). 

50 from .resolver import Resolver 

51 

52 

53@dataclass(frozen=True) 

54class DnssecResult: 

55 """The outcome of composing host derivation + chain validation for an origin. 

56 

57 ``outcome`` is always populated. ``record`` is the parsed, chain-validated 

58 ``BindingRecord`` for the ``ACTIVE`` and ``REVOKED`` outcomes and ``None`` 

59 for every other outcome (the binding either does not exist, is unsigned, or 

60 failed validation, so there is no trustworthy record to surface). 

61 """ 

62 

63 class Outcome(enum.Enum): 

64 """The seven terminal verdicts the first-trust ladder dispatches on. 

65 

66 Caller disposition (Rev 6 I3/I10): 

67 * ``ACTIVE`` -> trust the record's key (subject to epoch 

68 pin + age clamp in 3b). 

69 * ``REVOKED`` -> hard-reject the origin's key. 

70 * ``INSECURE`` -> genuinely-unsigned delegation; fall to 

71 operator-confirm. 

72 * ``ABSENT_AUTHENTICATED`` -> proven-absent binding; fall to 

73 operator-confirm. 

74 * ``UNVALIDATABLE`` -> reject (no validatable proof either way). 

75 * ``NOT_APPLICABLE`` -> entity_uri is not DNSSEC-capable; route 

76 to operator-confirm. 

77 * ``BOGUS`` -> reject (forged / broken chain). 

78 """ 

79 

80 ACTIVE = "active" 

81 REVOKED = "revoked" 

82 INSECURE = "insecure" 

83 ABSENT_AUTHENTICATED = "absent_authenticated" 

84 UNVALIDATABLE = "unvalidatable" 

85 NOT_APPLICABLE = "not_applicable" 

86 BOGUS = "bogus" 

87 

88 outcome: DnssecResult.Outcome 

89 record: BindingRecord | None = None 

90 host: str | None = None 

91 detail: str = "" 

92 # Epoch-seconds inception of the newest binding RRSIG, threaded from the 

93 # validator (Rev 6 I4). Only meaningful for SECURE-derived outcomes 

94 # (``ACTIVE``/``REVOKED``); ``None`` everywhere else. The first-trust ladder 

95 # derives the RRSIG age from this for its age clamp. 

96 rrsig_inception: float | None = None 

97 # DNS TTL (seconds) of the binding TXT RRset, threaded from the validator 

98 # (Rev 6 §7 / I5). Only meaningful for SECURE-derived outcomes 

99 # (``ACTIVE``/``REVOKED``); ``None`` everywhere else. The relay-path re-check 

100 # clamps its cadence to this (``clamp(ttl, floor, cap)``). 

101 ttl: int | None = None 

102 

103 

104# The SECURE-with-record outcomes are decided by the record's revoked flag; every 

105# other validator status maps 1:1. A status missing from this table is treated 

106# fail-closed as BOGUS. 

107_NON_SECURE_MAP: dict[Validation, DnssecResult.Outcome] = { 

108 Validation.INSECURE: DnssecResult.Outcome.INSECURE, 

109 Validation.ABSENT_AUTHENTICATED: DnssecResult.Outcome.ABSENT_AUTHENTICATED, 

110 Validation.UNVALIDATABLE: DnssecResult.Outcome.UNVALIDATABLE, 

111 Validation.BOGUS: DnssecResult.Outcome.BOGUS, 

112} 

113 

114 

115def resolve_dnssec_binding(entity_uri: str, *, resolver: Resolver) -> DnssecResult: 

116 """Resolve the DNSSEC binding for ``entity_uri`` into a ``DnssecResult``. 

117 

118 Off-path composition (3a.7): derive host (I3) -> validate chain (I2) -> map. 

119 Total + fail-closed: any unexpected error maps to ``BOGUS``; no exception 

120 escapes (Rev 6 I10). 

121 """ 

122 try: 

123 host = host_from_entity_uri(entity_uri) 

124 except Exception: # noqa: BLE001 — host derivation is fail-closed (I10). 

125 return DnssecResult(DnssecResult.Outcome.BOGUS, detail="host derivation error") 

126 

127 if host is None: 

128 # Not DNSSEC-capable (non-HTTP scheme, IP-literal, userinfo, port). The 

129 # resolver is never consulted; the caller routes to operator-confirm (I3). 

130 return DnssecResult(DnssecResult.Outcome.NOT_APPLICABLE) 

131 

132 try: 

133 verdict = validate_binding(host, resolver=resolver) 

134 except Exception: # noqa: BLE001 — the validator is designed not to raise, 

135 # but the composition contract is total: any escape is BOGUS (I10). 

136 return DnssecResult(DnssecResult.Outcome.BOGUS, host=host, detail="validation error") 

137 

138 if verdict.status is Validation.SECURE: 

139 record = verdict.record 

140 if record is None: 140 ↛ 143line 140 didn't jump to line 143 because the condition on line 140 was never true

141 # SECURE must carry a parsed record; absence is a contract breach we 

142 # treat fail-closed rather than trust. 

143 return DnssecResult( 

144 DnssecResult.Outcome.BOGUS, host=host, detail="secure without record" 

145 ) 

146 outcome = ( 

147 DnssecResult.Outcome.REVOKED if record.revoked else DnssecResult.Outcome.ACTIVE 

148 ) 

149 return DnssecResult( 

150 outcome, 

151 record=record, 

152 host=host, 

153 detail=verdict.detail, 

154 rrsig_inception=verdict.rrsig_inception, 

155 ttl=verdict.ttl, 

156 ) 

157 

158 mapped = _NON_SECURE_MAP.get(verdict.status, DnssecResult.Outcome.BOGUS) 

159 return DnssecResult(mapped, host=host, detail=verdict.detail)