Coverage for node / src / stigmem_node / federation / dnssec / record.py: 89%

48 statements  

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

1"""Strict ``v=stigmem1`` DNSSEC binding-record grammar (Rev 6 §7). 

2 

3The DNSSEC-signed TXT record at ``_stigmem-fed._key.<canonical-host>`` binds an 

4origin's key fingerprint to a monotonic rotation epoch. Two forms: 

5 

6 active: ``v=stigmem1; fpr=<key_fpr>; epoch=<n>; prev_fpr=<or-empty>; prev_until=<or-empty>`` 

7 revoked: ``v=stigmem1; status=revoked; epoch=<n>; fpr=`` (empty fpr) 

8 

9The grammar is freeze-safe by construction (Rev 6 §7): unknown ``k=v`` pairs are 

10ignored, so adding a field later is a routine zone re-sign rather than a 

11re-sign-of-committed-material break. 

12 

13Parsing is fail-closed (Rev 6 I10): the FIRST token MUST be ``v=stigmem1``, 

14``epoch`` is required and is a non-negative int, the active form requires a 

15non-empty ``fpr``, and the revoked form (``status=revoked``) has an empty 

16``fpr``. Any violation returns ``None``; no exception escapes. 

17""" 

18 

19from __future__ import annotations 

20 

21from dataclasses import dataclass 

22 

23# DNS binding-record version sentinel (the required first token), not a 

24# credential — bandit's B105 heuristic flags the embedded '=' as a hardcoded 

25# password string. 

26_VERSION_TOKEN = "v=stigmem1" # nosec B105 

27 

28# Keys this grammar assigns meaning to. A DUPLICATE of any of these is ambiguous 

29# and rejected (fail-closed); duplicates of unknown keys are tolerated for the 

30# forward-compat path (Rev 6 §7). 

31_KNOWN_KEYS = frozenset({"v", "fpr", "epoch", "status", "prev_fpr", "prev_until"}) 

32 

33 

34@dataclass(frozen=True) 

35class BindingRecord: 

36 """A parsed, structurally-valid DNSSEC binding record. 

37 

38 Validation of the DNSSEC chain itself happens in the validator (3a.4+); 

39 this dataclass represents only a record whose *grammar* is well-formed. 

40 """ 

41 

42 fpr: str 

43 epoch: int 

44 prev_fpr: str = "" 

45 prev_until: str = "" 

46 revoked: bool = False 

47 

48 

49def parse_binding_record(txt: str) -> BindingRecord | None: 

50 """Parse a binding-record TXT string. Returns ``None`` on any violation.""" 

51 if not txt: 

52 return None 

53 

54 # Split on ';' into tokens; tolerate surrounding whitespace. 

55 tokens = [t.strip() for t in txt.split(";")] 

56 tokens = [t for t in tokens if t] 

57 if not tokens: 57 ↛ 58line 57 didn't jump to line 58 because the condition on line 57 was never true

58 return None 

59 

60 # The FIRST token MUST be exactly the version token. 

61 if tokens[0] != _VERSION_TOKEN: 

62 return None 

63 

64 pairs: dict[str, str] = {} 

65 for tok in tokens[1:]: 

66 if "=" not in tok: 66 ↛ 67line 66 didn't jump to line 67 because the condition on line 66 was never true

67 return None # malformed token (not k=v) 

68 key, _, raw_value = tok.partition("=") 

69 key = key.strip() 

70 if not key: 70 ↛ 71line 70 didn't jump to line 71 because the condition on line 70 was never true

71 return None 

72 # Reject a DUPLICATE of any known key (the grammar assigns it meaning, so 

73 # a second occurrence is ambiguous -> fail closed). Unknown keys may 

74 # repeat for forward-compat; their last-write value is harmless. 

75 if key in _KNOWN_KEYS and key in pairs: 

76 return None 

77 # Keep the raw (unstripped) value alongside the stripped one so the 

78 # strict numeric gate below can reject embedded whitespace (e.g. 

79 # ``epoch= 5``), which ``int()`` would otherwise silently accept. 

80 pairs[key] = raw_value.strip() 

81 if key == "epoch": 

82 raw_epoch_token = raw_value 

83 

84 # epoch is required and must be a strict ASCII non-negative decimal integer. 

85 raw_epoch = pairs.get("epoch") 

86 if raw_epoch is None: 

87 return None 

88 # Gate against the RAW value: ``int()`` accepts ``+5``, ``1_000``, Unicode 

89 # digits (``٠١``), and surrounding whitespace — none of which are valid here. 

90 if not raw_epoch_token.isascii() or not raw_epoch_token.isdigit(): 

91 return None 

92 epoch = int(raw_epoch_token) 

93 

94 revoked = pairs.get("status") == "revoked" 

95 fpr = pairs.get("fpr", "") 

96 

97 if revoked: 

98 # Revoked tombstone: fpr must be empty (or omitted). 

99 if fpr: 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true

100 return None 

101 return BindingRecord(fpr="", epoch=epoch, revoked=True) 

102 

103 # Active form requires a non-empty fingerprint. 

104 if not fpr: 

105 return None 

106 

107 return BindingRecord( 

108 fpr=fpr, 

109 epoch=epoch, 

110 prev_fpr=pairs.get("prev_fpr", ""), 

111 prev_until=pairs.get("prev_until", ""), 

112 revoked=False, 

113 )