7Actor HMAC signing and verification.
11Build and verify canonical HMAC-SHA256 proofs for mutating registry
16- Canonicalize the signed command payload.
17- Produce proof mappings for CLI/API callers.
18- Verify signatures with constant-time comparison and skew checks.
30- The signed payload includes slot, command, args, timestamp, and key id.
31- Verification returns a structured failure reason for ``auth_events``.
32- This module performs no registry writes and reads no secret files.
36- dataclasses (proof, credential and result records)
37- hashlib (SHA-256 for signatures and payload digests)
38- hmac (HMAC signing and constant-time comparison)
39- oc_chat.registry.canonical (canonical JSON of the signed payload)
40- oc_chat.registry.clock (timestamps for signing, skew and expiry checks)
41- oc_diagnostics (structured debug logging via _dbg)
43from __future__
import annotations
45from dataclasses
import dataclass
48from typing
import Mapping
50from oc_diagnostics
import _dbg
56_dbg(
"OC_CHAT.MAIN",
"module",
"loaded", 4)
58SIGNATURE_ALGORITHM =
"HMAC-SHA256"
61@dataclass(frozen=True)
63 """Client-supplied proof fields for one registry mutation."""
68 algorithm: str = SIGNATURE_ALGORITHM
71 """Return a JSON-safe proof mapping."""
82 """Parse a proof mapping."""
84 key_id = value.get(
"hmac_key_id")
85 signature = value.get(
"signature_hex")
86 ts_ms = value.get(
"ts_ms")
87 algorithm = value.get(
"algorithm", SIGNATURE_ALGORITHM)
88 if not isinstance(key_id, str)
or not key_id:
89 raise ValueError(
"hmac_key_id is required")
90 if not isinstance(signature, str)
or not signature:
91 raise ValueError(
"signature_hex is required")
92 if not isinstance(ts_ms, int):
93 raise ValueError(
"ts_ms is required")
94 if algorithm != SIGNATURE_ALGORITHM:
95 raise ValueError(
"unsupported HMAC algorithm")
96 return cls(hmac_key_id=key_id, ts_ms=ts_ms, signature_hex=signature)
99@dataclass(frozen=True)
101 """Credential fields needed by HMAC verification."""
107 revoked_at: str |
None =
None
108 expires_at: str |
None =
None
111@dataclass(frozen=True)
113 """Outcome returned by ``verify_hmac``."""
116 hmac_key_id: str |
None
117 failure_reason: str |
None =
None
124 args: Mapping[str, object],
127 clock: Clock |
None =
None,
128 ts_ms: int |
None =
None,
130 """Return an HMAC proof for one command payload."""
132 effective_ts =
_now_ms(clock)
if ts_ms
is None else ts_ms
138 hmac_key_id=hmac_key_id,
141 hmac_key_id=hmac_key_id,
143 signature_hex=
_sign(payload, raw_secret),
151 args: Mapping[str, object],
153 credential: CredentialMaterial |
None,
154 clock: Clock |
None =
None,
155 max_skew_seconds: int = 300,
157 """Verify one HMAC proof against resolved credential material."""
159 if credential
is None:
160 return VerifyResult(
False, proof.hmac_key_id,
"no_credential")
161 if credential.slot != slot:
162 return VerifyResult(
False, proof.hmac_key_id,
"bad_signature")
163 if credential.revoked_at
is not None:
164 return VerifyResult(
False, proof.hmac_key_id,
"revoked_key")
165 now_wall_func = getattr(clock
or RealClock(),
"now_wall")
166 now_wall = now_wall_func()
167 if credential.expires_at
is not None and credential.expires_at < now_wall:
168 return VerifyResult(
False, proof.hmac_key_id,
"revoked_key")
169 if max_skew_seconds >= 0:
170 skew_ms = abs(
_now_ms(clock) - proof.ts_ms)
171 if skew_ms > max_skew_seconds * 1000:
172 return VerifyResult(
False, proof.hmac_key_id,
"clock_skew")
179 hmac_key_id=proof.hmac_key_id,
181 credential.raw_secret,
183 if not hmac.compare_digest(expected, proof.signature_hex):
184 return VerifyResult(
False, proof.hmac_key_id,
"bad_signature")
192 args: Mapping[str, object],
196 """Return canonical JSON text for the signed command."""
198 return canonical_json(
205 "hmac_key_id": hmac_key_id,
211 """Return the canonical payload digest used in ``auth_events``."""
213 return hashlib.sha256(canonical_json(args).encode(
"utf-8")).hexdigest()
216def _sign(payload: str, raw_secret: bytes) -> str:
219 payload.encode(
"utf-8"),
225 effective_clock =
RealClock()
if clock
is None else clock
226 return effective_clock.now_monotonic_ns() // 1_000_000
dict[str, object] to_mapping(self)
"HmacProof" from_mapping(cls, Mapping[str, object] value)
HmacProof sign_command(*, str slot, str command, Mapping[str, object] args, bytes raw_secret, str hmac_key_id, Clock|None clock=None, int|None ts_ms=None)
VerifyResult verify_hmac(*, str slot, str command, Mapping[str, object] args, HmacProof proof, CredentialMaterial|None credential, Clock|None clock=None, int max_skew_seconds=300)
str _sign(str payload, bytes raw_secret)
str payload_sha256(Mapping[str, object] args)
str canonical_payload(*, str slot, str command, Mapping[str, object] args, int ts_ms, str hmac_key_id)
int _now_ms(Clock|None clock)