blob: 0243009df6445cb16ebeddee48326761e98cb6c0 [file] [log] [blame]
/*
*
* 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 provides a transport credentials implementation where the
// security configuration is pushed by a management server using xDS APIs.
//
// Experimental
//
// Notice: All APIs in this package are EXPERIMENTAL and may be removed in a
// later release.
package xds
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"time"
"google.golang.org/grpc/credentials"
credinternal "google.golang.org/grpc/internal/credentials"
xdsinternal "google.golang.org/grpc/internal/credentials/xds"
)
// ClientOptions contains parameters to configure a new client-side xDS
// credentials implementation.
type ClientOptions struct {
// FallbackCreds specifies the fallback credentials to be used when either
// the `xds` scheme is not used in the user's dial target or when the
// management server does not return any security configuration. Attempts to
// create client credentials without fallback credentials will fail.
FallbackCreds credentials.TransportCredentials
}
// NewClientCredentials returns a new client-side transport credentials
// implementation which uses xDS APIs to fetch its security configuration.
func NewClientCredentials(opts ClientOptions) (credentials.TransportCredentials, error) {
if opts.FallbackCreds == nil {
return nil, errors.New("missing fallback credentials")
}
return &credsImpl{
isClient: true,
fallback: opts.FallbackCreds,
}, nil
}
// ServerOptions contains parameters to configure a new server-side xDS
// credentials implementation.
type ServerOptions struct {
// FallbackCreds specifies the fallback credentials to be used when the
// management server does not return any security configuration. Attempts to
// create server credentials without fallback credentials will fail.
FallbackCreds credentials.TransportCredentials
}
// NewServerCredentials returns a new server-side transport credentials
// implementation which uses xDS APIs to fetch its security configuration.
func NewServerCredentials(opts ServerOptions) (credentials.TransportCredentials, error) {
if opts.FallbackCreds == nil {
return nil, errors.New("missing fallback credentials")
}
return &credsImpl{
isClient: false,
fallback: opts.FallbackCreds,
}, nil
}
// credsImpl is an implementation of the credentials.TransportCredentials
// interface which uses xDS APIs to fetch its security configuration.
type credsImpl struct {
isClient bool
fallback credentials.TransportCredentials
}
// ClientHandshake performs the TLS handshake on the client-side.
//
// It looks for the presence of a HandshakeInfo value in the passed in context
// (added using a call to NewContextWithHandshakeInfo()), and retrieves identity
// and root certificates from there. It also retrieves a list of acceptable SANs
// and uses a custom verification function to validate the certificate presented
// by the peer. It uses fallback credentials if no HandshakeInfo is present in
// the passed in context.
func (c *credsImpl) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
if !c.isClient {
return nil, nil, errors.New("ClientHandshake() is not supported for server credentials")
}
// The CDS balancer constructs a new HandshakeInfo using a call to
// NewHandshakeInfo(), and then adds it to the attributes field of the
// resolver.Address when handling calls to NewSubConn(). The transport layer
// takes care of shipping these attributes in the context to this handshake
// function. We first read the credentials.ClientHandshakeInfo type from the
// context, which contains the attributes added by the CDS balancer. We then
// read the HandshakeInfo from the attributes to get to the actual data that
// we need here for the handshake.
chi := credentials.ClientHandshakeInfoFromContext(ctx)
// If there are no attributes in the received context or the attributes does
// not contain a HandshakeInfo, it could either mean that the user did not
// specify an `xds` scheme in their dial target or that the xDS server did
// not provide any security configuration. In both of these cases, we use
// the fallback credentials specified by the user.
if chi.Attributes == nil {
return c.fallback.ClientHandshake(ctx, authority, rawConn)
}
hi := xdsinternal.GetHandshakeInfo(chi.Attributes)
if hi.UseFallbackCreds() {
return c.fallback.ClientHandshake(ctx, authority, rawConn)
}
// We build the tls.Config with the following values
// 1. Root certificate as returned by the root provider.
// 2. Identity certificate as returned by the identity provider. This may be
// empty on the client side, if the client is not doing mTLS.
// 3. InsecureSkipVerify to true. Certificates used in Mesh environments
// usually contains the identity of the workload presenting the
// certificate as a SAN (instead of a hostname in the CommonName field).
// This means that normal certificate verification as done by the
// standard library will fail.
// 4. Key usage to match whether client/server usage.
// 5. A `VerifyPeerCertificate` function which performs normal peer
// cert verification using configured roots, and the custom SAN checks.
cfg, err := hi.ClientSideTLSConfig(ctx)
if err != nil {
return nil, nil, err
}
cfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
// 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.
intermediates := x509.NewCertPool()
for _, cert := range certs[1:] {
intermediates.AddCert(cert)
}
opts := x509.VerifyOptions{
Roots: cfg.RootCAs,
Intermediates: intermediates,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
if _, err := certs[0].Verify(opts); err != nil {
return err
}
// The SANs sent by the MeshCA are encoded as SPIFFE IDs. We need to
// only look at the SANs on the leaf cert.
if !hi.MatchingSANExists(certs[0]) {
return fmt.Errorf("SANs received in leaf certificate %+v does not match any of the accepted SANs", certs[0])
}
return nil
}
// Perform the TLS handshake with the tls.Config that we have. We run the
// actual Handshake() function in a goroutine because we need to respect the
// deadline specified on the passed in context, and we need a way to cancel
// the handshake if the context is cancelled.
conn := tls.Client(rawConn, cfg)
errCh := make(chan error, 1)
go func() {
errCh <- conn.Handshake()
close(errCh)
}()
select {
case err := <-errCh:
if err != nil {
conn.Close()
return nil, nil, err
}
case <-ctx.Done():
conn.Close()
return nil, nil, ctx.Err()
}
info := credentials.TLSInfo{
State: conn.ConnectionState(),
CommonAuthInfo: credentials.CommonAuthInfo{
SecurityLevel: credentials.PrivacyAndIntegrity,
},
SPIFFEID: credinternal.SPIFFEIDFromState(conn.ConnectionState()),
}
return credinternal.WrapSyscallConn(rawConn, conn), info, nil
}
// ServerHandshake performs the TLS handshake on the server-side.
func (c *credsImpl) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
if c.isClient {
return nil, nil, errors.New("ServerHandshake is not supported for client credentials")
}
// An xds-enabled gRPC server wraps the underlying raw net.Conn in a type
// that provides a way to retrieve `HandshakeInfo`, which contains the
// certificate providers to be used during the handshake. If the net.Conn
// passed to this function does not implement this interface, or if the
// `HandshakeInfo` does not contain the information we are looking for, we
// delegate the handshake to the fallback credentials.
hiConn, ok := rawConn.(interface {
XDSHandshakeInfo() (*xdsinternal.HandshakeInfo, error)
})
if !ok {
return c.fallback.ServerHandshake(rawConn)
}
hi, err := hiConn.XDSHandshakeInfo()
if err != nil {
return nil, nil, err
}
if hi.UseFallbackCreds() {
return c.fallback.ServerHandshake(rawConn)
}
// An xds-enabled gRPC server is expected to wrap the underlying raw
// net.Conn in a type which provides a way to retrieve the deadline set on
// it. If we cannot retrieve the deadline here, we fail (by setting deadline
// to time.Now()), instead of using a default deadline and possibly taking
// longer to eventually fail.
deadline := time.Now()
if dConn, ok := rawConn.(interface{ GetDeadline() time.Time }); ok {
deadline = dConn.GetDeadline()
}
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
cfg, err := hi.ServerSideTLSConfig(ctx)
if err != nil {
return nil, nil, err
}
conn := tls.Server(rawConn, cfg)
if err := conn.Handshake(); err != nil {
conn.Close()
return nil, nil, err
}
info := credentials.TLSInfo{
State: conn.ConnectionState(),
CommonAuthInfo: credentials.CommonAuthInfo{
SecurityLevel: credentials.PrivacyAndIntegrity,
},
}
info.SPIFFEID = credinternal.SPIFFEIDFromState(conn.ConnectionState())
return credinternal.WrapSyscallConn(rawConn, conn), info, nil
}
// Info provides the ProtocolInfo of this TransportCredentials.
func (c *credsImpl) Info() credentials.ProtocolInfo {
return credentials.ProtocolInfo{SecurityProtocol: "tls"}
}
// Clone makes a copy of this TransportCredentials.
func (c *credsImpl) Clone() credentials.TransportCredentials {
clone := *c
return &clone
}
func (c *credsImpl) OverrideServerName(_ string) error {
return errors.New("serverName for peer validation must be configured as a list of acceptable SANs")
}
// UsesXDS returns true if c uses xDS to fetch security configuration
// used at handshake time, and false otherwise.
func (c *credsImpl) UsesXDS() bool {
return true
}