feat(transport): Add Device Certificate Authentication support to GRPC (#682)

- Refactor GetClientCertificateSource and GetEndpoint helpers into a common "dca" package to be used by both http and grpc routes.
- Moved endpoint tests to dca_test.go.
- Update GetEndpoint to accept naked "host:port" as URL without merging with DefaultEndpoint. This is needed for GRPC DirectPath scenario, where "DefaultEndpoint" is likely not configured.
diff --git a/transport/grpc/dial.go b/transport/grpc/dial.go
index 19b7ddb..55c04a5 100644
--- a/transport/grpc/dial.go
+++ b/transport/grpc/dial.go
@@ -9,6 +9,7 @@
 
 import (
 	"context"
+	"crypto/tls"
 	"errors"
 	"log"
 	"os"
@@ -18,6 +19,7 @@
 	"golang.org/x/oauth2"
 	"google.golang.org/api/internal"
 	"google.golang.org/api/option"
+	"google.golang.org/api/transport/internal/dca"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	grpcgoogle "google.golang.org/grpc/credentials/google"
@@ -112,6 +114,10 @@
 	if o.GRPCConn != nil {
 		return o.GRPCConn, nil
 	}
+	clientCertSource, endpoint, err := dca.GetClientCertificateSourceAndEndpoint(o)
+	if err != nil {
+		return nil, err
+	}
 	var grpcOpts []grpc.DialOption
 	if insecure {
 		grpcOpts = []grpc.DialOption{grpc.WithInsecure()}
@@ -134,9 +140,9 @@
 		//   service account.
 		// * Opted in via GOOGLE_CLOUD_ENABLE_DIRECT_PATH environment variable.
 		//   For example, GOOGLE_CLOUD_ENABLE_DIRECT_PATH=spanner,pubsub
-		if isDirectPathEnabled(o.Endpoint) && isTokenSourceDirectPathCompatible(creds.TokenSource) {
-			if !strings.HasPrefix(o.Endpoint, "dns:///") {
-				o.Endpoint = "dns:///" + o.Endpoint
+		if isDirectPathEnabled(endpoint) && isTokenSourceDirectPathCompatible(creds.TokenSource) {
+			if !strings.HasPrefix(endpoint, "dns:///") {
+				endpoint = "dns:///" + endpoint
 			}
 			grpcOpts = []grpc.DialOption{
 				grpc.WithCredentialsBundle(
@@ -150,13 +156,16 @@
 			}
 			// TODO(cbro): add support for system parameters (quota project, request reason) via chained interceptor.
 		} else {
+			tlsConfig := &tls.Config{
+				GetClientCertificate: clientCertSource,
+			}
 			grpcOpts = []grpc.DialOption{
 				grpc.WithPerRPCCredentials(grpcTokenSource{
 					TokenSource:   oauth.TokenSource{creds.TokenSource},
 					quotaProject:  o.QuotaProject,
 					requestReason: o.RequestReason,
 				}),
-				grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
+				grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
 			}
 		}
 	}
@@ -180,34 +189,11 @@
 	// point when isDirectPathEnabled will default to true, we guard it by
 	// the Directpath env var for now once we can introspect user defined
 	// dialer (https://github.com/grpc/grpc-go/issues/2795).
-	if timeoutDialerOption != nil && isDirectPathEnabled(o.Endpoint) {
+	if timeoutDialerOption != nil && isDirectPathEnabled(endpoint) {
 		grpcOpts = append(grpcOpts, timeoutDialerOption)
 	}
 
-	return grpc.DialContext(ctx, o.Endpoint, grpcOpts...)
-}
-
-// generateDefaultMtlsEndpoint attempts to derive the mTLS version of the
-// defaultEndpoint via regex, and returns defaultEndpoint if unsuccessful.
-//
-// We need to applying the following 2 transformations:
-// 1. pubsub.googleapis.com to pubsub.mtls.googleapis.com
-// 2. pubsub.sandbox.googleapis.com to pubsub.mtls.sandbox.googleapis.com
-//
-// TODO(cbro): In the future, the mTLS endpoint will be read from Service Config
-// and passed in as defaultMtlsEndpoint instead of generated from defaultEndpoint,
-// and this function will be removed.
-func generateDefaultMtlsEndpoint(defaultEndpoint string) string {
-	var domains = []string{
-		".sandbox.googleapis.com", // must come first because .googleapis.com is a substring
-		".googleapis.com",
-	}
-	for _, domain := range domains {
-		if strings.Contains(defaultEndpoint, domain) {
-			return strings.Replace(defaultEndpoint, domain, ".mtls"+domain, -1)
-		}
-	}
-	return defaultEndpoint
+	return grpc.DialContext(ctx, endpoint, grpcOpts...)
 }
 
 func addOCStatsHandler(opts []grpc.DialOption, settings *internal.DialSettings) []grpc.DialOption {
@@ -295,12 +281,6 @@
 		return nil, err
 	}
 
-	// NOTE(cbro): this is used only by the nightly mtls_smoketest and should
-	// not otherwise be used. It will be removed or renamed at some point.
-	if os.Getenv("GOOGLE_API_USE_MTLS") == "always" {
-		o.Endpoint = generateDefaultMtlsEndpoint(o.Endpoint)
-	}
-
 	return &o, nil
 }
 
diff --git a/transport/http/dial.go b/transport/http/dial.go
index a1a01c9..8578cac 100644
--- a/transport/http/dial.go
+++ b/transport/http/dial.go
@@ -13,9 +13,6 @@
 	"errors"
 	"net"
 	"net/http"
-	"net/url"
-	"os"
-	"strings"
 	"time"
 
 	"go.opencensus.io/plugin/ochttp"
@@ -25,12 +22,7 @@
 	"google.golang.org/api/option"
 	"google.golang.org/api/transport/cert"
 	"google.golang.org/api/transport/http/internal/propagation"
-)
-
-const (
-	mTLSModeAlways = "always"
-	mTLSModeNever  = "never"
-	mTLSModeAuto   = "auto"
+	"google.golang.org/api/transport/internal/dca"
 )
 
 // NewClient returns an HTTP client for use communicating with a Google cloud
@@ -41,11 +33,7 @@
 	if err != nil {
 		return nil, "", err
 	}
-	clientCertSource, err := getClientCertificateSource(settings)
-	if err != nil {
-		return nil, "", err
-	}
-	endpoint, err := getEndpoint(settings, clientCertSource)
+	clientCertSource, endpoint, err := dca.GetClientCertificateSourceAndEndpoint(settings)
 	if err != nil {
 		return nil, "", err
 	}
@@ -218,101 +206,3 @@
 		Propagation: &propagation.HTTPFormat{},
 	}
 }
-
-// getClientCertificateSource returns a default client certificate source, if
-// not provided by the user.
-//
-// A nil default source can be returned if the source does not exist. Any exceptions
-// encountered while initializing the default source will be reported as client
-// error (ex. corrupt metadata file).
-//
-// The overall logic is as follows:
-// 1. If both endpoint override and client certificate are specified, use them as is.
-// 2. If user does not specify client certificate, we will attempt to use default
-//    client certificate.
-// 3. If user does not specify endpoint override, we will use defaultMtlsEndpoint if
-//    client certificate is available and defaultEndpoint otherwise.
-//
-// Implications of the above logic:
-// 1. If the user specifies a non-mTLS endpoint override but client certificate is
-//    available, we will pass along the cert anyway and let the server decide what to do.
-// 2. If the user specifies an mTLS endpoint override but client certificate is not
-//    available, we will not fail-fast, but let backend throw error when connecting.
-//
-// We would like to avoid introducing client-side logic that parses whether the
-// endpoint override is an mTLS url, since the url pattern may change at anytime.
-//
-// Important Note: For now, the environment variable GOOGLE_API_USE_CLIENT_CERTIFICATE
-// must be set to "true" to allow certificate to be used (including user provided
-// certificates). For details, see AIP-4114.
-func getClientCertificateSource(settings *internal.DialSettings) (cert.Source, error) {
-	if !isClientCertificateEnabled() {
-		return nil, nil
-	} else if settings.HTTPClient != nil {
-		return nil, nil // HTTPClient is incompatible with ClientCertificateSource
-	} else if settings.ClientCertSource != nil {
-		return settings.ClientCertSource, nil
-	} else {
-		return cert.DefaultSource()
-	}
-
-}
-
-func isClientCertificateEnabled() bool {
-	useClientCert := os.Getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE")
-	// TODO(andyrzhao): Update default to return "true" after DCA feature is fully released.
-	return strings.ToLower(useClientCert) == "true"
-}
-
-// getEndpoint returns the endpoint for the service, taking into account the
-// user-provided endpoint override "settings.Endpoint"
-//
-// If no endpoint override is specified, we will either return the default endpoint or
-// the default mTLS endpoint if a client certificate is available.
-//
-// You can override the default endpoint (mtls vs. regular) by setting the
-// GOOGLE_API_USE_MTLS_ENDPOINT environment variable.
-//
-// If the endpoint override is an address (host:port) rather than full base
-// URL (ex. https://...), then the user-provided address will be merged into
-// the default endpoint. For example, WithEndpoint("myhost:8000") and
-// WithDefaultEndpoint("https://foo.com/bar/baz") will return "https://myhost:8080/bar/baz"
-func getEndpoint(settings *internal.DialSettings, clientCertSource cert.Source) (string, error) {
-	if settings.Endpoint == "" {
-		mtlsMode := getMTLSMode()
-		if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) {
-			return settings.DefaultMTLSEndpoint, nil
-		}
-		return settings.DefaultEndpoint, nil
-	}
-	if strings.Contains(settings.Endpoint, "://") {
-		// User passed in a full URL path, use it verbatim.
-		return settings.Endpoint, nil
-	}
-	if settings.DefaultEndpoint == "" {
-		return "", errors.New("WithEndpoint requires a full URL path")
-	}
-
-	// Assume user-provided endpoint is host[:port], merge it with the default endpoint.
-	return mergeEndpoints(settings.DefaultEndpoint, settings.Endpoint)
-}
-
-func getMTLSMode() string {
-	mode := os.Getenv("GOOGLE_API_USE_MTLS_ENDPOINT")
-	if mode == "" {
-		mode = os.Getenv("GOOGLE_API_USE_MTLS") // Deprecated.
-	}
-	if mode == "" {
-		return mTLSModeAuto
-	}
-	return strings.ToLower(mode)
-}
-
-func mergeEndpoints(base, newHost string) (string, error) {
-	u, err := url.Parse(base)
-	if err != nil {
-		return "", err
-	}
-	u.Host = newHost
-	return u.String(), nil
-}
diff --git a/transport/internal/dca/dca.go b/transport/internal/dca/dca.go
new file mode 100644
index 0000000..827a945
--- /dev/null
+++ b/transport/internal/dca/dca.go
@@ -0,0 +1,139 @@
+// Copyright 2020 Google LLC.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package dca contains utils for implementing Device Certificate
+// Authentication according to https://google.aip.dev/auth/4114
+//
+// The overall logic for DCA is as follows:
+// 1. If both endpoint override and client certificate are specified, use them as is.
+// 2. If user does not specify client certificate, we will attempt to use default
+//    client certificate.
+// 3. If user does not specify endpoint override, we will use defaultMtlsEndpoint if
+//    client certificate is available and defaultEndpoint otherwise.
+//
+// Implications of the above logic:
+// 1. If the user specifies a non-mTLS endpoint override but client certificate is
+//    available, we will pass along the cert anyway and let the server decide what to do.
+// 2. If the user specifies an mTLS endpoint override but client certificate is not
+//    available, we will not fail-fast, but let backend throw error when connecting.
+//
+// We would like to avoid introducing client-side logic that parses whether the
+// endpoint override is an mTLS url, since the url pattern may change at anytime.
+//
+// This package is not intended for use by end developers. Use the
+// google.golang.org/api/option package to configure API clients.
+package dca
+
+import (
+	"net/url"
+	"os"
+	"strings"
+
+	"google.golang.org/api/internal"
+	"google.golang.org/api/transport/cert"
+)
+
+const (
+	mTLSModeAlways = "always"
+	mTLSModeNever  = "never"
+	mTLSModeAuto   = "auto"
+)
+
+// GetClientCertificateSourceAndEndpoint is a convenience function that invokes
+// getClientCertificateSource and getEndpoint sequentially and returns the client
+// cert source and endpoint as a tuple.
+func GetClientCertificateSourceAndEndpoint(settings *internal.DialSettings) (cert.Source, string, error) {
+	clientCertSource, err := getClientCertificateSource(settings)
+	if err != nil {
+		return nil, "", err
+	}
+	endpoint, err := getEndpoint(settings, clientCertSource)
+	if err != nil {
+		return nil, "", err
+	}
+	return clientCertSource, endpoint, nil
+}
+
+// getClientCertificateSource returns a default client certificate source, if
+// not provided by the user.
+//
+// A nil default source can be returned if the source does not exist. Any exceptions
+// encountered while initializing the default source will be reported as client
+// error (ex. corrupt metadata file).
+//
+// Important Note: For now, the environment variable GOOGLE_API_USE_CLIENT_CERTIFICATE
+// must be set to "true" to allow certificate to be used (including user provided
+// certificates). For details, see AIP-4114.
+func getClientCertificateSource(settings *internal.DialSettings) (cert.Source, error) {
+	if !isClientCertificateEnabled() {
+		return nil, nil
+	} else if settings.HTTPClient != nil {
+		return nil, nil // HTTPClient is incompatible with ClientCertificateSource
+	} else if settings.ClientCertSource != nil {
+		return settings.ClientCertSource, nil
+	} else {
+		return cert.DefaultSource()
+	}
+}
+
+func isClientCertificateEnabled() bool {
+	useClientCert := os.Getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE")
+	// TODO(andyrzhao): Update default to return "true" after DCA feature is fully released.
+	return strings.ToLower(useClientCert) == "true"
+}
+
+// getEndpoint returns the endpoint for the service, taking into account the
+// user-provided endpoint override "settings.Endpoint".
+//
+// If no endpoint override is specified, we will either return the default endpoint or
+// the default mTLS endpoint if a client certificate is available.
+//
+// You can override the default endpoint choice (mtls vs. regular) by setting the
+// GOOGLE_API_USE_MTLS_ENDPOINT environment variable.
+//
+// If the endpoint override is an address (host:port) rather than full base
+// URL (ex. https://...), then the user-provided address will be merged into
+// the default endpoint. For example, WithEndpoint("myhost:8000") and
+// WithDefaultEndpoint("https://foo.com/bar/baz") will return "https://myhost:8080/bar/baz"
+func getEndpoint(settings *internal.DialSettings, clientCertSource cert.Source) (string, error) {
+	if settings.Endpoint == "" {
+		mtlsMode := getMTLSMode()
+		if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) {
+			return settings.DefaultMTLSEndpoint, nil
+		}
+		return settings.DefaultEndpoint, nil
+	}
+	if strings.Contains(settings.Endpoint, "://") {
+		// User passed in a full URL path, use it verbatim.
+		return settings.Endpoint, nil
+	}
+	if settings.DefaultEndpoint == "" {
+		// If DefaultEndpoint is not configured, use the user provided endpoint verbatim.
+		// This allows a naked "host[:port]" URL to be used with GRPC Direct Path.
+		return settings.Endpoint, nil
+	}
+
+	// Assume user-provided endpoint is host[:port], merge it with the default endpoint.
+	return mergeEndpoints(settings.DefaultEndpoint, settings.Endpoint)
+}
+
+func getMTLSMode() string {
+	mode := os.Getenv("GOOGLE_API_USE_MTLS_ENDPOINT")
+	if mode == "" {
+		mode = os.Getenv("GOOGLE_API_USE_MTLS") // Deprecated.
+	}
+	if mode == "" {
+		return mTLSModeAuto
+	}
+	return strings.ToLower(mode)
+}
+
+func mergeEndpoints(base, newHost string) (string, error) {
+	u, err := url.Parse(base)
+	if err != nil {
+		return "", err
+	}
+	u.Host = newHost
+	return u.String(), nil
+}
diff --git a/transport/http/dial_test.go b/transport/internal/dca/dca_test.go
similarity index 97%
rename from transport/http/dial_test.go
rename to transport/internal/dca/dca_test.go
index 5009ca2..2f61f08 100644
--- a/transport/http/dial_test.go
+++ b/transport/internal/dca/dca_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package http
+package dca
 
 import (
 	"testing"
@@ -36,7 +36,7 @@
 		{
 			UserEndpoint:    "host:port",
 			DefaultEndpoint: "",
-			WantErr:         true,
+			Want:            "host:port",
 		},
 	}
 
@@ -91,7 +91,7 @@
 		{
 			UserEndpoint:    "host:port",
 			DefaultEndpoint: "",
-			WantErr:         true,
+			Want:            "host:port",
 		},
 	}