Skip to content

Commit

Permalink
xds/internal/xdsclient: A65 - mTLS Credentials
Browse files Browse the repository at this point in the history
Implement A65: mTLS Credentials in xDS Bootstrap File described in
https://github.com/grpc/proposal/blob/master/A65-xds-mtls-creds-in-bootstrap.md.
  • Loading branch information
atollena committed Oct 31, 2023
1 parent b82468a commit 9b46618
Show file tree
Hide file tree
Showing 4 changed files with 445 additions and 0 deletions.
14 changes: 14 additions & 0 deletions xds/internal/xdsclient/bootstrap/bootstrap.go
Expand Up @@ -24,6 +24,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"google.golang.org/grpc/xds/internal/xdsclient/creds"
"net/url"
"os"
"strings"
Expand Down Expand Up @@ -60,6 +61,7 @@ const (
func init() {
bootstrap.RegisterCredentials(&insecureCredsBuilder{})
bootstrap.RegisterCredentials(&googleDefaultCredsBuilder{})
bootstrap.RegisterCredentials(&tlsCredsBuilder{})
}

// For overriding in unit tests.
Expand All @@ -77,6 +79,18 @@ func (i *insecureCredsBuilder) Name() string {
return "insecure"
}

// tlsCredsBuilder implements the `Credentials` interface defined in
// package `xds/bootstrap` and encapsulates a TLS credential.
type tlsCredsBuilder struct{}

func (t *tlsCredsBuilder) Build(config json.RawMessage) (credentials.Bundle, error) {
return creds.NewTLS(config)
}

func (t *tlsCredsBuilder) Name() string {
return "tls"
}

// googleDefaultCredsBuilder implements the `Credentials` interface defined in
// package `xds/boostrap` and encapsulates a Google Default credential.
type googleDefaultCredsBuilder struct{}
Expand Down
23 changes: 23 additions & 0 deletions xds/internal/xdsclient/bootstrap/bootstrap_test.go
Expand Up @@ -1015,6 +1015,10 @@ func TestDefaultBundles(t *testing.T) {
if c := bootstrap.GetCredentials("insecure"); c == nil {
t.Errorf(`bootstrap.GetCredentials("insecure") credential is nil, want non-nil`)
}

if c := bootstrap.GetCredentials("tls"); c == nil {
t.Errorf(`bootstrap.GetCredentials("tls") credential is nil, want non-nil`)
}
}

func TestCredsBuilders(t *testing.T) {
Expand All @@ -1034,4 +1038,23 @@ func TestCredsBuilders(t *testing.T) {
if got, want := i.Name(), "insecure"; got != want {
t.Errorf("insecureCredsBuilder.Name = %v, want %v", got, want)
}

tcb := &tlsCredsBuilder{}
if _, err := tcb.Build(nil); err == nil {
t.Errorf("tlsCredsBuilder.Build succeeded, want failure")
}
if got, want := tcb.Name(), "tls"; got != want {
t.Errorf("tlsCredsBuilder.Name = %v, want %v", got, want)
}
}

func TestTlsCredsBuilder(t *testing.T) {
tls := &tlsCredsBuilder{}
if _, err := tls.Build(json.RawMessage(`{}`)); err != nil {
t.Errorf("unexpected error with empty config: %s", err)
}
if _, err := tls.Build(json.RawMessage(`{"ca_certificate_file":"/ca_certificates.pem","refresh_interval": "asdf"}`)); err == nil {
t.Errorf("expected an error with invalid refresh interval")
}
// more tests for config validity are defined in creds subpackage.
}
176 changes: 176 additions & 0 deletions xds/internal/xdsclient/creds/tls.go
@@ -0,0 +1,176 @@
/*
*
* Copyright 2023 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 creds implements gRFC A65: mTLS Credentials in xDS Bootstrap File
package creds

import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/tls/certprovider"
_ "google.golang.org/grpc/credentials/tls/certprovider/pemfile" // for file_watcher provider
"net"
"sync"
)

type tlsBundle struct {
jd json.RawMessage
transportCredentials credentials.TransportCredentials
}

// NewTLS returns a credentials.Bundle which delegates certificate loading to
// a file_watcher provider for mTLS transport security. See gRFC A65.
func NewTLS(jd json.RawMessage) (credentials.Bundle, error) {
cfg := &struct {
CertificateFile string `json:"certificate_file"`
CACertificateFile string `json:"ca_certificate_file"`
PrivateKeyFile string `json:"private_key_file"`
}{}

tlsConfig := tls.Config{}
if err := json.Unmarshal(jd, cfg); err != nil {
return nil, err
}

// We cannot simply always use a file_watcher provider because it behaves
// slightly differently from the xDS TLS config. Quoting A65:
//
// > The only difference between the file-watcher certificate provider
// > config and this one is that in the file-watcher certificate provider,
// > at least one of the "certificate_file" or "ca_certificate_file" fields
// > must be specified, whereas in this configuration, it is acceptable to
// > specify neither one.
//
// We only use a file_watcher provider if either one of them or both are
// specified.
if cfg.CACertificateFile != "" || cfg.CertificateFile != "" || cfg.PrivateKeyFile != "" {
// file_watcher currently ignores BuildOptions, but we set them for good
// measure.
opts := certprovider.BuildOptions{}
if cfg.CACertificateFile != "" {
opts.WantRoot = true
}
if cfg.CertificateFile != "" {
opts.WantIdentity = true
}
provider, err := certprovider.GetProvider("file_watcher", jd, opts)
if err != nil {
// GetProvider fails if jd is invalid, e.g. if only one of private
// key and certificate is specified.
return nil, fmt.Errorf("failed to get TLS provider: %w", err)
}
if cfg.CertificateFile != "" {
tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
// Client cert reloading for mTLS.
km, err := provider.KeyMaterial(context.Background())
if err != nil {
return nil, err
}

Check warning on line 86 in xds/internal/xdsclient/creds/tls.go

View check run for this annotation

Codecov / codecov/patch

xds/internal/xdsclient/creds/tls.go#L85-L86

Added lines #L85 - L86 were not covered by tests
if len(km.Certs) != 1 {
return nil, fmt.Errorf("there should be exactly exactly one certificate")
}

Check warning on line 89 in xds/internal/xdsclient/creds/tls.go

View check run for this annotation

Codecov / codecov/patch

xds/internal/xdsclient/creds/tls.go#L88-L89

Added lines #L88 - L89 were not covered by tests
return &km.Certs[0], nil
}
if cfg.CACertificateFile == "" {
// no need for a callback to load the CA. Use the normal mTLS
// transport credentials.
return &tlsBundle{
jd: jd,
transportCredentials: credentials.NewTLS(&tlsConfig),
}, nil
}
}
return &tlsBundle{
jd: jd,
transportCredentials: &caReloadingClientTLSCreds{
baseConfig: &tlsConfig,
provider: provider,
},
}, nil
}

// None of certificate_file and ca_certificate_file are set.
// Use the system-wide root certs.
return &tlsBundle{
jd: jd,
transportCredentials: credentials.NewTLS(&tlsConfig),
}, nil
}

func (t *tlsBundle) TransportCredentials() credentials.TransportCredentials {
return t.transportCredentials
}

func (t *tlsBundle) PerRPCCredentials() credentials.PerRPCCredentials {
// No per-RPC credentials in A65.
return nil
}

func (t *tlsBundle) NewWithMode(_ string) (credentials.Bundle, error) {
return NewTLS(t.jd)

Check warning on line 128 in xds/internal/xdsclient/creds/tls.go

View check run for this annotation

Codecov / codecov/patch

xds/internal/xdsclient/creds/tls.go#L127-L128

Added lines #L127 - L128 were not covered by tests
}

// caReloadingClientTLSCreds is a client mTLS credentials.TransportCredentials
// that attempts to reload the server root certificate from its provider on
// every client handshake. This is needed because Go's tls.Config does not
// support reloading the root CAs.
type caReloadingClientTLSCreds struct {
mu sync.Mutex
provider certprovider.Provider
baseConfig *tls.Config
}

func (c *caReloadingClientTLSCreds) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
km, err := c.provider.KeyMaterial(ctx)
if err != nil {
return nil, nil, err
}

Check warning on line 145 in xds/internal/xdsclient/creds/tls.go

View check run for this annotation

Codecov / codecov/patch

xds/internal/xdsclient/creds/tls.go#L144-L145

Added lines #L144 - L145 were not covered by tests
c.mu.Lock()
if !km.Roots.Equal(c.baseConfig.RootCAs) {
// provider returned a different root CA. Update it.
c.baseConfig.RootCAs = km.Roots
}
c.mu.Unlock()
return credentials.NewTLS(c.baseConfig).ClientHandshake(ctx, authority, rawConn)
}

func (c *caReloadingClientTLSCreds) Info() credentials.ProtocolInfo {
c.mu.Lock()
defer c.mu.Unlock()
return credentials.NewTLS(c.baseConfig).Info()
}

func (c *caReloadingClientTLSCreds) Clone() credentials.TransportCredentials {
c.mu.Lock()
defer c.mu.Unlock()
return &caReloadingClientTLSCreds{
provider: c.provider,
baseConfig: c.baseConfig.Clone(),
}

Check warning on line 167 in xds/internal/xdsclient/creds/tls.go

View check run for this annotation

Codecov / codecov/patch

xds/internal/xdsclient/creds/tls.go#L161-L167

Added lines #L161 - L167 were not covered by tests
}

func (c *caReloadingClientTLSCreds) OverrideServerName(_ string) error {
panic("cannot override server name for private xds tls credentials")

Check warning on line 171 in xds/internal/xdsclient/creds/tls.go

View check run for this annotation

Codecov / codecov/patch

xds/internal/xdsclient/creds/tls.go#L170-L171

Added lines #L170 - L171 were not covered by tests
}

func (c *caReloadingClientTLSCreds) ServerHandshake(_ net.Conn) (net.Conn, credentials.AuthInfo, error) {
panic("server handshake for xds tls credentials, which are client only")

Check warning on line 175 in xds/internal/xdsclient/creds/tls.go

View check run for this annotation

Codecov / codecov/patch

xds/internal/xdsclient/creds/tls.go#L174-L175

Added lines #L174 - L175 were not covered by tests
}

0 comments on commit 9b46618

Please sign in to comment.