idea182-implementation/gravityweave/ticket.py

92 lines
3.4 KiB
Python

"""Pass-Window Ticket signer/verifier.
Compact ticket structure intended for pre-signed pass-window triggers.
Uses HMAC (blake2b) signatures for tests/POC. The ticket hash is the sha256
of the canonical serialized payload (excluding signature), which can be used as
an external pointer in MFR-v2.pass_ticket_hash.
"""
from dataclasses import dataclass
from typing import Optional
import json
import time
import uuid
import hashlib
import hmac
@dataclass
class PassWindowTicket:
window_start: int
window_end: int
comm_budget_bytes: int
resource_budget_digest: str
trigger_policy_id: str
nonce: str
signer: str = ""
sig: str = ""
@classmethod
def mint(cls, window_start: int, window_end: int, comm_budget_bytes: int,
resource_budget_digest: str, trigger_policy_id: str = "default",
signer: str = "", key: Optional[bytes] = None) -> "PassWindowTicket":
nonce = uuid.uuid4().hex[:12]
t = cls(window_start=window_start, window_end=window_end,
comm_budget_bytes=int(comm_budget_bytes),
resource_budget_digest=resource_budget_digest,
trigger_policy_id=trigger_policy_id, nonce=nonce,
signer=signer, sig="")
if key:
t.sig = t._sign(key)
return t
def _canonical(self) -> str:
payload = {
"window_start": int(self.window_start),
"window_end": int(self.window_end),
"comm_budget_bytes": int(self.comm_budget_bytes),
"resource_budget_digest": self.resource_budget_digest,
"trigger_policy_id": self.trigger_policy_id,
"nonce": self.nonce,
"signer": self.signer,
}
return json.dumps(payload, sort_keys=True, separators=(",", ":"))
def _sign(self, key: bytes) -> str:
h = hmac.new(key, self._canonical().encode("utf-8"), hashlib.blake2b)
return h.hexdigest()[:64]
def serialize(self) -> bytes:
obj = {
"window_start": self.window_start,
"window_end": self.window_end,
"comm_budget_bytes": self.comm_budget_bytes,
"resource_budget_digest": self.resource_budget_digest,
"trigger_policy_id": self.trigger_policy_id,
"nonce": self.nonce,
"signer": self.signer,
"sig": self.sig,
}
return json.dumps(obj, sort_keys=True, separators=(",", ":")).encode("utf-8")
def ticket_hash(self) -> str:
# external pointer: sha256 of canonical payload (no sig)
return hashlib.sha256(self._canonical().encode("utf-8")).hexdigest()
def verify(self, key: bytes) -> bool:
if not self.sig:
return False
expected = self._sign(key)
return hmac.compare_digest(expected, self.sig)
@classmethod
def deserialize(cls, b: bytes) -> "PassWindowTicket":
obj = json.loads(b.decode("utf-8"))
return cls(window_start=int(obj.get("window_start", 0)),
window_end=int(obj.get("window_end", 0)),
comm_budget_bytes=int(obj.get("comm_budget_bytes", 0)),
resource_budget_digest=obj.get("resource_budget_digest", ""),
trigger_policy_id=obj.get("trigger_policy_id", ""),
nonce=obj.get("nonce", ""),
signer=obj.get("signer", ""),
sig=obj.get("sig", ""))