Verify key-signed plugin installs against a public key - #6524
Merged
Conversation
samuv
requested review from
ChrisJBurns,
JAORMX,
amirejaz,
aponcedeleonch,
jhrozek,
rdimitrov and
reyortiz3
as code owners
September 7, 2026 14:13
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6524 +/- ##
==========================================
+ Coverage 78.57% 78.61% +0.03%
==========================================
Files 776 776
Lines 76516 76626 +110
==========================================
+ Hits 60122 60238 +116
+ Misses 16389 16383 -6
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
12 tasks
Plugins had no key surface at all: a cosign key-pair-signed artifact could only fail install-time verification, and `--allow-unsigned` could not rescue it because the artifact is genuinely signed. Skills gained the install-side key path in #6447; this transliterates it onto plugins, so a key-signed plugin can be installed project-scoped by supplying the matching public key, which is then pinned in the lock entry and reused. The key is the only trust anchor a key-pair bundle can have — no certificate, no transparency log — so it has to come from outside the artifact, and dispatch is lock-first: a key-pinned entry verifies against the key the lock records, and every disagreement with a supplied key is refused rather than resolved by precedence. Scope is install-only, matching skills v1: no `--public-key` on upgrade or sync, and no in-place re-anchor. Part of #6442 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Samuele Verzi <samu@stacklok.com>
samuv
force-pushed
the
samuv/plugins-install-public-key
branch
from
September 8, 2026 08:21
e9c23dd to
9ceccbb
Compare
JAORMX
approved these changes
Sep 8, 2026
JAORMX
left a comment
Collaborator
There was a problem hiding this comment.
Reviewed 9ceccbb against 4f26111. The install-time public-key trust-anchor path, lock-first conflict handling, and API/CLI propagation are sound. The key-aware sync/upgrade work and trust-model documentation are explicitly and safely deferred to stacked #6525 and #6528; #6524 alone does not re-enable key signing. No blocking findings.
10 tasks
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A cosign key-pair-signed plugin artifact cannot be installed project-scoped today, and the failure has no escape hatch:
retrieveBundlesfinds the signature material soErrUnsignednever fires, keyless verification then fails for want of a Fulcio chain, and--allow-unsignedrefuses to help because the artifact genuinely is signed. Skills fixed this on the install side in #6447; plugins have had no key surface at all — no install option, no API field, no verify branch. Per the parity decision on #6442, this transliterates the skills install-side key path ontopkg/plugins.plugins.InstallOptions.PublicKey,installPluginRequest.public_key, the Go client'sinstallRequest.public_key, andthv ai-plugin install --public-key(reusing skills'readInstallPublicKey, which reads the PEM and sends key material — a path would not resolve on a server in another process).resolveKeyAnchorpicks the key path from what the lock records, so a supplied key can confirm a pin but never replace it; a key against a keyless-pinned entry, against anunsigned: trueentry, against a different pinned key, or combined withallow_signer_changeis refused rather than silently ignored. Silently preferring one of two conflicting anchors is how a mistyped--public-keyinstalls as though it had been honored.validateInstallPublicKeyrejects a key on a non-project-scope install and a malformed key as bad input (400), before anything is fetched.classifyKeyVerifyErrornames the keyless-artifact case (drop the key) separately from "wrong key or damaged signature", which are genuinely indistinguishable because the bundle records no key of its own. TheErrKeySignedarm no longer says "re-publish it with keyless signing" — with nothing pinned it names--public-key; with a keyless identity pinned it explains that a key cannot satisfy that pin and the entry must be removed and reinstalled.allow_unsignedis stated as no remedy throughout: the artifact is signed, and recording it as an unsigned exception would file a false trust decision in the lock.provenanceInfoFromLockcarriesPublicKey, and boththv ai-plugin installandthv ai-plugin infogained the key-pinned case ahead of the identity cases; today either would printsigned by "".lockedAnchorDescriptiondoes the same for the local-build refusal message.Scope is install-only, identical to skills v1: no
--public-keyonupgradeorsync, and no in-place re-anchor (uninstall, then reinstall with the new key).Part of #6442
Type of change
Test plan
task test)task test-e2e)task lint-fix)Unit tests mirror what skills shipped (there is no E2E for the key path on either surface). Invariants pinned explicitly:
--allow-unsigneddoes not rescueErrKeySigned; a key on a user-scope install and a malformed key are 400 before any fetch; a supplied key conflicting with a keyless pin, an unsigned exception, or a different pinned key is 403 and reaches no verifier (the mock carries no expectations, so a silently-ignored conflict shows up as an unexpected keyless call); key +allow_signer_changeis 400; first-use installs pinProvenance.PublicKeyand capture the bundle; a lock-pinned key is used with no flag supplied;ErrKeylessSignedunder a key names "install without a public key"; git and local installs refuse a key; andpublic_keyround-trips through the CLI flag, client DTO, and API handler.API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Changes
pkg/plugins/options.goInstallOptions.PublicKeypkg/api/v1/plugins_types.go,plugins.gopublic_keyon the install request, forwarded toInstallOptionspkg/plugins/client/dto.go,client.gopublic_keyso the CLI flag reaches the serverpkg/plugins/pluginsvc/verify.govalidateInstallPublicKey,resolveKeyAnchor,verifyOCIInstallWithKey,classifyKeyVerifyError,keySignedInstallError,lockedAnchorDescription; git/local key refusalspkg/plugins/pluginsvc/install.gocmd/thv/app/ai_plugin_install.go--public-key; key-pinned install outputcmd/thv/app/ai_plugin_info.goprintAIPluginTrustState(the added cases pushed the renderer over the gocyclo limit)docs/cli,docs/servertask docsoutputDoes this introduce a user-facing change?
Yes.
thv ai-plugin install --public-key <cosign.pub>installs a plugin signed with a cosign key pair into a project, pinning the key in the lock entry so later installs need no flag. Refusals that previously read as opaque signature failures now name the input that fixes them. Nothing changes for keyless or unsigned installs.Special notes for reviewers
verifyStoredSignature(offline re-verify viaVerifyBundleOfflineWithKey),adoptionTrustrefusingErrKeySignedwith an actionable message, and the keyed signer-change transitions on upgrade. PR 3 restores--keyonthv ai-plugin pushand is the one that closes--keypush signing has no install-time verification path (plugins) #6442 — restoring the publish flag any earlier would recreate exactly the publish-only dead end the issue was opened about.thv ai-plugin sync.verifyStoredSignaturestill handsentry.Provenanceto the keylessVerifyBundleOffline, which cannot check a key-pair bundle. That is expected, is the sync half of the same feature, and is what PR 2 fixes. A lock-driven reinstall is already correct: it goes throughverifyOCIInstall, so it verifies against the pinned key.resolveKeyAnchor, unlike skillsvc: a plugin install presents no catalog-declared provenance expectation, so first use has nothing but the supplied key to weigh.docs/arch/14-plugins-system.mdstill documents plugin signing as keyless-only. The trust-model rewrite belongs with PR 3, so the doc flips when the feature is complete rather than describing a half-built path.TestAIPluginPushSigningFlagsstill asserts--keyis absent from push; only its comment moved, to point at PR 3 rather than at "the change that makes key verification work".Generated with Claude Code