From 068825afdc42c752c48d7b1d8fe7aa97819fb06e Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 7 May 2026 14:44:32 -0600 Subject: [PATCH] add dev flag to override pgp key for plugin signature verification (#13559) (#14242) (#14256) * add dev flag to override pgp key for plugin signature verification -dev-plugin-pgp-key is the path to a PGP public key file to use for plugin signature verification in dev mode. This prevents developers from having to manually hard code their own pgp key into vault source code and rebuild vault to develop and test enterprise plugins. * fix sync once error handling * add tests * remove test * tests: set random listener addr * improve error message * dedupe getVerifyFunc annd verifyPGPSignatureDetached * accept raw key as well as path * keep hc key and add custom key to keyring Co-authored-by: John-Michael Faircloth --- command/server.go | 11 + command/server_dev_plugin_test.go | 221 ++++++++++++++++++ vault/core.go | 29 +++ vault/plugincatalog/plugin_artifact.go | 96 ++++++-- vault/plugincatalog/plugin_catalog.go | 20 +- .../plugin_catalog_custom_key_test.go | 203 ++++++++++++++++ 6 files changed, 562 insertions(+), 18 deletions(-) create mode 100644 command/server_dev_plugin_test.go create mode 100644 vault/plugincatalog/plugin_catalog_custom_key_test.go 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) +}