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
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
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
This commit is contained in:
469
internal/credentials/xds/handshake_info.go
Normal file
469
internal/credentials/xds/handshake_info.go
Normal file
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
*
|
||||
* 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 xds contains non-user facing functionality of the xds credentials.
|
||||
package xds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"google.golang.org/grpc/attributes"
|
||||
"google.golang.org/grpc/credentials/tls/certprovider"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/credentials/spiffe"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/grpcsync"
|
||||
"google.golang.org/grpc/internal/xds/matcher"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
internal.GetXDSHandshakeInfoForTesting = HandshakeInfoFromAttributes
|
||||
}
|
||||
|
||||
// handshakeAttrKey is the type used as the key to store HandshakeInfo in
|
||||
// the Attributes field of resolver.Address.
|
||||
type handshakeAttrKey struct{}
|
||||
|
||||
// hostnameKey is the type used as the key to store the hostname in the
|
||||
// Attributes field of resolver.Address.
|
||||
type hostnameKey struct{}
|
||||
|
||||
// SetAddressHostname returns a copy of addr in which the Attributes field is
|
||||
// updated with the provided hostname.
|
||||
func SetAddressHostname(addr resolver.Address, hostname string) resolver.Address {
|
||||
addr.Attributes = addr.Attributes.WithValue(hostnameKey{}, hostname)
|
||||
return addr
|
||||
}
|
||||
|
||||
// Hostname returns the endpoint hostname stored in attr.
|
||||
func Hostname(attr *attributes.Attributes) string {
|
||||
if attr == nil {
|
||||
return ""
|
||||
}
|
||||
v := attr.Value(hostnameKey{})
|
||||
hn, _ := v.(string)
|
||||
return hn
|
||||
}
|
||||
|
||||
// SetHandshakeInfo returns a copy of addr in which the Attributes field is
|
||||
// updated with hiPtr.
|
||||
func SetHandshakeInfo(addr resolver.Address, hiPtr *atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]]) resolver.Address {
|
||||
addr.Attributes = addr.Attributes.WithValue(handshakeAttrKey{}, hiPtr)
|
||||
return addr
|
||||
}
|
||||
|
||||
// HandshakeInfoFromAttributes returns the atomic pointer to the
|
||||
// reference-counted HandshakeInfo stored in attr.
|
||||
func HandshakeInfoFromAttributes(attr *attributes.Attributes) *atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]] {
|
||||
v := attr.Value(handshakeAttrKey{})
|
||||
hi, _ := v.(*atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]])
|
||||
return hi
|
||||
}
|
||||
|
||||
// HandshakeInfo wraps all the security configuration required by client and
|
||||
// server handshake methods in xds credentials. The xDS implementation will be
|
||||
// responsible for populating these fields.
|
||||
type HandshakeInfo struct {
|
||||
// The fields below are initialized when the HandshakeInfo is created, and are
|
||||
// not changed thereafter.
|
||||
sanMatchers []matcher.StringMatcher // Only on the client side.
|
||||
requireClientCert bool // Only on server side.
|
||||
sni string // Only on client side, used for Server Name Indication in TLS handshake.
|
||||
validateSANUsingSNI bool // Only on client side, indicates whether to perform validation of SANs based on SNI value.
|
||||
useAutoHostSNI bool // Only on client side, indicates whether to use endpoint hostname as SNI.
|
||||
|
||||
rootProvider certprovider.Provider
|
||||
identityProvider certprovider.Provider
|
||||
}
|
||||
|
||||
// NewHandshakeInfo returns a new reference counted HandshakeInfo configured
|
||||
// with the provided options.
|
||||
func NewHandshakeInfo(rootProvider, identityProvider certprovider.Provider, sanMatchers []matcher.StringMatcher, sni string, requireClientCert, validateSANUsingSNI, useAutoHostSNI bool) *grpcsync.RefCounted[*HandshakeInfo] {
|
||||
hi := &HandshakeInfo{
|
||||
rootProvider: rootProvider,
|
||||
identityProvider: identityProvider,
|
||||
sanMatchers: sanMatchers,
|
||||
sni: sni,
|
||||
requireClientCert: requireClientCert,
|
||||
validateSANUsingSNI: validateSANUsingSNI,
|
||||
useAutoHostSNI: useAutoHostSNI,
|
||||
}
|
||||
|
||||
return grpcsync.NewRefCounted(hi, hi.close)
|
||||
}
|
||||
|
||||
func (hi *HandshakeInfo) close() {
|
||||
if hi.rootProvider != nil {
|
||||
hi.rootProvider.Close()
|
||||
}
|
||||
if hi.identityProvider != nil {
|
||||
hi.identityProvider.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// ClientSideTLSConfig loads the HandshakeInfo from hiPtr, marks it as in-use,
|
||||
// and returns the tls.Config along with a done callback that MUST be invoked
|
||||
// when the handshake completes. If no HandshakeInfo is stored in hiPtr or if
|
||||
// fallback credentials should be used, useFallback returns true.
|
||||
func ClientSideTLSConfig(ctx context.Context, hiPtr *atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]], hostname string) (cfg *tls.Config, useFallback bool, done func(), err error) {
|
||||
if hiPtr == nil {
|
||||
return nil, true, func() {}, nil
|
||||
}
|
||||
for {
|
||||
hiRC := hiPtr.Load()
|
||||
if hiRC == nil {
|
||||
return nil, false, func() {}, errors.New("xds: connection closed or HandshakeInfo dead")
|
||||
}
|
||||
if !hiRC.TryIncrement() {
|
||||
if hiPtr.Load() != hiRC {
|
||||
continue
|
||||
}
|
||||
return nil, false, func() {}, errors.New("xds: connection closed or HandshakeInfo dead")
|
||||
}
|
||||
|
||||
hi := hiRC.Value()
|
||||
if hi == nil || hi.UseFallbackCreds() {
|
||||
hiRC.Decrement()
|
||||
return nil, true, func() {}, nil
|
||||
}
|
||||
cfg, err := hi.clientSideTLSConfigInternal(ctx, hostname)
|
||||
if err != nil {
|
||||
hiRC.Decrement()
|
||||
return nil, false, func() {}, err
|
||||
}
|
||||
return cfg, false, hiRC.Decrement, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ServerSideTLSConfig checks if hi is configured and marks it as in-use,
|
||||
// returning the tls.Config along with a done callback that MUST be invoked when
|
||||
// the handshake completes. If hi is nil or fallback credentials should be used,
|
||||
// useFallback returns true.
|
||||
func ServerSideTLSConfig(ctx context.Context, hiRC *grpcsync.RefCounted[*HandshakeInfo]) (cfg *tls.Config, useFallback bool, done func(), err error) {
|
||||
if hiRC == nil {
|
||||
return nil, true, func() {}, nil
|
||||
}
|
||||
hi := hiRC.Value()
|
||||
if hi == nil || hi.UseFallbackCreds() {
|
||||
return nil, true, func() {}, nil
|
||||
}
|
||||
if !hiRC.TryIncrement() {
|
||||
return nil, false, func() {}, errors.New("xds: connection closed or HandshakeInfo dead")
|
||||
}
|
||||
cfg, err = hi.serverSideTLSConfigInternal(ctx)
|
||||
if err != nil {
|
||||
hiRC.Decrement()
|
||||
return nil, false, func() {}, err
|
||||
}
|
||||
return cfg, false, hiRC.Decrement, nil
|
||||
}
|
||||
|
||||
// UseFallbackCreds returns true when fallback credentials are to be used based
|
||||
// on the contents of the HandshakeInfo.
|
||||
func (hi *HandshakeInfo) UseFallbackCreds() bool {
|
||||
if hi == nil {
|
||||
return true
|
||||
}
|
||||
return hi.identityProvider == nil && hi.rootProvider == nil
|
||||
}
|
||||
|
||||
// GetSANMatchersForTesting returns the SAN matchers stored in HandshakeInfo.
|
||||
// To be used only for testing purposes.
|
||||
func (hi *HandshakeInfo) GetSANMatchersForTesting() []matcher.StringMatcher {
|
||||
return append([]matcher.StringMatcher{}, hi.sanMatchers...)
|
||||
}
|
||||
|
||||
// clientSideTLSConfigInternal constructs a tls.Config to be used in a
|
||||
// client-side handshake based on the contents of the HandshakeInfo.
|
||||
//
|
||||
// hostname is passed as a parameter here instead of being part of the
|
||||
// HandshakeInfo because HandshakeInfo contains cluster-level security
|
||||
// configuration that applies to all endpoints in the cluster, while hostname is
|
||||
// specific to each endpoint. This allows sharing a single HandshakeInfo
|
||||
// instance across multiple endpoints in the same cluster.
|
||||
func (hi *HandshakeInfo) clientSideTLSConfigInternal(ctx context.Context, hostname string) (*tls.Config, error) {
|
||||
// On the client side, rootProvider is mandatory. IdentityProvider is
|
||||
// optional based on whether the client is doing TLS or mTLS.
|
||||
if hi.rootProvider == nil {
|
||||
return nil, errors.New("xds: CertificateProvider to fetch trusted roots is missing, cannot perform TLS handshake. Please check configuration on the management server")
|
||||
}
|
||||
|
||||
// InsecureSkipVerify needs to be set to true because we need to perform
|
||||
// custom verification to check the SAN on the received certificate.
|
||||
// Currently the Go stdlib does complete verification of the cert (which
|
||||
// includes hostname verification) or none. We are forced to go with the
|
||||
// latter and perform the normal cert validation ourselves.
|
||||
cfg := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
|
||||
km, err := hi.rootProvider.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xds: fetching trusted roots from CertificateProvider failed: %v", err)
|
||||
}
|
||||
cfg.RootCAs = km.Roots
|
||||
|
||||
// If AutoHostSNI is true, and the endpoint hostname is present, we use the
|
||||
// endpoint hostname as the SNI value and also for SAN validation.
|
||||
// Otherwise, we use the SNI value from HandshakeInfo (which is configured
|
||||
// by the control plane) and validating SANs based on that.
|
||||
sni := hi.sni
|
||||
if hi.useAutoHostSNI && hostname != "" {
|
||||
sni = hostname
|
||||
}
|
||||
|
||||
cfg.VerifyPeerCertificate = hi.buildVerifyFunc(km, true, sni)
|
||||
|
||||
if hi.identityProvider != nil {
|
||||
km, err := hi.identityProvider.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xds: fetching identity certificates from CertificateProvider failed: %v", err)
|
||||
}
|
||||
cfg.Certificates = km.Certs
|
||||
}
|
||||
|
||||
if envconfig.XDSSNIEnabled && sni != "" {
|
||||
cfg.ServerName = sni
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (hi *HandshakeInfo) buildVerifyFunc(km *certprovider.KeyMaterial, isClient bool, sni string) func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
||||
return func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
||||
if len(rawCerts) == 0 {
|
||||
return fmt.Errorf("xds: no peer certificates presented")
|
||||
}
|
||||
// Parse all raw certificates presented by the peer.
|
||||
var certs []*x509.Certificate
|
||||
for _, rc := range rawCerts {
|
||||
cert, err := x509.ParseCertificate(rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
}
|
||||
|
||||
// Build the intermediates list and verify that the leaf certificate is
|
||||
// signed by one of the root certificates. If a SPIFFE Bundle Map is
|
||||
// configured, it is used to get the root certs. Otherwise, the
|
||||
// configured roots in the root provider are used.
|
||||
intermediates := x509.NewCertPool()
|
||||
for _, cert := range certs[1:] {
|
||||
intermediates.AddCert(cert)
|
||||
}
|
||||
roots := km.Roots
|
||||
// If a SPIFFE Bundle Map is configured, find the roots for the trust
|
||||
// domain of the leaf certificate.
|
||||
if km.SPIFFEBundleMap != nil {
|
||||
var err error
|
||||
roots, err = spiffe.GetRootsFromSPIFFEBundleMap(km.SPIFFEBundleMap, certs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
}
|
||||
if isClient {
|
||||
opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
|
||||
} else {
|
||||
opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
|
||||
}
|
||||
if _, err := certs[0].Verify(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If XDSSNIEnabled and AutoSNISANValidation are both true and the SNI is
|
||||
// non-empty, validate only DNS SANs against the SNI. Otherwise, fallback to
|
||||
// validating all received SANs against the control plane provided SAN
|
||||
// matchers.
|
||||
if envconfig.XDSSNIEnabled && hi.validateSANUsingSNI && sni != "" {
|
||||
// Verify SAN of leaf certificate with SNI using exact DNS matcher.
|
||||
for _, san := range certs[0].DNSNames {
|
||||
if dnsMatch(sni, san) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("xds: received DNS SANs: %v do not match the SNI: %s", certs[0].DNSNames, sni)
|
||||
}
|
||||
// The SANs sent by the xDS control plane are encoded as SPIFFE IDs. We need to
|
||||
// only look at the SANs on the leaf cert.
|
||||
if cert := certs[0]; !hi.MatchingSANExists(cert) {
|
||||
// TODO: Print the complete certificate once the x509 package
|
||||
// supports a String() method on the Certificate type.
|
||||
return fmt.Errorf("xds: received SANs {DNSNames: %v, EmailAddresses: %v, IPAddresses: %v, URIs: %v} do not match any of the accepted SANs", cert.DNSNames, cert.EmailAddresses, cert.IPAddresses, cert.URIs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// serverSideTLSConfigInternal constructs a tls.Config to be used in a
|
||||
// server-side handshake based on the contents of the HandshakeInfo.
|
||||
func (hi *HandshakeInfo) serverSideTLSConfigInternal(ctx context.Context) (*tls.Config, error) {
|
||||
cfg := &tls.Config{
|
||||
ClientAuth: tls.NoClientCert,
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
// On the server side, identityProvider is mandatory. RootProvider is
|
||||
// optional based on whether the server is doing TLS or mTLS.
|
||||
if hi.identityProvider == nil {
|
||||
return nil, errors.New("xds: CertificateProvider to fetch identity certificate is missing, cannot perform TLS handshake. Please check configuration on the management server")
|
||||
}
|
||||
if hi.requireClientCert {
|
||||
cfg.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
|
||||
// identityProvider is mandatory on the server side.
|
||||
km, err := hi.identityProvider.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xds: fetching identity certificates from CertificateProvider failed: %v", err)
|
||||
}
|
||||
cfg.Certificates = km.Certs
|
||||
|
||||
if hi.rootProvider != nil {
|
||||
km, err := hi.rootProvider.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xds: fetching trusted roots from CertificateProvider failed: %v", err)
|
||||
}
|
||||
if km.SPIFFEBundleMap != nil && hi.requireClientCert {
|
||||
// ClientAuth, if set greater than tls.RequireAnyClientCert, must be
|
||||
// dropped to tls.RequireAnyClientCert so that custom verification
|
||||
// to use SPIFFE Bundles is done.
|
||||
cfg.ClientAuth = tls.RequireAnyClientCert
|
||||
cfg.VerifyPeerCertificate = hi.buildVerifyFunc(km, false, "")
|
||||
} else {
|
||||
cfg.ClientCAs = km.Roots
|
||||
}
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// MatchingSANExists returns true if the SANs contained in cert match the
|
||||
// criteria enforced by the list of SAN matchers in HandshakeInfo.
|
||||
//
|
||||
// If the list of SAN matchers in the HandshakeInfo is empty, this function
|
||||
// returns true for all input certificates.
|
||||
func (hi *HandshakeInfo) MatchingSANExists(cert *x509.Certificate) bool {
|
||||
if len(hi.sanMatchers) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// SANs can be specified in any of these four fields on the parsed cert.
|
||||
for _, san := range cert.DNSNames {
|
||||
if hi.matchSAN(san, true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, san := range cert.EmailAddresses {
|
||||
if hi.matchSAN(san, false) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, san := range cert.IPAddresses {
|
||||
if hi.matchSAN(san.String(), false) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, san := range cert.URIs {
|
||||
if hi.matchSAN(san.String(), false) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Caller must hold mu.
|
||||
func (hi *HandshakeInfo) matchSAN(san string, isDNS bool) bool {
|
||||
for _, matcher := range hi.sanMatchers {
|
||||
if em := matcher.ExactMatch(); em != "" && isDNS {
|
||||
// This is a special case which is documented in the xDS protos.
|
||||
// If the DNS SAN is a wildcard entry, and the match criteria is
|
||||
// `exact`, then we need to perform DNS wildcard matching
|
||||
// instead of regular string comparison.
|
||||
if dnsMatch(em, san) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if matcher.Match(san) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// dnsMatch implements a DNS wildcard matching algorithm based on RFC2828 and
|
||||
// grpc-java's implementation in `OkHostnameVerifier` class.
|
||||
//
|
||||
// NOTE: Here the `host` argument is the one from the set of string matchers in
|
||||
// the xDS proto and the `san` argument is a DNS SAN from the certificate, and
|
||||
// this is the one which can potentially contain a wildcard pattern.
|
||||
func dnsMatch(host, san string) bool {
|
||||
// Add trailing "." and turn them into absolute domain names.
|
||||
if !strings.HasSuffix(host, ".") {
|
||||
host += "."
|
||||
}
|
||||
if !strings.HasSuffix(san, ".") {
|
||||
san += "."
|
||||
}
|
||||
// Domain names are case-insensitive.
|
||||
host = strings.ToLower(host)
|
||||
san = strings.ToLower(san)
|
||||
|
||||
// If san does not contain a wildcard, do exact match.
|
||||
if !strings.Contains(san, "*") {
|
||||
return host == san
|
||||
}
|
||||
|
||||
// Wildcard dns matching rules
|
||||
// - '*' is only permitted in the left-most label and must be the only
|
||||
// character in that label. For example, *.example.com is permitted, while
|
||||
// *a.example.com, a*.example.com, a*b.example.com, a.*.example.com are
|
||||
// not permitted.
|
||||
// - '*' matches a single domain name component. For example, *.example.com
|
||||
// matches test.example.com but does not match sub.test.example.com.
|
||||
// - Wildcard patterns for single-label domain names are not permitted.
|
||||
if san == "*." || !strings.HasPrefix(san, "*.") || strings.Contains(san[1:], "*") {
|
||||
return false
|
||||
}
|
||||
// Optimization: at this point, we know that the san contains a '*' and
|
||||
// is the first domain component of san. So, the host name must be at
|
||||
// least as long as the san to be able to match.
|
||||
if len(host) < len(san) {
|
||||
return false
|
||||
}
|
||||
// Hostname must end with the non-wildcard portion of san.
|
||||
if !strings.HasSuffix(host, san[1:]) {
|
||||
return false
|
||||
}
|
||||
// At this point we know that the hostName and san share the same suffix
|
||||
// (the non-wildcard portion of san). Now, we just need to make sure
|
||||
// that the '*' does not match across domain components.
|
||||
hostPrefix := strings.TrimSuffix(host, san[1:])
|
||||
return !strings.Contains(hostPrefix, ".")
|
||||
}
|
||||
668
internal/credentials/xds/handshake_info_test.go
Normal file
668
internal/credentials/xds/handshake_info_test.go
Normal file
@@ -0,0 +1,668 @@
|
||||
/*
|
||||
*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user