48 lines
2.2 KiB
Python
48 lines
2.2 KiB
Python
|
|
"""Consumer-owned bounded time adapter; explicit opt-in, no configured fallback."""
|
||
|
|
from datetime import datetime, timezone, timedelta
|
||
|
|
from functools import lru_cache
|
||
|
|
from secrets_engine.errors import DecisionError
|
||
|
|
|
||
|
|
@lru_cache(maxsize=8)
|
||
|
|
def _clock(path):
|
||
|
|
try:
|
||
|
|
from railiance_clock.client import Clock, FileTrust
|
||
|
|
return Clock(FileTrust(path))
|
||
|
|
except (ImportError, OSError, ValueError, TypeError, KeyError) as exc:
|
||
|
|
raise DecisionError("Railiance clock unavailable or untrusted") from exc
|
||
|
|
|
||
|
|
def read_window(cfg):
|
||
|
|
path = getattr(cfg, "clock_trust_file", None)
|
||
|
|
if not path:
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
return _clock(str(path)).read()
|
||
|
|
except (OSError, ValueError) as exc:
|
||
|
|
raise DecisionError("Railiance clock unavailable or untrusted") from exc
|
||
|
|
|
||
|
|
def validity_bounds(now=None, window=None):
|
||
|
|
if window is None:
|
||
|
|
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
|
||
|
|
return current, current
|
||
|
|
if now is not None:
|
||
|
|
raise DecisionError("choose interval evidence or explicit test time, not both")
|
||
|
|
try:
|
||
|
|
lower, upper = window.lower_ns, window.upper_ns
|
||
|
|
if type(lower) is not int or type(upper) is not int or not 0 <= lower <= upper:
|
||
|
|
raise ValueError("invalid interval")
|
||
|
|
epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||
|
|
return epoch + timedelta(microseconds=lower // 1000), epoch + timedelta(microseconds=(upper + 999) // 1000)
|
||
|
|
except (ValueError, TypeError, AttributeError, OverflowError) as exc:
|
||
|
|
raise DecisionError("invalid Railiance time interval") from exc
|
||
|
|
|
||
|
|
def recheck_binding(cfg, binding):
|
||
|
|
if not getattr(cfg, "clock_trust_file", None):
|
||
|
|
return
|
||
|
|
if not binding.decision_expires_at:
|
||
|
|
raise DecisionError("bounded-time consume requires decision validity")
|
||
|
|
lower, upper = validity_bounds(window=read_window(cfg))
|
||
|
|
expires = datetime.fromisoformat(binding.decision_expires_at.replace("Z", "+00:00"))
|
||
|
|
start = datetime.fromisoformat(binding.decision_not_before.replace("Z", "+00:00")) if binding.decision_not_before else None
|
||
|
|
if upper >= expires or (start is not None and lower < start):
|
||
|
|
raise DecisionError("decision validity does not contain the Railiance interval")
|