...

Source file src/google.golang.org/api/internal/cba.go

Documentation: google.golang.org/api/internal

     1  // Copyright 2020 Google LLC.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // cba.go (certificate-based access) contains utils for implementing Device Certificate
     6  // Authentication according to https://google.aip.dev/auth/4114 and Default Credentials
     7  // for Google Cloud Virtual Environments according to https://google.aip.dev/auth/4115.
     8  //
     9  // The overall logic for DCA is as follows:
    10  //  1. If both endpoint override and client certificate are specified, use them as is.
    11  //  2. If user does not specify client certificate, we will attempt to use default
    12  //     client certificate.
    13  //  3. If user does not specify endpoint override, we will use defaultMtlsEndpoint if
    14  //     client certificate is available and defaultEndpoint otherwise.
    15  //
    16  // Implications of the above logic:
    17  //  1. If the user specifies a non-mTLS endpoint override but client certificate is
    18  //     available, we will pass along the cert anyway and let the server decide what to do.
    19  //  2. If the user specifies an mTLS endpoint override but client certificate is not
    20  //     available, we will not fail-fast, but let backend throw error when connecting.
    21  //
    22  // If running within Google's cloud environment, and client certificate is not specified
    23  // and not available through DCA, we will try mTLS with credentials held by
    24  // the Secure Session Agent, which is part of Google's cloud infrastructure.
    25  //
    26  // We would like to avoid introducing client-side logic that parses whether the
    27  // endpoint override is an mTLS url, since the url pattern may change at anytime.
    28  //
    29  // This package is not intended for use by end developers. Use the
    30  // google.golang.org/api/option package to configure API clients.
    31  
    32  // Package internal supports the options and transport packages.
    33  package internal
    34  
    35  import (
    36  	"context"
    37  	"crypto/tls"
    38  	"errors"
    39  	"net"
    40  	"net/url"
    41  	"os"
    42  	"strings"
    43  
    44  	"github.com/google/s2a-go"
    45  	"github.com/google/s2a-go/fallback"
    46  	"google.golang.org/api/internal/cert"
    47  	"google.golang.org/grpc/credentials"
    48  )
    49  
    50  const (
    51  	mTLSModeAlways = "always"
    52  	mTLSModeNever  = "never"
    53  	mTLSModeAuto   = "auto"
    54  
    55  	// Experimental: if true, the code will try MTLS with S2A as the default for transport security. Default value is false.
    56  	googleAPIUseS2AEnv = "EXPERIMENTAL_GOOGLE_API_USE_S2A"
    57  
    58  	universeDomainPlaceholder = "UNIVERSE_DOMAIN"
    59  )
    60  
    61  var (
    62  	errUniverseNotSupportedMTLS = errors.New("mTLS is not supported in any universe other than googleapis.com")
    63  )
    64  
    65  // getClientCertificateSourceAndEndpoint is a convenience function that invokes
    66  // getClientCertificateSource and getEndpoint sequentially and returns the client
    67  // cert source and endpoint as a tuple.
    68  func getClientCertificateSourceAndEndpoint(settings *DialSettings) (cert.Source, string, error) {
    69  	clientCertSource, err := getClientCertificateSource(settings)
    70  	if err != nil {
    71  		return nil, "", err
    72  	}
    73  	endpoint, err := getEndpoint(settings, clientCertSource)
    74  	if err != nil {
    75  		return nil, "", err
    76  	}
    77  	// TODO(chrisdsmith): https://github.com/googleapis/google-api-go-client/issues/2359
    78  	if settings.Endpoint == "" && !settings.IsUniverseDomainGDU() && settings.DefaultEndpointTemplate != "" {
    79  		// TODO(chrisdsmith): https://github.com/googleapis/google-api-go-client/issues/2359
    80  		// if settings.DefaultEndpointTemplate == "" {
    81  		// 	return nil, "", errors.New("internaloption.WithDefaultEndpointTemplate is required if option.WithUniverseDomain is not googleapis.com")
    82  		// }
    83  		endpoint = resolvedDefaultEndpoint(settings)
    84  	}
    85  	return clientCertSource, endpoint, nil
    86  }
    87  
    88  type transportConfig struct {
    89  	clientCertSource cert.Source // The client certificate source.
    90  	endpoint         string      // The corresponding endpoint to use based on client certificate source.
    91  	s2aAddress       string      // The S2A address if it can be used, otherwise an empty string.
    92  	s2aMTLSEndpoint  string      // The MTLS endpoint to use with S2A.
    93  }
    94  
    95  func getTransportConfig(settings *DialSettings) (*transportConfig, error) {
    96  	clientCertSource, endpoint, err := getClientCertificateSourceAndEndpoint(settings)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	defaultTransportConfig := transportConfig{
   101  		clientCertSource: clientCertSource,
   102  		endpoint:         endpoint,
   103  		s2aAddress:       "",
   104  		s2aMTLSEndpoint:  "",
   105  	}
   106  
   107  	if !shouldUseS2A(clientCertSource, settings) {
   108  		return &defaultTransportConfig, nil
   109  	}
   110  	if !settings.IsUniverseDomainGDU() {
   111  		return nil, errUniverseNotSupportedMTLS
   112  	}
   113  
   114  	s2aAddress := GetS2AAddress()
   115  	if s2aAddress == "" {
   116  		return &defaultTransportConfig, nil
   117  	}
   118  	return &transportConfig{
   119  		clientCertSource: clientCertSource,
   120  		endpoint:         endpoint,
   121  		s2aAddress:       s2aAddress,
   122  		s2aMTLSEndpoint:  settings.DefaultMTLSEndpoint,
   123  	}, nil
   124  }
   125  
   126  // getClientCertificateSource returns a default client certificate source, if
   127  // not provided by the user.
   128  //
   129  // A nil default source can be returned if the source does not exist. Any exceptions
   130  // encountered while initializing the default source will be reported as client
   131  // error (ex. corrupt metadata file).
   132  //
   133  // Important Note: For now, the environment variable GOOGLE_API_USE_CLIENT_CERTIFICATE
   134  // must be set to "true" to allow certificate to be used (including user provided
   135  // certificates). For details, see AIP-4114.
   136  func getClientCertificateSource(settings *DialSettings) (cert.Source, error) {
   137  	if !isClientCertificateEnabled() {
   138  		return nil, nil
   139  	} else if settings.ClientCertSource != nil {
   140  		return settings.ClientCertSource, nil
   141  	} else {
   142  		return cert.DefaultSource()
   143  	}
   144  }
   145  
   146  func isClientCertificateEnabled() bool {
   147  	useClientCert := os.Getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE")
   148  	// TODO(andyrzhao): Update default to return "true" after DCA feature is fully released.
   149  	return strings.ToLower(useClientCert) == "true"
   150  }
   151  
   152  // getEndpoint returns the endpoint for the service, taking into account the
   153  // user-provided endpoint override "settings.Endpoint".
   154  //
   155  // If no endpoint override is specified, we will either return the default endpoint or
   156  // the default mTLS endpoint if a client certificate is available.
   157  //
   158  // You can override the default endpoint choice (mtls vs. regular) by setting the
   159  // GOOGLE_API_USE_MTLS_ENDPOINT environment variable.
   160  //
   161  // If the endpoint override is an address (host:port) rather than full base
   162  // URL (ex. https://...), then the user-provided address will be merged into
   163  // the default endpoint. For example, WithEndpoint("myhost:8000") and
   164  // WithDefaultEndpoint("https://foo.com/bar/baz") will return "https://myhost:8080/bar/baz"
   165  func getEndpoint(settings *DialSettings, clientCertSource cert.Source) (string, error) {
   166  	if settings.Endpoint == "" {
   167  		if isMTLS(clientCertSource) {
   168  			if !settings.IsUniverseDomainGDU() {
   169  				return "", errUniverseNotSupportedMTLS
   170  			}
   171  			return settings.DefaultMTLSEndpoint, nil
   172  		}
   173  		return resolvedDefaultEndpoint(settings), nil
   174  	}
   175  	if strings.Contains(settings.Endpoint, "://") {
   176  		// User passed in a full URL path, use it verbatim.
   177  		return settings.Endpoint, nil
   178  	}
   179  	if resolvedDefaultEndpoint(settings) == "" {
   180  		// If DefaultEndpoint is not configured, use the user provided endpoint verbatim.
   181  		// This allows a naked "host[:port]" URL to be used with GRPC Direct Path.
   182  		return settings.Endpoint, nil
   183  	}
   184  
   185  	// Assume user-provided endpoint is host[:port], merge it with the default endpoint.
   186  	return mergeEndpoints(resolvedDefaultEndpoint(settings), settings.Endpoint)
   187  }
   188  
   189  func isMTLS(clientCertSource cert.Source) bool {
   190  	mtlsMode := getMTLSMode()
   191  	return mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto)
   192  }
   193  
   194  // resolvedDefaultEndpoint returns the DefaultEndpointTemplate merged with the
   195  // Universe Domain if the DefaultEndpointTemplate is set, otherwise returns the
   196  // deprecated DefaultEndpoint value.
   197  func resolvedDefaultEndpoint(settings *DialSettings) string {
   198  	if settings.DefaultEndpointTemplate == "" {
   199  		return settings.DefaultEndpoint
   200  	}
   201  	return strings.Replace(settings.DefaultEndpointTemplate, universeDomainPlaceholder, settings.GetUniverseDomain(), 1)
   202  }
   203  
   204  func getMTLSMode() string {
   205  	mode := os.Getenv("GOOGLE_API_USE_MTLS_ENDPOINT")
   206  	if mode == "" {
   207  		mode = os.Getenv("GOOGLE_API_USE_MTLS") // Deprecated.
   208  	}
   209  	if mode == "" {
   210  		return mTLSModeAuto
   211  	}
   212  	return strings.ToLower(mode)
   213  }
   214  
   215  func mergeEndpoints(baseURL, newHost string) (string, error) {
   216  	u, err := url.Parse(fixScheme(baseURL))
   217  	if err != nil {
   218  		return "", err
   219  	}
   220  	return strings.Replace(baseURL, u.Host, newHost, 1), nil
   221  }
   222  
   223  func fixScheme(baseURL string) string {
   224  	if !strings.Contains(baseURL, "://") {
   225  		return "https://" + baseURL
   226  	}
   227  	return baseURL
   228  }
   229  
   230  // GetGRPCTransportConfigAndEndpoint returns an instance of credentials.TransportCredentials, and the
   231  // corresponding endpoint to use for GRPC client.
   232  func GetGRPCTransportConfigAndEndpoint(settings *DialSettings) (credentials.TransportCredentials, string, error) {
   233  	config, err := getTransportConfig(settings)
   234  	if err != nil {
   235  		return nil, "", err
   236  	}
   237  
   238  	defaultTransportCreds := credentials.NewTLS(&tls.Config{
   239  		GetClientCertificate: config.clientCertSource,
   240  	})
   241  	if config.s2aAddress == "" {
   242  		return defaultTransportCreds, config.endpoint, nil
   243  	}
   244  
   245  	var fallbackOpts *s2a.FallbackOptions
   246  	// In case of S2A failure, fall back to the endpoint that would've been used without S2A.
   247  	if fallbackHandshake, err := fallback.DefaultFallbackClientHandshakeFunc(config.endpoint); err == nil {
   248  		fallbackOpts = &s2a.FallbackOptions{
   249  			FallbackClientHandshakeFunc: fallbackHandshake,
   250  		}
   251  	}
   252  
   253  	s2aTransportCreds, err := s2a.NewClientCreds(&s2a.ClientOptions{
   254  		S2AAddress:   config.s2aAddress,
   255  		FallbackOpts: fallbackOpts,
   256  	})
   257  	if err != nil {
   258  		// Use default if we cannot initialize S2A client transport credentials.
   259  		return defaultTransportCreds, config.endpoint, nil
   260  	}
   261  	return s2aTransportCreds, config.s2aMTLSEndpoint, nil
   262  }
   263  
   264  // GetHTTPTransportConfigAndEndpoint returns a client certificate source, a function for dialing MTLS with S2A,
   265  // and the endpoint to use for HTTP client.
   266  func GetHTTPTransportConfigAndEndpoint(settings *DialSettings) (cert.Source, func(context.Context, string, string) (net.Conn, error), string, error) {
   267  	config, err := getTransportConfig(settings)
   268  	if err != nil {
   269  		return nil, nil, "", err
   270  	}
   271  
   272  	if config.s2aAddress == "" {
   273  		return config.clientCertSource, nil, config.endpoint, nil
   274  	}
   275  
   276  	var fallbackOpts *s2a.FallbackOptions
   277  	// In case of S2A failure, fall back to the endpoint that would've been used without S2A.
   278  	if fallbackURL, err := url.Parse(config.endpoint); err == nil {
   279  		if fallbackDialer, fallbackServerAddr, err := fallback.DefaultFallbackDialerAndAddress(fallbackURL.Hostname()); err == nil {
   280  			fallbackOpts = &s2a.FallbackOptions{
   281  				FallbackDialer: &s2a.FallbackDialer{
   282  					Dialer:     fallbackDialer,
   283  					ServerAddr: fallbackServerAddr,
   284  				},
   285  			}
   286  		}
   287  	}
   288  
   289  	dialTLSContextFunc := s2a.NewS2ADialTLSContextFunc(&s2a.ClientOptions{
   290  		S2AAddress:   config.s2aAddress,
   291  		FallbackOpts: fallbackOpts,
   292  	})
   293  	return nil, dialTLSContextFunc, config.s2aMTLSEndpoint, nil
   294  }
   295  
   296  func shouldUseS2A(clientCertSource cert.Source, settings *DialSettings) bool {
   297  	// If client cert is found, use that over S2A.
   298  	if clientCertSource != nil {
   299  		return false
   300  	}
   301  	// If EXPERIMENTAL_GOOGLE_API_USE_S2A is not set to true, skip S2A.
   302  	if !isGoogleS2AEnabled() {
   303  		return false
   304  	}
   305  	// If DefaultMTLSEndpoint is not set or has endpoint override, skip S2A.
   306  	if settings.DefaultMTLSEndpoint == "" || settings.Endpoint != "" {
   307  		return false
   308  	}
   309  	// If custom HTTP client is provided, skip S2A.
   310  	if settings.HTTPClient != nil {
   311  		return false
   312  	}
   313  	return !settings.EnableDirectPath && !settings.EnableDirectPathXds
   314  }
   315  
   316  func isGoogleS2AEnabled() bool {
   317  	return strings.ToLower(os.Getenv(googleAPIUseS2AEnv)) == "true"
   318  }
   319  

View as plain text