mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-12 00:13:45 +02:00
* 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 <fairclothjm@users.noreply.github.com>
This commit is contained in:
parent
a3e1d00ea8
commit
068825afdc
@ -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 {
|
||||
|
||||
221
command/server_dev_plugin_test.go
Normal file
221
command/server_dev_plugin_test.go
Normal file
@ -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)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
203
vault/plugincatalog/plugin_catalog_custom_key_test.go
Normal file
203
vault/plugincatalog/plugin_catalog_custom_key_test.go
Normal file
@ -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)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user