/* * * 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 certprovider import ( "context" "crypto/tls" "crypto/x509" "errors" "fmt" "os" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/grpc/internal/grpctest" "google.golang.org/grpc/internal/testutils" "google.golang.org/grpc/testdata" ) const ( fakeProvider1Name = "fake-certificate-provider-1" fakeProvider2Name = "fake-certificate-provider-2" fakeConfig = "my fake config" defaultTestTimeout = 5 * time.Second defaultTestShortTimeout = 10 * time.Millisecond ) var fpb1, fpb2 *fakeProviderBuilder func init() { fpb1 = &fakeProviderBuilder{ name: fakeProvider1Name, providerChan: testutils.NewChannel(), } fpb2 = &fakeProviderBuilder{ name: fakeProvider2Name, providerChan: testutils.NewChannel(), } Register(fpb1) Register(fpb2) } type s struct { grpctest.Tester } func Test(t *testing.T) { grpctest.RunSubTests(t, s{}) } // fakeProviderBuilder builds new instances of fakeProvider and interprets the // config provided to it as a string. type fakeProviderBuilder struct { name string providerChan *testutils.Channel } func (b *fakeProviderBuilder) ParseConfig(config interface{}) (*BuildableConfig, error) { s, ok := config.(string) if !ok { return nil, fmt.Errorf("providerBuilder %s received config of type %T, want string", b.name, config) } return NewBuildableConfig(b.name, []byte(s), func(BuildOptions) Provider { fp := &fakeProvider{ Distributor: NewDistributor(), config: s, } b.providerChan.Send(fp) return fp }), nil } func (b *fakeProviderBuilder) Name() string { return b.name } // fakeProvider is an implementation of the Provider interface which provides a // method for tests to invoke to push new key materials. type fakeProvider struct { *Distributor config string } func (p *fakeProvider) Start(BuildOptions) Provider { // This is practically a no-op since this provider doesn't do any work which // needs to be started at this point. return p } // newKeyMaterial allows tests to push new key material to the fake provider // which will be made available to users of this provider. func (p *fakeProvider) newKeyMaterial(km *KeyMaterial, err error) { p.Distributor.Set(km, err) } // Close helps implement the Provider interface. func (p *fakeProvider) Close() { p.Distributor.Stop() } // loadKeyMaterials is a helper to read cert/key files from testdata and convert // them into a KeyMaterialReader struct. func loadKeyMaterials(t *testing.T, cert, key, ca string) *KeyMaterial { t.Helper() certs, err := tls.LoadX509KeyPair(testdata.Path(cert), testdata.Path(key)) if err != nil { t.Fatalf("Failed to load keyPair: %v", err) } pemData, err := os.ReadFile(testdata.Path(ca)) if err != nil { t.Fatal(err) } roots := x509.NewCertPool() roots.AppendCertsFromPEM(pemData) return &KeyMaterial{Certs: []tls.Certificate{certs}, Roots: roots} } // kmReader wraps the KeyMaterial method exposed by Provider and Distributor // implementations. Defining the interface here makes it possible to use the // same helper from both provider and distributor tests. type kmReader interface { KeyMaterial(context.Context) (*KeyMaterial, error) } // readAndVerifyKeyMaterial attempts to read key material from the given // provider and compares it against the expected key material. func readAndVerifyKeyMaterial(ctx context.Context, kmr kmReader, wantKM *KeyMaterial) error { gotKM, err := kmr.KeyMaterial(ctx) if err != nil { return fmt.Errorf("KeyMaterial(ctx) failed: %w", err) } return compareKeyMaterial(gotKM, wantKM) } func compareKeyMaterial(got, want *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) } } // x509.CertPool contains only unexported fields some of which contain other // unexported fields. So usage of cmp.AllowUnexported() or // cmpopts.IgnoreUnexported() does not help us much here. Also, the standard // library does not provide a way to compare CertPool values. Comparing the // subjects field of the certs in the CertPool seems like a reasonable // approach. if gotR, wantR := got.Roots.Subjects(), want.Roots.Subjects(); !cmp.Equal(gotR, wantR, cmpopts.EquateEmpty()) { return fmt.Errorf("keyMaterial roots = %v, want %v", gotR, wantR) } return nil } func createProvider(t *testing.T, name, config string, opts BuildOptions) Provider { t.Helper() prov, err := GetProvider(name, config, opts) if err != nil { t.Fatalf("GetProvider(%s, %s, %v) failed: %v", name, config, opts, err) } return prov } // TestStoreSingleProvider creates a single provider through the store and calls // methods on them. func (s) TestStoreSingleProvider(t *testing.T) { prov := createProvider(t, fakeProvider1Name, fakeConfig, BuildOptions{CertName: "default"}) defer prov.Close() // Our fakeProviderBuilder pushes newly created providers on a channel. Grab // the fake provider from that channel. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() p, err := fpb1.providerChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name) } fakeProv := p.(*fakeProvider) // Attempt to read from key material from the Provider returned by the // store. This will fail because we have not pushed any key material into // our fake provider. sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout) defer sCancel() if err := readAndVerifyKeyMaterial(sCtx, prov, nil); !errors.Is(err, context.DeadlineExceeded) { t.Fatal(err) } // Load key material from testdata directory, push it into out fakeProvider // and attempt to read from the Provider returned by the store. testKM1 := loadKeyMaterials(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem") fakeProv.newKeyMaterial(testKM1, nil) if err := readAndVerifyKeyMaterial(ctx, prov, testKM1); err != nil { t.Fatal(err) } // Push new key material and read from the Provider. This should returned // updated key material. testKM2 := loadKeyMaterials(t, "x509/server2_cert.pem", "x509/server2_key.pem", "x509/client_ca_cert.pem") fakeProv.newKeyMaterial(testKM2, nil) if err := readAndVerifyKeyMaterial(ctx, prov, testKM2); err != nil { t.Fatal(err) } } // TestStoreSingleProviderSameConfigDifferentOpts creates multiple providers of // same type, for same configs but different keyMaterial options through the // store (and expects the store's sharing mechanism to kick in) and calls // methods on them. func (s) TestStoreSingleProviderSameConfigDifferentOpts(t *testing.T) { // Create three readers on the same fake provider. Two of these readers use // certName `foo`, while the third one uses certName `bar`. optsFoo := BuildOptions{CertName: "foo"} provFoo1 := createProvider(t, fakeProvider1Name, fakeConfig, optsFoo) provFoo2 := createProvider(t, fakeProvider1Name, fakeConfig, optsFoo) defer func() { provFoo1.Close() provFoo2.Close() }() // Our fakeProviderBuilder pushes newly created providers on a channel. // Grab the fake provider for optsFoo. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() p, err := fpb1.providerChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name) } fakeProvFoo := p.(*fakeProvider) // Make sure only provider was created by the builder so far. The store // should be able to share the providers. sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout) defer sCancel() if _, err := fpb1.providerChan.Receive(sCtx); !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("A second provider created when expected to be shared by the store") } optsBar := BuildOptions{CertName: "bar"} provBar1 := createProvider(t, fakeProvider1Name, fakeConfig, optsBar) defer provBar1.Close() // Grab the fake provider for optsBar. p, err = fpb1.providerChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name) } fakeProvBar := p.(*fakeProvider) // Push key material for optsFoo, and make sure the foo providers return // appropriate key material and the bar provider times out. fooKM := loadKeyMaterials(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem") fakeProvFoo.newKeyMaterial(fooKM, nil) if err := readAndVerifyKeyMaterial(ctx, provFoo1, fooKM); err != nil { t.Fatal(err) } if err := readAndVerifyKeyMaterial(ctx, provFoo2, fooKM); err != nil { t.Fatal(err) } sCtx, sCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout) defer sCancel() if err := readAndVerifyKeyMaterial(sCtx, provBar1, nil); !errors.Is(err, context.DeadlineExceeded) { t.Fatal(err) } // Push key material for optsBar, and make sure the bar provider returns // appropriate key material. barKM := loadKeyMaterials(t, "x509/server2_cert.pem", "x509/server2_key.pem", "x509/client_ca_cert.pem") fakeProvBar.newKeyMaterial(barKM, nil) if err := readAndVerifyKeyMaterial(ctx, provBar1, barKM); err != nil { t.Fatal(err) } // Make sure the above push of new key material does not affect foo readers. if err := readAndVerifyKeyMaterial(ctx, provFoo1, fooKM); err != nil { t.Fatal(err) } } // TestStoreSingleProviderDifferentConfigs creates multiple instances of the // same type of provider through the store with different configs. The store // would end up creating different provider instances for these and no sharing // would take place. func (s) TestStoreSingleProviderDifferentConfigs(t *testing.T) { // Create two providers of the same type, but with different configs. opts := BuildOptions{CertName: "foo"} cfg1 := fakeConfig + "1111" cfg2 := fakeConfig + "2222" prov1 := createProvider(t, fakeProvider1Name, cfg1, opts) defer prov1.Close() // Our fakeProviderBuilder pushes newly created providers on a channel. Grab // the fake provider from that channel. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() p1, err := fpb1.providerChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name) } fakeProv1 := p1.(*fakeProvider) prov2 := createProvider(t, fakeProvider1Name, cfg2, opts) defer prov2.Close() // Grab the second provider from the channel. p2, err := fpb1.providerChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name) } fakeProv2 := p2.(*fakeProvider) // Push the same key material into both fake providers and verify that the // providers returned by the store return the appropriate key material. km1 := loadKeyMaterials(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem") fakeProv1.newKeyMaterial(km1, nil) fakeProv2.newKeyMaterial(km1, nil) if err := readAndVerifyKeyMaterial(ctx, prov1, km1); err != nil { t.Fatal(err) } if err := readAndVerifyKeyMaterial(ctx, prov2, km1); err != nil { t.Fatal(err) } // Push new key material into only one of the fake providers and verify // that the providers returned by the store return the appropriate key // material. km2 := loadKeyMaterials(t, "x509/server2_cert.pem", "x509/server2_key.pem", "x509/client_ca_cert.pem") fakeProv2.newKeyMaterial(km2, nil) if err := readAndVerifyKeyMaterial(ctx, prov1, km1); err != nil { t.Fatal(err) } if err := readAndVerifyKeyMaterial(ctx, prov2, km2); err != nil { t.Fatal(err) } // Close one of the providers and verify that the other one is not affected. prov1.Close() if err := readAndVerifyKeyMaterial(ctx, prov2, km2); err != nil { t.Fatal(err) } } // TestStoreMultipleProviders creates providers of different types and makes // sure closing of one does not affect the other. func (s) TestStoreMultipleProviders(t *testing.T) { opts := BuildOptions{CertName: "foo"} prov1 := createProvider(t, fakeProvider1Name, fakeConfig, opts) defer prov1.Close() // Our fakeProviderBuilder pushes newly created providers on a channel. Grab // the fake provider from that channel. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() p1, err := fpb1.providerChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name) } fakeProv1 := p1.(*fakeProvider) prov2 := createProvider(t, fakeProvider2Name, fakeConfig, opts) defer prov2.Close() // Grab the second provider from the channel. p2, err := fpb2.providerChan.Receive(ctx) if err != nil { t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider2Name) } fakeProv2 := p2.(*fakeProvider) // Push the key material into both providers and verify that the // readers return the appropriate key material. km1 := loadKeyMaterials(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem") fakeProv1.newKeyMaterial(km1, nil) km2 := loadKeyMaterials(t, "x509/server2_cert.pem", "x509/server2_key.pem", "x509/client_ca_cert.pem") fakeProv2.newKeyMaterial(km2, nil) if err := readAndVerifyKeyMaterial(ctx, prov1, km1); err != nil { t.Fatal(err) } if err := readAndVerifyKeyMaterial(ctx, prov2, km2); err != nil { t.Fatal(err) } // Close one of the providers and verify that the other one is not affected. prov1.Close() if err := readAndVerifyKeyMaterial(ctx, prov2, km2); err != nil { t.Fatal(err) } }