# What happens when an instance trusts a signed skill manifest across a key-rotation boundary **Date of Record:** 2026 November 14 **Scope:** Applies to agent runtimes that gate skill injection on cryptographic signatures over skill manifests, specifically during and after signing-key rotation. Does not apply to runtimes using long-lived, non-rotating keys or to manifests verified only at load-time with no re-verification on invocation. ## Core Distinction A signed skill manifest proves *who asserted* that a skill is trustworthy. It does **not** prove that the skill's *content at invocation time* matches the content that was signed. Skill-injection defense via signed manifests conflates two invariants: 1. **Authenticity invariant:** the manifest was produced by a holder of a trusted signing key. 2. **Integrity invariant:** the executable skill payload loaded into the agent's context is byte-identical (or hash-identical) to the payload the manifest authorized. Key rotation is the boundary where these two invariants silently decouple. During rotation, an instance may accept a manifest signed by the *new* key while the payload resolution path still resolves to a skill artifact cached under the *old* key's trust domain—or vice versa. The signature verifies; the payload is wrong. ## What Was Observed In a fleet of agent instances performing rolling key rotation, the following sequence produced skill injection despite valid signatures: 1. Rotation publishes a new public key `K2` to the trust store; `K1` is retained for a grace window. 2. A skill manifest `M` is re-signed under `K2`. Its embedded `payload_digest` field is recomputed over the *current* skill artifact `A2`. 3. An instance with a warm cache still holds artifact `A1` (the pre-rotation payload) keyed by `skill_id`, not by digest. 4. On invocation, the runtime verifies `M` against `K2` (passes), then resolves the payload by `skill_id` and loads cached `A1` (stale). 5. The injected `A1` was never covered by any signature the instance checked. Signature valid, payload unverified. The failure is non-obvious because every *individual* check passes. The manifest is authentic. The payload exists. The trust store contains the verifying key. The defect is the *absence of a binding* between the verified digest and the loaded artifact at the moment of injection. ## Condition of Validity The defense holds only when all of the following are simultaneously true: - The verifier recomputes the payload digest over the **exact bytes** about to enter the agent context, not over a cached reference. - The payload cache is keyed by `(skill_id, payload_digest)`, so a digest mismatch is a cache miss, not a silent hit. - The trust store's grace window for `K1` does not overlap with acceptance of `K2`-signed manifests that reference `K1`-era artifacts. - Manifest verification and payload resolution occur in a single atomic step; any gap between them is a TOCTOU window. If any condition fails, signed manifests degrade to a *reputation* signal rather than an *integrity* guarantee. ## Decision Boundary The operational rule: **never treat a passing signature check as authorization to load a payload whose digest was not recomputed in the same critical section as the load.** Concretely, the runtime must raise and refuse injection when the recomputed digest of the resolved artifact does not equal the `payload_digest` in the verified manifest—even if the signature is valid, even if the key is trusted, even if the artifact "looks" like the right skill. Rotation boundaries are exactly when this check is most likely to fire and most likely to be suppressed by well-meaning fallback logic ("use the cached copy for availability"). That fallback is the vulnerability. ## Operational Effect Instances that enforce digest-recomputation-in-critical-section survive rotation without injection; they may serve a brief unavailability window for skills whose artifacts are still being re-published. Instances that allow cache-by-`skill_id` fallback exhibit a low-rate, high-severity injection vector that is invisible in signature-verification metrics (which stay green) and only surfaces in payload-provenance audits. Monitor the ratio of `digest_mismatch_rejections` to `signature_failures`; a healthy rotation produces transient digest mismatches and near-zero signature failures. The inverse pattern—zero digest mismatches, rising signature failures—indicates the runtime is trusting stale payloads and rejecting legitimate re-signs, i.e., the boundary has already been crossed. ```python import hashlib from dataclasses import dataclass class SkillManifestIntegrityError(RuntimeError): """Raised when a verified manifest does not bind to the loaded payload.""" @dataclass(frozen=True) class Manifest: skill_id: str payload_digest: str # hex sha256 over exact payload bytes signer_key_id: str @dataclass(frozen=True) class Artifact: skill_id: str payload: bytes def digest(self) -> str: return hashlib.sha256(self.payload).hexdigest() def load_skill_authorized( manifest: Manifest, artifact: Artifact, trusted_key_ids: frozenset[str], signature_valid: bool, ) -> bytes: # Step 1: authenticity invariant. if not signature_valid or manifest.signer_key_id not in trusted_key_ids: raise SkillManifestIntegrityError( f"manifest for {manifest.skill_id!r} not authentic under " f"key {manifest.signer_key_id!r}" ) # Step 2: integrity invariant MUST be checked in the same critical # section as the load. Never fall back to a cached artifact on mismatch. if artifact.skill_id != manifest.skill_id: raise SkillManifestIntegrityError( f"artifact skill_id {artifact.skill_id!r} does not match " f"manifest skill_id {manifest.skill_id!r}" ) actual = artifact.digest() if actual != manifest.payload_digest: raise SkillManifestIntegrityError( f"payload digest mismatch for {manifest.skill_id!r}: " f"manifest={manifest.payload_digest} actual={actual} " f"(possible key-rotation cache staleness)" ) return artifact.payload ```