/* * * Copyright 2021 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 xds import ( "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "fmt" "net" "net/netip" "net/url" "os" "regexp" "strings" "sync/atomic" "testing" "time" "google.golang.org/grpc/testdata" "google.golang.org/grpc/credentials/tls/certprovider" "google.golang.org/grpc/internal/credentials/spiffe" "google.golang.org/grpc/internal/envconfig" "google.golang.org/grpc/internal/grpcsync" "google.golang.org/grpc/internal/grpctest" "google.golang.org/grpc/internal/testutils" "google.golang.org/grpc/internal/xds/matcher" ) type s struct { grpctest.Tester } func Test(t *testing.T) { grpctest.RunSubTests(t, s{}) } func (s) TestDNSMatch(t *testing.T) { tests := []struct { desc string host string pattern string wantMatch bool }{ { desc: "invalid wildcard 1", host: "aa.example.com", pattern: "*a.example.com", wantMatch: false, }, { desc: "invalid wildcard 2", host: "aa.example.com", pattern: "a*.example.com", wantMatch: false, }, { desc: "invalid wildcard 3", host: "abc.example.com", pattern: "a*c.example.com", wantMatch: false, }, { desc: "wildcard in one of the middle components", host: "abc.test.example.com", pattern: "abc.*.example.com", wantMatch: false, }, { desc: "single component wildcard", host: "a.example.com", pattern: "*", wantMatch: false, }, { desc: "short host name", host: "a.com", pattern: "*.example.com", wantMatch: false, }, { desc: "suffix mismatch", host: "a.notexample.com", pattern: "*.example.com", wantMatch: false, }, { desc: "wildcard match across components", host: "sub.test.example.com", pattern: "*.example.com.", wantMatch: false, }, { desc: "host doesn't end in period", host: "test.example.com", pattern: "test.example.com.", wantMatch: true, }, { desc: "pattern doesn't end in period", host: "test.example.com.", pattern: "test.example.com", wantMatch: true, }, { desc: "case insensitive", host: "TEST.EXAMPLE.COM.", pattern: "test.example.com.", wantMatch: true, }, { desc: "simple match", host: "test.example.com", pattern: "test.example.com", wantMatch: true, }, { desc: "good wildcard", host: "a.example.com", pattern: "*.example.com", wantMatch: true, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { gotMatch := dnsMatch(test.host, test.pattern) if gotMatch != test.wantMatch { t.Fatalf("dnsMatch(%s, %s) = %v, want %v", test.host, test.pattern, gotMatch, test.wantMatch) } }) } } func (s) TestMatchingSANExists_FailureCases(t *testing.T) { url1, err := url.Parse("http://golang.org") if err != nil { t.Fatalf("url.Parse() failed: %v", err) } url2, err := url.Parse("https://github.com/grpc/grpc-go") if err != nil { t.Fatalf("url.Parse() failed: %v", err) } inputCert := &x509.Certificate{ DNSNames: []string{"foo.bar.example.com", "bar.baz.test.com", "*.example.com"}, EmailAddresses: []string{"foobar@example.com", "barbaz@test.com"}, IPAddresses: []net.IP{ netip.MustParseAddr("192.0.0.1").AsSlice(), netip.MustParseAddr("2001:db8::68").AsSlice(), }, URIs: []*url.URL{url1, url2}, } tests := []struct { desc string sanMatchers []matcher.StringMatcher }{ { desc: "exact match", sanMatchers: []matcher.StringMatcher{ matcher.NewExactStringMatcher("abcd.test.com", false), matcher.NewExactStringMatcher("http://golang", false), matcher.NewExactStringMatcher("HTTP://GOLANG.ORG", false), }, }, { desc: "prefix match", sanMatchers: []matcher.StringMatcher{ matcher.NewPrefixStringMatcher("i-aint-the-one", false), matcher.NewPrefixStringMatcher("192.168.1.1", false), matcher.NewPrefixStringMatcher("FOO.BAR", false), }, }, { desc: "suffix match", sanMatchers: []matcher.StringMatcher{ matcher.NewSuffixStringMatcher("i-aint-the-one", false), matcher.NewSuffixStringMatcher("1::68", false), matcher.NewSuffixStringMatcher(".COM", false), }, }, { desc: "regex match", sanMatchers: []matcher.StringMatcher{ matcher.NewRegexStringMatcher(regexp.MustCompile(`.*\.examples\.com`)), matcher.NewRegexStringMatcher(regexp.MustCompile(`192\.[0-9]{1,3}\.1\.1`)), }, }, { desc: "contains match", sanMatchers: []matcher.StringMatcher{ matcher.NewContainsStringMatcher("i-aint-the-one", false), matcher.NewContainsStringMatcher("2001:db8:1:1::68", false), matcher.NewContainsStringMatcher("GRPC", false), }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { hi := NewHandshakeInfo(nil, nil, test.sanMatchers, "", false, false, false) defer hi.Decrement() if hi.Value().MatchingSANExists(inputCert) { t.Fatalf("hi.MatchingSANExists(%+v) with SAN matchers +%v succeeded when expected to fail", inputCert, test.sanMatchers) } }) } } func (s) TestMatchingSANExists_Success(t *testing.T) { url1, err := url.Parse("http://golang.org") if err != nil { t.Fatalf("url.Parse() failed: %v", err) } url2, err := url.Parse("https://github.com/grpc/grpc-go") if err != nil { t.Fatalf("url.Parse() failed: %v", err) } inputCert := &x509.Certificate{ DNSNames: []string{"baz.test.com", "*.example.com"}, EmailAddresses: []string{"foobar@example.com", "barbaz@test.com"}, IPAddresses: []net.IP{ netip.MustParseAddr("192.0.0.1").AsSlice(), netip.MustParseAddr("2001:db8::68").AsSlice(), }, URIs: []*url.URL{url1, url2}, } tests := []struct { desc string sanMatchers []matcher.StringMatcher }{ { desc: "no san matchers", }, { desc: "exact match dns wildcard", sanMatchers: []matcher.StringMatcher{ matcher.NewPrefixStringMatcher("192.168.1.1", false), matcher.NewExactStringMatcher("https://github.com/grpc/grpc-java", false), matcher.NewExactStringMatcher("abc.example.com", false), }, }, { desc: "exact match ignore case", sanMatchers: []matcher.StringMatcher{ matcher.NewExactStringMatcher("FOOBAR@EXAMPLE.COM", true), }, }, { desc: "prefix match", sanMatchers: []matcher.StringMatcher{ matcher.NewSuffixStringMatcher(".co.in", false), matcher.NewPrefixStringMatcher("192.168.1.1", false), matcher.NewPrefixStringMatcher("baz.test", false), }, }, { desc: "prefix match ignore case", sanMatchers: []matcher.StringMatcher{ matcher.NewPrefixStringMatcher("BAZ.test", true), }, }, { desc: "suffix match", sanMatchers: []matcher.StringMatcher{ matcher.NewRegexStringMatcher(regexp.MustCompile(`192\.[0-9]{1,3}\.1\.1`)), matcher.NewSuffixStringMatcher("192.168.1.1", false), matcher.NewSuffixStringMatcher("@test.com", false), }, }, { desc: "suffix match ignore case", sanMatchers: []matcher.StringMatcher{ matcher.NewSuffixStringMatcher("@test.COM", true), }, }, { desc: "regex match", sanMatchers: []matcher.StringMatcher{ matcher.NewContainsStringMatcher("https://github.com/grpc/grpc-java", false), matcher.NewRegexStringMatcher(regexp.MustCompile(`192\.[0-9]{1,3}\.1\.1`)), matcher.NewRegexStringMatcher(regexp.MustCompile(`.*\.test\.com`)), }, }, { desc: "contains match", sanMatchers: []matcher.StringMatcher{ matcher.NewExactStringMatcher("https://github.com/grpc/grpc-java", false), matcher.NewContainsStringMatcher("2001:68::db8", false), matcher.NewContainsStringMatcher("192.0.0", false), }, }, { desc: "contains match ignore case", sanMatchers: []matcher.StringMatcher{ matcher.NewContainsStringMatcher("GRPC", true), }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { hi := NewHandshakeInfo(nil, nil, test.sanMatchers, "", false, false, false) defer hi.Decrement() if !hi.Value().MatchingSANExists(inputCert) { t.Fatalf("hi.MatchingSANExists(%+v) with SAN matchers +%v failed when expected to succeed", inputCert, test.sanMatchers) } }) } } // Equal reports whether the handshake info structs are identical. func (hi *HandshakeInfo) Equal(other *HandshakeInfo) bool { if hi == nil && other == nil { return true } if hi == nil || other == nil { return false } r1, i1 := hi.rootProvider, hi.identityProvider r2, i2 := other.rootProvider, other.identityProvider switch { case r1 != r2 || i1 != i2: return false case hi.requireClientCert != other.requireClientCert: return false case hi.sni != other.sni: return false case hi.validateSANUsingSNI != other.validateSANUsingSNI: return false case hi.useAutoHostSNI != other.useAutoHostSNI: return false case len(hi.sanMatchers) != len(other.sanMatchers): return false } for i := range hi.sanMatchers { if !hi.sanMatchers[i].Equal(other.sanMatchers[i]) { return false } } return true } type testCertProvider struct { certprovider.Provider } func (s) TestEqual(t *testing.T) { tests := []struct { desc string hi1 *grpcsync.RefCounted[*HandshakeInfo] hi2 *grpcsync.RefCounted[*HandshakeInfo] wantMatch bool }{ { desc: "both HandshakeInfo are nil", hi1: func() *grpcsync.RefCounted[*HandshakeInfo] { return grpcsync.NewRefCounted((*HandshakeInfo)(nil), func() {}) }(), hi2: func() *grpcsync.RefCounted[*HandshakeInfo] { return grpcsync.NewRefCounted((*HandshakeInfo)(nil), func() {}) }(), wantMatch: true, }, { desc: "one HandshakeInfo is nil", hi1: func() *grpcsync.RefCounted[*HandshakeInfo] { return grpcsync.NewRefCounted((*HandshakeInfo)(nil), func() {}) }(), hi2: NewHandshakeInfo(&testCertProvider{}, nil, nil, "", false, false, false), wantMatch: false, }, { desc: "different root providers", hi1: NewHandshakeInfo(&testCertProvider{}, nil, nil, "", false, false, false), hi2: NewHandshakeInfo(&testCertProvider{}, nil, nil, "", false, false, false), wantMatch: false, }, { desc: "same providers, same SAN matchers", hi1: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{ matcher.NewExactStringMatcher("foo.com", false), }, "", false, false, false), hi2: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{ matcher.NewExactStringMatcher("foo.com", false), }, "", false, false, false), wantMatch: true, }, { desc: "same providers, different SAN matchers", hi1: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{ matcher.NewExactStringMatcher("foo.com", false), }, "", false, false, false), hi2: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{ matcher.NewExactStringMatcher("bar.com", false), }, "", false, false, false), wantMatch: false, }, { desc: "same SAN matchers with different content", hi1: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{ matcher.NewExactStringMatcher("foo.com", false), }, "", false, false, false), hi2: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{ matcher.NewExactStringMatcher("foo.com", false), matcher.NewExactStringMatcher("bar.com", false), }, "", false, false, false), wantMatch: false, }, { desc: "different requireClientCert flags", hi1: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, nil, "", true, false, false), hi2: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, nil, "", false, false, false), wantMatch: false, }, { desc: "same identity provider, different root provider", hi1: NewHandshakeInfo(&testCertProvider{}, testCertProvider{}, nil, "", false, false, false), hi2: NewHandshakeInfo(&testCertProvider{}, testCertProvider{}, nil, "", false, false, false), wantMatch: false, }, { desc: "different identity provider, same root provider", hi1: NewHandshakeInfo(testCertProvider{}, &testCertProvider{}, nil, "", false, false, false), hi2: NewHandshakeInfo(testCertProvider{}, &testCertProvider{}, nil, "", false, false, false), wantMatch: false, }, { desc: "same sni and validateSANUsingSNI", hi1: NewHandshakeInfo(nil, nil, nil, "sni", false, true, false), hi2: NewHandshakeInfo(nil, nil, nil, "sni", false, true, false), wantMatch: true, }, { desc: "different sni", hi1: NewHandshakeInfo(nil, nil, nil, "sni1", false, false, false), hi2: NewHandshakeInfo(nil, nil, nil, "sni2", false, false, false), wantMatch: false, }, { desc: "same sni, different validateSANUsingSNI", hi1: NewHandshakeInfo(nil, nil, nil, "sni", false, false, false), hi2: NewHandshakeInfo(nil, nil, nil, "sni", false, true, false), wantMatch: false, }, { desc: "different autoHostSNI", hi1: NewHandshakeInfo(nil, nil, nil, "", false, false, true), hi2: NewHandshakeInfo(nil, nil, nil, "", false, false, false), wantMatch: false, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { if gotMatch := test.hi1.Value().Equal(test.hi2.Value()); gotMatch != test.wantMatch { t.Errorf("hi1.Equal(hi2) = %v; wantMatch %v", gotMatch, test.wantMatch) } }) } } type testCertProviderWithKeyMaterial struct{} func (p *testCertProviderWithKeyMaterial) KeyMaterial(_ context.Context) (*certprovider.KeyMaterial, error) { km := &certprovider.KeyMaterial{} spiffeBundleMapContents, err := os.ReadFile(testdata.Path("spiffe_end2end/client_spiffebundle.json")) if err != nil { return nil, err } bundleMap, err := spiffe.BundleMapFromBytes(spiffeBundleMapContents) if err != nil { return nil, err } km.SPIFFEBundleMap = bundleMap rootFileContents, err := os.ReadFile(testdata.Path("spiffe_end2end/ca.pem")) if err != nil { return nil, err } trustPool := x509.NewCertPool() if !trustPool.AppendCertsFromPEM(rootFileContents) { return nil, fmt.Errorf("Failed to parse root certificate") } km.Roots = trustPool certFileContents, err := os.ReadFile(testdata.Path("spiffe_end2end/client_spiffe.pem")) if err != nil { return nil, err } keyFileContents, err := os.ReadFile(testdata.Path("spiffe_end2end/client.key")) if err != nil { return nil, err } cert, err := tls.X509KeyPair(certFileContents, keyFileContents) if err != nil { return nil, err } km.Certs = []tls.Certificate{cert} return km, nil } func (p *testCertProviderWithKeyMaterial) Close() {} func (s) TestBuildVerifyFuncFailures(t *testing.T) { tests := []struct { desc string peerCertChain [][]byte wantErr string }{ { desc: "invalid x509", peerCertChain: [][]byte{[]byte("NOT_A_CERT")}, wantErr: "x509: malformed certificate", }, { desc: "invalid SPIFFE ID in peer cert", // server1.pem doesn't have a valid SPIFFE ID, so attempted to get a // root from the SPIFFE Bundle Map will fail peerCertChain: loadCert(t, testdata.Path("server1.pem"), testdata.Path("server1.key")), wantErr: "spiffe: could not get spiffe ID from peer leaf cert but verification with spiffe trust map was configure", }, } testProvider := testCertProviderWithKeyMaterial{} hi := NewHandshakeInfo(&testProvider, &testProvider, nil, "", true, false, false) defer hi.Decrement() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() hiPtr := new(atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]]) hiPtr.Store(hi) cfg, _, done, err := ClientSideTLSConfig(ctx, hiPtr, "") if err != nil { t.Fatalf("GetClientSideTLSConfig() failed with err %v", err) } defer done() for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { err = cfg.VerifyPeerCertificate(tc.peerCertChain, nil) if !strings.Contains(err.Error(), tc.wantErr) { t.Errorf("VerifyPeerCertificate got err %v, want: %v", err, tc.wantErr) } }) } } func loadCert(t *testing.T, certPath, keyPath string) [][]byte { cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { t.Fatalf("LoadX509KeyPair(%s, %s) failed: %v", certPath, keyPath, err) } return cert.Certificate } type testProviderWithRoots struct { roots *x509.CertPool } func (m *testProviderWithRoots) KeyMaterial(context.Context) (*certprovider.KeyMaterial, error) { return &certprovider.KeyMaterial{ Roots: m.roots, }, nil } func (m *testProviderWithRoots) Close() {} // Tests the scenario where the SNI provided by the xDS server is only present // in the URI SAN of the certificate presented by the server, and not in the DNS // SAN. It verifies that if the SNI-based SAN validation is turned on, the TLS // handshake fails due to SNI mismatch. func (s) TestAutoHostSNI_DNS_SANValidation_Failures(t *testing.T) { testutils.SetEnvConfig(t, &envconfig.XDSSNIEnabled, true) priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("rsa.GenerateKey() failed: %v", err) } testURL, err := url.Parse("http://wrong.sni.domain") if err != nil { t.Fatalf("url.Parse() failed: %v", err) } template := &x509.Certificate{ NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), DNSNames: []string{"unrelated.example.com"}, URIs: []*url.URL{testURL}, // URI matching SNI } derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) if err != nil { t.Fatalf("x509.CreateCertificate() failed: %v", err) } cert, err := x509.ParseCertificate(derBytes) if err != nil { t.Fatalf("x509.ParseCertificate() failed: %v", err) } roots := x509.NewCertPool() roots.AddCert(cert) provider := &testProviderWithRoots{roots: roots} hi := NewHandshakeInfo(provider, nil, nil, "wrong.sni.domain", true, true, false) defer hi.Decrement() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() hiPtr := new(atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]]) hiPtr.Store(hi) cfg, _, done, err := ClientSideTLSConfig(ctx, hiPtr, "") if err != nil { t.Fatalf("GetClientSideTLSConfig() failed: %v", err) } defer done() err = cfg.VerifyPeerCertificate([][]byte{derBytes}, nil) if err == nil { t.Fatalf("VerifyPeerCertificate() unexpectedly succeeded when SNI was only in URI SAN") } if !strings.Contains(err.Error(), "received DNS SANs: [unrelated.example.com] do not match the SNI: wrong.sni.domain") { t.Fatalf("VerifyPeerCertificate() failed with %v, want error containing 'do not match the SNI'", err) } } func (s) TestVerifyPeerCertificateZeroCerts(t *testing.T) { provider := &testProviderWithRoots{roots: x509.NewCertPool()} hi := NewHandshakeInfo(provider, nil, nil, "", true, false, false) defer hi.Decrement() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() hiPtr := new(atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]]) hiPtr.Store(hi) cfg, _, done, err := ClientSideTLSConfig(ctx, hiPtr, "") if err != nil { t.Fatalf("GetClientSideTLSConfig() failed: %v", err) } defer done() const wantErr = "no peer certificates presented" if err := cfg.VerifyPeerCertificate(nil, nil); err == nil || !strings.Contains(err.Error(), wantErr) { t.Errorf("VerifyPeerCertificate(nil, nil) = %v, want error containing %q", err, wantErr) } }