From 05fa37973dc9e42b76e1d2883494c87174b6074f Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Fri, 6 Mar 2026 07:40:11 -0800 Subject: [PATCH] fix: Plugin missing provenance bypass Signed-off-by: George Jenkins --- internal/plugin/installer/installer.go | 33 +++++------ .../plugin/installer/verification_test.go | 58 ++++++++++++------- pkg/cmd/plugin_install.go | 6 +- 3 files changed, 55 insertions(+), 42 deletions(-) diff --git a/internal/plugin/installer/installer.go b/internal/plugin/installer/installer.go index e3975c2d7..da661851c 100644 --- a/internal/plugin/installer/installer.go +++ b/internal/plugin/installer/installer.go @@ -98,24 +98,23 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error) // Check if provenance data exists if len(provData) == 0 { - // No .prov file found - emit warning but continue installation - fmt.Fprintf(os.Stderr, "WARNING: No provenance file found for plugin. Plugin is not signed and cannot be verified.\n") - } else { - // Provenance data exists - verify the plugin - verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring) - if err != nil { - return nil, fmt.Errorf("plugin verification failed: %w", err) - } + return nil, fmt.Errorf("plugin verification failed: no provenance file (.prov) found") + } - // Collect verification info - result = &VerificationResult{ - SignedBy: make([]string, 0), - Fingerprint: fmt.Sprintf("%X", verification.SignedBy.PrimaryKey.Fingerprint), - FileHash: verification.FileHash, - } - for name := range verification.SignedBy.Identities { - result.SignedBy = append(result.SignedBy, name) - } + // Provenance data exists - verify the plugin + verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring) + if err != nil { + return nil, fmt.Errorf("plugin verification failed: %w", err) + } + + // Collect verification info + result = &VerificationResult{ + SignedBy: make([]string, 0), + Fingerprint: fmt.Sprintf("%X", verification.SignedBy.PrimaryKey.Fingerprint), + FileHash: verification.FileHash, + } + for name := range verification.SignedBy.Identities { + result.SignedBy = append(result.SignedBy, name) } } diff --git a/internal/plugin/installer/verification_test.go b/internal/plugin/installer/verification_test.go index 22f0a8308..21e671142 100644 --- a/internal/plugin/installer/verification_test.go +++ b/internal/plugin/installer/verification_test.go @@ -16,10 +16,8 @@ limitations under the License. package installer import ( - "bytes" "crypto/sha256" "fmt" - "io" "os" "path/filepath" "strings" @@ -44,33 +42,49 @@ func TestInstallWithOptions_VerifyMissingProvenance(t *testing.T) { } defer os.RemoveAll(installer.Path()) - // Capture stderr to check warning message - oldStderr := os.Stderr - r, w, _ := os.Pipe() - os.Stderr = w - - // Install with verification enabled (should warn but succeed) + // Install with verification enabled should fail when .prov is missing result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: "dummy"}) - // Restore stderr and read captured output - w.Close() - os.Stderr = oldStderr - var buf bytes.Buffer - io.Copy(&buf, r) - output := buf.String() - - // Should succeed with nil result (no verification performed) - if err != nil { - t.Fatalf("Expected installation to succeed despite missing .prov file, got error: %v", err) + // Should fail with a missing provenance error + if err == nil { + t.Fatal("Expected installation to fail when .prov file is missing and verification is enabled") + } + if !strings.Contains(err.Error(), "no provenance file") { + t.Errorf("Expected 'no provenance file' in error message, got: %v", err) } if result != nil { t.Errorf("Expected nil verification result when .prov file is missing, got: %+v", result) } - // Should contain warning message - expectedWarning := "WARNING: No provenance file found for plugin" - if !strings.Contains(output, expectedWarning) { - t.Errorf("Expected warning message '%s' in output, got: %s", expectedWarning, output) + // Plugin should NOT be installed + if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) { + t.Errorf("Plugin should not be installed when verification fails due to missing .prov") + } +} + +func TestInstallWithOptions_NoVerifyMissingProvenance(t *testing.T) { + ensure.HelmHome(t) + + // Create a temporary plugin tarball without .prov file + pluginDir := createTestPluginDir(t) + pluginTgz := createTarballFromPluginDir(t, pluginDir) + defer os.Remove(pluginTgz) + + // Create local installer + installer, err := NewLocalInstaller(pluginTgz) + if err != nil { + t.Fatalf("Failed to create installer: %v", err) + } + defer os.RemoveAll(installer.Path()) + + // Install with verification explicitly disabled should succeed without .prov + result, err := InstallWithOptions(installer, Options{Verify: false}) + + if err != nil { + t.Fatalf("Expected installation to succeed with --verify=false, got error: %v", err) + } + if result != nil { + t.Errorf("Expected nil verification result when verification is disabled, got: %+v", result) } // Plugin should be installed diff --git a/pkg/cmd/plugin_install.go b/pkg/cmd/plugin_install.go index efa9b466c..81c538758 100644 --- a/pkg/cmd/plugin_install.go +++ b/pkg/cmd/plugin_install.go @@ -50,11 +50,11 @@ const pluginInstallDesc = ` This command allows you to install a plugin from a url to a VCS repo or a local path. By default, plugin signatures are verified before installation when installing from -tarballs (.tgz or .tar.gz). This requires a corresponding .prov file to be available -alongside the tarball. +tarballs (.tgz or .tar.gz). A corresponding .prov file must be available alongside +the tarball; installation will fail if it is missing or invalid. For local development, plugins installed from local directories are automatically treated as "local dev" and do not require signatures. -Use --verify=false to skip signature verification for remote plugins. +Use --verify=false to explicitly skip signature verification (NOT recommended). ` func newPluginInstallCmd(out io.Writer) *cobra.Command {