snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
This commit is contained in:
103
credentials/tls/certprovider/pemfile/builder.go
Normal file
103
credentials/tls/certprovider/pemfile/builder.go
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package pemfile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/credentials/tls/certprovider"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
const (
|
||||
// PluginName is the name of the PEM file watcher plugin.
|
||||
PluginName = "file_watcher"
|
||||
defaultRefreshInterval = 10 * time.Minute
|
||||
)
|
||||
|
||||
func init() {
|
||||
certprovider.Register(&pluginBuilder{})
|
||||
}
|
||||
|
||||
type pluginBuilder struct{}
|
||||
|
||||
func (p *pluginBuilder) ParseConfig(c any) (*certprovider.BuildableConfig, error) {
|
||||
data, ok := c.(json.RawMessage)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("meshca: unsupported config type: %T", c)
|
||||
}
|
||||
opts, err := pluginConfigFromJSON(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return certprovider.NewBuildableConfig(PluginName, opts.canonical(), func(certprovider.BuildOptions) certprovider.Provider {
|
||||
return newProvider(opts)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p *pluginBuilder) Name() string {
|
||||
return PluginName
|
||||
}
|
||||
|
||||
func pluginConfigFromJSON(jd json.RawMessage) (Options, error) {
|
||||
// The only difference between this anonymous struct and the Options struct
|
||||
// is that the refresh_interval is represented here as a duration proto,
|
||||
// while in the latter a time.Duration is used.
|
||||
cfg := &struct {
|
||||
CertificateFile string `json:"certificate_file,omitempty"`
|
||||
PrivateKeyFile string `json:"private_key_file,omitempty"`
|
||||
CACertificateFile string `json:"ca_certificate_file,omitempty"`
|
||||
SPIFFETrustBundleMapFile string `json:"spiffe_trust_bundle_map_file,omitempty"`
|
||||
RefreshInterval json.RawMessage `json:"refresh_interval,omitempty"`
|
||||
}{}
|
||||
if err := json.Unmarshal(jd, cfg); err != nil {
|
||||
return Options{}, fmt.Errorf("pemfile: json.Unmarshal(%s) failed: %v", string(jd), err)
|
||||
}
|
||||
if !envconfig.XDSSPIFFEEnabled {
|
||||
cfg.SPIFFETrustBundleMapFile = ""
|
||||
}
|
||||
|
||||
opts := Options{
|
||||
CertFile: cfg.CertificateFile,
|
||||
KeyFile: cfg.PrivateKeyFile,
|
||||
RootFile: cfg.CACertificateFile,
|
||||
SPIFFEBundleMapFile: cfg.SPIFFETrustBundleMapFile,
|
||||
// Refresh interval is the only field in the configuration for which we
|
||||
// support a default value. We cannot possibly have valid defaults for
|
||||
// file paths to watch. Also, it is valid to specify an empty path for
|
||||
// some of those fields if the user does not want to watch them.
|
||||
RefreshDuration: defaultRefreshInterval,
|
||||
}
|
||||
if cfg.RefreshInterval != nil {
|
||||
dur := &durationpb.Duration{}
|
||||
if err := protojson.Unmarshal(cfg.RefreshInterval, dur); err != nil {
|
||||
return Options{}, fmt.Errorf("pemfile: protojson.Unmarshal(%+v) failed: %v", cfg.RefreshInterval, err)
|
||||
}
|
||||
opts.RefreshDuration = dur.AsDuration()
|
||||
}
|
||||
|
||||
if err := opts.validate(); err != nil {
|
||||
return Options{}, err
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
162
credentials/tls/certprovider/pemfile/builder_test.go
Normal file
162
credentials/tls/certprovider/pemfile/builder_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package pemfile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
)
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
input any
|
||||
wantOutput string
|
||||
wantErr bool
|
||||
enabledSpiffe bool
|
||||
}{
|
||||
{
|
||||
desc: "non JSON input",
|
||||
input: new(int),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid JSON",
|
||||
input: json.RawMessage(`bad bad json`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "JSON input does not match expected",
|
||||
input: json.RawMessage(`["foo": "bar"]`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "no credential files",
|
||||
input: json.RawMessage(`{}`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "only cert file",
|
||||
input: json.RawMessage(`
|
||||
{
|
||||
"certificate_file": "/a/b/cert.pem"
|
||||
}`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "only key file",
|
||||
input: json.RawMessage(`
|
||||
{
|
||||
"private_key_file": "/a/b/key.pem"
|
||||
}`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "cert and key in different directories",
|
||||
input: json.RawMessage(`
|
||||
{
|
||||
"certificate_file": "/b/a/cert.pem",
|
||||
"private_key_file": "/a/b/key.pem"
|
||||
}`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "bad refresh duration",
|
||||
input: json.RawMessage(`
|
||||
{
|
||||
"certificate_file": "/a/b/cert.pem",
|
||||
"private_key_file": "/a/b/key.pem",
|
||||
"ca_certificate_file": "/a/b/ca.pem",
|
||||
"refresh_interval": "duration"
|
||||
}`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "good config with default refresh interval",
|
||||
input: json.RawMessage(`
|
||||
{
|
||||
"certificate_file": "/a/b/cert.pem",
|
||||
"private_key_file": "/a/b/key.pem",
|
||||
"ca_certificate_file": "/a/b/ca.pem"
|
||||
}`),
|
||||
wantOutput: "file_watcher:/a/b/cert.pem:/a/b/key.pem:/a/b/ca.pem::10m0s",
|
||||
},
|
||||
{
|
||||
desc: "good config",
|
||||
input: json.RawMessage(`
|
||||
{
|
||||
"certificate_file": "/a/b/cert.pem",
|
||||
"private_key_file": "/a/b/key.pem",
|
||||
"ca_certificate_file": "/a/b/ca.pem",
|
||||
"refresh_interval": "200s"
|
||||
}`),
|
||||
wantOutput: "file_watcher:/a/b/cert.pem:/a/b/key.pem:/a/b/ca.pem::3m20s",
|
||||
},
|
||||
{
|
||||
desc: "good config with spiffe disabled",
|
||||
input: json.RawMessage(`
|
||||
{
|
||||
"certificate_file": "/a/b/cert.pem",
|
||||
"private_key_file": "/a/b/key.pem",
|
||||
"ca_certificate_file": "/a/b/ca.pem",
|
||||
"spiffe_trust_bundle_map_file": "/a/b/spiffe_bundle.json",
|
||||
"refresh_interval": "200s"
|
||||
}`),
|
||||
wantOutput: "file_watcher:/a/b/cert.pem:/a/b/key.pem:/a/b/ca.pem::3m20s",
|
||||
},
|
||||
{
|
||||
desc: "good config with spiffe enabled",
|
||||
input: json.RawMessage(`
|
||||
{
|
||||
"certificate_file": "/a/b/cert.pem",
|
||||
"private_key_file": "/a/b/key.pem",
|
||||
"ca_certificate_file": "/a/b/ca.pem",
|
||||
"spiffe_trust_bundle_map_file": "/a/b/spiffe_bundle.json",
|
||||
"refresh_interval": "200s"
|
||||
}`),
|
||||
wantOutput: "file_watcher:/a/b/cert.pem:/a/b/key.pem:/a/b/ca.pem:/a/b/spiffe_bundle.json:3m20s",
|
||||
enabledSpiffe: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if test.enabledSpiffe {
|
||||
testutils.SetEnvConfig(t, &envconfig.XDSSPIFFEEnabled, true)
|
||||
}
|
||||
builder := &pluginBuilder{}
|
||||
|
||||
bc, err := builder.ParseConfig(test.input)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("ParseConfig(%+v) failed: %v", test.input, err)
|
||||
}
|
||||
if test.wantErr {
|
||||
return
|
||||
}
|
||||
|
||||
gotConfig := bc.String()
|
||||
if gotConfig != test.wantOutput {
|
||||
t.Fatalf("ParseConfig(%v) = %s, want %s", test.input, gotConfig, test.wantOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
297
credentials/tls/certprovider/pemfile/watcher.go
Normal file
297
credentials/tls/certprovider/pemfile/watcher.go
Normal file
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package pemfile provides a file watching certificate provider plugin
|
||||
// implementation which works for files with PEM contents.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: All APIs in this package are experimental and may be removed in a
|
||||
// later release.
|
||||
package pemfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/credentials/tls/certprovider"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/internal/credentials/spiffe"
|
||||
)
|
||||
|
||||
const defaultCertRefreshDuration = 1 * time.Hour
|
||||
|
||||
var (
|
||||
// For overriding from unit tests.
|
||||
newDistributor = func() distributor { return certprovider.NewDistributor() }
|
||||
|
||||
logger = grpclog.Component("pemfile")
|
||||
)
|
||||
|
||||
// Options configures a certificate provider plugin that watches a specified set
|
||||
// of files that contain certificates and keys in PEM format.
|
||||
type Options struct {
|
||||
// CertFile is the file that holds the identity certificate.
|
||||
// Optional. If this is set, KeyFile must also be set.
|
||||
CertFile string
|
||||
// KeyFile is the file that holds identity private key.
|
||||
// Optional. If this is set, CertFile must also be set.
|
||||
KeyFile string
|
||||
// RootFile is the file that holds trusted root certificate(s).
|
||||
// Optional.
|
||||
RootFile string
|
||||
// SPIFFEBundleMapFile is the file that holds the spiffe bundle map.
|
||||
// If a given provider configures both the RootFile and the
|
||||
// SPIFFEBundleMapFile, the SPIFFEBundleMapFile will be preferred.
|
||||
// Optional.
|
||||
SPIFFEBundleMapFile string
|
||||
// RefreshDuration is the amount of time the plugin waits before checking
|
||||
// for updates in the specified files.
|
||||
// Optional. If not set, a default value (1 hour) will be used.
|
||||
RefreshDuration time.Duration
|
||||
}
|
||||
|
||||
func (o Options) canonical() []byte {
|
||||
return []byte(fmt.Sprintf("%s:%s:%s:%s:%s", o.CertFile, o.KeyFile, o.RootFile, o.SPIFFEBundleMapFile, o.RefreshDuration))
|
||||
}
|
||||
|
||||
func (o Options) validate() error {
|
||||
if o.CertFile == "" && o.KeyFile == "" && o.RootFile == "" && o.SPIFFEBundleMapFile == "" {
|
||||
return fmt.Errorf("pemfile: at least one credential file needs to be specified")
|
||||
}
|
||||
if keySpecified, certSpecified := o.KeyFile != "", o.CertFile != ""; keySpecified != certSpecified {
|
||||
return fmt.Errorf("pemfile: private key file and identity cert file should be both specified or not specified")
|
||||
}
|
||||
// C-core has a limitation that they cannot verify that a certificate file
|
||||
// matches a key file. So, the only way to get around this is to make sure
|
||||
// that both files are in the same directory and that they do an atomic
|
||||
// read. Even though Java/Go do not have this limitation, we want the
|
||||
// overall plugin behavior to be consistent across languages.
|
||||
if certDir, keyDir := filepath.Dir(o.CertFile), filepath.Dir(o.KeyFile); certDir != keyDir {
|
||||
return errors.New("pemfile: certificate and key file must be in the same directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewProvider returns a new certificate provider plugin that is configured to
|
||||
// watch the PEM files specified in the passed in options.
|
||||
func NewProvider(o Options) (certprovider.Provider, error) {
|
||||
if err := o.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newProvider(o), nil
|
||||
}
|
||||
|
||||
// newProvider is used to create a new certificate provider plugin after
|
||||
// validating the options, and hence does not return an error.
|
||||
func newProvider(o Options) certprovider.Provider {
|
||||
if o.RefreshDuration == 0 {
|
||||
o.RefreshDuration = defaultCertRefreshDuration
|
||||
}
|
||||
|
||||
provider := &watcher{opts: o}
|
||||
if o.CertFile != "" && o.KeyFile != "" {
|
||||
provider.identityDistributor = newDistributor()
|
||||
}
|
||||
if o.RootFile != "" || o.SPIFFEBundleMapFile != "" {
|
||||
provider.rootDistributor = newDistributor()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
provider.cancel = cancel
|
||||
go provider.run(ctx)
|
||||
return provider
|
||||
}
|
||||
|
||||
// watcher is a certificate provider plugin that implements the
|
||||
// certprovider.Provider interface. It watches a set of certificate and key
|
||||
// files and provides the most up-to-date key material for consumption by
|
||||
// credentials implementation.
|
||||
type watcher struct {
|
||||
identityDistributor distributor
|
||||
rootDistributor distributor
|
||||
opts Options
|
||||
certFileContents []byte
|
||||
keyFileContents []byte
|
||||
rootFileContents []byte
|
||||
spiffeBundleMapFileContents []byte
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// distributor wraps the methods on certprovider.Distributor which are used by
|
||||
// the plugin. This is very useful in tests which need to know exactly when the
|
||||
// plugin updates its key material.
|
||||
type distributor interface {
|
||||
KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error)
|
||||
Set(km *certprovider.KeyMaterial, err error)
|
||||
Stop()
|
||||
}
|
||||
|
||||
// updateIdentityDistributor checks if the cert/key files that the plugin is
|
||||
// watching have changed, and if so, reads the new contents and updates the
|
||||
// identityDistributor with the new key material.
|
||||
//
|
||||
// Skips updates when file reading or parsing fails.
|
||||
// TODO(easwars): Retry with limit (on the number of retries or the amount of
|
||||
// time) upon failures.
|
||||
func (w *watcher) updateIdentityDistributor() {
|
||||
if w.identityDistributor == nil {
|
||||
return
|
||||
}
|
||||
|
||||
certFileContents, err := os.ReadFile(w.opts.CertFile)
|
||||
if err != nil {
|
||||
logger.Warningf("certFile (%s) read failed: %v", w.opts.CertFile, err)
|
||||
return
|
||||
}
|
||||
keyFileContents, err := os.ReadFile(w.opts.KeyFile)
|
||||
if err != nil {
|
||||
logger.Warningf("keyFile (%s) read failed: %v", w.opts.KeyFile, err)
|
||||
return
|
||||
}
|
||||
// If the file contents have not changed, skip updating the distributor.
|
||||
if bytes.Equal(w.certFileContents, certFileContents) && bytes.Equal(w.keyFileContents, keyFileContents) {
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(certFileContents, keyFileContents)
|
||||
if err != nil {
|
||||
logger.Warningf("tls.X509KeyPair(%q, %q) failed: %v", certFileContents, keyFileContents, err)
|
||||
return
|
||||
}
|
||||
w.certFileContents = certFileContents
|
||||
w.keyFileContents = keyFileContents
|
||||
w.identityDistributor.Set(&certprovider.KeyMaterial{Certs: []tls.Certificate{cert}}, nil)
|
||||
}
|
||||
|
||||
// updateRootDistributor checks if the root cert file that the plugin is
|
||||
// watching hs changed, and if so, updates the rootDistributor with the new key
|
||||
// material.
|
||||
//
|
||||
// Skips updates when root cert reading or parsing fails.
|
||||
// TODO(easwars): Retry with limit (on the number of retries or the amount of
|
||||
// time) upon failures.
|
||||
func (w *watcher) updateRootDistributor() {
|
||||
if w.rootDistributor == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If SPIFFEBundleMap is set, use it and DON'T use the RootFile, even if it
|
||||
// fails
|
||||
if w.opts.SPIFFEBundleMapFile != "" {
|
||||
w.maybeUpdateSPIFFEBundleMap()
|
||||
} else {
|
||||
w.maybeUpdateRootFile()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) maybeUpdateSPIFFEBundleMap() {
|
||||
spiffeBundleMapContents, err := os.ReadFile(w.opts.SPIFFEBundleMapFile)
|
||||
if err != nil {
|
||||
logger.Warningf("spiffeBundleMapFile (%s) read failed: %v", w.opts.SPIFFEBundleMapFile, err)
|
||||
return
|
||||
}
|
||||
// If the file contents have not changed, skip updating the distributor.
|
||||
if bytes.Equal(w.spiffeBundleMapFileContents, spiffeBundleMapContents) {
|
||||
return
|
||||
}
|
||||
bundleMap, err := spiffe.BundleMapFromBytes(spiffeBundleMapContents)
|
||||
if err != nil {
|
||||
logger.Warning("Failed to parse spiffe bundle map")
|
||||
return
|
||||
}
|
||||
w.spiffeBundleMapFileContents = spiffeBundleMapContents
|
||||
w.rootDistributor.Set(&certprovider.KeyMaterial{SPIFFEBundleMap: bundleMap}, nil)
|
||||
}
|
||||
|
||||
func (w *watcher) maybeUpdateRootFile() {
|
||||
rootFileContents, err := os.ReadFile(w.opts.RootFile)
|
||||
if err != nil {
|
||||
logger.Warningf("rootFile (%s) read failed: %v", w.opts.RootFile, err)
|
||||
return
|
||||
}
|
||||
trustPool := x509.NewCertPool()
|
||||
if !trustPool.AppendCertsFromPEM(rootFileContents) {
|
||||
logger.Warning("Failed to parse root certificate")
|
||||
return
|
||||
}
|
||||
// If the file contents have not changed, skip updating the distributor.
|
||||
if bytes.Equal(w.rootFileContents, rootFileContents) {
|
||||
return
|
||||
}
|
||||
|
||||
w.rootFileContents = rootFileContents
|
||||
w.rootDistributor.Set(&certprovider.KeyMaterial{Roots: trustPool}, nil)
|
||||
}
|
||||
|
||||
// run is a long running goroutine which watches the configured files for
|
||||
// changes, and pushes new key material into the appropriate distributors which
|
||||
// is returned from calls to KeyMaterial().
|
||||
func (w *watcher) run(ctx context.Context) {
|
||||
ticker := time.NewTicker(w.opts.RefreshDuration)
|
||||
for {
|
||||
w.updateIdentityDistributor()
|
||||
w.updateRootDistributor()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
if w.identityDistributor != nil {
|
||||
w.identityDistributor.Stop()
|
||||
}
|
||||
if w.rootDistributor != nil {
|
||||
w.rootDistributor.Stop()
|
||||
}
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KeyMaterial returns the key material sourced by the watcher.
|
||||
// Callers are expected to use the returned value as read-only.
|
||||
func (w *watcher) KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error) {
|
||||
km := &certprovider.KeyMaterial{}
|
||||
if w.identityDistributor != nil {
|
||||
identityKM, err := w.identityDistributor.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
km.Certs = identityKM.Certs
|
||||
}
|
||||
if w.rootDistributor != nil {
|
||||
rootKM, err := w.rootDistributor.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
km.SPIFFEBundleMap = rootKM.SPIFFEBundleMap
|
||||
km.Roots = rootKM.Roots
|
||||
}
|
||||
return km, nil
|
||||
}
|
||||
|
||||
// Close cleans up resources allocated by the watcher.
|
||||
func (w *watcher) Close() {
|
||||
w.cancel()
|
||||
}
|
||||
629
credentials/tls/certprovider/pemfile/watcher_test.go
Normal file
629
credentials/tls/certprovider/pemfile/watcher_test.go
Normal file
@@ -0,0 +1,629 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package pemfile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/credentials/tls/certprovider"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/testdata"
|
||||
)
|
||||
|
||||
const (
|
||||
// These are the names of files inside temporary directories, which the
|
||||
// plugin is asked to watch.
|
||||
certFile = "cert.pem"
|
||||
keyFile = "key.pem"
|
||||
rootFile = "ca.pem"
|
||||
spiffeBundleFile = "spiffebundle.json"
|
||||
|
||||
defaultTestRefreshDuration = 100 * time.Millisecond
|
||||
defaultTestTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
func compareKeyMaterial(got, want *certprovider.KeyMaterial) error {
|
||||
if len(got.Certs) != len(want.Certs) {
|
||||
return fmt.Errorf("keyMaterial certs = %+v, want %+v", got, want)
|
||||
}
|
||||
for i := 0; i < len(got.Certs); i++ {
|
||||
if !got.Certs[i].Leaf.Equal(want.Certs[i].Leaf) {
|
||||
return fmt.Errorf("keyMaterial certs = %+v, want %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
if gotR, wantR := got.Roots, want.Roots; !gotR.Equal(wantR) {
|
||||
return fmt.Errorf("keyMaterial roots = %v, want %v", gotR, wantR)
|
||||
}
|
||||
|
||||
if gotBundle, wantBundle := got.SPIFFEBundleMap, want.SPIFFEBundleMap; !cmp.Equal(gotBundle, wantBundle) {
|
||||
return fmt.Errorf("keyMaterial spiffe bundle map = %v, want %v", gotBundle, wantBundle)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestNewProvider tests the NewProvider() function with different inputs.
|
||||
func (s) TestNewProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
options Options
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
desc: "No credential files specified",
|
||||
options: Options{},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
desc: "Only identity cert is specified",
|
||||
options: Options{
|
||||
CertFile: testdata.Path("x509/client1_cert.pem"),
|
||||
},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
desc: "Only identity key is specified",
|
||||
options: Options{
|
||||
KeyFile: testdata.Path("x509/client1_key.pem"),
|
||||
},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
desc: "Identity cert/key pair is specified",
|
||||
options: Options{
|
||||
KeyFile: testdata.Path("x509/client1_key.pem"),
|
||||
CertFile: testdata.Path("x509/client1_cert.pem"),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Only root certs are specified",
|
||||
options: Options{
|
||||
RootFile: testdata.Path("x509/client_ca_cert.pem"),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Only spiffe bundle map specified",
|
||||
options: Options{
|
||||
SPIFFEBundleMapFile: testdata.Path("spiffe/spiffebundle.json"),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Everything is specified",
|
||||
options: Options{
|
||||
KeyFile: testdata.Path("x509/client1_key.pem"),
|
||||
CertFile: testdata.Path("x509/client1_cert.pem"),
|
||||
RootFile: testdata.Path("x509/client_ca_cert.pem"),
|
||||
SPIFFEBundleMapFile: testdata.Path("spiffe/spiffebundle.json"),
|
||||
},
|
||||
wantError: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
provider, err := NewProvider(test.options)
|
||||
if (err != nil) != test.wantError {
|
||||
t.Fatalf("NewProvider(%v) = %v, want %v", test.options, err, test.wantError)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
provider.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// wrappedDistributor wraps a distributor and pushes on a channel whenever new
|
||||
// key material is pushed to the distributor.
|
||||
type wrappedDistributor struct {
|
||||
*certprovider.Distributor
|
||||
distCh *testutils.Channel
|
||||
}
|
||||
|
||||
func newWrappedDistributor(distCh *testutils.Channel) *wrappedDistributor {
|
||||
return &wrappedDistributor{
|
||||
distCh: distCh,
|
||||
Distributor: certprovider.NewDistributor(),
|
||||
}
|
||||
}
|
||||
|
||||
func (wd *wrappedDistributor) Set(km *certprovider.KeyMaterial, err error) {
|
||||
wd.Distributor.Set(km, err)
|
||||
wd.distCh.Send(nil)
|
||||
}
|
||||
|
||||
func createTmpFile(t *testing.T, src, dst string) {
|
||||
t.Helper()
|
||||
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
t.Fatalf("os.ReadFile(%q) failed: %v", src, err)
|
||||
}
|
||||
if err := os.WriteFile(dst, data, os.ModePerm); err != nil {
|
||||
t.Fatalf("os.WriteFile(%q) failed: %v", dst, err)
|
||||
}
|
||||
t.Logf("Wrote file at: %s", dst)
|
||||
t.Logf("%s", string(data))
|
||||
}
|
||||
|
||||
func removeTmpFile(t *testing.T, filePath string) {
|
||||
t.Helper()
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
t.Fatalf("os.RemoveFIle(%q) failed: %v", filePath, err)
|
||||
}
|
||||
t.Logf("Removed file at: %s", filePath)
|
||||
}
|
||||
|
||||
// createTempDirWithFiles creates a temporary directory under the system default
|
||||
// tempDir with the given dirSuffix. It also reads from certSrc, keySrc and
|
||||
// rootSrc files are creates appropriate files under the newly create tempDir.
|
||||
// Returns the name of the created tempDir.
|
||||
func createTmpDirWithFiles(t *testing.T, dirSuffix, certSrc, keySrc, rootSrc, spiffeBundleSrc string) string {
|
||||
t.Helper()
|
||||
|
||||
// Create a temp directory. Passing an empty string for the first argument
|
||||
// uses the system temp directory.
|
||||
dir, err := os.MkdirTemp("", dirSuffix)
|
||||
if err != nil {
|
||||
t.Fatalf("os.MkdirTemp() failed: %v", err)
|
||||
}
|
||||
t.Logf("Using tmpdir: %s", dir)
|
||||
|
||||
createTmpFile(t, testdata.Path(certSrc), path.Join(dir, certFile))
|
||||
createTmpFile(t, testdata.Path(keySrc), path.Join(dir, keyFile))
|
||||
createTmpFile(t, testdata.Path(rootSrc), path.Join(dir, rootFile))
|
||||
createTmpFile(t, testdata.Path(spiffeBundleSrc), path.Join(dir, spiffeBundleFile))
|
||||
return dir
|
||||
}
|
||||
|
||||
// initializeProvider performs setup steps common to all tests (except the one
|
||||
// which uses symlinks).
|
||||
func initializeProvider(t *testing.T, testName string, useSPIFFEBundle bool) (string, certprovider.Provider, *testutils.Channel, func()) {
|
||||
t.Helper()
|
||||
|
||||
// Override the newDistributor to one which pushes on a channel that we
|
||||
// can block on.
|
||||
origDistributorFunc := newDistributor
|
||||
distCh := testutils.NewChannel()
|
||||
d := newWrappedDistributor(distCh)
|
||||
newDistributor = func() distributor { return d }
|
||||
|
||||
// Create a new provider to watch the files in tmpdir.
|
||||
dir := createTmpDirWithFiles(t, testName+"*", "x509/client1_cert.pem", "x509/client1_key.pem", "x509/client_ca_cert.pem", "spiffe/spiffebundle.json")
|
||||
opts := Options{
|
||||
CertFile: path.Join(dir, certFile),
|
||||
KeyFile: path.Join(dir, keyFile),
|
||||
RootFile: path.Join(dir, rootFile),
|
||||
RefreshDuration: defaultTestRefreshDuration,
|
||||
}
|
||||
if useSPIFFEBundle {
|
||||
opts.SPIFFEBundleMapFile = path.Join(dir, spiffeBundleFile)
|
||||
}
|
||||
prov, err := NewProvider(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider(%+v) failed: %v", opts, err)
|
||||
}
|
||||
|
||||
// Make sure the provider picks up the files and pushes the key material on
|
||||
// to the distributors.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
for i := 0; i < 2; i++ {
|
||||
// Since we have root and identity certs, we need to make sure the
|
||||
// update is pushed on both of them.
|
||||
if _, err := distCh.Receive(ctx); err != nil {
|
||||
t.Fatalf("Timeout waiting for provider to read files and push key material to distributor: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return dir, prov, distCh, func() {
|
||||
newDistributor = origDistributorFunc
|
||||
prov.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// TestProvider_NoUpdate tests the case where a file watcher plugin is created
|
||||
// successfully, and the underlying files do not change. Verifies that the
|
||||
// plugin does not push new updates to the distributor in this case.
|
||||
func (s) TestProvider_NoUpdate(t *testing.T) {
|
||||
baseName := "no_update"
|
||||
for _, useSPIFFEBundle := range []bool{true, false} {
|
||||
testName := baseName
|
||||
if useSPIFFEBundle {
|
||||
testName = testName + "_" + "withSPIFFEBundle"
|
||||
}
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
_, prov, distCh, cancel := initializeProvider(t, "no_update", useSPIFFEBundle)
|
||||
defer cancel()
|
||||
|
||||
// Make sure the provider is healthy and returns key material.
|
||||
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cc()
|
||||
if _, err := prov.KeyMaterial(ctx); err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
|
||||
// Files haven't change. Make sure no updates are pushed by the provider.
|
||||
sCtx, sc := context.WithTimeout(context.Background(), 2*defaultTestRefreshDuration)
|
||||
defer sc()
|
||||
if _, err := distCh.Receive(sCtx); err == nil {
|
||||
t.Fatal("New key material pushed to distributor when underlying files did not change")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProvider_UpdateSuccess tests the case where a file watcher plugin is
|
||||
// created successfully and the underlying files change. Verifies that the
|
||||
// changes are picked up by the provider.
|
||||
func (s) TestProvider_UpdateSuccess(t *testing.T) {
|
||||
baseName := "update_success"
|
||||
for _, useSPIFFEBundle := range []bool{true, false} {
|
||||
testName := baseName
|
||||
if useSPIFFEBundle {
|
||||
testName = testName + "_" + "withSPIFFEBundle"
|
||||
}
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
dir, prov, distCh, cancel := initializeProvider(t, "update_success", useSPIFFEBundle)
|
||||
defer cancel()
|
||||
|
||||
// Make sure the provider is healthy and returns key material.
|
||||
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cc()
|
||||
km1, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
|
||||
// Change only the root file.
|
||||
if useSPIFFEBundle {
|
||||
createTmpFile(t, testdata.Path("spiffe/spiffebundle2.json"), path.Join(dir, spiffeBundleFile))
|
||||
} else {
|
||||
createTmpFile(t, testdata.Path("x509/server_ca_cert.pem"), path.Join(dir, rootFile))
|
||||
}
|
||||
if _, err := distCh.Receive(ctx); err != nil {
|
||||
t.Fatal("Timeout waiting for new key material to be pushed to the distributor")
|
||||
}
|
||||
|
||||
// Make sure update is picked up.
|
||||
km2, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
if err := compareKeyMaterial(km1, km2); err == nil {
|
||||
t.Fatal("Expected provider to return new key material after update to underlying file")
|
||||
}
|
||||
|
||||
// Change only cert/key files.
|
||||
createTmpFile(t, testdata.Path("x509/client2_cert.pem"), path.Join(dir, certFile))
|
||||
createTmpFile(t, testdata.Path("x509/client2_key.pem"), path.Join(dir, keyFile))
|
||||
if _, err := distCh.Receive(ctx); err != nil {
|
||||
t.Fatal("Timeout waiting for new key material to be pushed to the distributor")
|
||||
}
|
||||
|
||||
// Make sure update is picked up.
|
||||
km3, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
if err := compareKeyMaterial(km2, km3); err == nil {
|
||||
t.Fatal("Expected provider to return new key material after update to underlying file")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProvider_UpdateSuccessWithSymlink tests the case where a file watcher
|
||||
// plugin is created successfully to watch files through a symlink and the
|
||||
// symlink is updates to point to new files. Verifies that the changes are
|
||||
// picked up by the provider.
|
||||
func (s) TestProvider_UpdateSuccessWithSymlink(t *testing.T) {
|
||||
baseName := "update_with_symlink"
|
||||
for _, useSPIFFEBundle := range []bool{true, false} {
|
||||
testName := baseName
|
||||
if useSPIFFEBundle {
|
||||
testName = testName + "_" + "withSPIFFEBundle"
|
||||
}
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
// Override the newDistributor to one which pushes on a channel that we
|
||||
// can block on.
|
||||
origDistributorFunc := newDistributor
|
||||
distCh := testutils.NewChannel()
|
||||
d := newWrappedDistributor(distCh)
|
||||
newDistributor = func() distributor { return d }
|
||||
defer func() { newDistributor = origDistributorFunc }()
|
||||
|
||||
// Create two tempDirs with different files.
|
||||
dir1 := createTmpDirWithFiles(t, "update_with_symlink1_*", "x509/client1_cert.pem", "x509/client1_key.pem", "x509/client_ca_cert.pem", "spiffe/spiffebundle.json")
|
||||
dir2 := createTmpDirWithFiles(t, "update_with_symlink2_*", "x509/server1_cert.pem", "x509/server1_key.pem", "x509/server_ca_cert.pem", "spiffe/spiffebundle2.json")
|
||||
|
||||
// Create a symlink under a new tempdir, and make it point to dir1.
|
||||
tmpdir, err := os.MkdirTemp("", "test_symlink_*")
|
||||
if err != nil {
|
||||
t.Fatalf("os.MkdirTemp() failed: %v", err)
|
||||
}
|
||||
symLinkName := path.Join(tmpdir, "test_symlink")
|
||||
if err := os.Symlink(dir1, symLinkName); err != nil {
|
||||
t.Fatalf("Failed to create symlink to %q: %v", dir1, err)
|
||||
}
|
||||
|
||||
// Create a provider which watches the files pointed to by the symlink.
|
||||
opts := Options{
|
||||
CertFile: path.Join(symLinkName, certFile),
|
||||
KeyFile: path.Join(symLinkName, keyFile),
|
||||
RootFile: path.Join(symLinkName, rootFile),
|
||||
SPIFFEBundleMapFile: path.Join(symLinkName, spiffeBundleFile),
|
||||
RefreshDuration: defaultTestRefreshDuration,
|
||||
}
|
||||
if useSPIFFEBundle {
|
||||
opts.SPIFFEBundleMapFile = path.Join(symLinkName, spiffeBundleFile)
|
||||
}
|
||||
prov, err := NewProvider(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider(%+v) failed: %v", opts, err)
|
||||
}
|
||||
defer prov.Close()
|
||||
|
||||
// Make sure the provider picks up the files and pushes the key material on
|
||||
// to the distributors.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
for i := 0; i < 2; i++ {
|
||||
// Since we have root and identity certs, we need to make sure the
|
||||
// update is pushed on both of them.
|
||||
if _, err := distCh.Receive(ctx); err != nil {
|
||||
t.Fatalf("Timeout waiting for provider to read files and push key material to distributor: %v", err)
|
||||
}
|
||||
}
|
||||
km1, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
|
||||
// Update the symlink to point to dir2.
|
||||
symLinkTmpName := path.Join(tmpdir, "test_symlink.tmp")
|
||||
if err := os.Symlink(dir2, symLinkTmpName); err != nil {
|
||||
t.Fatalf("Failed to create symlink to %q: %v", dir2, err)
|
||||
}
|
||||
if err := os.Rename(symLinkTmpName, symLinkName); err != nil {
|
||||
t.Fatalf("Failed to update symlink: %v", err)
|
||||
}
|
||||
|
||||
// Make sure the provider picks up the new files and pushes the key material
|
||||
// on to the distributors.
|
||||
for i := 0; i < 2; i++ {
|
||||
// Since we have root and identity certs, we need to make sure the
|
||||
// update is pushed on both of them.
|
||||
if _, err := distCh.Receive(ctx); err != nil {
|
||||
t.Fatalf("Timeout waiting for provider to read files and push key material to distributor: %v", err)
|
||||
}
|
||||
}
|
||||
km2, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
|
||||
if err := compareKeyMaterial(km1, km2); err == nil {
|
||||
t.Fatal("Expected provider to return new key material after symlink update")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProvider_UpdateFailure_ThenSuccess tests the case where updating cert/key
|
||||
// files fail. Verifies that the failed update does not push anything on the
|
||||
// distributor. Then the update succeeds, and the test verifies that the key
|
||||
// material is updated.
|
||||
func (s) TestProvider_UpdateFailure_ThenSuccess(t *testing.T) {
|
||||
dir, prov, distCh, cancel := initializeProvider(t, "update_failure", false)
|
||||
defer cancel()
|
||||
|
||||
// Make sure the provider is healthy and returns key material.
|
||||
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cc()
|
||||
km1, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
|
||||
// Update only the cert file. The key file is left unchanged. This should
|
||||
// lead to these two files being not compatible with each other. This
|
||||
// simulates the case where the watching goroutine might catch the files in
|
||||
// the midst of an update.
|
||||
createTmpFile(t, testdata.Path("x509/server1_cert.pem"), path.Join(dir, certFile))
|
||||
|
||||
// Since the last update left the files in an incompatible state, the update
|
||||
// should not be picked up by our provider.
|
||||
sCtx, sc := context.WithTimeout(context.Background(), 2*defaultTestRefreshDuration)
|
||||
defer sc()
|
||||
if _, err := distCh.Receive(sCtx); err == nil {
|
||||
t.Fatal("New key material pushed to distributor when underlying files did not change")
|
||||
}
|
||||
|
||||
// The provider should return key material corresponding to the old state.
|
||||
km2, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
if err := compareKeyMaterial(km1, km2); err != nil {
|
||||
t.Fatalf("Expected provider to not update key material: %v", err)
|
||||
}
|
||||
|
||||
// Update the key file to match the cert file.
|
||||
createTmpFile(t, testdata.Path("x509/server1_key.pem"), path.Join(dir, keyFile))
|
||||
|
||||
// Make sure update is picked up.
|
||||
if _, err := distCh.Receive(ctx); err != nil {
|
||||
t.Fatal("Timeout waiting for new key material to be pushed to the distributor")
|
||||
}
|
||||
km3, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
if err := compareKeyMaterial(km2, km3); err == nil {
|
||||
t.Fatal("Expected provider to return new key material after update to underlying file")
|
||||
}
|
||||
}
|
||||
|
||||
// TestProvider_UpdateFailure_ThenSuccess tests the case where updating cert/key
|
||||
// files fail. Verifies that the failed update does not push anything on the
|
||||
// distributor. Then the update succeeds, and the test verifies that the key
|
||||
// material is updated.
|
||||
func (s) TestProvider_UpdateFailureSPIFFE(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
badFile string
|
||||
}{
|
||||
{
|
||||
name: "malformed spiffe",
|
||||
badFile: "spiffe/spiffebundle_malformed.json",
|
||||
},
|
||||
{
|
||||
name: "invalid bundle",
|
||||
badFile: "spiffe/spiffebundle_wrong_kty.json",
|
||||
},
|
||||
{
|
||||
name: "cert in the x5c field is invalid",
|
||||
badFile: "spiffe/spiffebundle_corrupted_cert.json",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dir, prov, distCh, cancel := initializeProvider(t, tc.name, true)
|
||||
defer cancel()
|
||||
|
||||
// Make sure the provider is healthy and returns key material.
|
||||
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cc()
|
||||
km1, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
|
||||
// Update the file with a bad update
|
||||
createTmpFile(t, testdata.Path(tc.badFile), path.Join(dir, spiffeBundleFile))
|
||||
|
||||
// Since the last update left the files in an incompatible state, the update
|
||||
// should not be picked up by our provider.
|
||||
sCtx, sc := context.WithTimeout(context.Background(), 2*defaultTestRefreshDuration)
|
||||
defer sc()
|
||||
if _, err := distCh.Receive(sCtx); err == nil {
|
||||
t.Fatal("New key material pushed to distributor when underlying files did not change")
|
||||
}
|
||||
|
||||
// The provider should return key material corresponding to the old state.
|
||||
km2, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
if err := compareKeyMaterial(km1, km2); err != nil {
|
||||
t.Fatalf("Expected provider to not update key material: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProvider_UpdateFailure_ThenSuccess tests the case where updating cert/key
|
||||
// files fail. Verifies that the failed update does not push anything on the
|
||||
// distributor. Then the update succeeds, and the test verifies that the key
|
||||
// material is updated.
|
||||
func (s) TestProvider_UpdateFailureSPIFFE_MissingFile(t *testing.T) {
|
||||
dir, prov, distCh, cancel := initializeProvider(t, "Delete spiffe file being read", true)
|
||||
defer cancel()
|
||||
|
||||
// Make sure the provider is healthy and returns key material.
|
||||
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cc()
|
||||
km1, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
|
||||
// Remove the file that we are reading
|
||||
removeTmpFile(t, path.Join(dir, spiffeBundleFile))
|
||||
|
||||
// Since the last update left the files in an incompatible state, the update
|
||||
// should not be picked up by our provider.
|
||||
sCtx, sc := context.WithTimeout(context.Background(), 2*defaultTestRefreshDuration)
|
||||
defer sc()
|
||||
if _, err := distCh.Receive(sCtx); err == nil {
|
||||
t.Fatal("new key material pushed to distributor when underlying files did not change")
|
||||
}
|
||||
|
||||
// The provider should return key material corresponding to the old state.
|
||||
km2, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
if err := compareKeyMaterial(km1, km2); err != nil {
|
||||
t.Fatalf("expected provider to not update key material: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestProvider_UpdateFailure_ThenSuccess tests the case where updating cert/key
|
||||
// files fail. Verifies that the failed update does not push anything on the
|
||||
// distributor. Then the update succeeds, and the test verifies that the key
|
||||
// material is updated.
|
||||
func (s) TestProvider_UpdateFailureRoot_MissingFile(t *testing.T) {
|
||||
dir, prov, distCh, cancel := initializeProvider(t, "Delete root file being read", false)
|
||||
defer cancel()
|
||||
|
||||
// Make sure the provider is healthy and returns key material.
|
||||
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cc()
|
||||
km1, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
|
||||
// Remove the file that we are reading
|
||||
removeTmpFile(t, path.Join(dir, rootFile))
|
||||
|
||||
// Since the last update left the files in an incompatible state, the update
|
||||
// should not be picked up by our provider.
|
||||
sCtx, sc := context.WithTimeout(context.Background(), 2*defaultTestRefreshDuration)
|
||||
defer sc()
|
||||
if _, err := distCh.Receive(sCtx); err == nil {
|
||||
t.Fatal("new key material pushed to distributor when underlying files did not change")
|
||||
}
|
||||
|
||||
// The provider should return key material corresponding to the old state.
|
||||
km2, err := prov.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("provider.KeyMaterial() failed: %v", err)
|
||||
}
|
||||
if err := compareKeyMaterial(km1, km2); err != nil {
|
||||
t.Fatalf("expected provider to not update key material: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user