Skip to content

--key push signing has no install-time verification path (plugins) #6442

Description

@samuv

Status update 2026-09-03. The skills half of this issue is resolved; the plugins half is not, and its shape changed. Original diagnosis preserved below, since it still explains why the failure cannot be overridden.

What remains is one decision, not the original bug: implement the skills key path for plugins, or ratify keyless-only for plugins permanently. #6300 recorded keyless-only as the agreed interim contract when it closed.

Remaining scope (plugins)

Plugins now have no key surface at all — no --key at push, no PublicKey on install options, no key branch in verify or sync:

Concern Skills (done) Plugins (absent)
--key push flag cmd/thv/app/skill_push.go:35 none — cmd/thv/app/ai_plugin_push.go:28 documents why
Public key on install skills.InstallOptions.PublicKey (pkg/skills/options.go:51) none
Pinned key in lock lockfile.Provenance.PublicKey (pkg/skills/lockfile/lockfile.go:140) shared schema, unused by plugins
Install verify pkg/skills/skillsvc/verify.go:264 keyless only (pkg/plugins/pluginsvc/verify.go)
Upgrade verify pkg/skills/skillsvc/upgrade.go:283 keyless only
Offline sync re-verify pkg/skills/skillsvc/sync.go:307 keyless only (pkg/plugins/pluginsvc/sync.go)

The lock schema already carries publicKey, so the plugins work is install options, the install API DTO, the verify/upgrade key branches, the sync re-verify branch, and finally restoring the push flag. Restoring --key first would recreate exactly the dead end this issue was opened about, so it has to come last.

Original diagnosis (still accurate for plugins)

thv skill push --key and thv ai-plugin push --key both accepted a cosign private key and produced a validly key-signed artifact — but install-time verification was keyless-only, so neither could be installed project-scoped. --key was a publish-only dead end on both surfaces.

It is worse than "installs as unsigned". Tracing the failure:

  1. retrieveBundles does find the key-signed bundle — it is a real signature referrer — so ErrUnsigned never fires. That sentinel means no signature material at all (pkg/skills/verifier/errors.go).
  2. Keyless verification then fails, because a key-pair bundle carries no Fulcio certificate chain to check against the keyless trusted material.
  3. Classification finds no signer mismatch and no expected identity on first use, so it falls through to ErrSignatureInvalid.
  4. isAllowedUnsigned requires errors.Is(err, verifier.ErrUnsigned) — false here — so --allow-unsigned does not override it.

Net effect: a hard 403 with no escape hatch. This is why the resolution could not be "let it install as unsigned", and why removing the plugin flag was preferred to leaving it in place with a warning.

Reproduce

Skills — now works, and is the shape plugins should acquire:

cosign generate-key-pair
thv skill push --key cosign.key ghcr.io/<org>/<skill>:v1
thv skill install ghcr.io/<org>/<skill>:v1 --project-root . --public-key <base64 DER SPKI>

Plugins — not reproducible: thv ai-plugin push has no --key to pass.

Expected behavior

Originally either of:

  • Support it. A key-signed artifact can be installed project-scoped by supplying the matching public key, the pinned key is recorded in the lock entry, and offline sync --check re-verifies against it.
  • Or reject it at push time, so users cannot publish an artifact ToolHive itself refuses to consume.

Skills took the first branch. Plugins took the second, by not offering the flag. Both are acceptable outcomes of this issue; what remains is whether plugins should move to the first for parity.

Additional context

Introduced with keyless-by-default push signing for skills (#6383, #6385, #6390). #6438 mirrored plugin push from the skills surface but deliberately diverged on this one point, omitting Key rather than reproducing the dead end.

The interim mitigation is now a permanent-until-decided contract: cmd/thv/app/ai_plugin_push.go and the plugins trust-model doc both state that plugin signing is keyless-only and why. The skills --key help text has since been updated to name the --public-key step, which the original version of this issue flagged as missing.

Discovered during review of #6438 (thanks @JAORMX).

Related: #6300 (plugin Sigstore stack, closed 2026-09-03), #6307 (keyless push signing), #5740 (RFC THV-0080).

Activity

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingsecurityskillsSkills lifecycle management

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions