Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
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
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
Stale bot / stale (push) Failing after 1s
669 lines
20 KiB
Go
669 lines
20 KiB
Go
/*
|
|
*
|
|
* 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)
|
|
}
|
|
}
|