Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow plugins to be specified as direct file paths #36

Merged
merged 5 commits into from Apr 1, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions configutil/Makefile
@@ -0,0 +1,8 @@
PLUGIN_TMP_DIR := $(shell mktemp -d)

test-plugin:
jefferai marked this conversation as resolved.
Show resolved Hide resolved
go build -o "${PLUGIN_TMP_DIR}/aeadplugin" testplugins/aead/main.go
PLUGIN_PATH="${PLUGIN_TMP_DIR}/aeadplugin" go test -v -run TestFilePlugin


.PHONY: test-plugin
168 changes: 168 additions & 0 deletions configutil/file_plugin_test.go
@@ -0,0 +1,168 @@
package configutil

import (
context "context"
jefferai marked this conversation as resolved.
Show resolved Hide resolved
jefferai marked this conversation as resolved.
Show resolved Hide resolved
jefferai marked this conversation as resolved.
Show resolved Hide resolved
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"testing"

wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-secure-stdlib/pluginutil/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/sha3"
)

func TestFilePlugin(t *testing.T) {
ctx := context.Background()

pluginPath := os.Getenv("PLUGIN_PATH")
if pluginPath == "" {
t.Skipf("skipping plugin test as no PLUGIN_PATH specified")
}

pluginBytes, err := os.ReadFile(pluginPath)
require.NoError(t, err)

sha2256Bytes := sha256.Sum256(pluginBytes)
modifiedSha2 := sha256.Sum256(pluginBytes)
modifiedSha2[0] = '0'
modifiedSha2[1] = '0'
sha3384Hash := sha3.New384()
_, err = sha3384Hash.Write(pluginBytes)
require.NoError(t, err)
sha3384Bytes := sha3384Hash.Sum(nil)

testCases := []struct {
name string // name of the test
pluginChecksum []byte // checksum to use
pluginHashMethod pluginutil.HashMethod // hash method to use
wantErrContains string // Error from the plugin process
hacheSeeEll string // If set, will be parsed and used to populate values
wantConfigErrContains string // Error from any set config
}{
{
name: "valid checksum",
pluginChecksum: sha2256Bytes[:],
pluginHashMethod: pluginutil.HashMethodSha2256,
},
{
name: "invalid checksum",
pluginChecksum: modifiedSha2[:],
pluginHashMethod: pluginutil.HashMethodSha2256,
wantErrContains: "checksums did not match",
},
{
name: "valid checksum, other type",
pluginChecksum: sha3384Bytes[:],
pluginHashMethod: pluginutil.HashMethodSha3384,
},
{
name: "invalid hcl no checksum",
hacheSeeEll: fmt.Sprintf(`
kms "aead" {
purpose = "root"
aead_type = "aes-gcm"
plugin_path = "%s"
}
`, pluginPath),
wantConfigErrContains: "plugin_path specified but plugin_checksum empty",
},
{
name: "invalid hcl no path",
hacheSeeEll: fmt.Sprintf(`
kms "aead" {
purpose = "root"
aead_type = "aes-gcm"
plugin_checksum = "%s"
}
`, hex.EncodeToString(sha2256Bytes[:])),
wantConfigErrContains: "plugin_checksum specified but plugin_path empty",
},
{
name: "invalid hcl unknown hash method",
hacheSeeEll: fmt.Sprintf(`
kms "aead" {
purpose = "root"
aead_type = "aes-gcm"
plugin_path = "%s"
plugin_checksum = "%s"
plugin_hash_method = "foobar"
}
`, pluginPath, hex.EncodeToString(sha2256Bytes[:])),
wantErrContains: "unsupported hash method",
},
{
name: "valid hcl",
hacheSeeEll: fmt.Sprintf(`
kms "aead" {
purpose = "root"
aead_type = "aes-gcm"
plugin_path = "%s"
plugin_checksum = "%s"
}
`, pluginPath, hex.EncodeToString(sha2256Bytes[:])),
},
{
name: "valid hcl alternate checksum",
hacheSeeEll: fmt.Sprintf(`
kms "aead" {
purpose = "root"
aead_type = "aes-gcm"
plugin_path = "%s"
plugin_checksum = "%s"
plugin_hash_method = "%s"
}
`, pluginPath, hex.EncodeToString(sha3384Bytes[:]), pluginutil.HashMethodSha3384),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
var kms *KMS
var pluginOpts []pluginutil.Option
switch tc.hacheSeeEll == "" {
case true:
kms = &KMS{
Type: string(wrapping.WrapperTypeAead),
Purpose: []string{"foobar"},
}
pluginOpts = append(pluginOpts, pluginutil.WithPluginFile(
pluginutil.PluginFileInfo{
Name: "aead",
Path: pluginPath,
Checksum: tc.pluginChecksum,
HashMethod: tc.pluginHashMethod,
}),
)
default:
conf, err := ParseConfig(tc.hacheSeeEll)
if tc.wantConfigErrContains != "" {
require.Error(err)
assert.Contains(err.Error(), tc.wantConfigErrContains)
return
}
require.NoError(err)
require.Len(conf.Seals, 1)
kms = conf.Seals[0]
}
wrapper, cleanup, err := configureWrapper(
ctx,
kms,
nil,
nil,
WithPluginOptions(pluginOpts...),
)
if tc.wantErrContains != "" {
require.Error(err)
assert.Contains(err.Error(), tc.wantErrContains)
return
}
require.NoError(err)
assert.NotNil(wrapper)
assert.NoError(cleanup())
})
}
}
4 changes: 2 additions & 2 deletions configutil/go.mod
Expand Up @@ -10,9 +10,10 @@ require (
github.com/hashicorp/go-plugin v1.4.3
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.4
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.0
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.1-0.20220318133345-4b0c6b19f281
github.com/hashicorp/hcl v1.0.0
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000
google.golang.org/protobuf v1.27.1
)

Expand Down Expand Up @@ -48,7 +49,6 @@ require (
github.com/posener/complete v1.1.1 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a // indirect
golang.org/x/text v0.3.7 // indirect
Expand Down
4 changes: 2 additions & 2 deletions configutil/go.sum
Expand Up @@ -84,8 +84,8 @@ github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.4/go.mod h1:myX7XYMJRIP4
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2 h1:Tz6v3Jb2DRnDCfifRSjYKG0m8dLdNq6bcDkB41en7nw=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.0 h1:TszfJJ1vJWdvPkPxYivP5TUTkOaXziE1jTKGmfOck94=
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.0/go.mod h1:Io+g8iYs//ESo5x2Gy1rTTYaIHUyCZD7h16K7DiOUI4=
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.1-0.20220318133345-4b0c6b19f281 h1:Yp+M74XOcvLcpIF1yQXjEJq4lbG3F7F2fUmpNSZ/+Bo=
github.com/hashicorp/go-secure-stdlib/pluginutil/v2 v2.0.1-0.20220318133345-4b0c6b19f281/go.mod h1:Mh8pP9IH9qnBQSSH3PYtSkGq3+KlIuw5kAS9W8++BLc=
github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1 h1:SMGUnbpAcat8rIKHkBPjfv81yC46a8eCNZ2hsR2l1EI=
github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1/go.mod h1:Ch/bf00Qnx77MZd49JRgHYqHQjtEmTgGU2faufpVZb0=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788=
Expand Down
103 changes: 94 additions & 9 deletions configutil/kms.go
Expand Up @@ -3,6 +3,8 @@ package configutil
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"strings"
Expand Down Expand Up @@ -42,8 +44,28 @@ type KMS struct {
// one KMS to be specified
Purpose []string `hcl:"-"`

// Disabled can be used by an application to understand intent. This was
// mostly for Vault to enable seal migration and should be considered
// deprecated in favor of using purposes.
Disabled bool
Config map[string]string

// PluginPath can be used, if using a file on disk as a wrapper plugin, to
// specify a path to the file. This can also be specified via pluginutil
// options from the application.
PluginPath string `hcl:"plugin_path"`
// PluginChecksum is a hex-encoded checksum using the specified
// PluginHashMethod. Required when specifying a file path. It's hex-encoded
// since most command-line tools output e.g. SHA sums as hex so it's
// generally easier for the user to specify.
PluginChecksum string `hcl:"plugin_checksum"`
pluginChecksumBytes []byte `hcl:"-"` // To store decoded checksum bytes
// PluginHashMethod specifies the hash algorithm to use. See pluginutil
// for currently-supported hash mechanisms and their string representations.
// Empty will default to "sha2-256".
PluginHashMethod string `hcl:"plugin_hash_method"`

// Config is passed to the underlying wrappers
Config map[string]string
}

func (k *KMS) GoString() string {
Expand Down Expand Up @@ -101,6 +123,50 @@ func parseKMS(result *[]*KMS, list *ast.ObjectList, blockName string, opt ...Opt
delete(m, "disabled")
}

seal := &KMS{
Type: strings.ToLower(key),
Purpose: purpose,
Disabled: disabled,
}

const (
pluginPath = "plugin_path"
pluginChecksum = "plugin_checksum"
pluginHashMethod = "plugin_hash_method"
)
for _, v := range []string{pluginPath, pluginChecksum, pluginHashMethod} {
currVal := m[v]
if currVal == nil {
continue
}
s, err := parseutil.ParseString(currVal)
if err != nil {
return multierror.Prefix(err, fmt.Sprintf("%s.%s:", blockName, key))
}
switch v {
case pluginPath:
seal.PluginPath = s
delete(m, pluginPath)
case pluginChecksum:
seal.PluginChecksum = s
delete(m, pluginChecksum)
seal.pluginChecksumBytes, err = hex.DecodeString(seal.PluginChecksum)
if err != nil {
return multierror.Prefix(fmt.Errorf("error parsing %s as hex: %w", pluginChecksum, err), fmt.Sprintf("%s.%s:", blockName, key))
}
case pluginHashMethod:
seal.PluginHashMethod = s
delete(m, pluginHashMethod)
}
}
switch {
case seal.PluginPath != "" && seal.PluginChecksum == "":
return multierror.Prefix(fmt.Errorf("%s specified but %s empty", pluginPath, pluginChecksum), fmt.Sprintf("%s.%s:", blockName, key))
case seal.PluginPath == "" && seal.PluginChecksum != "":
return multierror.Prefix(fmt.Errorf("%s specified but %s empty", pluginChecksum, pluginPath), fmt.Sprintf("%s.%s:", blockName, key))
}

// Put the rest into config
strMap := make(map[string]string, len(m))
for k, v := range m {
s, err := parseutil.ParseString(v)
Expand All @@ -109,12 +175,6 @@ func parseKMS(result *[]*KMS, list *ast.ObjectList, blockName string, opt ...Opt
}
strMap[k] = s
}

seal := &KMS{
Type: strings.ToLower(key),
Purpose: purpose,
Disabled: disabled,
}
if len(strMap) > 0 {
seal.Config = strMap
}
Expand Down Expand Up @@ -208,10 +268,35 @@ func configureWrapper(
return nil, nil, fmt.Errorf("error parsing config options: %w", err)
}

// If the KMS block contained plugin file information, add it
pluginOpts := opts.withPluginOptions
switch {
case configKMS.PluginPath == "" && configKMS.PluginChecksum == "":
case configKMS.PluginPath == "" && configKMS.PluginChecksum != "":
return nil, nil, errors.New("plugin checksum specified in kms but plugin path empty")
case configKMS.PluginPath != "" && configKMS.PluginChecksum == "" && len(configKMS.pluginChecksumBytes) == 0:
return nil, nil, errors.New("plugin path specified in kms but plugin checksum empty")
jefferai marked this conversation as resolved.
Show resolved Hide resolved
default:
if len(configKMS.pluginChecksumBytes) == 0 {
configKMS.pluginChecksumBytes, err = hex.DecodeString(configKMS.PluginChecksum)
if err != nil {
return nil, nil, fmt.Errorf("error parsing plugin checksum as hex: %w", err)
}
}
pluginOpts = append(pluginOpts, pluginutil.WithPluginFile(
pluginutil.PluginFileInfo{
Name: configKMS.Type,
Path: configKMS.PluginPath,
Checksum: configKMS.pluginChecksumBytes,
HashMethod: pluginutil.HashMethod(configKMS.PluginHashMethod),
},
))
}

// First, scan available plugins and build info
pluginMap, err := pluginutil.BuildPluginMap(
append(
opts.withPluginOptions,
pluginOpts,
pluginutil.WithPluginClientCreationFunc(
func(pluginPath string, rtOpt ...pluginutil.Option) (*plugin.Client, error) {
rtOpts, err := pluginutil.GetOpts(rtOpt...)
Expand Down Expand Up @@ -271,7 +356,7 @@ func configureWrapper(
return nil, cleanup, fmt.Errorf("error setting configuration on the kms plugin: %w", err)
}
kmsInfo := wrapperConfigResult.GetMetadata()
if len(kmsInfo) > 0 {
if len(kmsInfo) > 0 && infoKeys != nil && info != nil && *info != nil {
populateInfo(configKMS, infoKeys, info, kmsInfo)
}

Expand Down
17 changes: 17 additions & 0 deletions configutil/testplugins/aead/main.go
@@ -0,0 +1,17 @@
package main

import (
"fmt"
"os"

gkwp "github.com/hashicorp/go-kms-wrapping/plugin/v2"
aead "github.com/hashicorp/go-kms-wrapping/v2/aead"
)

func main() {
if err := gkwp.ServePlugin(aead.NewWrapper()); err != nil {
fmt.Println("Error serving plugin", err)
os.Exit(1)
}
os.Exit(0)
johanbrandhorst marked this conversation as resolved.
Show resolved Hide resolved
}
1 change: 1 addition & 0 deletions pluginutil/go.mod
Expand Up @@ -5,6 +5,7 @@ go 1.17
require (
github.com/hashicorp/go-plugin v1.4.3
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
)

require (
Expand Down
1 change: 1 addition & 0 deletions pluginutil/go.sum
Expand Up @@ -84,6 +84,7 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand Down