Vault 34905 support register ce plugin with extracted artifact (#30673)

* apply oss changes from https://github.com/hashicorp/vault-enterprise/pull/8071

* handle oss file deletions

* go mod tidy

* add changelog
This commit is contained in:
helenfufu 2025-05-22 08:39:47 -07:00 committed by GitHub
parent 403720c1fd
commit 71edba2ccb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1136 additions and 203 deletions

3
changelog/30673.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:improvement
plugins: Support registration of CE plugins with extracted artifact directory.
```

View file

@ -17,6 +17,12 @@ var (
_ cli.CommandAutocomplete = (*PluginRegisterCommand)(nil)
)
func NewPluginRegisterCommand(baseCommand *BaseCommand) cli.Command {
return &PluginRegisterCommand{
BaseCommand: baseCommand,
}
}
type PluginRegisterCommand struct {
*BaseCommand
@ -144,8 +150,8 @@ func (c *PluginRegisterCommand) Run(args []string) int {
case len(args) > 2:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1 or 2, got %d)", len(args)))
return 1
case c.flagSHA256 == "":
c.UI.Error("SHA256 is required for all plugins, please provide -sha256")
case c.flagSHA256 == "" && c.flagVersion == "":
c.UI.Error("One of -sha256 or -version is required. If registering with binary, please provide at least -sha256 (-version optional). If registering with extracted artifact directory, please provide -version only.")
return 1
// These cases should come after invalid cases have been checked

View file

@ -1,14 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !enterprise
package command
import "github.com/hashicorp/cli"
func NewPluginRegisterCommand(baseCommand *BaseCommand) cli.Command {
return &PluginRegisterCommand{
BaseCommand: baseCommand,
}
}

5
go.mod
View file

@ -39,7 +39,8 @@ require (
github.com/Azure/azure-storage-blob-go v0.15.0
github.com/Azure/go-autorest/autorest v0.11.29
github.com/Azure/go-autorest/autorest/adal v0.9.23
github.com/ProtonMail/go-crypto v1.1.5
github.com/ProtonMail/go-crypto v1.2.0
github.com/ProtonMail/gopenpgp/v3 v3.2.1
github.com/SAP/go-hdb v1.10.1
github.com/Sectorbob/mlab-ns2 v0.0.0-20171030222938-d3aa0c295a8a
github.com/aerospike/aerospike-client-go/v5 v5.6.0
@ -408,7 +409,7 @@ require (
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/google/uuid v1.6.0
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gophercloud/gophercloud v0.1.0 // indirect

6
go.sum
View file

@ -753,8 +753,10 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/ProtonMail/gopenpgp/v3 v3.2.1 h1:ohRlKL5YwyIkN5kk7uBvijiMsyA57mK0yBEJg9xButU=
github.com/ProtonMail/gopenpgp/v3 v3.2.1/go.mod h1:x7RduTo/0n/2PjTFRoEHApaxye/8PFbhoCquwfYBUGM=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/SAP/go-hdb v1.10.1 h1:c9dGT5xHZNDwPL3NQcRpnNISn3MchwYaGoMZpCAllUs=

View file

@ -13,6 +13,7 @@ import (
"github.com/golang/protobuf/ptypes"
"github.com/hashicorp/vault/sdk/database/dbplugin/v5/proto"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
)
@ -29,6 +30,9 @@ type gRPCClient struct {
client proto.DatabaseClient
versionClient logical.PluginVersionClient
doneCtx context.Context
// tier is the plugin tier
tier consts.PluginTier
}
func (c gRPCClient) PluginVersion() logical.PluginVersion {

View file

@ -37,6 +37,7 @@ func NewPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, config plug
// order to enable multiplexing on multiplexed plugins
c.client = proto.NewDatabaseClient(pluginClient.Conn())
c.versionClient = logical.NewPluginVersionClient(pluginClient.Conn())
c.tier = config.Tier
db = c
default:

View file

@ -59,8 +59,8 @@ func PluginFactoryVersion(ctx context.Context, pluginName string, pluginVersion
IsMetadataMode: false,
AutoMTLS: true,
Wrapper: sys,
Tier: pluginRunner.Tier,
}
config.EntUpdate(pluginRunner)
// create a DatabasePluginClient instance
db, err = NewPluginClient(ctx, sys, config)

View file

@ -0,0 +1,102 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package consts
import (
"encoding/json"
"fmt"
)
var PluginTiers = []PluginTier{
PluginTierUnknown,
PluginTierCommunity,
PluginTierPartner,
PluginTierOfficial,
}
type PluginTier uint32
const (
// PluginTierUnknown defines unknown plugin tier
// DO NOT change the order of the enum as it
// could cause the wrong plugin tier to be read
// from storage for a given underlying number
PluginTierUnknown PluginTier = iota
// PluginTierCommunity defines community plugin tier
// DO NOT change the order of the enum as it
// could cause the wrong plugin tier to be read
// from storage for a given underlying number
PluginTierCommunity
// PluginTierPartner defines partner plugin tier
// DO NOT change the order of the enum as it
// could cause the wrong plugin tier to be read
// from storage for a given underlying number
PluginTierPartner
// PluginTierOfficial defines enterprise plugin tier
// DO NOT change the order of the enum as it
// could cause the wrong plugin tier to be read
// from storage for a given underlying number
PluginTierOfficial
)
func (p PluginTier) String() string {
switch p {
case PluginTierUnknown:
return "unknown"
case PluginTierCommunity:
return "community"
case PluginTierPartner:
return "partner"
case PluginTierOfficial:
return "official"
default:
return "unsupported"
}
}
func ParsePluginTier(pluginTier string) (PluginTier, error) {
switch pluginTier {
case "unknown", "":
return PluginTierUnknown, nil
case "community":
return PluginTierCommunity, nil
case "partner":
return PluginTierPartner, nil
case "official":
return PluginTierOfficial, nil
default:
return PluginTierUnknown, fmt.Errorf("%q is not a supported plugin tier", pluginTier)
}
}
// UnmarshalJSON implements json.Unmarshaler. It supports unmarshaling either a
// string or a uint32. All new serialization will be as a string, but we
// previously serialized as a uint32 so we need to support that for backwards
// compatibility.
func (p *PluginTier) UnmarshalJSON(data []byte) error {
var asString string
err := json.Unmarshal(data, &asString)
if err == nil {
*p, err = ParsePluginTier(asString)
return err
}
var asUint32 uint32
err = json.Unmarshal(data, &asUint32)
if err != nil {
return err
}
*p = PluginTier(asUint32)
switch *p {
case PluginTierUnknown, PluginTierCommunity, PluginTierPartner, PluginTierOfficial:
return nil
default:
return fmt.Errorf("%d is not a supported plugin tier", asUint32)
}
}
// MarshalJSON implements json.Marshaler.
func (p PluginTier) MarshalJSON() ([]byte, error) {
return json.Marshal(p.String())
}

View file

@ -0,0 +1,230 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package consts
import (
"encoding/json"
"reflect"
"testing"
)
func TestParsePluginTier(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pluginTier string
want PluginTier
wantErr bool
}{
{
name: "unknown",
pluginTier: "unknown",
want: PluginTierUnknown,
},
{
name: "empty unknown",
pluginTier: "",
want: PluginTierUnknown,
},
{
name: "community",
pluginTier: "community",
want: PluginTierCommunity,
},
{
name: "partner",
pluginTier: "partner",
want: PluginTierPartner,
},
{
name: "official",
pluginTier: "official",
want: PluginTierOfficial,
},
{
name: "unsupported",
pluginTier: "foo",
want: PluginTierUnknown,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParsePluginTier(tt.pluginTier)
if (err != nil) != tt.wantErr {
t.Fatalf("ParsePluginTier() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Fatalf("ParsePluginTier() got = %v, want %v", got, tt.want)
}
})
}
}
func TestPluginTier_MarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
p PluginTier
want []byte
}{
{
name: "unknown",
p: PluginTierUnknown,
want: []byte(`"unknown"`),
},
{
name: "community",
p: PluginTierCommunity,
want: []byte(`"community"`),
},
{
name: "partner",
p: PluginTierPartner,
want: []byte(`"partner"`),
},
{
name: "offical",
p: PluginTierOfficial,
want: []byte(`"official"`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.p.MarshalJSON()
if err != nil {
t.Fatalf("MarshalJSON() error = %v, want nil", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("MarshalJSON() got = %v, want %v", got, tt.want)
}
})
}
}
func TestPluginTier_UnmarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
wantTier PluginTier
data []byte
wantErr bool
}{
{
name: "unknown",
wantTier: PluginTierUnknown,
data: []byte(`"unknown"`),
},
{
name: "community",
wantTier: PluginTierCommunity,
data: []byte(`"community"`),
},
{
name: "partner",
wantTier: PluginTierPartner,
data: []byte(`"partner"`),
},
{
name: "offical",
wantTier: PluginTierOfficial,
data: []byte(`"official"`),
},
{
name: "unsupported",
wantTier: PluginTierUnknown,
data: []byte(`"foo"`),
wantErr: true,
},
// The following test cases ensures new plugin tiers are added at the end of the enum list
// Inserting a new plugin tier in the middle of the enum list will fail
// New plugin tiers should be added at the end of the test case list
{
name: "0-unknown",
wantTier: PluginTierUnknown,
data: []byte(`0`),
},
{
name: "1-community",
wantTier: PluginTierCommunity,
data: []byte(`1`),
},
{
name: "2-partner",
wantTier: PluginTierPartner,
data: []byte(`2`),
},
{
name: "3-official",
wantTier: PluginTierOfficial,
data: []byte(`3`),
},
{
name: "tier number unsupported",
data: []byte(`2345678`),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var tier PluginTier
err := tier.UnmarshalJSON(tt.data)
if (err != nil) != tt.wantErr {
t.Fatalf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && tier != tt.wantTier {
t.Fatalf("UnmarshalJSON() got = %v, want %v", tier, tt.wantTier)
}
})
}
}
// TestPluginTierJSONRoundTrip tests that PluginTier can be marshaled and unmarshaled
// to/from JSON in a round trip.
func TestPluginTierJSONRoundTrip(t *testing.T) {
type testTier struct {
PluginTier PluginTier `json:"plugin_tier"`
}
for _, tier := range PluginTiers {
t.Run(tier.String(), func(t *testing.T) {
original := testTier{
PluginTier: tier,
}
asBytes, err := json.Marshal(original)
if err != nil {
t.Fatal(err)
}
var roundTripped testTier
err = json.Unmarshal(asBytes, &roundTripped)
if err != nil {
t.Fatal(err)
}
if original != roundTripped {
t.Fatalf("expected %v, got %v", original, roundTripped)
}
})
}
}
func TestUnknownTierExcludedWithOmitEmpty(t *testing.T) {
type testTierOmitEmpty struct {
Type PluginTier `json:"tier,omitempty"`
}
bytes, err := json.Marshal(testTierOmitEmpty{})
if err != nil {
t.Fatal(err)
}
m := map[string]any{}
json.Unmarshal(bytes, &m)
if _, exists := m["tier"]; exists {
t.Fatal("tier should not be present")
}
}

View file

@ -30,7 +30,6 @@ const (
)
type PluginClientConfig struct {
EntPluginClientConfig
Name string
PluginType consts.PluginType
Version string
@ -41,6 +40,7 @@ type PluginClientConfig struct {
AutoMTLS bool
MLock bool
Wrapper RunnerUtil
Tier consts.PluginTier
}
type runConfig struct {

View file

@ -1,12 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build !enterprise
package pluginutil
type EntPluginClientConfig struct{}
func (p *PluginClientConfig) EntUpdate(_ *PluginRunner) {
// no-op
}

View file

@ -18,6 +18,13 @@ import (
"google.golang.org/grpc"
)
const (
// ConfigPluginTier is the key for the plugin tier for Config of logical.BackendConfig
ConfigPluginTier = "plugin_tier"
// ConfigPluginVersion is the key for the plugin version for Config of logical.BackendConfig
ConfigPluginVersion = "plugin_version"
)
// ErrPluginNotFound is returned when a plugin does not have a pinned version.
var ErrPinnedVersionNotFound = errors.New("pinned version not found")
@ -57,8 +64,6 @@ const MultiplexingCtxKey string = "multiplex_id"
// PluginRunner defines the metadata needed to run a plugin securely with
// go-plugin.
type PluginRunner struct {
EntPluginRunner
Name string `json:"name" structs:"name"`
Type consts.PluginType `json:"type" structs:"type"`
Version string `json:"version" structs:"version"`
@ -69,6 +74,7 @@ type PluginRunner struct {
Env []string `json:"env" structs:"env"`
Sha256 []byte `json:"sha256" structs:"sha256"`
Builtin bool `json:"builtin" structs:"builtin"`
Tier consts.PluginTier `json:"tier" structs:"tier"`
BuiltinFactory func() (interface{}, error) `json:"-" structs:"-"`
RuntimeConfig *prutil.PluginRuntimeConfig `json:"-" structs:"-"`
Tmpdir string `json:"-" structs:"-"`

View file

@ -1,8 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build !enterprise
package pluginutil
type EntPluginRunner struct{}

View file

@ -987,7 +987,7 @@ func (c *Core) newCredentialBackend(ctx context.Context, entry *MountEntry, sysV
factory = wrapFactoryCheckPerms(c, plugin.Factory)
}
entSetExternalPluginConfig(plug, conf)
setExternalPluginConfig(plug, conf)
}
// Set up conf to pass in plugin_name

View file

@ -541,9 +541,10 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, _ *logica
sha256 := d.Get("sha256").(string)
if sha256 == "" {
sha256 = d.Get("sha_256").(string)
if resp := validateSHA256(sha256); resp.IsError() {
return resp, nil
}
}
if sha256 == "" && pluginVersion == "" {
return logical.ErrorResponse("must provide at least one of sha256 or version: use sha256 for binary registration (version optional) or version only for artifact registration"), nil
}
if resp := validateSha256IsEmptyForEntPluginVersion(pluginVersion, sha256); resp.IsError() {

View file

@ -9,13 +9,6 @@ import (
"github.com/hashicorp/vault/sdk/logical"
)
func validateSHA256(sha256 string) *logical.Response {
if sha256 == "" {
return logical.ErrorResponse("missing SHA-256 value")
}
return nil
}
func validateSha256IsEmptyForEntPluginVersion(pluginVersion string, sha256 string) *logical.Response {
return nil
}

View file

@ -1717,7 +1717,7 @@ func (c *Core) newLogicalBackend(ctx context.Context, entry *MountEntry, sysView
factory = wrapFactoryCheckPerms(c, factory)
}
entSetExternalPluginConfig(plug, conf)
setExternalPluginConfig(plug, conf)
}
// Set up conf to pass in plugin_name

View file

@ -10,7 +10,6 @@ import (
"path"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
)
@ -77,8 +76,3 @@ func (c *Core) mountEntrySysView(entry *MountEntry) extendedSystemView {
func (c *Core) entBuiltinPluginMetrics(ctx context.Context, entry *MountEntry, val float32) error {
return nil
}
// entSetExternalPluginConfig (Vault Community edition) makes no changes to config for external plugins.
func entSetExternalPluginConfig(_ *pluginutil.PluginRunner, _ map[string]string) {
// No-op
}

View file

@ -4,6 +4,7 @@
package vault
import (
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
)
@ -29,3 +30,8 @@ func collectBackendSpecialPaths(backend logical.Backend, viewPath string, access
return ret
}
// setExternalPluginConfig sets key value pairs to config based on pluginutil.PluginRunner
func setExternalPluginConfig(runner *pluginutil.PluginRunner, config map[string]string) {
config[pluginutil.ConfigPluginTier] = runner.Tier.String()
}

View file

@ -0,0 +1,252 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package plugincatalog
import (
"crypto/sha256"
"errors"
"fmt"
"io"
"os"
"path"
"runtime"
"strings"
"github.com/ProtonMail/gopenpgp/v3/crypto"
)
// 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 = `
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
ZF5q4h4I33PSGDdSvGXn9UMY5Isjpg==
=7pIB
-----END PGP PUBLIC KEY BLOCK-----
`
var defaultPGPPubKey = hashiCorpPGPPubKey
const (
extractedArtifactDirFmt = "%s_%s_%s_%s" // vault-plugin-database-oracle_1.2.3+ent_linux_amd64
metadataFile = "metadata.json"
metadataSig = "metadata.json.sig"
)
var (
errExtractedArtifactDirNotFound = errors.New("extracted artifact directory not found")
errReadMetadata = errors.New("failed to read metadata")
errReadMetadataSig = errors.New("failed to read metadata signature")
errReadPlugin = errors.New("failed to read plugin binary")
errReadPluginMetadata = errors.New("failed to read plugin metadata")
errReadPluginPGPSig = errors.New("failed to read plugin binary PGP signature")
errVerifyMetadataSig = errors.New("failed to verify metadata detached signature")
errVerifyPluginSig = errors.New("failed to verify plugin binary PGP signature")
)
func getExtractedArtifactDir(pluginName, pluginVersion string) string {
if strings.HasPrefix(pluginVersion, "v") {
pluginVersion = pluginVersion[1:]
}
return fmt.Sprintf(extractedArtifactDirFmt, pluginName, pluginVersion, runtime.GOOS, runtime.GOARCH)
}
func verifyPlugin(pluginDir, pluginName, pubKey string) (*pluginMetadata, error) {
// verify the extracted plugin artifact directory structure and each file inside
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/EULA.txt (optional)
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/TermsOfEvaluation.txt (optional)
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/LICENSE (optional)
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/vault-plugin-secrets-example
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/metadata.json
// vault-plugin-secrets-example_1.2.3+ent_darwin_arm64/metadata.json.sig
if _, err := os.Stat(pluginDir); os.IsNotExist(err) {
return nil, fmt.Errorf("%w: %w", errExtractedArtifactDirNotFound, err)
}
pgp := crypto.PGP()
key, err := crypto.NewKeyFromArmored(pubKey)
if err != nil {
return nil, err
}
verifier, err := pgp.Verify().VerificationKey(key).New()
if err != nil {
return nil, err
}
// verify metadata.json is untampered
metadataPath := path.Join(pluginDir, "metadata.json")
metadataBytes, err := os.ReadFile(metadataPath)
if err != nil {
return nil, fmt.Errorf("%w: %w", errReadMetadata, err)
}
metadataSigBytes, err := os.ReadFile(path.Join(pluginDir, "metadata.json.sig"))
if err != nil {
return nil, fmt.Errorf("%w: %w", errReadMetadataSig, err)
}
verifyResult, err := verifier.VerifyDetached(metadataBytes, metadataSigBytes, crypto.Armor)
if err != nil {
return nil, fmt.Errorf("%w: %w", errVerifyMetadataSig, err)
}
if sigErr := verifyResult.SignatureError(); sigErr != nil {
return nil, fmt.Errorf("%w: %w", errVerifyMetadataSig, sigErr)
}
// verify plugin binary is untampred
pluginBytes, err := os.ReadFile(path.Join(pluginDir, pluginName))
if err != nil {
return nil, fmt.Errorf("%w: %w", errReadPlugin, err)
}
metadata, err := readPluginMetadata(metadataPath)
if err != nil {
return nil, fmt.Errorf("%w: %w", errReadPluginMetadata, err)
}
verifyResult, err = verifier.VerifyDetached(pluginBytes, []byte(metadata.Plugin.PGPSig), crypto.Armor)
if err != nil {
return nil, fmt.Errorf("%w: %w", errVerifyPluginSig, err)
}
if sigErr := verifyResult.SignatureError(); sigErr != nil {
return nil, fmt.Errorf("%w: %w", errVerifyPluginSig, sigErr)
}
return metadata, nil
}
func pluginSHA256Sum(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
hash := sha256.New()
if _, err = io.Copy(hash, file); err != nil {
return nil, err
}
return hash.Sum(nil), nil
}

View file

@ -0,0 +1,190 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package plugincatalog
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"testing"
"github.com/ProtonMail/gopenpgp/v3/crypto"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/stretchr/testify/assert"
)
// Test_verifyPlugin tests the verifyPlugin function.
func Test_verifyPlugin(t *testing.T) {
t.Parallel()
type args struct {
pluginName string
pluginVersion string
pluginType consts.PluginType
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "success",
args: args{
pluginName: "vault-plugin-auth-example",
pluginVersion: "0.1.1+ent",
pluginType: consts.PluginTypeCredential,
},
expectedErr: nil,
},
{
name: "missing metadata",
args: args{
pluginName: "vault-plugin-auth-example",
pluginVersion: "0.1.2+ent",
pluginType: consts.PluginTypeCredential,
},
expectedErr: errReadMetadata,
},
{
name: "missing metadata signature",
args: args{
pluginName: "vault-plugin-auth-example",
pluginVersion: "0.1.3+ent",
pluginType: consts.PluginTypeCredential,
},
expectedErr: errReadMetadataSig,
},
{
name: "bad metadata signature verify",
args: args{
pluginName: "vault-plugin-secret-example",
pluginVersion: "0.1.4+ent",
pluginType: consts.PluginTypeSecrets,
},
expectedErr: errVerifyMetadataSig,
},
{
name: "missing plugin binary",
args: args{
pluginName: "vault-plugin-database-example",
pluginVersion: "0.1.5+ent",
pluginType: consts.PluginTypeDatabase,
},
expectedErr: errReadPlugin,
},
{
name: "bad plugin binary signature verify",
args: args{
pluginName: "vault-plugin-database-example",
pluginVersion: "0.1.6+ent",
pluginType: consts.PluginTypeDatabase,
},
expectedErr: errVerifyPluginSig,
},
{
name: "bad extracted artifact directory",
args: args{
pluginName: "vault-plugin-database-example",
pluginVersion: "0.1.6+ent",
pluginType: consts.PluginTypeDatabase,
},
expectedErr: errExtractedArtifactDirNotFound,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
privKey, pubKeyArmored := generatePGPKeyPair(t)
contents := generatePluginArtifactContents(t, tt.args.pluginName,
tt.args.pluginVersion, tt.args.pluginType, !errors.Is(tt.expectedErr, errReadPluginPGPSig), privKey)
actualExtractedArtifactDir := getExtractedArtifactDir(tt.args.pluginName, tt.args.pluginVersion)
switch {
case tt.expectedErr == nil:
case errors.Is(tt.expectedErr, errReadPluginPGPSig):
// no-op
case errors.Is(tt.expectedErr, errReadMetadata):
delete(contents, metadataFile)
case errors.Is(tt.expectedErr, errReadMetadataSig):
delete(contents, metadataSig)
case errors.Is(tt.expectedErr, errVerifyMetadataSig):
contents[metadataFile] = []byte(`{"will not" : "match signature"}`)
case errors.Is(tt.expectedErr, errReadPlugin):
delete(contents, tt.args.pluginName)
case errors.Is(tt.expectedErr, errVerifyPluginSig):
contents[tt.args.pluginName] = []byte("will not match signature")
case errors.Is(tt.expectedErr, errExtractedArtifactDirNotFound):
actualExtractedArtifactDir += "not_found"
default:
t.Fatalf("unexpected error: %v", tt.expectedErr)
}
tempDir := t.TempDir()
actualPluginDir := filepath.Join(tempDir, actualExtractedArtifactDir)
err := os.MkdirAll(actualPluginDir, 0o755)
assert.NoError(t, err, "expected successful create extracted plugin directory")
// Write the files to the extracted plugin directory
for name, content := range contents {
err = os.WriteFile(filepath.Join(actualPluginDir, name), content, 0o644)
assert.NoError(t, err, "expected successful file write")
}
var metadata *pluginMetadata
metadata, err = verifyPlugin(path.Join(tempDir, getExtractedArtifactDir(tt.args.pluginName, tt.args.pluginVersion)),
tt.args.pluginName, pubKeyArmored)
assert.ErrorIs(t, err, tt.expectedErr, "expected verify plugin error to match")
if tt.expectedErr == nil {
assert.NotNil(t, metadata)
assert.Equal(t, tt.args.pluginName, metadata.Plugin.Name)
assert.Equal(t, tt.args.pluginVersion, metadata.Plugin.Version)
}
})
}
}
// Test_getExtractedArtifactDir tests the getExtractedArtifactDir function.
func Test_getExtractedArtifactDir(t *testing.T) {
t.Parallel()
type args struct {
command string
version string
}
tests := []struct {
name string
args args
want string
}{
{
name: "v-prefixed version",
args: args{"vault-plugin-auth-aws", "v0.18.0+ent"},
want: fmt.Sprintf("vault-plugin-auth-aws_0.18.0+ent_%s_%s", runtime.GOOS, runtime.GOARCH),
},
{
name: "un-prefixed version",
args: args{"vault-plugin-auth-aws", "0.18.0+ent"},
want: fmt.Sprintf("vault-plugin-auth-aws_0.18.0+ent_%s_%s", runtime.GOOS, runtime.GOARCH),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, getExtractedArtifactDir(tt.args.command, tt.args.version))
})
}
}
// TestPluginCatalog_hashiCorpPubPGPKey tests hashiCorpPubPGPKey read
// and verification key creation.
func TestPluginCatalog_hashiCorpPubPGPKey(t *testing.T) {
pgp := crypto.PGP()
key, err := crypto.NewKeyFromArmored(hashiCorpPGPPubKey)
assert.NoError(t, err)
_, err = pgp.Verify().VerificationKey(key).New()
assert.NoError(t, err)
}

View file

@ -174,7 +174,7 @@ func SetupPluginCatalog(ctx context.Context, in *PluginCatalogInput) (*PluginCat
}
// Sanitize the plugin catalog
err = catalog.entValidate(ctx)
err = catalog.verifyOfficialPlugins(ctx)
if err != nil {
logger.Error("error while sanitizing plugin storage", "error", err)
return nil, err
@ -864,6 +864,33 @@ func (c *PluginCatalog) upgradePlugins(ctx context.Context, logger log.Logger) e
return retErr
}
// verifyOfficialPlugins verifies all official HashiCorp plugins
func (c *PluginCatalog) verifyOfficialPlugins(ctx context.Context) error {
plugins, err := c.collectAllPlugins(ctx)
if err != nil {
return err
}
var hasOfficialPlugins bool
for _, plugin := range plugins {
if plugin.Tier != consts.PluginTierOfficial {
continue
}
hasOfficialPlugins = true
pluginDir := path.Join(c.directory, path.Dir(plugin.Command))
if _, err = verifyPlugin(pluginDir, plugin.Name, hashiCorpPGPPubKey); err != nil {
return fmt.Errorf("failed to verify plugin %q version %q: %w", plugin.Name, plugin.Version, err)
}
}
if hasOfficialPlugins {
c.logger.Info("all official plugins verified")
}
return nil
}
// Get retrieves a plugin with the specified name from the catalog. It first
// looks for external plugins with this name and then looks for builtin plugins.
// It returns a PluginRunner or an error if no plugin was found.
@ -966,6 +993,165 @@ func (c *PluginCatalog) Set(ctx context.Context, plugin pluginutil.SetPluginInpu
return err
}
// setInternal sets a plugin entry in the catalog. In addition to its CE functionality,
// it will attempt to verify the plugin in its extracted artifact directory
// if the sha256 is not specified.
func (c *PluginCatalog) setInternal(ctx context.Context, plugin pluginutil.SetPluginInput) (*pluginutil.PluginRunner, error) {
command := plugin.Command
var pluginTier consts.PluginTier
if plugin.OCIImage == "" {
// When OCIImage is empty, then we want to register with the binary either directly (if Sha256 is set) or via the extracted artifact directory (if Sha256 is unset).
var expectedPluginDir string
if len(plugin.Sha256) > 0 {
// When Sha256 is set, we can assume the binary is already available.
expectedPluginDir = c.directory
command = filepath.Join(c.directory, plugin.Command)
} else {
// When Sha256 is unset, ensure Version is set then attempt to verify the plugin
// in its extracted artifact directory.
if len(plugin.Version) == 0 {
return nil, fmt.Errorf("must specify sha256 to register plugin with binary or version to register plugin with extracted artifact directory")
}
var err error
extractedArtifactDir := getExtractedArtifactDir(plugin.Name, plugin.Version)
expectedPluginDir = path.Join(c.directory, extractedArtifactDir)
plugin.Command = path.Join(extractedArtifactDir, plugin.Name)
metadata, err := verifyPlugin(expectedPluginDir, plugin.Name, defaultPGPPubKey)
if err != nil {
return nil, fmt.Errorf("failed to verify plugin plugin %q version %q: %w",
plugin.Name, plugin.Version, err)
}
pluginTier = metadata.Plugin.Tier
plugin.Sha256, err = pluginSHA256Sum(path.Join(c.directory, plugin.Command))
if err != nil {
return nil, fmt.Errorf("failed to calculate SHA256 of plugin: %w", err)
}
command = filepath.Join(c.directory, plugin.Command)
}
sym, err := filepath.EvalSymlinks(command)
if err != nil {
return nil, fmt.Errorf("error while validating the command path: %w", err)
}
// Best effort check to make sure the command isn't breaking out of the
// configured plugin directory.
symAbs, err := filepath.Abs(filepath.Dir(sym))
if err != nil {
return nil, fmt.Errorf("error while validating the command path: %w", err)
}
if symAbs != expectedPluginDir {
return nil, fmt.Errorf("cannot execute files outside of configured plugin directory %s: expected %s, got %s", c.directory, expectedPluginDir, symAbs)
}
}
// entryTmp should only be used for the below type and version checks. It uses the
// full command instead of the relative command because get() normally prepends
// the plugin directory to the command, but we can't use get() here.
entryTmp := &pluginutil.PluginRunner{
Name: plugin.Name,
Command: command,
OCIImage: plugin.OCIImage,
Runtime: plugin.Runtime,
Args: plugin.Args,
Env: plugin.Env,
Sha256: plugin.Sha256,
Builtin: false,
Tmpdir: c.tmpdir,
Tier: pluginTier,
}
if entryTmp.OCIImage != "" && entryTmp.Runtime != "" {
var err error
entryTmp.RuntimeConfig, err = c.runtimeCatalog.Get(ctx, entryTmp.Runtime, consts.PluginRuntimeTypeContainer)
if err != nil {
return nil, fmt.Errorf("failed to get configured runtime for plugin %q: %w", plugin.Name, err)
}
}
// If the plugin type is unknown, we want to attempt to determine the type
if plugin.Type == consts.PluginTypeUnknown {
var err error
plugin.Type, err = c.getPluginTypeFromUnknown(ctx, entryTmp)
if err != nil {
return nil, err
}
if plugin.Type == consts.PluginTypeUnknown {
return nil, ErrPluginBadType
}
}
// getting the plugin version is best-effort, so errors are not fatal
runningVersion := logical.EmptyPluginVersion
var versionErr error
switch plugin.Type {
case consts.PluginTypeSecrets, consts.PluginTypeCredential:
runningVersion, versionErr = c.getBackendRunningVersion(ctx, entryTmp)
case consts.PluginTypeDatabase:
runningVersion, versionErr = c.getDatabaseRunningVersion(ctx, entryTmp)
default:
return nil, fmt.Errorf("unknown plugin type: %v", plugin.Type)
}
if versionErr != nil {
c.logger.Warn("Error determining plugin version", "error", versionErr)
if errors.Is(versionErr, ErrPluginUnableToRun) {
return nil, versionErr
}
} else if plugin.Version != "" && runningVersion.Version != "" && plugin.Version != runningVersion.Version {
c.logger.Error("Plugin self-reported version did not match requested version",
"plugin", plugin.Name, "requestedVersion", plugin.Version, "reportedVersion", runningVersion.Version)
return nil, fmt.Errorf("%w: %s reported version (%s) did not match requested version (%s)",
ErrPluginVersionMismatch, plugin.Name, runningVersion.Version, plugin.Version)
} else if plugin.Version == "" && runningVersion.Version != "" {
plugin.Version = runningVersion.Version
_, err := semver.NewVersion(plugin.Version)
if err != nil {
return nil, fmt.Errorf("plugin self-reported version %q is not a valid semantic version: %w", plugin.Version, err)
}
}
entry := &pluginutil.PluginRunner{
Name: plugin.Name,
Type: plugin.Type,
Version: plugin.Version,
Command: plugin.Command,
OCIImage: plugin.OCIImage,
Runtime: plugin.Runtime,
Args: plugin.Args,
Env: plugin.Env,
Sha256: plugin.Sha256,
Builtin: false,
Tmpdir: c.tmpdir,
Tier: pluginTier,
}
buf, err := json.Marshal(entry)
if err != nil {
return nil, fmt.Errorf("failed to encode plugin entry: %w", err)
}
storageKey := path.Join(plugin.Type.String(), plugin.Name)
if plugin.Version != "" {
storageKey = path.Join(storageKey, plugin.Version)
}
logicalEntry := logical.StorageEntry{
Key: storageKey,
Value: buf,
}
if err := c.catalogView.Put(ctx, &logicalEntry); err != nil {
return nil, fmt.Errorf("failed to persist plugin entry: %w", err)
}
return entry, nil
}
// Delete is used to remove an external plugin from the catalog. Builtin plugins
// can not be deleted.
func (c *PluginCatalog) Delete(ctx context.Context, name string, pluginType consts.PluginType, pluginVersion string) error {

View file

@ -1,140 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !enterprise
package plugincatalog
import (
"context"
"encoding/json"
"errors"
"fmt"
"path"
"path/filepath"
semver "github.com/hashicorp/go-version"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
)
// setInternal creates a new plugin entry in the catalog and persists it to storage
func (c *PluginCatalog) setInternal(ctx context.Context, plugin pluginutil.SetPluginInput) (*pluginutil.PluginRunner, error) {
command := plugin.Command
if plugin.OCIImage == "" {
// Best effort check to make sure the command isn't breaking out of the
// configured plugin directory.
command = filepath.Join(c.directory, plugin.Command)
sym, err := filepath.EvalSymlinks(command)
if err != nil {
return nil, fmt.Errorf("error while validating the command path: %w", err)
}
symAbs, err := filepath.Abs(filepath.Dir(sym))
if err != nil {
return nil, fmt.Errorf("error while validating the command path: %w", err)
}
if symAbs != c.directory {
return nil, errors.New("cannot execute files outside of configured plugin directory")
}
}
// entryTmp should only be used for the below type and version checks. It uses the
// full command instead of the relative command because get() normally prepends
// the plugin directory to the command, but we can't use get() here.
entryTmp := &pluginutil.PluginRunner{
Name: plugin.Name,
Command: command,
OCIImage: plugin.OCIImage,
Runtime: plugin.Runtime,
Args: plugin.Args,
Env: plugin.Env,
Sha256: plugin.Sha256,
Builtin: false,
Tmpdir: c.tmpdir,
}
if entryTmp.OCIImage != "" && entryTmp.Runtime != "" {
var err error
entryTmp.RuntimeConfig, err = c.runtimeCatalog.Get(ctx, entryTmp.Runtime, consts.PluginRuntimeTypeContainer)
if err != nil {
return nil, fmt.Errorf("failed to get configured runtime for plugin %q: %w", plugin.Name, err)
}
}
// If the plugin type is unknown, we want to attempt to determine the type
if plugin.Type == consts.PluginTypeUnknown {
var err error
plugin.Type, err = c.getPluginTypeFromUnknown(ctx, entryTmp)
if err != nil {
return nil, err
}
if plugin.Type == consts.PluginTypeUnknown {
return nil, ErrPluginBadType
}
}
// getting the plugin version is best-effort, so errors are not fatal
runningVersion := logical.EmptyPluginVersion
var versionErr error
switch plugin.Type {
case consts.PluginTypeSecrets, consts.PluginTypeCredential:
runningVersion, versionErr = c.getBackendRunningVersion(ctx, entryTmp)
case consts.PluginTypeDatabase:
runningVersion, versionErr = c.getDatabaseRunningVersion(ctx, entryTmp)
default:
return nil, fmt.Errorf("unknown plugin type: %v", plugin.Type)
}
if versionErr != nil {
c.logger.Warn("Error determining plugin version", "error", versionErr)
if errors.Is(versionErr, ErrPluginUnableToRun) {
return nil, versionErr
}
} else if plugin.Version != "" && runningVersion.Version != "" && plugin.Version != runningVersion.Version {
c.logger.Error("Plugin self-reported version did not match requested version",
"plugin", plugin.Name, "requestedVersion", plugin.Version, "reportedVersion", runningVersion.Version)
return nil, fmt.Errorf("%w: %s reported version (%s) did not match requested version (%s)",
ErrPluginVersionMismatch, plugin.Name, runningVersion.Version, plugin.Version)
} else if plugin.Version == "" && runningVersion.Version != "" {
plugin.Version = runningVersion.Version
_, err := semver.NewVersion(plugin.Version)
if err != nil {
return nil, fmt.Errorf("plugin self-reported version %q is not a valid semantic version: %w", plugin.Version, err)
}
}
entry := &pluginutil.PluginRunner{
Name: plugin.Name,
Type: plugin.Type,
Version: plugin.Version,
Command: plugin.Command,
OCIImage: plugin.OCIImage,
Runtime: plugin.Runtime,
Args: plugin.Args,
Env: plugin.Env,
Sha256: plugin.Sha256,
Builtin: false,
Tmpdir: c.tmpdir,
}
buf, err := json.Marshal(entry)
if err != nil {
return nil, fmt.Errorf("failed to encode plugin entry: %w", err)
}
storageKey := path.Join(plugin.Type.String(), plugin.Name)
if plugin.Version != "" {
storageKey = path.Join(storageKey, plugin.Version)
}
logicalEntry := logical.StorageEntry{
Key: storageKey,
Value: buf,
}
if err := c.catalogView.Put(ctx, &logicalEntry); err != nil {
return nil, fmt.Errorf("failed to persist plugin entry: %w", err)
}
return entry, nil
}
func (c *PluginCatalog) entValidate(context.Context) error {
return nil
}

View file

@ -0,0 +1,43 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package plugincatalog
import (
"encoding/json"
"os"
"github.com/hashicorp/vault/sdk/helper/consts"
)
type pluginMetadata struct {
Version string `json:"version"`
Plugin Plugin `json:"plugin"`
}
type Plugin struct {
Name string `json:"name"`
Type consts.PluginType `json:"type"`
Tier consts.PluginTier `json:"tier,omitempty"`
// By is the plugin author's GitHub account, following Terraform Registry's convention
By string `json:"by"`
Version string `json:"version"`
Platform string `json:"platform"`
Arch string `json:"arch"`
// PGPSig is PGP ASCII armored detached signature
PGPSig string `json:"pgp_sig"`
}
func readPluginMetadata(metadataPath string) (*pluginMetadata, error) {
metadataBytes, err := os.ReadFile(metadataPath)
if err != nil {
return nil, err
}
metadata := pluginMetadata{}
if err = json.Unmarshal(metadataBytes, &metadata); err != nil {
return nil, err
}
return &metadata, nil
}

View file

@ -0,0 +1,87 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package plugincatalog
import (
"encoding/json"
"fmt"
"runtime"
"testing"
"github.com/ProtonMail/gopenpgp/v3/crypto"
"github.com/google/uuid"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/stretchr/testify/assert"
)
// gereratePGPKeyPair generates a PGP key pair for testing purposes
func generatePGPKeyPair(t *testing.T) (*crypto.Key, string) {
pgp := crypto.PGP()
user := "test" + uuid.NewString()[:5]
privKey, err := pgp.KeyGeneration().AddUserId(user, fmt.Sprintf("%s@hashicorp.com", user)).New().GenerateKey()
assert.NoError(t, err)
pubkey, err := privKey.ToPublic()
assert.NoError(t, err)
armored, err := pubkey.Armor()
assert.NoError(t, err)
return privKey, armored
}
// generatePluginArtifactContents generates file contents for a plugin artifact for testing purposes
// If key is nil, signatures will carry a placeholder value
func generatePluginArtifactContents(t *testing.T, pluginName, pluginVersion string, pluginType consts.PluginType,
includeBinarySig bool, privKey *crypto.Key,
) map[string][]byte {
t.Helper()
metadata := pluginMetadata{
Version: "v0",
Plugin: Plugin{
Name: pluginName,
Type: pluginType,
Tier: consts.PluginTierOfficial,
By: "hashicorp",
Version: pluginVersion,
Platform: runtime.GOOS,
Arch: runtime.GOARCH,
PGPSig: "signature-placeholder",
},
}
metadataBytes, err := json.Marshal(metadata)
assert.NoError(t, err)
pluginBytes := []byte("plugin-binary-placeholder")
metadataSigBytes := []byte("signature-placeholder")
pgp := crypto.PGP()
if privKey != nil {
signer, err := pgp.Sign().SigningKey(privKey).Detached().New()
defer signer.ClearPrivateParams()
assert.NoError(t, err)
// exclude binary signature for bad plugin signature read test
metadata.Plugin.PGPSig = ""
if includeBinarySig {
signature, err := signer.Sign(pluginBytes, crypto.Armor)
assert.NoError(t, err)
metadata.Plugin.PGPSig = string(signature)
}
metadataBytes, err = json.Marshal(metadata)
assert.NoError(t, err)
metadataSigBytes, err = signer.Sign(metadataBytes, crypto.Armor)
assert.NoError(t, err)
}
return map[string][]byte{
metadataFile: metadataBytes,
metadataSig: metadataSigBytes,
pluginName: pluginBytes,
"LICENSE": []byte("license-placeholder"),
}
}