# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-a-revoked-but-cached-mcp-tool-manifest-during-capability-attestation.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-a-revoked-but-cached-mcp-tool-manifest-during-capability-attestation.warrant.json
# Markdown SHA-256:   ea2427a60fb5991efedb5ab0bfc82975d546dfe51639cf01827ad888c07b1635
# Code AST SHA-256:   74f17121a34de33dee0b21efe284b9213c8090778a3dd60218b6162043e1f446
# Verified Classes:   AttestationError, RevocationList, CachedManifest, ManifestAttestor
# Defensive Guards:   8 fail-closed invariants (zero external deps)

import time
from dataclasses import dataclass


class AttestationError(RuntimeError):
    """Raised when a cached manifest cannot be soundly attested."""
    pass


@dataclass(frozen=True)
class RevocationList:
    issued_at: float          # unix seconds
    revoked_key_ids: frozenset
    signature_valid: bool


@dataclass(frozen=True)
class CachedManifest:
    key_id: str
    not_before: float
    not_after: float
    signature_valid: bool


class ManifestAttestor:
    def __init__(self, revocation_max_age: float, clock_skew_tolerance: float = 30.0):
        self.revocation_max_age = revocation_max_age
        self.clock_skew_tolerance = clock_skew_tolerance
        self._last_accepted_revocation_issued_at = float("-inf")

    def attest(self, manifest: CachedManifest, revocation: RevocationList | None,
               now: float | None = None) -> None:
        now = time.time() if now is None else now

        # Gate 1: manifest authenticity and temporal validity.
        if not manifest.signature_valid:
            raise AttestationError(f"manifest signature invalid for key {manifest.key_id}")
        if now + self.clock_skew_tolerance < manifest.not_before:
            raise AttestationError(f"manifest for {manifest.key_id} not yet valid")
        if now - self.clock_skew_tolerance > manifest.not_after:
            raise AttestationError(f"manifest for {manifest.key_id} expired")

        # Gate 2: revocation freshness and non-revocation. Fail closed on absence.
        if revocation is None:
            raise AttestationError(
                f"no revocation list available; refusing to attest {manifest.key_id}"
            )
        if not revocation.signature_valid:
            raise AttestationError("revocation list signature invalid; refusing to attest")
        age = now - revocation.issued_at
        if age > self.revocation_max_age:
            raise AttestationError(
                f"revocation list stale ({age:.0f}s > {self.revocation_max_age:.0f}s); "
                f"refusing to attest {manifest.key_id}"
            )
        if revocation.issued_at < self._last_accepted_revocation_issued_at:
            raise AttestationError(
                "revocation list rollback detected; refusing to attest"
            )
        if manifest.key_id in revocation.revoked_key_ids:
            raise AttestationError(
                f"signing key {manifest.key_id} is revoked; cached manifest is not a valid attestation"
            )

        # Both gates passed; only now is the cached manifest sound to use.
        self._last_accepted_revocation_issued_at = revocation.issued_at
