mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-13 16:46:52 +02:00
Merge remote-tracking branch 'remotes/from/ce/release/2.x.x' into release/2.x.x
This commit is contained in:
commit
a8a76de54c
@ -18,6 +18,9 @@ active_versions {
|
||||
ce_active = false
|
||||
}
|
||||
|
||||
version "1.20.x" {
|
||||
ce_active = false
|
||||
}
|
||||
|
||||
version "1.19.x" {
|
||||
ce_active = false
|
||||
|
||||
@ -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