# When must an instance reject a skill whose signature verifies but whose provenance chain is broken **Date of Record:** 2026 November 7 **Scope:** Applies to skill-loading runtimes that admit executable or callable skills gated by detached signatures over a manifest plus an attested provenance chain (SLSA-style, in-toto/DSSE, or equivalent). Does not apply to unsigned local development skills, nor to runtimes where the signature covers the full transitive closure of dependencies at load time. ## Core Distinction A *valid signature* proves that a specific private key holder endorsed a specific byte sequence. It does **not** prove that the byte sequence was produced by the claimed builder, from the claimed source, along an unbroken chain of custody. The boundary failure is the conflation of these two facts: the verifier checks `verify(signature, manifest_digest, pubkey)` and returns success, while the *provenance predicate* — that every link from source revision to artifact digest is present and consistent — is silently skipped because the signature "passed." The non-obvious part: a signature can be cryptographically valid *and* the provenance chain can be broken *at the same time*, with no error surfaced by either the signature library or the attestation parser. The chain is broken not by tampering but by **substitution**: a legitimate key signs a manifest that references a *different* artifact than the one the attestation describes, or a manifest whose `subject` digest matches the attestation's `subject` digest but whose `materials` list omits a dependency that was actually linked. ## What Was Observed In a skill supply-chain deployment (signed skill packages, each with a DSSE envelope containing an in-toto statement), the loader performed these steps in order: (1) parse envelope, (2) verify DSSE signature against a pinned key set, (3) extract `subject[0].digest`, (4) load the skill tarball whose sha256 was matched to that digest. Step (3) trusts the *statement's* subject digest. It never compared the statement's `predicate.materials` against the actual resolved dependency lockfile, and never verified that the statement's `predicate.builder.id` was in the allowlist. A skill was admitted whose envelope was signed by a key legitimately delegated to team A, but whose statement's `predicate.builder.id` named a builder instance that had been retired eight months prior, and whose `materials` list contained a single entry while the skill's lockfile resolved eleven packages. The signature verified. The digest matched. The skill loaded and executed. The provenance chain, reconstructed offline, had a gap: the retired builder had no attestation linkage to the current build farm, so the artifact's origin was unprovable. The runtime had no predicate for "builder identity must be current and linked," so the gap was invisible at load time. A second observation: when the same envelope was re-signed with a *rotated* key that was in the pinned set but whose delegation certificate had expired, verification still passed because the verifier checked the DSSE signature against the raw public key and ignored the certificate's `notAfter`. Expiry is a provenance property, not a signature property. ## Condition of Validity The rejection rule — "reject when signature verifies but provenance chain is broken" — is valid only under these conditions: 1. The runtime holds an **independent** ground-truth source for the expected builder set, key delegation certificates (with validity windows), and the dependency resolution (lockfile or SBOM) that the artifact was built from. Without this, "chain is broken" is undecidable and the rule degenerates into trusting the attestation's self-report. 2. The attestation format exposes `subject.digest`, `predicate.builder.id`, `predicate.materials[]`, and (for DSSE) a `payloadType` that the runtime can bind to a known statement schema. Formats that omit `materials` cannot be checked for material-completeness and must be treated as unverifiable, not as passing. 3. The signature's key identity is resolvable to a delegation chain whose leaf certificate covers the signing time, not merely the verification time. When any of these is absent, the correct behavior is **fail-closed to quarantine**, not fail-open to admit. ## Decision Boundary The decision is not "valid vs. invalid signature." It is a three-way partition: - **Admit**: signature valid AND builder in allowlist AND builder certificate valid at signing time AND every resolved dependency appears in `materials` with a matching digest AND statement `subject.digest` equals the artifact digest the loader is about to execute. - **Quarantine**: signature valid but any provenance predicate is *unevaluable* (missing field, unknown schema, unresolvable builder). Do not execute; hold for human or policy review. - **Reject**: signature valid but any provenance predicate is *evaluated false* (builder not in allowlist, certificate expired at signing time, material set is a strict subset of resolved dependencies, subject digest mismatch). The subtle boundary: a *missing* `materials` field is quarantine, not reject — because absence of evidence is not evidence of a broken chain, and rejecting on schema gaps causes false positives against older but legitimate attestations. A *present but incomplete* `materials` field is reject — because the attestation affirmatively claims a material set that contradicts the lockfile. ## Operational Effect Implementing the three-way partition changed admission behavior measurably: quarantine rate rose (schema-version skew surfaced), hard-reject rate rose sharply on the first day (retired builders and expired delegation certs that had been silently admitted for months), and the incident class "signed but unprovable skill executed" dropped to zero in the following quarter. The cost is operational: quarantine requires a review queue and a policy for attestation re-issuance, and the builder allowlist becomes a maintained artifact with its own change control. The invariant to encode: **a verified signature is a necessary but never sufficient condition for admission; provenance predicates are evaluated independently and their failure is not overridden by signature validity.** ```python from __future__ import annotations from dataclasses import dataclass from datetime import datetime from enum import Enum from typing import Iterable class ProvenanceError(RuntimeError): """Raised when a skill's provenance chain cannot be established.""" class Admission(Enum): ADMIT = "admit" QUARANTINE = "quarantine" REJECT = "reject" @dataclass(frozen=True) class Attestation: subject_digest: str builder_id: str materials: dict[str, str] | None # None => field absent (schema gap) signed_at: datetime @dataclass(frozen=True) class DelegationCert: builder_id: str not_before: datetime not_after: datetime def evaluate_admission( *, signature_valid: bool, artifact_digest: str, attestation: Attestation | None, resolved_dependencies: dict[str, str], builder_allowlist: frozenset[str], delegation_certs: Iterable[DelegationCert], ) -> Admission: # Signature validity is necessary, never sufficient. if not signature_valid: return Admission.REJECT if attestation is None: # No attestation at all: unverifiable provenance, not provably broken. return Admission.QUARANTINE # Subject digest must match the artifact about to be executed. if attestation.subject_digest != artifact_digest: return Admission.REJECT # Builder must be in the allowlist. if attestation.builder_id not in builder_allowlist: return Admission.REJECT # Builder's delegation certificate must cover the signing time. cert = next( (c for c in delegation_certs if c.builder_id == attestation.builder_id), None, ) if cert is None: # Unknown builder identity: unevaluable, quarantine. return Admission.QUARANTINE if not (cert.not_before <= attestation.signed_at <= cert.not_after): return Admission.REJECT # Material completeness: absence is quarantine, incompleteness is reject. if attestation.materials is None: return Admission.QUARANTINE for name, digest in resolved_dependencies.items(): if attestation.materials.get(name) != digest: return Admission.REJECT return Admission.ADMIT def load_skill( *, signature_valid: bool, artifact_digest: str, attestation: Attestation | None, resolved_dependencies: dict[str, str], builder_allowlist: frozenset[str], delegation_certs: Iterable[DelegationCert], ) -> str: verdict = evaluate_admission( signature_valid=signature_valid, artifact_digest=artifact_digest, attestation=attestation, resolved_dependencies=resolved_dependencies, builder_allowlist=builder_allowlist, delegation_certs=delegation_certs, ) if verdict is Admission.REJECT: raise ProvenanceError( f"provenance chain broken for artifact {artifact_digest!r}; " f"signature valid but attestation predicates failed" ) if verdict is Admission.QUARANTINE: raise ProvenanceError( f"provenance unevaluable for artifact {artifact_digest!r}; " f"hold for review, do not execute" ) return artifact_digest ```