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
« 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).
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:
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.
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.
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"""
29from __future__ import annotations
31import enum
32from typing import Any
35class AgeClass(enum.Enum):
36 """The age verdict the first-trust ladder dispatches on (Rev 6 I4)."""
38 OK = "ok"
39 FALLTHROUGH_CONFIRM = "fallthrough_confirm"
40 REJECT = "reject"
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).
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.
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
71def mark_fresh(conn: Any, host: str, *, now: str) -> None:
72 """Stamp ``host`` as having served a fresh RRSIG at ``now`` (ISO-8601).
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 )
97def was_previously_fresh(conn: Any, host: str) -> bool:
98 """Return whether ``host`` has ever served a fresh RRSIG (last_fresh_at set).
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