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",
},
}