from __future__ import annotations import time from dataclasses import dataclass from typing import Any from .authority import verify_permit_signature, verify_workload_proof from .models import CandidateAct, SignedPermit from .replay_store import ReplayDetected, SQLiteFinalityStore from .vaults import CryptoPolicyVault class FinalityDenied(Exception): def __init__(self, reason: str): super().__init__(reason) self.reason = reason @dataclass class FinalitySink: sink_id: str authority_public_key_b64: str crypto_policy: CryptoPolicyVault store: SQLiteFinalityStore def release( self, act: CandidateAct, permit: SignedPermit, workload_proof_b64: str, effect_payload: dict[str, Any], now: int | None = None, ) -> dict[str, Any]: current_time = int(time.time() if now is None else now) p = permit.payload if not verify_permit_signature(permit, self.authority_public_key_b64): raise FinalityDenied("permit_signature_invalid") if not verify_workload_proof(permit, workload_proof_b64): raise FinalityDenied("workload_proof_invalid") if str(p.get("workload_id")) != act.workload_id: raise FinalityDenied("workload_id_mismatch") if not self.crypto_policy.workload_allowed(act.workload_id): raise FinalityDenied("workload_revoked") if str(p.get("act_digest")) != act.digest(): raise FinalityDenied("candidate_act_changed") if str(p.get("purpose")) != act.purpose: raise FinalityDenied("purpose_mismatch") if tuple(sorted(p.get("permitted_attributes", []))) != tuple(sorted(set(act.requested_attributes))): raise FinalityDenied("attribute_scope_mismatch") if str(p.get("recipient")) != act.recipient: raise FinalityDenied("recipient_mismatch") if str(p.get("destination")) != act.destination: raise FinalityDenied("destination_mismatch") if int(p.get("policy_epoch")) != self.crypto_policy.policy_epoch: raise FinalityDenied("stale_policy_epoch") if int(p.get("policy_epoch")) != act.policy_epoch: raise FinalityDenied("act_policy_epoch_mismatch") if str(p.get("nonce")) != act.nonce: raise FinalityDenied("nonce_mismatch") if str(p.get("finality_sink")) != self.sink_id or act.finality_sink != self.sink_id: raise FinalityDenied("wrong_finality_sink") if current_time > int(p.get("expires_at")): raise FinalityDenied("permit_expired") allowed = set(str(x) for x in p.get("permitted_attributes", [])) leaked = set(effect_payload.keys()) - allowed if leaked: raise FinalityDenied("effect_contains_unpermitted_attributes") try: self.store.consume_and_record(str(p["jti"]), act.digest(), effect_payload) except ReplayDetected as exc: raise FinalityDenied("replay_detected") from exc return dict(effect_payload)