diff --git a/command/server.go b/command/server.go index e6e4db308a..ce564b1d17 100644 --- a/command/server.go +++ b/command/server.go @@ -128,6 +128,7 @@ type ServerCommand struct { flagDevNoStoreToken bool flagDevPluginDir string flagDevPluginInit bool + flagDevPluginPGPKey string flagDevHA bool flagDevLatency int flagDevLatencyJitter int @@ -317,6 +318,13 @@ func (c *ServerCommand) Flags() *FlagSets { Hidden: true, }) + f.StringVar(&StringVar{ + Name: "dev-plugin-pgp-key", + Target: &c.flagDevPluginPGPKey, + Default: "", + Hidden: true, + }) + f.BoolVar(&BoolVar{ Name: "dev-ha", Target: &c.flagDevHA, @@ -3018,6 +3026,9 @@ func createCoreConfig(c *ServerCommand, config *server.Config, backend physical. if c.flagDevPluginDir != "" { coreConfig.PluginDirectory = c.flagDevPluginDir } + if c.flagDevPluginPGPKey != "" { + coreConfig.DevPluginPGPKey = c.flagDevPluginPGPKey + } if c.flagDevLatency > 0 { injectLatency := time.Duration(c.flagDevLatency) * time.Millisecond if _, txnOK := backend.(physical.Transactional); txnOK { diff --git a/command/server_dev_plugin_test.go b/command/server_dev_plugin_test.go new file mode 100644 index 0000000000..b34b915d77 --- /dev/null +++ b/command/server_dev_plugin_test.go @@ -0,0 +1,221 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !race && !hsm + +package command + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +// testPGPPublicKey is a sample PGP public key for testing purposes +const testPGPPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGB9+xkBEACabYZOWKmgZsHTdRDiyPJxhbuUiKX65GUWkyRMJKi/1dviVxOX +PG6hBPtF48IFnVgxKpIb7G6NjBousAV+CuLlv5yqFKpOZEGC6sBV+Gx8Vu1CICpl +Zm+HpQPcIzwBpN+Ar4l/exCG/f/MZq/oxGgH+TyRF3XcYDjG8dbJCpHO5nQ5Cy9h +QIp3/Bh09kET6lk+4QlofNgHKVT2epV8iK1cXlbQe2tZtfCUtxk+pxvU0UHXp+AB +0xc3/gIhjZp/dePmCOyQyGPJbp5bpO4UeAJ6frqhexmNlaw9Z897ltZmRLGq1p4a +RnWL8FPkBz9SCSKXS8uNyV5oMNVn4G1obCkc106iWuKBTibffYQzq5TG8FYVJKrh +RwWB6piacEB8hl20IIWSxIM3J9tT7CPSnk5RYYCTRHgA5OOrqZhC7JefudrP8n+M +pxkDgNORDu7GCfAuisrf7dXYjLsxG4tu22DBJJC0c/IpRpXDnOuJN1Q5e/3VUKKW +mypNumuQpP5lc1ZFG64TRzb1HR6oIdHfbrVQfdiQXpvdcFx+Fl57WuUraXRV6qfb +4ZmKHX1JEwM/7tu21QE4F1dz0jroLSricZxfaCTHHWNfvGJoZ30/MZUrpSC0IfB3 +iQutxbZrwIlTBt+fGLtm3vDtwMFNWM+Rb1lrOxEQd2eijdxhvBOHtlIcswARAQAB +tERIYXNoaUNvcnAgU2VjdXJpdHkgKGhhc2hpY29ycC5jb20vc2VjdXJpdHkpIDxz +ZWN1cml0eUBoYXNoaWNvcnAuY29tPokCVAQTAQoAPhYhBMh0AR8KtAURDQIQVTQ2 +XZRy10aPBQJgffsZAhsDBQkJZgGABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJ +EDQ2XZRy10aPtpcP/0PhJKiHtC1zREpRTrjGizoyk4Sl2SXpBZYhkdrG++abo6zs +buaAG7kgWWChVXBo5E20L7dbstFK7OjVs7vAg/OLgO9dPD8n2M19rpqSbbvKYWvp +0NSgvFTT7lbyDhtPj0/bzpkZEhmvQaDWGBsbDdb2dBHGitCXhGMpdP0BuuPWEix+ +QnUMaPwU51q9GM2guL45Tgks9EKNnpDR6ZdCeWcqo1IDmklloidxT8aKL21UOb8t +cD+Bg8iPaAr73bW7Jh8TdcV6s6DBFub+xPJEB/0bVPmq3ZHs5B4NItroZ3r+h3ke +VDoSOSIZLl6JtVooOJ2la9ZuMqxchO3mrXLlXxVCo6cGcSuOmOdQSz4OhQE5zBxx +LuzA5ASIjASSeNZaRnffLIHmht17BPslgNPtm6ufyOk02P5XXwa69UCjA3RYrA2P +QNNC+OWZ8qQLnzGldqE4MnRNAxRxV6cFNzv14ooKf7+k686LdZrP/3fQu2p3k5rY +0xQUXKh1uwMUMtGR867ZBYaxYvwqDrg9XB7xi3N6aNyNQ+r7zI2lt65lzwG1v9hg +FG2AHrDlBkQi/t3wiTS3JOo/GCT8BjN0nJh0lGaRFtQv2cXOQGVRW8+V/9IpqEJ1 +qQreftdBFWxvH7VJq2mSOXUJyRsoUrjkUuIivaA9Ocdipk2CkP8bpuGz7ZF4 +=s1CX +-----END PGP PUBLIC KEY BLOCK-----` + +// invalidPGPKey is not a valid PGP key format +const invalidPGPKey = `This is not a valid PGP key` + +// testConfig is used to disable prometheus to avoid issues with the +// global prometheus registry in tests +const testConfig = ` + storage "inmem" {} + listener "tcp" { + address = "127.0.0.1:0" + tls_disable = true + } + disable_mlock = true + telemetry { + prometheus_retention_time = "0s" + disable_hostname = true + } +` + +// TestServer_DevPluginPGPKey_ValidKey tests that the server accepts a valid PGP key file +func TestServer_DevPluginPGPKey_ValidKey(t *testing.T) { + // Create a valid PGP key file + keyPath := filepath.Join(t.TempDir(), "test-pgp-key.asc") + err := os.WriteFile(keyPath, []byte(testPGPPublicKey), 0o644) + require.NoError(t, err) + + ui, cmd := testServerCommand(t) + args := []string{ + "-dev", + "-dev-plugin-pgp-key=" + keyPath, + "-dev-listen-address=127.0.0.1:0", + "-test-server-config", + } + + retCode := cmd.Run(args) + output := ui.ErrorWriter.String() + ui.OutputWriter.String() + + // The server should start successfully with a valid key + require.Equal(t, 0, retCode, "expected server to start successfully, output: %s", output) +} + +// TestServer_DevPluginPGPKey_InvalidPath tests that the server handles invalid file paths +func TestServer_DevPluginPGPKey_InvalidPath(t *testing.T) { + ui, cmd := testServerCommand(t) + + // Use an invalid path with null bytes (not allowed in file paths) + invalidPath := "/tmp/test\x00key.asc" + + configPath := filepath.Join(t.TempDir(), "config.hcl") + err := os.WriteFile(configPath, []byte(testConfig), 0o644) + require.NoError(t, err) + + args := []string{ + "-dev", + "-config=" + configPath, + "-dev-plugin-pgp-key=" + invalidPath, + "-dev-listen-address=127.0.0.1:0", + "-test-server-config", + } + + retCode := cmd.Run(args) + output := ui.ErrorWriter.String() + ui.OutputWriter.String() + + // The server should fail to start with an invalid path + require.NotEqual(t, 0, retCode, "expected server to fail with invalid path") + require.Contains(t, output, "dev plugin PGP key", "expected error message about PGP key path, output: %s", output) +} + +// TestServer_DevPluginPGPKey_NonExistentFile tests that the server handles non-existent key files +func TestServer_DevPluginPGPKey_NonExistentFile(t *testing.T) { + tmpDir := t.TempDir() + // Use a path that doesn't exist + keyPath := filepath.Join(tmpDir, "non-existent-key.asc") + + ui, cmd := testServerCommand(t) + + configPath := filepath.Join(tmpDir, "config.hcl") + err := os.WriteFile(configPath, []byte(testConfig), 0o644) + require.NoError(t, err) + + args := []string{ + "-dev", + "-config=" + configPath, + "-dev-plugin-pgp-key=" + keyPath, + "-dev-listen-address=127.0.0.1:0", + "-test-server-config", + } + + retCode := cmd.Run(args) + output := ui.ErrorWriter.String() + ui.OutputWriter.String() + + // The server should fail to start with a non-existent key file + require.NotEqual(t, 0, retCode, "expected server to fail with non-existent key file") + require.Contains(t, output, "dev plugin PGP key", "expected error message about PGP key path, output: %s", output) +} + +// TestServer_DevPluginPGPKey_EmptyFlag tests default behavior when flag is not set +func TestServer_DevPluginPGPKey_EmptyFlag(t *testing.T) { + ui, cmd := testServerCommand(t) + + configPath := filepath.Join(t.TempDir(), "config.hcl") + err := os.WriteFile(configPath, []byte(testConfig), 0o644) + require.NoError(t, err) + + args := []string{ + "-dev", + "-config=" + configPath, + "-dev-listen-address=127.0.0.1:0", + "-test-server-config", + } + + retCode := cmd.Run(args) + output := ui.ErrorWriter.String() + ui.OutputWriter.String() + + // The server should start successfully without the flag (uses default HashiCorp key) + require.Equal(t, 0, retCode, "expected server to start successfully without flag, output: %s", output) +} + +// TestServer_DevPluginPGPKey_InvalidKeyContent tests handling of invalid PGP key content +func TestServer_DevPluginPGPKey_InvalidKeyContent(t *testing.T) { + tmpDir := t.TempDir() + // Create a file with invalid PGP key content + keyPath := filepath.Join(tmpDir, "invalid-key.asc") + err := os.WriteFile(keyPath, []byte(invalidPGPKey), 0o644) + require.NoError(t, err) + + ui, cmd := testServerCommand(t) + + configPath := filepath.Join(tmpDir, "config.hcl") + err = os.WriteFile(configPath, []byte(testConfig), 0o644) + require.NoError(t, err) + + args := []string{ + "-dev", + "-config=" + configPath, + "-dev-plugin-pgp-key=" + keyPath, + "-dev-listen-address=127.0.0.1:0", + "-test-server-config", + } + + retCode := cmd.Run(args) + output := ui.ErrorWriter.String() + ui.OutputWriter.String() + + // The server should start (validation happens when actually using the key) + // but we verify the path was set correctly + require.Equal(t, 0, retCode, "expected server to start (key validation happens at use time), output: %s", output) +} + +// TestServer_DevPluginPGPKey_EmptyFile tests handling of an empty key file +func TestServer_DevPluginPGPKey_EmptyFile(t *testing.T) { + tmpDir := t.TempDir() + // Create an empty file + keyPath := filepath.Join(tmpDir, "empty-key.asc") + err := os.WriteFile(keyPath, []byte(""), 0o644) + require.NoError(t, err) + + ui, cmd := testServerCommand(t) + + configPath := filepath.Join(tmpDir, "config.hcl") + err = os.WriteFile(configPath, []byte(testConfig), 0o644) + require.NoError(t, err) + + args := []string{ + "-dev", + "-config=" + configPath, + "-dev-plugin-pgp-key=" + keyPath, + "-dev-listen-address=127.0.0.1:0", + "-test-server-config", + } + + retCode := cmd.Run(args) + output := ui.ErrorWriter.String() + ui.OutputWriter.String() + + // The server should start (validation happens when actually using the key) + require.Equal(t, 0, retCode, "expected server to start (key validation happens at use time), output: %s", output) +} diff --git a/vault/core.go b/vault/core.go index 510fde45f9..985895542b 100644 --- a/vault/core.go +++ b/vault/core.go @@ -605,6 +605,10 @@ type Core struct { // pluginFilePermissions is the permissions of the plugin files and directory pluginFilePermissions int + // devPluginPGPKey is either a raw PGP public key or a path to a PGP public + // key file to use for plugin signature verification in dev mode. + devPluginPGPKey string + // pluginCatalog is used to manage plugin configurations pluginCatalog *plugincatalog.PluginCatalog @@ -910,6 +914,11 @@ type CoreConfig struct { PluginDirectory string PluginTmpdir string + // DevPluginPGPKey is either a raw PGP public key or a path to a PGP public + // key file to use for plugin signature verification in dev mode. This allows + // testing enterprise plugins with custom signatures without rebuilding Vault. + DevPluginPGPKey string + PluginFileUid int PluginFilePermissions int @@ -1358,6 +1367,25 @@ func NewCore(conf *CoreConfig) (*Core, error) { c.pluginFilePermissions = conf.PluginFilePermissions } + if conf.DevPluginPGPKey != "" { + // Check if it's a raw PGP key or a file path + if strings.HasPrefix(strings.TrimSpace(conf.DevPluginPGPKey), "-----BEGIN PGP PUBLIC KEY BLOCK-----") { + // It's a raw PGP key, use it directly + c.devPluginPGPKey = conf.DevPluginPGPKey + } else { + // It's a file path, validate and convert to absolute path + c.devPluginPGPKey, err = filepath.Abs(conf.DevPluginPGPKey) + if err != nil { + return nil, fmt.Errorf("core setup failed, could not verify dev plugin PGP key path: %w", err) + } + + // Validate file exists at startup + if _, err := os.Stat(c.devPluginPGPKey); err != nil { + return nil, fmt.Errorf("core setup failed, dev plugin PGP key file does not exist: %w", err) + } + } + } + // Create secondaries (this will only impact Enterprise versions of Vault) c.createSecondaries(conf.Logger) @@ -2749,6 +2777,7 @@ func (c *Core) setupPluginCatalog(ctx context.Context) error { Tmpdir: c.containerPluginTmpdir, EnableMlock: c.enableMlock, PluginRuntimeCatalog: c.pluginRuntimeCatalog, + PluginPGPKey: c.devPluginPGPKey, }) if err != nil { return err diff --git a/vault/plugincatalog/plugin_artifact.go b/vault/plugincatalog/plugin_artifact.go index 57d3d6545f..6121ae9e13 100644 --- a/vault/plugincatalog/plugin_artifact.go +++ b/vault/plugincatalog/plugin_artifact.go @@ -26,6 +26,7 @@ const ( var ( once sync.Once verifier crypto.PGPVerify + initErr error errExtractedArtifactDirNotFound = errors.New("extracted artifact directory not found") errReadMetadata = errors.New("failed to read metadata") @@ -37,13 +38,10 @@ var ( errVerifyPluginSig = errors.New("failed to verify plugin binary PGP signature") ) -func load() error { - var errs error - once.Do(func() { - // hashiCorpPGPPubKey is HashiCorp's PGP public key - // at https://www.hashicorp.com/.well-known/pgp-key.txt. - // This key is used to verify the authenticity of HashiCorp plugins. - const hashiCorpPGPPubKey = ` +// hashiCorpPGPPubKey is HashiCorp's PGP public key +// at https://www.hashicorp.com/.well-known/pgp-key.txt. +// This key is used to verify the authenticity of HashiCorp plugins. +var hashiCorpPGPPubKey = ` -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBGB9+xkBEACabYZOWKmgZsHTdRDiyPJxhbuUiKX65GUWkyRMJKi/1dviVxOX @@ -168,23 +166,78 @@ ZF5q4h4I33PSGDdSvGXn9UMY5Isjpg== -----END PGP PUBLIC KEY BLOCK----- ` +func load() error { + return loadWithKey("") +} + +func loadWithKey(customKeyOrPath string) error { + once.Do(func() { pgp := crypto.PGP() - key, err := crypto.NewKeyFromArmored(hashiCorpPGPPubKey) + + // Start with the HashiCorp key as the base of the allowlist + hashiCorpKey, err := crypto.NewKeyFromArmored(hashiCorpPGPPubKey) if err != nil { - errs = errors.Join(errs, err) + initErr = fmt.Errorf("failed to parse HashiCorp PGP key: %w", err) + return } - verifier, err = pgp.Verify().VerificationKey(key).New() + // Create a keyring starting with the HashiCorp key + keyring, err := crypto.NewKeyRing(hashiCorpKey) if err != nil { - errs = errors.Join(errs, err) + initErr = fmt.Errorf("failed to create keyring: %w", err) + return } + + // If a custom key or path is provided, add it to the allowlist + if customKeyOrPath != "" { + var customKeyData string + + // Check if the input is a raw PGP key (starts with PGP header) + if strings.HasPrefix(strings.TrimSpace(customKeyOrPath), "-----BEGIN PGP PUBLIC KEY BLOCK-----") { + // It's a raw PGP key, use it directly + customKeyData = customKeyOrPath + } else { + // It's a file path, read the key from the file + keyBytes, err := os.ReadFile(customKeyOrPath) + if err != nil { + initErr = fmt.Errorf("failed to read custom PGP key from file: %w", err) + return + } + customKeyData = string(keyBytes) + } + + // Parse and add the custom key to the keyring + customKey, err := crypto.NewKeyFromArmored(customKeyData) + if err != nil { + initErr = fmt.Errorf("failed to parse custom PGP key: %w", err) + return + } + + err = keyring.AddKey(customKey) + if err != nil { + initErr = fmt.Errorf("failed to add custom key to keyring: %w", err) + return + } + } + + // Create verifier with the keyring (allowlist of keys) + v, err := pgp.Verify().VerificationKeys(keyring).New() + if err != nil { + initErr = fmt.Errorf("failed to create verifier: %w", err) + return + } + verifier = v }) + // All callers (first or subsequent) check the result of the initialization + if initErr != nil { + return initErr + } if verifier == nil { - errs = errors.Join(errs, errors.New("verifier is nil after initialization")) + return errors.New("verifier initialization failed unexpectedly") } - return errs + return nil } func GetExtractedArtifactDir(pluginName, pluginVersion string) string { @@ -197,8 +250,13 @@ func GetExtractedArtifactDir(pluginName, pluginVersion string) string { type verifyFunc func(data, sig []byte) error -func verifyPGPSignatureDetached(data, sig []byte) error { - if err := load(); err != nil { +// verifyPGPSignatureDetachedWithKey verifies a detached PGP signature using either +// the default HashiCorp key (when customKeyPath is empty) or a custom key. +func verifyPGPSignatureDetachedWithKey(data, sig []byte, customKeyPath string) error { + if err := loadWithKey(customKeyPath); err != nil { + if customKeyPath != "" { + return fmt.Errorf("failed to load verifier with custom key: %w", err) + } return fmt.Errorf("failed to load verifier: %w", err) } @@ -207,12 +265,18 @@ func verifyPGPSignatureDetached(data, sig []byte) error { return fmt.Errorf("failed to verify data: %w", err) } if sigErr := verifyResult.SignatureError(); sigErr != nil { - return fmt.Errorf("unexpected signature error: %w", sigErr) + fp := hex.EncodeToString(verifyResult.SignedByFingerprint()) + return fmt.Errorf("signature verification failed for key with fingerprint %s: %w", fp, sigErr) } return nil } +// verifyPGPSignatureDetached verifies a detached PGP signature using the default HashiCorp key. +func verifyPGPSignatureDetached(data, sig []byte) error { + return verifyPGPSignatureDetachedWithKey(data, sig, "") +} + func VerifyPlugin(pluginDir string, pluginName string, verifyFunc verifyFunc) (*PluginMetadata, error) { if st, err := os.Stat(pluginDir); err != nil { if os.IsNotExist(err) { diff --git a/vault/plugincatalog/plugin_catalog.go b/vault/plugincatalog/plugin_catalog.go index fb039c03be..73d657552b 100644 --- a/vault/plugincatalog/plugin_catalog.go +++ b/vault/plugincatalog/plugin_catalog.go @@ -69,6 +69,10 @@ type PluginCatalog struct { wrapper pluginutil.RunnerUtil runtimeCatalog *PluginRuntimeCatalog + + // pluginPGPKey is the path to a PGP public key file to use for plugin + // signature verification. + pluginPGPKey string } // Only plugins running with identical PluginRunner config can be multiplexed, @@ -151,6 +155,9 @@ type PluginCatalogInput struct { Tmpdir string EnableMlock bool PluginRuntimeCatalog *PluginRuntimeCatalog + // PluginPGPKey is the path to a PGP public key file to use for plugin + // signature verification. + PluginPGPKey string } func SetupPluginCatalog(ctx context.Context, in *PluginCatalogInput) (*PluginCatalog, error) { @@ -164,6 +171,7 @@ func SetupPluginCatalog(ctx context.Context, in *PluginCatalogInput) (*PluginCat mlockPlugins: in.EnableMlock, wrapper: logical.StaticSystemView{VersionString: version.GetVersion().Version}, runtimeCatalog: in.PluginRuntimeCatalog, + pluginPGPKey: in.PluginPGPKey, } // Run upgrade if untyped plugins exist @@ -213,6 +221,14 @@ func SetupPluginCatalog(ctx context.Context, in *PluginCatalogInput) (*PluginCat return catalog, nil } +// getVerifyFunc returns the appropriate verification function based on whether +// a custom plugin PGP key is configured. +func (c *PluginCatalog) getVerifyFunc() verifyFunc { + return func(data, sig []byte) error { + return verifyPGPSignatureDetachedWithKey(data, sig, c.pluginPGPKey) + } +} + func envKeys(env []string) map[string]struct{} { keys := make(map[string]struct{}, len(env)) for _, env := range env { @@ -891,7 +907,7 @@ func (c *PluginCatalog) verifyOfficialPlugins(ctx context.Context) error { hasOfficialPlugins = true pluginDir := path.Join(c.directory, path.Dir(plugin.Command)) - if _, err = VerifyPlugin(pluginDir, plugin.Name, verifyPGPSignatureDetached); err != nil { + if _, err = VerifyPlugin(pluginDir, plugin.Name, c.getVerifyFunc()); err != nil { return fmt.Errorf("failed to verify plugin %q version %q: %w", plugin.Name, plugin.Version, err) } } @@ -1052,7 +1068,7 @@ func (c *PluginCatalog) set(ctx context.Context, plugin pluginutil.SetPluginInpu // e.g., plugins/vault-plugin-secrets-kv_0.24.0_linux_amd64/vault-plugin-secrets-kv plugin.Command = path.Join(extractedArtifactDir, plugin.Name) - metadata, err := VerifyPlugin(expectedPluginDir, plugin.Name, verifyPGPSignatureDetached) + metadata, err := VerifyPlugin(expectedPluginDir, plugin.Name, c.getVerifyFunc()) if err != nil { return nil, fmt.Errorf("failed to verify plugin plugin %q version %q: %w", plugin.Name, plugin.Version, err) diff --git a/vault/plugincatalog/plugin_catalog_custom_key_test.go b/vault/plugincatalog/plugin_catalog_custom_key_test.go new file mode 100644 index 0000000000..0c00ca4666 --- /dev/null +++ b/vault/plugincatalog/plugin_catalog_custom_key_test.go @@ -0,0 +1,203 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +package plugincatalog + +import ( + "context" + "os" + "path/filepath" + "testing" + + log "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/helper/testhelpers/corehelpers" + "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/hashicorp/vault/sdk/helper/pluginutil" + "github.com/hashicorp/vault/sdk/logical" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPluginCatalog_SetupWithCustomKey tests SetupPluginCatalog with a custom PGP key +func TestPluginCatalog_SetupWithCustomKey(t *testing.T) { + t.Parallel() + + // Generate a test PGP key pair + _, pubKeyArmored := generatePGPKeyPair(t) + + // Create temporary directories + tmpDir := t.TempDir() + pluginDir := filepath.Join(tmpDir, "plugins") + err := os.MkdirAll(pluginDir, 0o755) + require.NoError(t, err) + + keyPath := filepath.Join(tmpDir, "custom-key.asc") + err = os.WriteFile(keyPath, []byte(pubKeyArmored), 0o644) + require.NoError(t, err) + + // Setup plugin catalog with custom key + catalog, err := SetupPluginCatalog(context.Background(), &PluginCatalogInput{ + BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(), + CatalogView: &logical.InmemStorage{}, + PluginDirectory: pluginDir, + Logger: log.NewNullLogger(), + PluginPGPKey: keyPath, + }) + + require.NoError(t, err) + assert.NotNil(t, catalog) + assert.Equal(t, keyPath, catalog.pluginPGPKey, "custom key path should be set") + + // Verify that getVerifyFunc returns a function that uses the custom key + verifyFunc := catalog.getVerifyFunc() + assert.NotNil(t, verifyFunc) +} + +// TestPluginCatalog_verifyOfficialPlugins_WithCustomKey tests verifyOfficialPlugins with custom key +func TestPluginCatalog_verifyOfficialPlugins_WithCustomKey(t *testing.T) { + t.Parallel() + + // Generate a test PGP key pair + privKey, pubKeyArmored := generatePGPKeyPair(t) + + // Create temporary directories + tmpDir := t.TempDir() + pluginDir := filepath.Join(tmpDir, "plugins") + err := os.MkdirAll(pluginDir, 0o755) + require.NoError(t, err) + + keyPath := filepath.Join(tmpDir, "custom-key.asc") + err = os.WriteFile(keyPath, []byte(pubKeyArmored), 0o644) + require.NoError(t, err) + + // Create a plugin artifact with proper signatures + pluginName := "vault-plugin-test" + pluginVersion := "1.0.0" + artifactDir := filepath.Join(pluginDir, GetExtractedArtifactDir(pluginName, pluginVersion)) + err = os.MkdirAll(artifactDir, 0o755) + require.NoError(t, err) + + contents := generatePluginArtifactContents(t, pluginName, pluginVersion, consts.PluginTypeSecrets, true, privKey) + for filename, data := range contents { + err := os.WriteFile(filepath.Join(artifactDir, filename), data, 0o644) + require.NoError(t, err) + } + + storage := &logical.InmemStorage{} + // Setup plugin catalog with custom key + catalog, err := SetupPluginCatalog(context.Background(), &PluginCatalogInput{ + BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(), + CatalogView: storage, + PluginDirectory: pluginDir, + Logger: log.NewNullLogger(), + PluginPGPKey: keyPath, + }) + require.NoError(t, err) + + // Register the plugin as official + pluginEntry := &pluginutil.PluginRunner{ + Name: pluginName, + Type: consts.PluginTypeSecrets, + Version: pluginVersion, + Command: filepath.Join(GetExtractedArtifactDir(pluginName, pluginVersion), pluginName), + Builtin: false, + } + + // Store the plugin in catalog + entry, err := logical.StorageEntryJSON(pluginEntry.Name, pluginEntry) + require.NoError(t, err) + err = storage.Put(context.Background(), entry) + require.NoError(t, err) + + // Verify official plugins - should succeed with custom key + err = catalog.verifyOfficialPlugins(context.Background()) + require.NoError(t, err) +} + +// TestPluginCatalog_SetupWithRawCustomKey tests SetupPluginCatalog with a raw PGP key (not a file path) +func TestPluginCatalog_SetupWithRawCustomKey(t *testing.T) { + t.Parallel() + + // Generate a test PGP key pair + _, pubKeyArmored := generatePGPKeyPair(t) + + // Create temporary directories + tmpDir := t.TempDir() + pluginDir := filepath.Join(tmpDir, "plugins") + err := os.MkdirAll(pluginDir, 0o755) + require.NoError(t, err) + + // Setup plugin catalog with raw PGP key (not a file path) + catalog, err := SetupPluginCatalog(context.Background(), &PluginCatalogInput{ + BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(), + CatalogView: &logical.InmemStorage{}, + PluginDirectory: pluginDir, + Logger: log.NewNullLogger(), + PluginPGPKey: pubKeyArmored, // Pass raw key directly + }) + + require.NoError(t, err) + assert.NotNil(t, catalog) + assert.Equal(t, pubKeyArmored, catalog.pluginPGPKey, "raw custom key should be set") + + // Verify that getVerifyFunc returns a function that uses the custom key + verifyFunc := catalog.getVerifyFunc() + assert.NotNil(t, verifyFunc) +} + +// TestPluginCatalog_verifyOfficialPlugins_WithRawCustomKey tests verifyOfficialPlugins with raw custom key +func TestPluginCatalog_verifyOfficialPlugins_WithRawCustomKey(t *testing.T) { + t.Parallel() + + // Generate a test PGP key pair + privKey, pubKeyArmored := generatePGPKeyPair(t) + + // Create temporary directories + tmpDir := t.TempDir() + pluginDir := filepath.Join(tmpDir, "plugins") + err := os.MkdirAll(pluginDir, 0o755) + require.NoError(t, err) + + // Create a plugin artifact with proper signatures + pluginName := "vault-plugin-test-raw" + pluginVersion := "1.0.0" + artifactDir := filepath.Join(pluginDir, GetExtractedArtifactDir(pluginName, pluginVersion)) + err = os.MkdirAll(artifactDir, 0o755) + require.NoError(t, err) + + contents := generatePluginArtifactContents(t, pluginName, pluginVersion, consts.PluginTypeSecrets, true, privKey) + for filename, data := range contents { + err := os.WriteFile(filepath.Join(artifactDir, filename), data, 0o644) + require.NoError(t, err) + } + + storage := &logical.InmemStorage{} + // Setup plugin catalog with raw PGP key (not a file path) + catalog, err := SetupPluginCatalog(context.Background(), &PluginCatalogInput{ + BuiltinRegistry: corehelpers.NewMockBuiltinRegistry(), + CatalogView: storage, + PluginDirectory: pluginDir, + Logger: log.NewNullLogger(), + PluginPGPKey: pubKeyArmored, // Pass raw key directly + }) + require.NoError(t, err) + + // Register the plugin as official + pluginEntry := &pluginutil.PluginRunner{ + Name: pluginName, + Type: consts.PluginTypeSecrets, + Version: pluginVersion, + Command: filepath.Join(GetExtractedArtifactDir(pluginName, pluginVersion), pluginName), + Builtin: false, + } + + // Store the plugin in catalog + entry, err := logical.StorageEntryJSON(pluginEntry.Name, pluginEntry) + require.NoError(t, err) + err = storage.Put(context.Background(), entry) + require.NoError(t, err) + + // Verify official plugins - should succeed with raw custom key + err = catalog.verifyOfficialPlugins(context.Background()) + require.NoError(t, err) +}