Coverage for node / src / stigmem_node / federation / dnssec / freshness.py: 100%

21 statements  

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

1"""RRSIG-age clamp with operator-confirm fallthrough (Rev 6 I4 / build-phase 3b.6). 

2 

3The DNSSEC chain validator (3a) proves a binding's signatures are valid; this 

4module adds the *age* policy on top of that, per Rev 6 I4: 

5 

6 * A FRESH RRSIG (age within ``federation_dnssec_max_rrsig_age``, per-origin 

7 overridable) is ``OK`` — trust it (subject to the epoch pin). 

8 * An AGED RRSIG (older than the ceiling) on a host that has NEVER served a 

9 fresh signature falls through to operator-confirm (``FALLTHROUGH_CONFIRM``), 

10 NOT a hard reject — a genuinely slow-resigning zone stays usable behind a 

11 human gate rather than being silently denied. 

12 * An AGED RRSIG on a host that PREVIOUSLY served a fresh signature is an attack 

13 signal (``REJECT``): a previously-fresh zone that suddenly serves only aged 

14 signatures looks like a replay/suppression attempt, not a slow re-sign. 

15 

16"Previously fresh" is durable per-host state in ``dnssec_epoch_pins.last_fresh_at`` 

17(migration 056): ``mark_fresh`` stamps it whenever a fresh RRSIG is accepted and 

18``was_previously_fresh`` reports whether it has ever been stamped. Like 

19``signed_delegation_seen`` it is sticky in meaning — only ever refreshed forward, 

20never cleared — so the previously-fresh hard-reject cannot be erased by a later 

21aged record. 

22 

23The classifier ``classify_rrsig_age`` is pure (no DB, no time source) so the age 

24policy is directly unit-testable; the DB-backed previously-fresh state is a thin 

25upsert. Off-path: nothing here is wired into the resolver yet (a later 3b task). 

26No DNSSEC / ``dnspython`` import is reachable from this module (I11). 

27""" 

28 

29from __future__ import annotations 

30 

31import enum 

32from typing import Any 

33 

34 

35class AgeClass(enum.Enum): 

36 """The age verdict the first-trust ladder dispatches on (Rev 6 I4).""" 

37 

38 OK = "ok" 

39 FALLTHROUGH_CONFIRM = "fallthrough_confirm" 

40 REJECT = "reject" 

41 

42 

43def classify_rrsig_age( 

44 *, 

45 rrsig_age_seconds: float, 

46 max_age: int, 

47 previously_fresh: bool, 

48) -> AgeClass: 

49 """Classify a binding RRSIG by age (Rev 6 I4). 

50 

51 ``rrsig_age_seconds`` is how old the signature is relative to now (a negative 

52 value means a not-yet-valid / future-inception signature — not aged, so the 

53 age clamp leaves it ``OK`` and inception/expiration is the chain validator's 

54 concern). ``max_age`` is the per-origin-overridable ceiling 

55 (``federation_dnssec_max_rrsig_age``). ``previously_fresh`` is the host's 

56 durable previously-fresh marker. 

57 

58 * age <= max_age -> OK (fresh; the previously-fresh signal is 

59 irrelevant for a fresh signature). 

60 * age > max_age, never fresh -> FALLTHROUGH_CONFIRM (slow-resigning zone 

61 stays usable behind a human gate). 

62 * age > max_age, prev fresh -> REJECT (attack signal). 

63 """ 

64 if rrsig_age_seconds <= max_age: 

65 return AgeClass.OK 

66 if previously_fresh: 

67 return AgeClass.REJECT 

68 return AgeClass.FALLTHROUGH_CONFIRM 

69 

70 

71def mark_fresh(conn: Any, host: str, *, now: str) -> None: 

72 """Stamp ``host`` as having served a fresh RRSIG at ``now`` (ISO-8601). 

73 

74 Upserts ``dnssec_epoch_pins.last_fresh_at`` in place — preserving the 

75 monotonic epoch floor and the sticky signed-delegation flag on an existing 

76 row, or creating a neutral row (``max_epoch_seen=0``) if the host is new. The 

77 stamp only moves forward in meaning (the previously-fresh predicate is 

78 "non-null"); the caller owns the transaction. 

79 """ 

80 row = conn.execute( 

81 "SELECT host FROM dnssec_epoch_pins WHERE host=?", (host,) 

82 ).fetchone() 

83 if row is None: 

84 conn.execute( 

85 "INSERT INTO dnssec_epoch_pins " 

86 "(host, max_epoch_seen, signed_delegation_seen, last_validated_at, last_fresh_at) " 

87 "VALUES (?, 0, 0, ?, ?)", 

88 (host, now, now), 

89 ) 

90 else: 

91 conn.execute( 

92 "UPDATE dnssec_epoch_pins SET last_fresh_at=? WHERE host=?", 

93 (now, host), 

94 ) 

95 

96 

97def was_previously_fresh(conn: Any, host: str) -> bool: 

98 """Return whether ``host`` has ever served a fresh RRSIG (last_fresh_at set). 

99 

100 False when the host has no row, or has a row but no fresh observation yet.""" 

101 row = conn.execute( 

102 "SELECT last_fresh_at FROM dnssec_epoch_pins WHERE host=?", (host,) 

103 ).fetchone() 

104 return row is not None and row[0] is not None