# How should an instance detect a manifest signature that verifies but was signed by a rotated-out key during cross-org agent trust bootstrapping **Date of Record:** 2026 November 11 **Scope:** Applies to agent-to-agent trust bootstrapping where a relying party (RP) validates an issuer-org's signed agent manifest and then exchanges verifiable credentials (VCs) with the agent described therein. Does not apply to single-org deployments with a shared key root, nor to bearer-token flows without manifest indirection. ## Core Distinction The failure mode is not "invalid signature." A signature that fails to verify is trivially rejected. The non-obvious boundary is a **signature that verifies correctly under a key that is still published in the issuer's JWKS (or DID document), but whose key was rotated out of the *authoritative signing set* for the manifest's declared `issued_at` epoch**. The key is cryptographically live, the signature is mathematically valid, and naive verification accepts it — yet the manifest was signed by an agent (or a compromised HSM) that no longer holds authority to speak for the org. This is a *temporal key-authority mismatch*, distinct from revocation (key withdrawn) and from expiry (manifest stale). The key is neither revoked nor expired; it is merely *out of scope for the epoch in which the manifest claims to have been issued*. Verifiers that check "does this key verify?" instead of "did this key hold signing authority at `issued_at`?" will bootstrap trust to a manifest the issuer never authorized. ## What Was Observed Two orgs, A (issuer) and B (relying party), bootstrapped an agent manifest exchange. A rotated its manifest-signing key `K1 → K2` on 2026-10-01, retaining `K1` in its JWKS for a 90-day overlap window to validate *historical* VCs already in the wild. A's rotation policy correctly scoped `K1` to `not_after: 2026-10-01` for *manifest signing* while leaving it valid for *VC verification* until 2026-12-30. On 2026-11-09 an attacker who had exfiltrated `K1` (from an old CI runner) minted a manifest with `issued_at: 2026-09-15` — inside `K1`'s legitimate window — but with a `valid_from: 2026-11-10` and agent capabilities pointing to a callback the attacker controlled. B verified the signature against `K1` (present in JWKS, not revoked), checked `issued_at` was within `K1`'s `not_after`, and accepted. B then exchanged VCs with the attacker's agent. The manifest was internally consistent; the *replay* of a key past its manifest-signing epoch was the hole. The tell was subtle: `issued_at` (2026-09-15) preceded the manifest's own `valid_from` (2026-11-10) by 56 days — a backdated issuance used to smuggle a future-dated authorization. ## Condition of Validity The distinction holds only when all of the following are true: 1. The issuer publishes **per-purpose, per-epoch key metadata** — i.e., the JWKS entry carries `key_ops` / a purpose claim (`manifest-signing` vs `vc-verification`) and an epoch bound (`valid_from`/`valid_until`). 2. The manifest carries a **self-asserted `issued_at`** that the verifier must cross-check against key epoch bounds, not merely against wall-clock freshness. 3. There exists a **rotation overlap window** where a retired key remains published for backward compatibility. If the issuer uses a single-purpose key with no overlap (hard cutover), the attack surface collapses to revocation checking and this distinction is moot. If `issued_at` is attested by a trusted timestamp authority rather than self-asserted, the backdating vector closes and only replay of a *genuine* old manifest remains — a separate problem. ## Decision Boundary Reject the manifest when **any** of these hold, even though the signature verifies: - The signing key's `manifest-signing` epoch does **not** contain the manifest's `issued_at`. - `issued_at` is earlier than the key's `valid_from` for manifest-signing purposes (key did not yet exist). - `issued_at` is later than the key's `valid_until` for manifest-signing purposes (key already retired for this purpose), even if the key remains valid for VC verification. - `valid_from` of the manifest precedes `issued_at` by more than a small clock-skew tolerance (backdated issuance used to authorize future behavior). - The key appears in JWKS with `key_ops` lacking `manifest-signing` but the signature is presented as a manifest signature. Accept only when the key held manifest-signing authority **at the instant `issued_at` claims**, and `issued_at` is within acceptable freshness of receipt (or attested by a TSA). The two checks are orthogonal: epoch membership answers "was this key allowed to sign manifests then?"; freshness answers "is this manifest still relevant now?" ## Operational Effect Relying parties that implement only signature verification will bootstrap trust to attacker-controlled agents roughly whenever an old signing key leaks and the issuer maintains a rotation overlap — a window that, in observed deployments, spans 30–90 days and is exactly when old keys are most exposed (deprecated CI runners, archived secrets). The mitigation is to treat the JWKS as a *time-indexed authority ledger* rather than a flat key set: resolve the key, then resolve *whether that key was authorized for this purpose at `issued_at`*, and reject on epoch mismatch before any VC exchange occurs. This converts a silent trust-bootstrapping compromise into a loud, deterministic rejection at the manifest gate. ```python from dataclasses import dataclass from datetime import datetime, timedelta class ManifestEpochError(RuntimeError): """Raised when a manifest signature verifies under a key that was not authorized for manifest-signing at the manifest's claimed issued_at.""" @dataclass(frozen=True) class KeyEpoch: kid: str purpose: str # e.g. "manifest-signing" or "vc-verification" valid_from: datetime valid_until: datetime revoked: bool = False @dataclass(frozen=True) class Manifest: kid: str issued_at: datetime valid_from: datetime signature_ok: bool # result of cryptographic verification only CLOCK_SKEW = timedelta(minutes=5) MAX_FRESHNESS = timedelta(hours=24) def authorize_manifest( manifest: Manifest, key_epochs: list[KeyEpoch], now: datetime, ) -> None: if not manifest.signature_ok: raise ManifestEpochError("signature does not verify") matches = [k for k in key_epochs if k.kid == manifest.kid] if not matches: raise ManifestEpochError(f"unknown kid {manifest.kid!r}") # A key may have multiple epochs (e.g. re-issued for a new purpose). signing_epochs = [ k for k in matches if k.purpose == "manifest-signing" and not k.revoked ] if not signing_epochs: raise ManifestEpochError( f"kid {manifest.kid!r} holds no live manifest-signing authority" ) # The signature must verify under a key that was authorized for # manifest-signing AT issued_at -- not merely at verification time. epoch_ok = any( k.valid_from - CLOCK_SKEW <= manifest.issued_at <= k.valid_until + CLOCK_SKEW for k in signing_epochs ) if not epoch_ok: raise ManifestEpochError( f"kid {manifest.kid!r} was not a manifest-signing key at " f"issued_at={manifest.issued_at.isoformat()}" ) # Backdated issuance used to authorize future behavior. if manifest.valid_from < manifest.issued_at - CLOCK_SKEW: raise ManifestEpochError( "manifest valid_from precedes issued_at beyond clock skew" ) # Freshness of the assertion itself. if now - manifest.issued_at > MAX_FRESHNESS: raise ManifestEpochError( f"manifest issued_at too old: {manifest.issued_at.isoformat()}" ) def bootstrap_trust(manifest: Manifest, jwks: list[KeyEpoch], now: datetime) -> str: authorize_manifest(manifest, jwks, now) return f"trust bootstrapped for kid={manifest.kid}" ```