Skip to content

Key-pinned entries need a lock-driven re-anchor path (skills and plugins) #6640

Description

@samuv

Context

#6442 shipped cosign key-pair verification for skills (#6447, #6478) and plugins (#6524, #6525, #6528) with a deliberately narrow v1 scope, recorded in docs/arch/12-skills-system.md ("Scope for v1") and mirrored in 14-plugins-system.md:

  • --public-key is accepted on install only.
  • upgrade and sync use the anchor the lock already records and take no key of their own.
  • sync --adopt refuses a key-signed install outright.
  • There is no in-place re-anchor: key → different key and keyless → key both refuse with "uninstall and reinstall with --public-key".

Every one of those is an honest fail-closed state, and v1 only made the messages truthful. This issue tracks the follow-up that was explicitly deferred (option A in the design comment on #6442).

Problem

A publisher rotating their cosign key, or moving from keyless to a key, leaves every consuming project with N key-pinned entries and no lock-driven way to move them: each one is a thv skill uninstall … --scope project followed by thv skill install … --scope project --public-key <new> (same for thv ai-plugin). upgrade --allow-signer-change cannot help by design — its contract is "verify chain of trust only and re-record what is observed", and a key-pair bundle carries nothing to observe, so honouring a key under it would re-anchor on the caller's say-so alone.

Adoption has the same shape: sync --adopt of an existing key-signed project install has no way to supply the anchor, so it refuses and points at a fresh install.

Proposed scope

Same contract on both surfaces, same PR sequencing as #6442 (skills first, plugins mirror, or both together if the diff stays reviewable):

  1. --public-key on upgrade. Accepted only together with --allow-signer-change; the pair means "I am re-anchoring this entry to this key". The candidate is verified against the supplied key (VerifyOCIWithKey), and only on success is Provenance.publicKey rewritten. Without the key, --allow-signer-change keeps today's semantics (key → keyless is the only move it can make on its own). A supplied key without --allow-signer-change is a 400, as resolveKeyAnchor already enforces on install.
  2. --public-key on sync --adopt. The stored bundle is verified against the supplied key with VerifyBundleOfflineWithKey against the installed digest, and adoption records publicKey instead of refusing.
  3. resolveKeyAnchor gains the re-anchor arm so a locked key and a different supplied key, under AllowSignerChange, verify against the supplied one and record it — replacing today's keyAnchorConflict.
  4. Per-entry narrowing already exists (resolveSignerPolicy, Fail closed on keyed skill upgrade verdicts #6628/Reject unsigned upgrade candidates in both modes #6629): a project-wide --allow-signer-change --public-key must only re-anchor the entries whose candidate actually verifies against the new key, and must never unpin an entry whose candidate still verifies against its old key.
  5. Docs: update the "Scope for v1" paragraphs in both arch docs.

Invariants to keep

  • --allow-signer-change alone never records a key. The lock is the only source of a key on upgrade/sync unless the caller names one explicitly.
  • --allow-unsigned never substitutes for a key in either direction (ErrKeySigned is distinct from ErrUnsigned for exactly this reason).
  • Dispatch stays lock-first; the artifact never selects its own verification policy.
  • An operational verifier error is never read as evidence about the key (judgeKeyedCandidate's undecided verdict fails in both modes).

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestneeds-triageIssue needs initial triage by a maintainersecurityskillsSkills lifecycle management

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions