snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 20:40:15 +05:00
commit e241b33f26
1365 changed files with 339570 additions and 0 deletions

322
credentials/alts/alts.go Normal file
View File

@@ -0,0 +1,322 @@
/*
*
* Copyright 2018 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 alts implements the ALTS credential support by gRPC library, which
// encapsulates all the state needed by a client to authenticate with a server
// using ALTS and make various assertions, e.g., about the client's identity,
// role, or whether it is authorized to make a particular call.
// This package is experimental.
package alts
import (
"context"
"errors"
"fmt"
"net"
"sync"
"time"
"google.golang.org/grpc/credentials"
core "google.golang.org/grpc/credentials/alts/internal"
"google.golang.org/grpc/credentials/alts/internal/handshaker"
"google.golang.org/grpc/credentials/alts/internal/handshaker/service"
altspb "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/internal/googlecloud"
)
const (
// hypervisorHandshakerServiceAddress represents the default ALTS gRPC
// handshaker service address in the hypervisor.
hypervisorHandshakerServiceAddress = "dns:///metadata.google.internal.:8080"
// defaultTimeout specifies the server handshake timeout.
defaultTimeout = 30.0 * time.Second
// The following constants specify the minimum and maximum acceptable
// protocol versions.
protocolVersionMaxMajor = 2
protocolVersionMaxMinor = 1
protocolVersionMinMajor = 2
protocolVersionMinMinor = 1
)
var (
vmOnGCP bool
once sync.Once
maxRPCVersion = &altspb.RpcProtocolVersions_Version{
Major: protocolVersionMaxMajor,
Minor: protocolVersionMaxMinor,
}
minRPCVersion = &altspb.RpcProtocolVersions_Version{
Major: protocolVersionMinMajor,
Minor: protocolVersionMinMinor,
}
// ErrUntrustedPlatform is returned from ClientHandshake and
// ServerHandshake is running on a platform where the trustworthiness of
// the handshaker service is not guaranteed.
ErrUntrustedPlatform = errors.New("ALTS: untrusted platform. ALTS is only supported on GCP")
logger = grpclog.Component("alts")
)
// AuthInfo exposes security information from the ALTS handshake to the
// application. This interface is to be implemented by ALTS. Users should not
// need a brand new implementation of this interface. For situations like
// testing, any new implementation should embed this interface. This allows
// ALTS to add new methods to this interface.
type AuthInfo interface {
// ApplicationProtocol returns application protocol negotiated for the
// ALTS connection.
ApplicationProtocol() string
// RecordProtocol returns the record protocol negotiated for the ALTS
// connection.
RecordProtocol() string
// SecurityLevel returns the security level of the created ALTS secure
// channel.
SecurityLevel() altspb.SecurityLevel
// PeerServiceAccount returns the peer service account.
PeerServiceAccount() string
// LocalServiceAccount returns the local service account.
LocalServiceAccount() string
// PeerRPCVersions returns the RPC version supported by the peer.
PeerRPCVersions() *altspb.RpcProtocolVersions
}
// ClientOptions contains the client-side options of an ALTS channel. These
// options will be passed to the underlying ALTS handshaker.
type ClientOptions struct {
// TargetServiceAccounts contains a list of expected target service
// accounts.
TargetServiceAccounts []string
// HandshakerServiceAddress represents the ALTS handshaker gRPC service
// address to connect to.
HandshakerServiceAddress string
}
// DefaultClientOptions creates a new ClientOptions object with the default
// values.
func DefaultClientOptions() *ClientOptions {
return &ClientOptions{
HandshakerServiceAddress: hypervisorHandshakerServiceAddress,
}
}
// ServerOptions contains the server-side options of an ALTS channel. These
// options will be passed to the underlying ALTS handshaker.
type ServerOptions struct {
// HandshakerServiceAddress represents the ALTS handshaker gRPC service
// address to connect to.
HandshakerServiceAddress string
}
// DefaultServerOptions creates a new ServerOptions object with the default
// values.
func DefaultServerOptions() *ServerOptions {
return &ServerOptions{
HandshakerServiceAddress: hypervisorHandshakerServiceAddress,
}
}
// altsTC is the credentials required for authenticating a connection using ALTS.
// It implements credentials.TransportCredentials interface.
type altsTC struct {
info *credentials.ProtocolInfo
side core.Side
accounts []string
hsAddress string
boundAccessToken string
}
// NewClientCreds constructs a client-side ALTS TransportCredentials object.
func NewClientCreds(opts *ClientOptions) credentials.TransportCredentials {
return newALTS(core.ClientSide, opts.TargetServiceAccounts, opts.HandshakerServiceAddress)
}
// NewServerCreds constructs a server-side ALTS TransportCredentials object.
func NewServerCreds(opts *ServerOptions) credentials.TransportCredentials {
return newALTS(core.ServerSide, nil, opts.HandshakerServiceAddress)
}
func newALTS(side core.Side, accounts []string, hsAddress string) credentials.TransportCredentials {
once.Do(func() {
vmOnGCP = googlecloud.OnGCE()
})
if hsAddress == "" {
hsAddress = hypervisorHandshakerServiceAddress
}
return &altsTC{
info: &credentials.ProtocolInfo{
SecurityProtocol: "alts",
SecurityVersion: "1.0",
},
side: side,
accounts: accounts,
hsAddress: hsAddress,
}
}
// ClientHandshake implements the client side handshake protocol.
func (g *altsTC) ClientHandshake(ctx context.Context, addr string, rawConn net.Conn) (_ net.Conn, _ credentials.AuthInfo, err error) {
if !vmOnGCP {
return nil, nil, ErrUntrustedPlatform
}
// Connecting to ALTS handshaker service.
hsConn, err := service.Dial(g.hsAddress)
if err != nil {
return nil, nil, err
}
// Do not close hsConn since it is shared with other handshakes.
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(ctx)
defer cancel()
opts := handshaker.DefaultClientHandshakerOptions()
opts.TargetName = addr
opts.TargetServiceAccounts = g.accounts
opts.RPCVersions = &altspb.RpcProtocolVersions{
MaxRpcVersion: maxRPCVersion,
MinRpcVersion: minRPCVersion,
}
opts.BoundAccessToken = g.boundAccessToken
chs, err := handshaker.NewClientHandshaker(ctx, hsConn, rawConn, opts)
if err != nil {
return nil, nil, err
}
// Close the handshaker since we have obtained a connection.
defer chs.Close()
secConn, authInfo, err := chs.ClientHandshake(ctx)
if err != nil {
return nil, nil, err
}
altsAuthInfo, ok := authInfo.(AuthInfo)
if !ok {
return nil, nil, errors.New("client-side auth info is not of type alts.AuthInfo")
}
match, _ := checkRPCVersions(opts.RPCVersions, altsAuthInfo.PeerRPCVersions())
if !match {
return nil, nil, fmt.Errorf("server-side RPC versions are not compatible with this client, local versions: %v, peer versions: %v", opts.RPCVersions, altsAuthInfo.PeerRPCVersions())
}
return secConn, authInfo, nil
}
// ServerHandshake implements the server side ALTS handshaker.
func (g *altsTC) ServerHandshake(rawConn net.Conn) (_ net.Conn, _ credentials.AuthInfo, err error) {
if !vmOnGCP {
return nil, nil, ErrUntrustedPlatform
}
// Connecting to ALTS handshaker service.
hsConn, err := service.Dial(g.hsAddress)
if err != nil {
return nil, nil, err
}
// Do not close hsConn since it's shared with other handshakes.
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
opts := handshaker.DefaultServerHandshakerOptions()
opts.RPCVersions = &altspb.RpcProtocolVersions{
MaxRpcVersion: maxRPCVersion,
MinRpcVersion: minRPCVersion,
}
shs, err := handshaker.NewServerHandshaker(ctx, hsConn, rawConn, opts)
if err != nil {
return nil, nil, err
}
secConn, authInfo, err := shs.ServerHandshake(ctx)
if err != nil {
return nil, nil, err
}
// Close the handshaker since we have obtained a connection.
defer shs.Close()
altsAuthInfo, ok := authInfo.(AuthInfo)
if !ok {
return nil, nil, errors.New("server-side auth info is not of type alts.AuthInfo")
}
match, _ := checkRPCVersions(opts.RPCVersions, altsAuthInfo.PeerRPCVersions())
if !match {
return nil, nil, fmt.Errorf("client-side RPC versions is not compatible with this server, local versions: %v, peer versions: %v", opts.RPCVersions, altsAuthInfo.PeerRPCVersions())
}
return secConn, authInfo, nil
}
func (g *altsTC) Info() credentials.ProtocolInfo {
return *g.info
}
func (g *altsTC) Clone() credentials.TransportCredentials {
info := *g.info
var accounts []string
if g.accounts != nil {
accounts = make([]string, len(g.accounts))
copy(accounts, g.accounts)
}
return &altsTC{
info: &info,
side: g.side,
hsAddress: g.hsAddress,
accounts: accounts,
boundAccessToken: g.boundAccessToken,
}
}
func (g *altsTC) OverrideServerName(serverNameOverride string) error {
g.info.ServerName = serverNameOverride
return nil
}
// compareRPCVersion returns 0 if v1 == v2, 1 if v1 > v2 and -1 if v1 < v2.
func compareRPCVersions(v1, v2 *altspb.RpcProtocolVersions_Version) int {
switch {
case v1.GetMajor() > v2.GetMajor(),
v1.GetMajor() == v2.GetMajor() && v1.GetMinor() > v2.GetMinor():
return 1
case v1.GetMajor() < v2.GetMajor(),
v1.GetMajor() == v2.GetMajor() && v1.GetMinor() < v2.GetMinor():
return -1
}
return 0
}
// checkRPCVersions performs a version check between local and peer rpc protocol
// versions. This function returns true if the check passes which means both
// parties agreed on a common rpc protocol to use, and false otherwise. The
// function also returns the highest common RPC protocol version both parties
// agreed on.
func checkRPCVersions(local, peer *altspb.RpcProtocolVersions) (bool, *altspb.RpcProtocolVersions_Version) {
if local == nil || peer == nil {
logger.Error("invalid checkRPCVersions argument, either local or peer is nil.")
return false, nil
}
// maxCommonVersion is MIN(local.max, peer.max).
maxCommonVersion := local.GetMaxRpcVersion()
if compareRPCVersions(local.GetMaxRpcVersion(), peer.GetMaxRpcVersion()) > 0 {
maxCommonVersion = peer.GetMaxRpcVersion()
}
// minCommonVersion is MAX(local.min, peer.min).
minCommonVersion := peer.GetMinRpcVersion()
if compareRPCVersions(local.GetMinRpcVersion(), peer.GetMinRpcVersion()) > 0 {
minCommonVersion = local.GetMinRpcVersion()
}
if compareRPCVersions(maxCommonVersion, minCommonVersion) < 0 {
return false, nil
}
return true, maxCommonVersion
}

View File

@@ -0,0 +1,505 @@
//go:build linux || windows
// +build linux windows
/*
*
* Copyright 2018 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 alts
import (
"context"
"reflect"
"sync"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/alts/internal/handshaker"
"google.golang.org/grpc/credentials/alts/internal/handshaker/service"
altsgrpc "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"
altspb "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"
"google.golang.org/grpc/credentials/alts/internal/testutil"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/stubserver"
"google.golang.org/grpc/internal/testutils"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
const (
defaultTestLongTimeout = 60 * time.Second
defaultTestShortTimeout = 10 * time.Millisecond
)
type s struct {
grpctest.Tester
}
func init() {
// The vmOnGCP global variable MUST be forced to true. Otherwise, if
// this test is run anywhere except on a GCP VM, then an ALTS handshake
// will immediately fail.
once.Do(func() {})
vmOnGCP = true
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
func (s) TestInfoServerName(t *testing.T) {
// This is not testing any handshaker functionality, so it's fine to only
// use NewServerCreds and not NewClientCreds.
alts := NewServerCreds(DefaultServerOptions())
if got, want := alts.Info().ServerName, ""; got != want {
t.Fatalf("%v.Info().ServerName = %v, want %v", alts, got, want)
}
}
func (s) TestOverrideServerName(t *testing.T) {
wantServerName := "server.name"
// This is not testing any handshaker functionality, so it's fine to only
// use NewServerCreds and not NewClientCreds.
c := NewServerCreds(DefaultServerOptions())
c.OverrideServerName(wantServerName)
if got, want := c.Info().ServerName, wantServerName; got != want {
t.Fatalf("c.Info().ServerName = %v, want %v", got, want)
}
}
func (s) TestCloneClient(t *testing.T) {
wantServerName := "server.name"
opt := DefaultClientOptions()
opt.TargetServiceAccounts = []string{"not", "empty"}
c := NewClientCreds(opt)
c.(*altsTC).boundAccessToken = "bound-access-token"
c.OverrideServerName(wantServerName)
cc := c.Clone()
if got, want := cc.Info().ServerName, wantServerName; got != want {
t.Fatalf("cc.Info().ServerName = %v, want %v", got, want)
}
cc.OverrideServerName("")
if got, want := c.Info().ServerName, wantServerName; got != want {
t.Fatalf("Change in clone should not affect the original, c.Info().ServerName = %v, want %v", got, want)
}
if got, want := cc.Info().ServerName, ""; got != want {
t.Fatalf("cc.Info().ServerName = %v, want %v", got, want)
}
ct := c.(*altsTC)
cct := cc.(*altsTC)
if ct.side != cct.side {
t.Errorf("cc.side = %q, want %q", cct.side, ct.side)
}
if ct.hsAddress != cct.hsAddress {
t.Errorf("cc.hsAddress = %q, want %q", cct.hsAddress, ct.hsAddress)
}
if !reflect.DeepEqual(ct.accounts, cct.accounts) {
t.Errorf("cc.accounts = %q, want %q", cct.accounts, ct.accounts)
}
if ct.boundAccessToken != cct.boundAccessToken {
t.Errorf("cc.boundAccessToken = %q, want %q", cct.boundAccessToken, ct.boundAccessToken)
}
}
func (s) TestCloneServer(t *testing.T) {
wantServerName := "server.name"
c := NewServerCreds(DefaultServerOptions())
c.OverrideServerName(wantServerName)
cc := c.Clone()
if got, want := cc.Info().ServerName, wantServerName; got != want {
t.Fatalf("cc.Info().ServerName = %v, want %v", got, want)
}
cc.OverrideServerName("")
if got, want := c.Info().ServerName, wantServerName; got != want {
t.Fatalf("Change in clone should not affect the original, c.Info().ServerName = %v, want %v", got, want)
}
if got, want := cc.Info().ServerName, ""; got != want {
t.Fatalf("cc.Info().ServerName = %v, want %v", got, want)
}
ct := c.(*altsTC)
cct := cc.(*altsTC)
if ct.side != cct.side {
t.Errorf("cc.side = %q, want %q", cct.side, ct.side)
}
if ct.hsAddress != cct.hsAddress {
t.Errorf("cc.hsAddress = %q, want %q", cct.hsAddress, ct.hsAddress)
}
if !reflect.DeepEqual(ct.accounts, cct.accounts) {
t.Errorf("cc.accounts = %q, want %q", cct.accounts, ct.accounts)
}
}
func (s) TestInfo(t *testing.T) {
// This is not testing any handshaker functionality, so it's fine to only
// use NewServerCreds and not NewClientCreds.
c := NewServerCreds(DefaultServerOptions())
info := c.Info()
if got, want := info.SecurityProtocol, "alts"; got != want {
t.Errorf("info.SecurityProtocol=%v, want %v", got, want)
}
if got, want := info.SecurityVersion, "1.0"; got != want {
t.Errorf("info.SecurityVersion=%v, want %v", got, want)
}
if got, want := info.ServerName, ""; got != want {
t.Errorf("info.ServerName=%v, want %v", got, want)
}
}
func (s) TestCompareRPCVersions(t *testing.T) {
for _, tc := range []struct {
v1 *altspb.RpcProtocolVersions_Version
v2 *altspb.RpcProtocolVersions_Version
output int
}{
{
version(3, 2),
version(2, 1),
1,
},
{
version(3, 2),
version(3, 1),
1,
},
{
version(2, 1),
version(3, 2),
-1,
},
{
version(3, 1),
version(3, 2),
-1,
},
{
version(3, 2),
version(3, 2),
0,
},
} {
if got, want := compareRPCVersions(tc.v1, tc.v2), tc.output; got != want {
t.Errorf("compareRPCVersions(%v, %v)=%v, want %v", tc.v1, tc.v2, got, want)
}
}
}
func (s) TestCheckRPCVersions(t *testing.T) {
for _, tc := range []struct {
desc string
local *altspb.RpcProtocolVersions
peer *altspb.RpcProtocolVersions
output bool
maxCommonVersion *altspb.RpcProtocolVersions_Version
}{
{
"local.max > peer.max and local.min > peer.min",
versions(2, 1, 3, 2),
versions(1, 2, 2, 1),
true,
version(2, 1),
},
{
"local.max > peer.max and local.min < peer.min",
versions(1, 2, 3, 2),
versions(2, 1, 2, 1),
true,
version(2, 1),
},
{
"local.max > peer.max and local.min = peer.min",
versions(2, 1, 3, 2),
versions(2, 1, 2, 1),
true,
version(2, 1),
},
{
"local.max < peer.max and local.min > peer.min",
versions(2, 1, 2, 1),
versions(1, 2, 3, 2),
true,
version(2, 1),
},
{
"local.max = peer.max and local.min > peer.min",
versions(2, 1, 2, 1),
versions(1, 2, 2, 1),
true,
version(2, 1),
},
{
"local.max < peer.max and local.min < peer.min",
versions(1, 2, 2, 1),
versions(2, 1, 3, 2),
true,
version(2, 1),
},
{
"local.max < peer.max and local.min = peer.min",
versions(1, 2, 2, 1),
versions(1, 2, 3, 2),
true,
version(2, 1),
},
{
"local.max = peer.max and local.min < peer.min",
versions(1, 2, 2, 1),
versions(2, 1, 2, 1),
true,
version(2, 1),
},
{
"all equal",
versions(2, 1, 2, 1),
versions(2, 1, 2, 1),
true,
version(2, 1),
},
{
"max is smaller than min",
versions(2, 1, 1, 2),
versions(2, 1, 1, 2),
false,
nil,
},
{
"no overlap, local > peer",
versions(4, 3, 6, 5),
versions(1, 0, 2, 1),
false,
nil,
},
{
"no overlap, local < peer",
versions(1, 0, 2, 1),
versions(4, 3, 6, 5),
false,
nil,
},
{
"no overlap, max < min",
versions(6, 5, 4, 3),
versions(2, 1, 1, 0),
false,
nil,
},
} {
output, maxCommonVersion := checkRPCVersions(tc.local, tc.peer)
if got, want := output, tc.output; got != want {
t.Errorf("%v: checkRPCVersions(%v, %v)=(%v, _), want (%v, _)", tc.desc, tc.local, tc.peer, got, want)
}
if got, want := maxCommonVersion, tc.maxCommonVersion; !proto.Equal(got, want) {
t.Errorf("%v: checkRPCVersions(%v, %v)=(_, %v), want (_, %v)", tc.desc, tc.local, tc.peer, got, want)
}
}
}
// TestFullHandshake performs a full ALTS handshake between a test client and
// server, where both client and server offload to a local, fake handshaker
// service.
func (s) TestFullHandshake(t *testing.T) {
// Start the fake handshaker service and the server.
var wait sync.WaitGroup
defer wait.Wait()
stopHandshaker, handshakerAddress := startFakeHandshakerService(t, &wait)
defer stopHandshaker()
stopServer, serverAddress := startServer(t, handshakerAddress)
defer stopServer()
// Ping the server, authenticating with ALTS.
establishAltsConnection(t, handshakerAddress, serverAddress)
// Close open connections to the fake handshaker service.
if err := service.CloseForTesting(); err != nil {
t.Errorf("service.CloseForTesting() failed: %v", err)
}
}
// TestHandshakeWithAccessToken performs an ALTS handshake between a test client and
// server, where both client and server offload to a local, fake handshaker
// service, and expects the StartClient request to include a bound access token.
func (s) TestHandshakeWithAccessToken(t *testing.T) {
// Start the fake handshaker service and the server.
var wait sync.WaitGroup
defer wait.Wait()
boundAccessToken := "fake-bound-access-token"
stopHandshaker, handshakerAddress := startFakeHandshakerServiceWithExpectedBoundAccessToken(t, &wait, boundAccessToken)
defer stopHandshaker()
stopServer, serverAddress := startServer(t, handshakerAddress)
defer stopServer()
// Ping the server, authenticating with ALTS and a bound access token.
establishAltsConnectionWithBoundAccessToken(t, handshakerAddress, serverAddress, boundAccessToken)
// Close open connections to the fake handshaker service.
if err := service.CloseForTesting(); err != nil {
t.Errorf("service.CloseForTesting() failed: %v", err)
}
}
// TestConcurrentHandshakes performs a several, concurrent ALTS handshakes
// between a test client and server, where both client and server offload to a
// local, fake handshaker service.
func (s) TestConcurrentHandshakes(t *testing.T) {
// Set the max number of concurrent handshakes to 3, so that we can
// test the handshaker behavior when handshakes are queued by
// performing more than 3 concurrent handshakes (specifically, 10).
handshaker.ResetConcurrentHandshakeSemaphoreForTesting(3)
// Start the fake handshaker service and the server.
var wait sync.WaitGroup
defer wait.Wait()
stopHandshaker, handshakerAddress := startFakeHandshakerService(t, &wait)
defer stopHandshaker()
stopServer, serverAddress := startServer(t, handshakerAddress)
defer stopServer()
// Ping the server, authenticating with ALTS.
var waitForConnections sync.WaitGroup
for i := 0; i < 10; i++ {
waitForConnections.Add(1)
go func() {
establishAltsConnection(t, handshakerAddress, serverAddress)
waitForConnections.Done()
}()
}
waitForConnections.Wait()
// Close open connections to the fake handshaker service.
if err := service.CloseForTesting(); err != nil {
t.Errorf("service.CloseForTesting() failed: %v", err)
}
}
func version(major, minor uint32) *altspb.RpcProtocolVersions_Version {
return &altspb.RpcProtocolVersions_Version{
Major: major,
Minor: minor,
}
}
func versions(minMajor, minMinor, maxMajor, maxMinor uint32) *altspb.RpcProtocolVersions {
return &altspb.RpcProtocolVersions{
MinRpcVersion: version(minMajor, minMinor),
MaxRpcVersion: version(maxMajor, maxMinor),
}
}
func establishAltsConnection(t *testing.T, handshakerAddress, serverAddress string) {
establishAltsConnectionWithBoundAccessToken(t, handshakerAddress, serverAddress, "")
}
func establishAltsConnectionWithBoundAccessToken(t *testing.T, handshakerAddress, serverAddress, boundAccessToken string) {
clientCreds := NewClientCreds(&ClientOptions{HandshakerServiceAddress: handshakerAddress})
if boundAccessToken != "" {
altsCreds := clientCreds.(*altsTC)
altsCreds.boundAccessToken = boundAccessToken
}
conn, err := grpc.NewClient(serverAddress, grpc.WithTransportCredentials(clientCreds))
if err != nil {
t.Fatalf("grpc.NewClient(%v) failed: %v", serverAddress, err)
}
defer conn.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestLongTimeout)
defer cancel()
c := testgrpc.NewTestServiceClient(conn)
var peer peer.Peer
success := false
for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) {
_, err = c.UnaryCall(ctx, &testpb.SimpleRequest{}, grpc.Peer(&peer))
if err == nil {
success = true
break
}
if code := status.Code(err); code == codes.Unavailable || code == codes.DeadlineExceeded {
// The server is not ready yet or there were too many concurrent handshakes.
// Try again.
continue
}
t.Fatalf("c.UnaryCall() failed: %v", err)
}
if !success {
t.Fatalf("c.UnaryCall() timed out after %v", defaultTestShortTimeout)
}
// Check that peer.AuthInfo was populated with an ALTS AuthInfo
// instance. As a sanity check, also verify that the AuthType() and
// ApplicationProtocol() have the expected values.
if got, want := peer.AuthInfo.AuthType(), "alts"; got != want {
t.Errorf("authInfo.AuthType() = %s, want = %s", got, want)
}
authInfo, err := AuthInfoFromPeer(&peer)
if err != nil {
t.Errorf("AuthInfoFromPeer failed: %v", err)
}
if got, want := authInfo.ApplicationProtocol(), "grpc"; got != want {
t.Errorf("authInfo.ApplicationProtocol() = %s, want = %s", got, want)
}
}
func startFakeHandshakerService(t *testing.T, wait *sync.WaitGroup) (stop func(), address string) {
return startFakeHandshakerServiceWithExpectedBoundAccessToken(t, wait, "")
}
func startFakeHandshakerServiceWithExpectedBoundAccessToken(t *testing.T, wait *sync.WaitGroup, boundAccessToken string) (stop func(), address string) {
listener, err := testutils.LocalTCPListener()
if err != nil {
t.Fatalf("LocalTCPListener() failed: %v", err)
}
s := grpc.NewServer()
hs := &testutil.FakeHandshaker{}
if boundAccessToken != "" {
hs.ExpectedBoundAccessToken = boundAccessToken
}
altsgrpc.RegisterHandshakerServiceServer(s, hs)
wait.Add(1)
go func() {
defer wait.Done()
if err := s.Serve(listener); err != nil {
t.Errorf("failed to serve: %v", err)
}
}()
return func() { s.Stop() }, listener.Addr().String()
}
func startServer(t *testing.T, handshakerServiceAddress string) (stop func(), address string) {
listener, err := testutils.LocalTCPListener()
if err != nil {
t.Fatalf("LocalTCPListener() failed: %v", err)
}
serverOpts := &ServerOptions{HandshakerServiceAddress: handshakerServiceAddress}
creds := NewServerCreds(serverOpts)
stub := &stubserver.StubServer{
Listener: listener,
UnaryCallF: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
return &testpb.SimpleResponse{
Payload: &testpb.Payload{},
}, nil
},
S: grpc.NewServer(grpc.Creds(creds)),
}
stubserver.StartTestService(t, stub)
return func() { stub.S.Stop() }, listener.Addr().String()
}

View File

@@ -0,0 +1,95 @@
/*
*
* Copyright 2018 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 authinfo provide authentication information returned by handshakers.
package authinfo
import (
"google.golang.org/grpc/credentials"
altspb "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"
)
var _ credentials.AuthInfo = (*altsAuthInfo)(nil)
// altsAuthInfo exposes security information from the ALTS handshake to the
// application. altsAuthInfo is immutable and implements credentials.AuthInfo.
type altsAuthInfo struct {
p *altspb.AltsContext
credentials.CommonAuthInfo
}
// New returns a new altsAuthInfo object given handshaker results.
func New(result *altspb.HandshakerResult) credentials.AuthInfo {
return newAuthInfo(result)
}
func newAuthInfo(result *altspb.HandshakerResult) *altsAuthInfo {
return &altsAuthInfo{
p: &altspb.AltsContext{
ApplicationProtocol: result.GetApplicationProtocol(),
RecordProtocol: result.GetRecordProtocol(),
// TODO: assign security level from result.
SecurityLevel: altspb.SecurityLevel_INTEGRITY_AND_PRIVACY,
PeerServiceAccount: result.GetPeerIdentity().GetServiceAccount(),
LocalServiceAccount: result.GetLocalIdentity().GetServiceAccount(),
PeerRpcVersions: result.GetPeerRpcVersions(),
PeerAttributes: result.GetPeerIdentity().GetAttributes(),
},
CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity},
}
}
// AuthType identifies the context as providing ALTS authentication information.
func (s *altsAuthInfo) AuthType() string {
return "alts"
}
// ApplicationProtocol returns the context's application protocol.
func (s *altsAuthInfo) ApplicationProtocol() string {
return s.p.GetApplicationProtocol()
}
// RecordProtocol returns the context's record protocol.
func (s *altsAuthInfo) RecordProtocol() string {
return s.p.GetRecordProtocol()
}
// SecurityLevel returns the context's security level.
func (s *altsAuthInfo) SecurityLevel() altspb.SecurityLevel {
return s.p.GetSecurityLevel()
}
// PeerServiceAccount returns the context's peer service account.
func (s *altsAuthInfo) PeerServiceAccount() string {
return s.p.GetPeerServiceAccount()
}
// LocalServiceAccount returns the context's local service account.
func (s *altsAuthInfo) LocalServiceAccount() string {
return s.p.GetLocalServiceAccount()
}
// PeerRPCVersions returns the context's peer RPC versions.
func (s *altsAuthInfo) PeerRPCVersions() *altspb.RpcProtocolVersions {
return s.p.GetPeerRpcVersions()
}
// PeerAttributes returns the context's peer attributes.
func (s *altsAuthInfo) PeerAttributes() map[string]string {
return s.p.GetPeerAttributes()
}

View File

@@ -0,0 +1,156 @@
/*
*
* Copyright 2018 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 authinfo
import (
"reflect"
"testing"
altspb "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"
"google.golang.org/grpc/internal/grpctest"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
const (
testAppProtocol = "my_app"
testRecordProtocol = "very_secure_protocol"
testPeerAccount = "peer_service_account"
testLocalAccount = "local_service_account"
testPeerHostname = "peer_hostname"
testLocalHostname = "local_hostname"
testLocalPeerAttributeKey = "peer"
testLocalPeerAttributeValue = "attributes"
)
func (s) TestALTSAuthInfo(t *testing.T) {
testPeerAttributes := make(map[string]string)
testPeerAttributes[testLocalPeerAttributeKey] = testLocalPeerAttributeValue
for _, tc := range []struct {
result *altspb.HandshakerResult
outAppProtocol string
outRecordProtocol string
outSecurityLevel altspb.SecurityLevel
outPeerAccount string
outLocalAccount string
outPeerRPCVersions *altspb.RpcProtocolVersions
outPeerAttributes map[string]string
}{
{
&altspb.HandshakerResult{
ApplicationProtocol: testAppProtocol,
RecordProtocol: testRecordProtocol,
PeerIdentity: &altspb.Identity{
IdentityOneof: &altspb.Identity_ServiceAccount{
ServiceAccount: testPeerAccount,
},
Attributes: testPeerAttributes,
},
LocalIdentity: &altspb.Identity{
IdentityOneof: &altspb.Identity_ServiceAccount{
ServiceAccount: testLocalAccount,
},
},
},
testAppProtocol,
testRecordProtocol,
altspb.SecurityLevel_INTEGRITY_AND_PRIVACY,
testPeerAccount,
testLocalAccount,
nil,
testPeerAttributes,
},
{
&altspb.HandshakerResult{
ApplicationProtocol: testAppProtocol,
RecordProtocol: testRecordProtocol,
PeerIdentity: &altspb.Identity{
IdentityOneof: &altspb.Identity_Hostname{
Hostname: testPeerHostname,
},
Attributes: testPeerAttributes,
},
LocalIdentity: &altspb.Identity{
IdentityOneof: &altspb.Identity_Hostname{
Hostname: testLocalHostname,
},
},
PeerRpcVersions: &altspb.RpcProtocolVersions{
MaxRpcVersion: &altspb.RpcProtocolVersions_Version{
Major: 20,
Minor: 21,
},
MinRpcVersion: &altspb.RpcProtocolVersions_Version{
Major: 10,
Minor: 11,
},
},
},
testAppProtocol,
testRecordProtocol,
altspb.SecurityLevel_INTEGRITY_AND_PRIVACY,
"",
"",
&altspb.RpcProtocolVersions{
MaxRpcVersion: &altspb.RpcProtocolVersions_Version{
Major: 20,
Minor: 21,
},
MinRpcVersion: &altspb.RpcProtocolVersions_Version{
Major: 10,
Minor: 11,
},
},
testPeerAttributes,
},
} {
authInfo := newAuthInfo(tc.result)
if got, want := authInfo.AuthType(), "alts"; got != want {
t.Errorf("authInfo.AuthType()=%v, want %v", got, want)
}
if got, want := authInfo.ApplicationProtocol(), tc.outAppProtocol; got != want {
t.Errorf("authInfo.ApplicationProtocol()=%v, want %v", got, want)
}
if got, want := authInfo.RecordProtocol(), tc.outRecordProtocol; got != want {
t.Errorf("authInfo.RecordProtocol()=%v, want %v", got, want)
}
if got, want := authInfo.SecurityLevel(), tc.outSecurityLevel; got != want {
t.Errorf("authInfo.SecurityLevel()=%v, want %v", got, want)
}
if got, want := authInfo.PeerServiceAccount(), tc.outPeerAccount; got != want {
t.Errorf("authInfo.PeerServiceAccount()=%v, want %v", got, want)
}
if got, want := authInfo.LocalServiceAccount(), tc.outLocalAccount; got != want {
t.Errorf("authInfo.LocalServiceAccount()=%v, want %v", got, want)
}
if got, want := authInfo.PeerRPCVersions(), tc.outPeerRPCVersions; !reflect.DeepEqual(got, want) {
t.Errorf("authinfo.PeerRpcVersions()=%v, want %v", got, want)
}
if got, want := authInfo.PeerAttributes(), tc.outPeerAttributes; !reflect.DeepEqual(got, want) {
t.Errorf("authinfo.PeerAttributes()=%v, want %v", got, want)
}
}
}

View File

@@ -0,0 +1,67 @@
/*
*
* Copyright 2018 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 internal contains common core functionality for ALTS.
package internal
import (
"context"
"net"
"google.golang.org/grpc/credentials"
)
const (
// ClientSide identifies the client in this communication.
ClientSide Side = iota
// ServerSide identifies the server in this communication.
ServerSide
)
// PeerNotRespondingError is returned when a peer server is not responding
// after a channel has been established. It is treated as a temporary connection
// error and re-connection to the server should be attempted.
var PeerNotRespondingError = &peerNotRespondingError{}
// Side identifies the party's role: client or server.
type Side int
type peerNotRespondingError struct{}
// Return an error message for the purpose of logging.
func (e *peerNotRespondingError) Error() string {
return "peer server is not responding and re-connection should be attempted."
}
// Temporary indicates if this connection error is temporary or fatal.
func (e *peerNotRespondingError) Temporary() bool {
return true
}
// Handshaker defines a ALTS handshaker interface.
type Handshaker interface {
// ClientHandshake starts and completes a client-side handshaking and
// returns a secure connection and corresponding auth information.
ClientHandshake(ctx context.Context) (net.Conn, credentials.AuthInfo, error)
// ServerHandshake starts and completes a server-side handshaking and
// returns a secure connection and corresponding auth information.
ServerHandshake(ctx context.Context) (net.Conn, credentials.AuthInfo, error)
// Close terminates the Handshaker. It should be called when the caller
// obtains the secure connection.
Close()
}

View File

@@ -0,0 +1,131 @@
/*
*
* Copyright 2018 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 conn
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"fmt"
"strconv"
)
// rekeyAEAD holds the necessary information for an AEAD based on
// AES-GCM that performs nonce-based key derivation and XORs the
// nonce with a random mask.
type rekeyAEAD struct {
kdfKey []byte
kdfCounter []byte
nonceMask []byte
nonceBuf []byte
gcmAEAD cipher.AEAD
}
// KeySizeError signals that the given key does not have the correct size.
type KeySizeError int
func (k KeySizeError) Error() string {
return "alts/conn: invalid key size " + strconv.Itoa(int(k))
}
// newRekeyAEAD creates a new instance of aes128gcm with rekeying.
// The key argument should be 44 bytes, the first 32 bytes are used as a key
// for HKDF-expand and the remaining 12 bytes are used as a random mask for
// the counter.
func newRekeyAEAD(key []byte) (*rekeyAEAD, error) {
k := len(key)
if k != kdfKeyLen+nonceLen {
return nil, KeySizeError(k)
}
return &rekeyAEAD{
kdfKey: key[:kdfKeyLen],
kdfCounter: make([]byte, kdfCounterLen),
nonceMask: key[kdfKeyLen:],
nonceBuf: make([]byte, nonceLen),
gcmAEAD: nil,
}, nil
}
// Seal rekeys if nonce[2:8] is different than in the last call, masks the nonce,
// and calls Seal for aes128gcm.
func (s *rekeyAEAD) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
if err := s.rekeyIfRequired(nonce); err != nil {
panic(fmt.Sprintf("Rekeying failed with: %s", err.Error()))
}
maskNonce(s.nonceBuf, nonce, s.nonceMask)
return s.gcmAEAD.Seal(dst, s.nonceBuf, plaintext, additionalData)
}
// Open rekeys if nonce[2:8] is different than in the last call, masks the nonce,
// and calls Open for aes128gcm.
func (s *rekeyAEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
if err := s.rekeyIfRequired(nonce); err != nil {
return nil, err
}
maskNonce(s.nonceBuf, nonce, s.nonceMask)
return s.gcmAEAD.Open(dst, s.nonceBuf, ciphertext, additionalData)
}
// rekeyIfRequired creates a new aes128gcm AEAD if the existing AEAD is nil
// or cannot be used with given nonce.
func (s *rekeyAEAD) rekeyIfRequired(nonce []byte) error {
newKdfCounter := nonce[kdfCounterOffset : kdfCounterOffset+kdfCounterLen]
if s.gcmAEAD != nil && bytes.Equal(newKdfCounter, s.kdfCounter) {
return nil
}
copy(s.kdfCounter, newKdfCounter)
a, err := aes.NewCipher(hkdfExpand(s.kdfKey, s.kdfCounter))
if err != nil {
return err
}
s.gcmAEAD, err = cipher.NewGCM(a)
return err
}
// maskNonce XORs the given nonce with the mask and stores the result in dst.
func maskNonce(dst, nonce, mask []byte) {
nonce1 := binary.LittleEndian.Uint64(nonce[:sizeUint64])
nonce2 := binary.LittleEndian.Uint32(nonce[sizeUint64:])
mask1 := binary.LittleEndian.Uint64(mask[:sizeUint64])
mask2 := binary.LittleEndian.Uint32(mask[sizeUint64:])
binary.LittleEndian.PutUint64(dst[:sizeUint64], nonce1^mask1)
binary.LittleEndian.PutUint32(dst[sizeUint64:], nonce2^mask2)
}
// NonceSize returns the required nonce size.
func (s *rekeyAEAD) NonceSize() int {
return s.gcmAEAD.NonceSize()
}
// Overhead returns the ciphertext overhead.
func (s *rekeyAEAD) Overhead() int {
return s.gcmAEAD.Overhead()
}
// hkdfExpand computes the first 16 bytes of the HKDF-expand function
// defined in RFC5869.
func hkdfExpand(key, info []byte) []byte {
mac := hmac.New(sha256.New, key)
mac.Write(info)
mac.Write([]byte{0x01}[:])
return mac.Sum(nil)[:aeadKeyLen]
}

View File

@@ -0,0 +1,263 @@
/*
*
* Copyright 2018 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 conn
import (
"bytes"
"encoding/hex"
"testing"
)
// cryptoTestVector is struct for a rekey test vector
type rekeyAEADTestVector struct {
desc string
key, nonce, plaintext, aad, ciphertext []byte
}
// Test encrypt and decrypt using (adapted) test vectors for AES-GCM.
func (s) TestAES128GCMRekeyEncrypt(t *testing.T) {
for _, test := range []rekeyAEADTestVector{
// NIST vectors from:
// http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-revised-spec.pdf
//
// IEEE vectors from:
// http://www.ieee802.org/1/files/public/docs2011/bn-randall-test-vectors-0511-v1.pdf
//
// Key expanded by setting
// expandedKey = (key ||
// key ^ {0x01,..,0x01} ||
// key ^ {0x02,..,0x02})[0:44].
{
desc: "Derived from NIST test vector 1",
key: dehex("0000000000000000000000000000000001010101010101010101010101010101020202020202020202020202"),
nonce: dehex("000000000000000000000000"),
aad: dehex(""),
plaintext: dehex(""),
ciphertext: dehex("85e873e002f6ebdc4060954eb8675508"),
},
{
desc: "Derived from NIST test vector 2",
key: dehex("0000000000000000000000000000000001010101010101010101010101010101020202020202020202020202"),
nonce: dehex("000000000000000000000000"),
aad: dehex(""),
plaintext: dehex("00000000000000000000000000000000"),
ciphertext: dehex("51e9a8cb23ca2512c8256afff8e72d681aca19a1148ac115e83df4888cc00d11"),
},
{
desc: "Derived from NIST test vector 3",
key: dehex("feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f688d96"),
nonce: dehex("cafebabefacedbaddecaf888"),
aad: dehex(""),
plaintext: dehex("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255"),
ciphertext: dehex("1018ed5a1402a86516d6576d70b2ffccca261b94df88b58f53b64dfba435d18b2f6e3b7869f9353d4ac8cf09afb1663daa7b4017e6fc2c177c0c087c0df1162129952213cee1bc6e9c8495dd705e1f3d"),
},
{
desc: "Derived from NIST test vector 4",
key: dehex("feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f688d96"),
nonce: dehex("cafebabefacedbaddecaf888"),
aad: dehex("feedfacedeadbeeffeedfacedeadbeefabaddad2"),
plaintext: dehex("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39"),
ciphertext: dehex("1018ed5a1402a86516d6576d70b2ffccca261b94df88b58f53b64dfba435d18b2f6e3b7869f9353d4ac8cf09afb1663daa7b4017e6fc2c177c0c087c4764565d077e9124001ddb27fc0848c5"),
},
{
desc: "Derived from adapted NIST test vector 4 for KDF counter boundary (flip nonce bit 15)",
key: dehex("feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f688d96"),
nonce: dehex("ca7ebabefacedbaddecaf888"),
aad: dehex("feedfacedeadbeeffeedfacedeadbeefabaddad2"),
plaintext: dehex("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39"),
ciphertext: dehex("e650d3c0fb879327f2d03287fa93cd07342b136215adbca00c3bd5099ec41832b1d18e0423ed26bb12c6cd09debb29230a94c0cee15903656f85edb6fc509b1b28216382172ecbcc31e1e9b1"),
},
{
desc: "Derived from adapted NIST test vector 4 for KDF counter boundary (flip nonce bit 16)",
key: dehex("feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f688d96"),
nonce: dehex("cafebbbefacedbaddecaf888"),
aad: dehex("feedfacedeadbeeffeedfacedeadbeefabaddad2"),
plaintext: dehex("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39"),
ciphertext: dehex("c0121e6c954d0767f96630c33450999791b2da2ad05c4190169ccad9ac86ff1c721e3d82f2ad22ab463bab4a0754b7dd68ca4de7ea2531b625eda01f89312b2ab957d5c7f8568dd95fcdcd1f"),
},
{
desc: "Derived from adapted NIST test vector 4 for KDF counter boundary (flip nonce bit 63)",
key: dehex("feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f688d96"),
nonce: dehex("cafebabefacedb2ddecaf888"),
aad: dehex("feedfacedeadbeeffeedfacedeadbeefabaddad2"),
plaintext: dehex("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39"),
ciphertext: dehex("8af37ea5684a4d81d4fd817261fd9743099e7e6a025eaacf8e54b124fb5743149e05cb89f4a49467fe2e5e5965f29a19f99416b0016b54585d12553783ba59e9f782e82e097c336bf7989f08"),
},
{
desc: "Derived from adapted NIST test vector 4 for KDF counter boundary (flip nonce bit 64)",
key: dehex("feffe9928665731c6d6a8f9467308308fffee8938764721d6c6b8e9566318209fcfdeb908467711e6f688d96"),
nonce: dehex("cafebabefacedbaddfcaf888"),
aad: dehex("feedfacedeadbeeffeedfacedeadbeefabaddad2"),
plaintext: dehex("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39"),
ciphertext: dehex("fbd528448d0346bfa878634864d407a35a039de9db2f1feb8e965b3ae9356ce6289441d77f8f0df294891f37ea438b223e3bf2bdc53d4c5a74fb680bb312a8dec6f7252cbcd7f5799750ad78"),
},
{
desc: "Derived from IEEE 2.1.1 54-byte auth",
key: dehex("ad7a2bd03eac835a6f620fdcb506b345ac7b2ad13fad825b6e630eddb407b244af7829d23cae81586d600dde"),
nonce: dehex("12153524c0895e81b2c28465"),
aad: dehex("d609b1f056637a0d46df998d88e5222ab2c2846512153524c0895e8108000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233340001"),
plaintext: dehex(""),
ciphertext: dehex("3ea0b584f3c85e93f9320ea591699efb"),
},
{
desc: "Derived from IEEE 2.1.2 54-byte auth",
key: dehex("e3c08a8f06c6e3ad95a70557b23f75483ce33021a9c72b7025666204c69c0b72e1c2888d04c4e1af97a50755"),
nonce: dehex("12153524c0895e81b2c28465"),
aad: dehex("d609b1f056637a0d46df998d88e5222ab2c2846512153524c0895e8108000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233340001"),
plaintext: dehex(""),
ciphertext: dehex("294e028bf1fe6f14c4e8f7305c933eb5"),
},
{
desc: "Derived from IEEE 2.2.1 60-byte crypt",
key: dehex("ad7a2bd03eac835a6f620fdcb506b345ac7b2ad13fad825b6e630eddb407b244af7829d23cae81586d600dde"),
nonce: dehex("12153524c0895e81b2c28465"),
aad: dehex("d609b1f056637a0d46df998d88e52e00b2c2846512153524c0895e81"),
plaintext: dehex("08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a0002"),
ciphertext: dehex("db3d25719c6b0a3ca6145c159d5c6ed9aff9c6e0b79f17019ea923b8665ddf52137ad611f0d1bf417a7ca85e45afe106ff9c7569d335d086ae6c03f00987ccd6"),
},
{
desc: "Derived from IEEE 2.2.2 60-byte crypt",
key: dehex("e3c08a8f06c6e3ad95a70557b23f75483ce33021a9c72b7025666204c69c0b72e1c2888d04c4e1af97a50755"),
nonce: dehex("12153524c0895e81b2c28465"),
aad: dehex("d609b1f056637a0d46df998d88e52e00b2c2846512153524c0895e81"),
plaintext: dehex("08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a0002"),
ciphertext: dehex("1641f28ec13afcc8f7903389787201051644914933e9202bb9d06aa020c2a67ef51dfe7bc00a856c55b8f8133e77f659132502bad63f5713d57d0c11e0f871ed"),
},
{
desc: "Derived from IEEE 2.3.1 60-byte auth",
key: dehex("071b113b0ca743fecccf3d051f737382061a103a0da642ffcdce3c041e727283051913390ea541fccecd3f07"),
nonce: dehex("f0761e8dcd3d000176d457ed"),
aad: dehex("e20106d7cd0df0761e8dcd3d88e5400076d457ed08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a0003"),
plaintext: dehex(""),
ciphertext: dehex("58837a10562b0f1f8edbe58ca55811d3"),
},
{
desc: "Derived from IEEE 2.3.2 60-byte auth",
key: dehex("691d3ee909d7f54167fd1ca0b5d769081f2bde1aee655fdbab80bd5295ae6be76b1f3ceb0bd5f74365ff1ea2"),
nonce: dehex("f0761e8dcd3d000176d457ed"),
aad: dehex("e20106d7cd0df0761e8dcd3d88e5400076d457ed08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a0003"),
plaintext: dehex(""),
ciphertext: dehex("c2722ff6ca29a257718a529d1f0c6a3b"),
},
{
desc: "Derived from IEEE 2.4.1 54-byte crypt",
key: dehex("071b113b0ca743fecccf3d051f737382061a103a0da642ffcdce3c041e727283051913390ea541fccecd3f07"),
nonce: dehex("f0761e8dcd3d000176d457ed"),
aad: dehex("e20106d7cd0df0761e8dcd3d88e54c2a76d457ed"),
plaintext: dehex("08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233340004"),
ciphertext: dehex("fd96b715b93a13346af51e8acdf792cdc7b2686f8574c70e6b0cbf16291ded427ad73fec48cd298e0528a1f4c644a949fc31dc9279706ddba33f"),
},
{
desc: "Derived from IEEE 2.4.2 54-byte crypt",
key: dehex("691d3ee909d7f54167fd1ca0b5d769081f2bde1aee655fdbab80bd5295ae6be76b1f3ceb0bd5f74365ff1ea2"),
nonce: dehex("f0761e8dcd3d000176d457ed"),
aad: dehex("e20106d7cd0df0761e8dcd3d88e54c2a76d457ed"),
plaintext: dehex("08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233340004"),
ciphertext: dehex("b68f6300c2e9ae833bdc070e24021a3477118e78ccf84e11a485d861476c300f175353d5cdf92008a4f878e6cc3577768085c50a0e98fda6cbb8"),
},
{
desc: "Derived from IEEE 2.5.1 65-byte auth",
key: dehex("013fe00b5f11be7f866d0cbbc55a7a90003ee10a5e10bf7e876c0dbac45b7b91033de2095d13bc7d846f0eb9"),
nonce: dehex("7cfde9f9e33724c68932d612"),
aad: dehex("84c5d513d2aaf6e5bbd2727788e523008932d6127cfde9f9e33724c608000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f0005"),
plaintext: dehex(""),
ciphertext: dehex("cca20eecda6283f09bb3543dd99edb9b"),
},
{
desc: "Derived from IEEE 2.5.2 65-byte auth",
key: dehex("83c093b58de7ffe1c0da926ac43fb3609ac1c80fee1b624497ef942e2f79a82381c291b78fe5fde3c2d89068"),
nonce: dehex("7cfde9f9e33724c68932d612"),
aad: dehex("84c5d513d2aaf6e5bbd2727788e523008932d6127cfde9f9e33724c608000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f0005"),
plaintext: dehex(""),
ciphertext: dehex("b232cc1da5117bf15003734fa599d271"),
},
{
desc: "Derived from IEEE 2.6.1 61-byte crypt",
key: dehex("013fe00b5f11be7f866d0cbbc55a7a90003ee10a5e10bf7e876c0dbac45b7b91033de2095d13bc7d846f0eb9"),
nonce: dehex("7cfde9f9e33724c68932d612"),
aad: dehex("84c5d513d2aaf6e5bbd2727788e52f008932d6127cfde9f9e33724c6"),
plaintext: dehex("08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b0006"),
ciphertext: dehex("ff1910d35ad7e5657890c7c560146fd038707f204b66edbc3d161f8ace244b985921023c436e3a1c3532ecd5d09a056d70be583f0d10829d9387d07d33d872e490"),
},
{
desc: "Derived from IEEE 2.6.2 61-byte crypt",
key: dehex("83c093b58de7ffe1c0da926ac43fb3609ac1c80fee1b624497ef942e2f79a82381c291b78fe5fde3c2d89068"),
nonce: dehex("7cfde9f9e33724c68932d612"),
aad: dehex("84c5d513d2aaf6e5bbd2727788e52f008932d6127cfde9f9e33724c6"),
plaintext: dehex("08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b0006"),
ciphertext: dehex("0db4cf956b5f97eca4eab82a6955307f9ae02a32dd7d93f83d66ad04e1cfdc5182ad12abdea5bbb619a1bd5fb9a573590fba908e9c7a46c1f7ba0905d1b55ffda4"),
},
{
desc: "Derived from IEEE 2.7.1 79-byte crypt",
key: dehex("88ee087fd95da9fbf6725aa9d757b0cd89ef097ed85ca8faf7735ba8d656b1cc8aec0a7ddb5fabf9f47058ab"),
nonce: dehex("7ae8e2ca4ec500012e58495c"),
aad: dehex("68f2e77696ce7ae8e2ca4ec588e541002e58495c08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d0007"),
plaintext: dehex(""),
ciphertext: dehex("813f0e630f96fb2d030f58d83f5cdfd0"),
},
{
desc: "Derived from IEEE 2.7.2 79-byte crypt",
key: dehex("4c973dbc7364621674f8b5b89e5c15511fced9216490fb1c1a2caa0ffe0407e54e953fbe7166601476fab7ba"),
nonce: dehex("7ae8e2ca4ec500012e58495c"),
aad: dehex("68f2e77696ce7ae8e2ca4ec588e541002e58495c08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d0007"),
plaintext: dehex(""),
ciphertext: dehex("77e5a44c21eb07188aacbd74d1980e97"),
},
{
desc: "Derived from IEEE 2.8.1 61-byte crypt",
key: dehex("88ee087fd95da9fbf6725aa9d757b0cd89ef097ed85ca8faf7735ba8d656b1cc8aec0a7ddb5fabf9f47058ab"),
nonce: dehex("7ae8e2ca4ec500012e58495c"),
aad: dehex("68f2e77696ce7ae8e2ca4ec588e54d002e58495c"),
plaintext: dehex("08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748490008"),
ciphertext: dehex("958ec3f6d60afeda99efd888f175e5fcd4c87b9bcc5c2f5426253a8b506296c8c43309ab2adb5939462541d95e80811e04e706b1498f2c407c7fb234f8cc01a647550ee6b557b35a7e3945381821f4"),
},
{
desc: "Derived from IEEE 2.8.2 61-byte crypt",
key: dehex("4c973dbc7364621674f8b5b89e5c15511fced9216490fb1c1a2caa0ffe0407e54e953fbe7166601476fab7ba"),
nonce: dehex("7ae8e2ca4ec500012e58495c"),
aad: dehex("68f2e77696ce7ae8e2ca4ec588e54d002e58495c"),
plaintext: dehex("08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748490008"),
ciphertext: dehex("b44d072011cd36d272a9b7a98db9aa90cbc5c67b93ddce67c854503214e2e896ec7e9db649ed4bcf6f850aac0223d0cf92c83db80795c3a17ecc1248bb00591712b1ae71e268164196252162810b00"),
}} {
aead, err := newRekeyAEAD(test.key)
if err != nil {
t.Fatal("unexpected failure in newRekeyAEAD: ", err.Error())
}
if got := aead.Seal(nil, test.nonce, test.plaintext, test.aad); !bytes.Equal(got, test.ciphertext) {
t.Errorf("Unexpected ciphertext for test vector '%s':\nciphertext=%s\nwant= %s",
test.desc, hex.EncodeToString(got), hex.EncodeToString(test.ciphertext))
}
if got, err := aead.Open(nil, test.nonce, test.ciphertext, test.aad); err != nil || !bytes.Equal(got, test.plaintext) {
t.Errorf("Unexpected plaintext for test vector '%s':\nplaintext=%s (err=%v)\nwant= %s",
test.desc, hex.EncodeToString(got), err, hex.EncodeToString(test.plaintext))
}
}
}
func dehex(s string) []byte {
if len(s) == 0 {
return make([]byte, 0)
}
b, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return b
}

View File

@@ -0,0 +1,105 @@
/*
*
* Copyright 2018 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 conn
import (
"crypto/aes"
"crypto/cipher"
core "google.golang.org/grpc/credentials/alts/internal"
)
const (
// Overflow length n in bytes, never encrypt more than 2^(n*8) frames (in
// each direction).
overflowLenAES128GCM = 5
)
// aes128gcm is the struct that holds necessary information for ALTS record.
// The counter value is NOT included in the payload during the encryption and
// decryption operations.
type aes128gcm struct {
// inCounter is used in ALTS record to check that incoming counters are
// as expected, since ALTS record guarantees that messages are unwrapped
// in the same order that the peer wrapped them.
inCounter Counter
outCounter Counter
aead cipher.AEAD
}
// NewAES128GCM creates an instance that uses aes128gcm for ALTS record.
func NewAES128GCM(side core.Side, key []byte) (ALTSRecordCrypto, error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
a, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
return &aes128gcm{
inCounter: NewInCounter(side, overflowLenAES128GCM),
outCounter: NewOutCounter(side, overflowLenAES128GCM),
aead: a,
}, nil
}
// Encrypt is the encryption function. dst can contain bytes at the beginning of
// the ciphertext that will not be encrypted but will be authenticated. If dst
// has enough capacity to hold these bytes, the ciphertext and the tag, no
// allocation and copy operations will be performed. dst and plaintext do not
// overlap.
func (s *aes128gcm) Encrypt(dst, plaintext []byte) ([]byte, error) {
// If we need to allocate an output buffer, we want to include space for
// GCM tag to avoid forcing ALTS record to reallocate as well.
dlen := len(dst)
dst, out := SliceForAppend(dst, len(plaintext)+GcmTagSize)
seq, err := s.outCounter.Value()
if err != nil {
return nil, err
}
data := out[:len(plaintext)]
copy(data, plaintext) // data may alias plaintext
// Seal appends the ciphertext and the tag to its first argument and
// returns the updated slice. However, SliceForAppend above ensures that
// dst has enough capacity to avoid a reallocation and copy due to the
// append.
dst = s.aead.Seal(dst[:dlen], seq, data, nil)
s.outCounter.Inc()
return dst, nil
}
func (s *aes128gcm) EncryptionOverhead() int {
return GcmTagSize
}
func (s *aes128gcm) Decrypt(dst, ciphertext []byte) ([]byte, error) {
seq, err := s.inCounter.Value()
if err != nil {
return nil, err
}
// If dst is equal to ciphertext[:0], ciphertext storage is reused.
plaintext, err := s.aead.Open(dst, seq, ciphertext, nil)
if err != nil {
return nil, ErrAuth
}
s.inCounter.Inc()
return plaintext, nil
}

View File

@@ -0,0 +1,223 @@
/*
*
* Copyright 2018 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 conn
import (
"bytes"
"testing"
core "google.golang.org/grpc/credentials/alts/internal"
)
// cryptoTestVector is struct for a GCM test vector
type cryptoTestVector struct {
key, counter, plaintext, ciphertext, tag []byte
allocateDst bool
}
// getGCMCryptoPair outputs a client/server pair on aes128gcm.
func getGCMCryptoPair(key []byte, counter []byte, t *testing.T) (ALTSRecordCrypto, ALTSRecordCrypto) {
client, err := NewAES128GCM(core.ClientSide, key)
if err != nil {
t.Fatalf("NewAES128GCM(ClientSide, key) = %v", err)
}
server, err := NewAES128GCM(core.ServerSide, key)
if err != nil {
t.Fatalf("NewAES128GCM(ServerSide, key) = %v", err)
}
// set counter if provided.
if counter != nil {
if CounterSide(counter) == core.ClientSide {
client.(*aes128gcm).outCounter = CounterFromValue(counter, overflowLenAES128GCM)
server.(*aes128gcm).inCounter = CounterFromValue(counter, overflowLenAES128GCM)
} else {
server.(*aes128gcm).outCounter = CounterFromValue(counter, overflowLenAES128GCM)
client.(*aes128gcm).inCounter = CounterFromValue(counter, overflowLenAES128GCM)
}
}
return client, server
}
func testGCMEncryptionDecryption(sender ALTSRecordCrypto, receiver ALTSRecordCrypto, test *cryptoTestVector, withCounter bool, t *testing.T) {
// Ciphertext is: counter + encrypted text + tag.
ciphertext := []byte(nil)
if withCounter {
ciphertext = append(ciphertext, test.counter...)
}
ciphertext = append(ciphertext, test.ciphertext...)
ciphertext = append(ciphertext, test.tag...)
// Decrypt.
if got, err := receiver.Decrypt(nil, ciphertext); err != nil || !bytes.Equal(got, test.plaintext) {
t.Errorf("key=%v\ncounter=%v\ntag=%v\nciphertext=%v\nDecrypt = %v, %v\nwant: %v",
test.key, test.counter, test.tag, test.ciphertext, got, err, test.plaintext)
}
// Encrypt.
var dst []byte
if test.allocateDst {
dst = make([]byte, len(test.plaintext)+sender.EncryptionOverhead())
}
if got, err := sender.Encrypt(dst[:0], test.plaintext); err != nil || !bytes.Equal(got, ciphertext) {
t.Errorf("key=%v\ncounter=%v\nplaintext=%v\nEncrypt = %v, %v\nwant: %v",
test.key, test.counter, test.plaintext, got, err, ciphertext)
}
}
// Test encrypt and decrypt using test vectors for aes128gcm.
func (s) TestAES128GCMEncrypt(t *testing.T) {
for _, test := range []cryptoTestVector{
{
key: dehex("11754cd72aec309bf52f7687212e8957"),
counter: dehex("3c819d9a9bed087615030b65"),
plaintext: nil,
ciphertext: nil,
tag: dehex("250327c674aaf477aef2675748cf6971"),
allocateDst: false,
},
{
key: dehex("ca47248ac0b6f8372a97ac43508308ed"),
counter: dehex("ffd2b598feabc9019262d2be"),
plaintext: nil,
ciphertext: nil,
tag: dehex("60d20404af527d248d893ae495707d1a"),
allocateDst: false,
},
{
key: dehex("7fddb57453c241d03efbed3ac44e371c"),
counter: dehex("ee283a3fc75575e33efd4887"),
plaintext: dehex("d5de42b461646c255c87bd2962d3b9a2"),
ciphertext: dehex("2ccda4a5415cb91e135c2a0f78c9b2fd"),
tag: dehex("b36d1df9b9d5e596f83e8b7f52971cb3"),
allocateDst: false,
},
{
key: dehex("ab72c77b97cb5fe9a382d9fe81ffdbed"),
counter: dehex("54cc7dc2c37ec006bcc6d1da"),
plaintext: dehex("007c5e5b3e59df24a7c355584fc1518d"),
ciphertext: dehex("0e1bde206a07a9c2c1b65300f8c64997"),
tag: dehex("2b4401346697138c7a4891ee59867d0c"),
allocateDst: false,
},
{
key: dehex("11754cd72aec309bf52f7687212e8957"),
counter: dehex("3c819d9a9bed087615030b65"),
plaintext: nil,
ciphertext: nil,
tag: dehex("250327c674aaf477aef2675748cf6971"),
allocateDst: true,
},
{
key: dehex("ca47248ac0b6f8372a97ac43508308ed"),
counter: dehex("ffd2b598feabc9019262d2be"),
plaintext: nil,
ciphertext: nil,
tag: dehex("60d20404af527d248d893ae495707d1a"),
allocateDst: true,
},
{
key: dehex("7fddb57453c241d03efbed3ac44e371c"),
counter: dehex("ee283a3fc75575e33efd4887"),
plaintext: dehex("d5de42b461646c255c87bd2962d3b9a2"),
ciphertext: dehex("2ccda4a5415cb91e135c2a0f78c9b2fd"),
tag: dehex("b36d1df9b9d5e596f83e8b7f52971cb3"),
allocateDst: true,
},
{
key: dehex("ab72c77b97cb5fe9a382d9fe81ffdbed"),
counter: dehex("54cc7dc2c37ec006bcc6d1da"),
plaintext: dehex("007c5e5b3e59df24a7c355584fc1518d"),
ciphertext: dehex("0e1bde206a07a9c2c1b65300f8c64997"),
tag: dehex("2b4401346697138c7a4891ee59867d0c"),
allocateDst: true,
},
} {
// Test encryption and decryption for aes128gcm.
client, server := getGCMCryptoPair(test.key, test.counter, t)
if CounterSide(test.counter) == core.ClientSide {
testGCMEncryptionDecryption(client, server, &test, false, t)
} else {
testGCMEncryptionDecryption(server, client, &test, false, t)
}
}
}
func testGCMEncryptRoundtrip(client ALTSRecordCrypto, server ALTSRecordCrypto, t *testing.T) {
// Encrypt.
const plaintext = "This is plaintext."
var err error
buf := []byte(plaintext)
buf, err = client.Encrypt(buf[:0], buf)
if err != nil {
t.Fatal("Encrypting with client-side context: unexpected error", err, "\n",
"Plaintext:", []byte(plaintext))
}
// Encrypt a second message.
const plaintext2 = "This is a second plaintext."
buf2 := []byte(plaintext2)
buf2, err = client.Encrypt(buf2[:0], buf2)
if err != nil {
t.Fatal("Encrypting with client-side context: unexpected error", err, "\n",
"Plaintext:", []byte(plaintext2))
}
// Decryption fails: cannot decrypt second message before first.
if got, err := server.Decrypt(nil, buf2); err == nil {
t.Error("Decrypting client-side ciphertext with a client-side context unexpectedly succeeded; want unexpected counter error:\n",
" Original plaintext:", []byte(plaintext2), "\n",
" Ciphertext:", buf2, "\n",
" Decrypted plaintext:", got)
}
// Decryption fails: wrong counter space.
if got, err := client.Decrypt(nil, buf); err == nil {
t.Error("Decrypting client-side ciphertext with a client-side context unexpectedly succeeded; want counter space error:\n",
" Original plaintext:", []byte(plaintext), "\n",
" Ciphertext:", buf, "\n",
" Decrypted plaintext:", got)
}
// Decrypt first message.
ciphertext := append([]byte(nil), buf...)
buf, err = server.Decrypt(buf[:0], buf)
if err != nil || string(buf) != plaintext {
t.Fatal("Decrypting client-side ciphertext with a server-side context did not produce original content:\n",
" Original plaintext:", []byte(plaintext), "\n",
" Ciphertext:", ciphertext, "\n",
" Decryption error:", err, "\n",
" Decrypted plaintext:", buf)
}
// Decryption fails: replay attack.
if got, err := server.Decrypt(nil, buf); err == nil {
t.Error("Decrypting client-side ciphertext with a client-side context unexpectedly succeeded; want unexpected counter error:\n",
" Original plaintext:", []byte(plaintext), "\n",
" Ciphertext:", buf, "\n",
" Decrypted plaintext:", got)
}
}
// Test encrypt and decrypt on roundtrip messages for aes128gcm.
func (s) TestAES128GCMEncryptRoundtrip(t *testing.T) {
// Test for aes128gcm.
key := make([]byte, 16)
client, server := getGCMCryptoPair(key, nil, t)
testGCMEncryptRoundtrip(client, server, t)
}

View File

@@ -0,0 +1,116 @@
/*
*
* Copyright 2018 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 conn
import (
"crypto/cipher"
core "google.golang.org/grpc/credentials/alts/internal"
)
const (
// Overflow length n in bytes, never encrypt more than 2^(n*8) frames (in
// each direction).
overflowLenAES128GCMRekey = 8
nonceLen = 12
aeadKeyLen = 16
kdfKeyLen = 32
kdfCounterOffset = 2
kdfCounterLen = 6
sizeUint64 = 8
)
// aes128gcmRekey is the struct that holds necessary information for ALTS record.
// The counter value is NOT included in the payload during the encryption and
// decryption operations.
type aes128gcmRekey struct {
// inCounter is used in ALTS record to check that incoming counters are
// as expected, since ALTS record guarantees that messages are unwrapped
// in the same order that the peer wrapped them.
inCounter Counter
outCounter Counter
inAEAD cipher.AEAD
outAEAD cipher.AEAD
}
// NewAES128GCMRekey creates an instance that uses aes128gcm with rekeying
// for ALTS record. The key argument should be 44 bytes, the first 32 bytes
// are used as a key for HKDF-expand and the remaining 12 bytes are used
// as a random mask for the counter.
func NewAES128GCMRekey(side core.Side, key []byte) (ALTSRecordCrypto, error) {
inCounter := NewInCounter(side, overflowLenAES128GCMRekey)
outCounter := NewOutCounter(side, overflowLenAES128GCMRekey)
inAEAD, err := newRekeyAEAD(key)
if err != nil {
return nil, err
}
outAEAD, err := newRekeyAEAD(key)
if err != nil {
return nil, err
}
return &aes128gcmRekey{
inCounter,
outCounter,
inAEAD,
outAEAD,
}, nil
}
// Encrypt is the encryption function. dst can contain bytes at the beginning of
// the ciphertext that will not be encrypted but will be authenticated. If dst
// has enough capacity to hold these bytes, the ciphertext and the tag, no
// allocation and copy operations will be performed. dst and plaintext do not
// overlap.
func (s *aes128gcmRekey) Encrypt(dst, plaintext []byte) ([]byte, error) {
// If we need to allocate an output buffer, we want to include space for
// GCM tag to avoid forcing ALTS record to reallocate as well.
dlen := len(dst)
dst, out := SliceForAppend(dst, len(plaintext)+GcmTagSize)
seq, err := s.outCounter.Value()
if err != nil {
return nil, err
}
data := out[:len(plaintext)]
copy(data, plaintext) // data may alias plaintext
// Seal appends the ciphertext and the tag to its first argument and
// returns the updated slice. However, SliceForAppend above ensures that
// dst has enough capacity to avoid a reallocation and copy due to the
// append.
dst = s.outAEAD.Seal(dst[:dlen], seq, data, nil)
s.outCounter.Inc()
return dst, nil
}
func (s *aes128gcmRekey) EncryptionOverhead() int {
return GcmTagSize
}
func (s *aes128gcmRekey) Decrypt(dst, ciphertext []byte) ([]byte, error) {
seq, err := s.inCounter.Value()
if err != nil {
return nil, err
}
plaintext, err := s.inAEAD.Open(dst, seq, ciphertext, nil)
if err != nil {
return nil, ErrAuth
}
s.inCounter.Inc()
return plaintext, nil
}

View File

@@ -0,0 +1,112 @@
/*
*
* Copyright 2018 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 conn
import (
"testing"
core "google.golang.org/grpc/credentials/alts/internal"
)
// getGCMCryptoPair outputs a client/server pair on aes128gcmRekey.
func getRekeyCryptoPair(key []byte, counter []byte, t *testing.T) (ALTSRecordCrypto, ALTSRecordCrypto) {
client, err := NewAES128GCMRekey(core.ClientSide, key)
if err != nil {
t.Fatalf("NewAES128GCMRekey(ClientSide, key) = %v", err)
}
server, err := NewAES128GCMRekey(core.ServerSide, key)
if err != nil {
t.Fatalf("NewAES128GCMRekey(ServerSide, key) = %v", err)
}
// set counter if provided.
if counter != nil {
if CounterSide(counter) == core.ClientSide {
client.(*aes128gcmRekey).outCounter = CounterFromValue(counter, overflowLenAES128GCMRekey)
server.(*aes128gcmRekey).inCounter = CounterFromValue(counter, overflowLenAES128GCMRekey)
} else {
server.(*aes128gcmRekey).outCounter = CounterFromValue(counter, overflowLenAES128GCMRekey)
client.(*aes128gcmRekey).inCounter = CounterFromValue(counter, overflowLenAES128GCMRekey)
}
}
return client, server
}
func testRekeyEncryptRoundtrip(client ALTSRecordCrypto, server ALTSRecordCrypto, t *testing.T) {
// Encrypt.
const plaintext = "This is plaintext."
var err error
buf := []byte(plaintext)
buf, err = client.Encrypt(buf[:0], buf)
if err != nil {
t.Fatal("Encrypting with client-side context: unexpected error", err, "\n",
"Plaintext:", []byte(plaintext))
}
// Encrypt a second message.
const plaintext2 = "This is a second plaintext."
buf2 := []byte(plaintext2)
buf2, err = client.Encrypt(buf2[:0], buf2)
if err != nil {
t.Fatal("Encrypting with client-side context: unexpected error", err, "\n",
"Plaintext:", []byte(plaintext2))
}
// Decryption fails: cannot decrypt second message before first.
if got, err := server.Decrypt(nil, buf2); err == nil {
t.Error("Decrypting client-side ciphertext with a client-side context unexpectedly succeeded; want unexpected counter error:\n",
" Original plaintext:", []byte(plaintext2), "\n",
" Ciphertext:", buf2, "\n",
" Decrypted plaintext:", got)
}
// Decryption fails: wrong counter space.
if got, err := client.Decrypt(nil, buf); err == nil {
t.Error("Decrypting client-side ciphertext with a client-side context unexpectedly succeeded; want counter space error:\n",
" Original plaintext:", []byte(plaintext), "\n",
" Ciphertext:", buf, "\n",
" Decrypted plaintext:", got)
}
// Decrypt first message.
ciphertext := append([]byte(nil), buf...)
buf, err = server.Decrypt(buf[:0], buf)
if err != nil || string(buf) != plaintext {
t.Fatal("Decrypting client-side ciphertext with a server-side context did not produce original content:\n",
" Original plaintext:", []byte(plaintext), "\n",
" Ciphertext:", ciphertext, "\n",
" Decryption error:", err, "\n",
" Decrypted plaintext:", buf)
}
// Decryption fails: replay attack.
if got, err := server.Decrypt(nil, buf); err == nil {
t.Error("Decrypting client-side ciphertext with a client-side context unexpectedly succeeded; want unexpected counter error:\n",
" Original plaintext:", []byte(plaintext), "\n",
" Ciphertext:", buf, "\n",
" Decrypted plaintext:", got)
}
}
// Test encrypt and decrypt on roundtrip messages for aes128gcmRekey.
func (s) TestAES128GCMRekeyEncryptRoundtrip(t *testing.T) {
// Test for aes128gcmRekey.
key := make([]byte, 44)
client, server := getRekeyCryptoPair(key, nil, t)
testRekeyEncryptRoundtrip(client, server, t)
}

View File

@@ -0,0 +1,80 @@
/*
*
* Copyright 2018 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 conn
import (
"encoding/binary"
"errors"
"fmt"
)
const (
// GcmTagSize is the GCM tag size is the difference in length between
// plaintext and ciphertext. From crypto/cipher/gcm.go in Go crypto
// library.
GcmTagSize = 16
)
// ErrAuth occurs on authentication failure.
var ErrAuth = errors.New("message authentication failed")
// SliceForAppend takes a slice and a requested number of bytes. It returns a
// slice with the contents of the given slice followed by that many bytes and a
// second slice that aliases into it and contains only the extra bytes. If the
// original slice has sufficient capacity then no allocation is performed.
func SliceForAppend(in []byte, n int) (head, tail []byte) {
if total := len(in) + n; cap(in) >= total {
head = in[:total]
} else {
head = make([]byte, total)
copy(head, in)
}
tail = head[len(in):]
return head, tail
}
// ParseFramedMsg parse the provided buffer and returns a frame of the format
// msgLength+msg and any remaining bytes in that buffer.
func ParseFramedMsg(b []byte, maxLen uint32) ([]byte, []byte, error) {
// If the size field is not complete, return the provided buffer as
// remaining buffer.
length, sufficientBytes := parseMessageLength(b)
if !sufficientBytes {
return nil, b, nil
}
if length > maxLen {
return nil, nil, fmt.Errorf("received the frame length %d larger than the limit %d", length, maxLen)
}
if len(b) < int(length)+4 { // account for the first 4 msg length bytes.
// Frame is not complete yet.
return nil, b, nil
}
return b[:MsgLenFieldSize+length], b[MsgLenFieldSize+length:], nil
}
// parseMessageLength returns the message length based on frame header. It also
// returns a boolean indicating if the buffer contains sufficient bytes to parse
// the length header. If there are insufficient bytes, (0, false) is returned.
func parseMessageLength(b []byte) (uint32, bool) {
if len(b) < MsgLenFieldSize {
return 0, false
}
msgLenField := b[:MsgLenFieldSize]
return binary.LittleEndian.Uint32(msgLenField), true
}

View File

@@ -0,0 +1,62 @@
/*
*
* Copyright 2018 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 conn
import (
"errors"
)
const counterLen = 12
var (
errInvalidCounter = errors.New("invalid counter")
)
// Counter is a 96-bit, little-endian counter.
type Counter struct {
value [counterLen]byte
invalid bool
overflowLen int
}
// Value returns the current value of the counter as a byte slice.
func (c *Counter) Value() ([]byte, error) {
if c.invalid {
return nil, errInvalidCounter
}
return c.value[:], nil
}
// Inc increments the counter and checks for overflow.
func (c *Counter) Inc() {
// If the counter is already invalid, there is no need to increase it.
if c.invalid {
return
}
i := 0
for ; i < c.overflowLen; i++ {
c.value[i]++
if c.value[i] != 0 {
break
}
}
if i == c.overflowLen {
c.invalid = true
}
}

View File

@@ -0,0 +1,141 @@
/*
*
* Copyright 2018 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 conn
import (
"bytes"
"testing"
core "google.golang.org/grpc/credentials/alts/internal"
)
const (
testOverflowLen = 5
)
func (s) TestCounterSides(t *testing.T) {
for _, side := range []core.Side{core.ClientSide, core.ServerSide} {
outCounter := NewOutCounter(side, testOverflowLen)
inCounter := NewInCounter(side, testOverflowLen)
for i := 0; i < 1024; i++ {
value, _ := outCounter.Value()
if g, w := CounterSide(value), side; g != w {
t.Errorf("after %d iterations, CounterSide(outCounter.Value()) = %v, want %v", i, g, w)
break
}
value, _ = inCounter.Value()
if g, w := CounterSide(value), side; g == w {
t.Errorf("after %d iterations, CounterSide(inCounter.Value()) = %v, want %v", i, g, w)
break
}
outCounter.Inc()
inCounter.Inc()
}
}
}
func (s) TestCounterInc(t *testing.T) {
for _, test := range []struct {
counter []byte
want []byte
}{
{
counter: []byte{0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
want: []byte{0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
{
counter: []byte{0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x80},
want: []byte{0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x80},
},
{
counter: []byte{0xff, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
want: []byte{0x00, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
{
counter: []byte{0x42, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
want: []byte{0x43, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
{
counter: []byte{0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
want: []byte{0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
},
{
counter: []byte{0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80},
want: []byte{0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80},
},
} {
c := CounterFromValue(test.counter, overflowLenAES128GCM)
c.Inc()
value, _ := c.Value()
if g, w := value, test.want; !bytes.Equal(g, w) || c.invalid {
t.Errorf("counter(%v).Inc() =\n%v, want\n%v", test.counter, g, w)
}
}
}
func (s) TestRolloverCounter(t *testing.T) {
for _, test := range []struct {
desc string
value []byte
overflowLen int
}{
{
desc: "testing overflow without rekeying 1",
value: []byte{0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80},
overflowLen: 5,
},
{
desc: "testing overflow without rekeying 2",
value: []byte{0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
overflowLen: 5,
},
{
desc: "testing overflow for rekeying mode 1",
value: []byte{0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x80},
overflowLen: 8,
},
{
desc: "testing overflow for rekeying mode 2",
value: []byte{0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00},
overflowLen: 8,
},
} {
c := CounterFromValue(test.value, overflowLenAES128GCM)
// First Inc() + Value() should work.
c.Inc()
_, err := c.Value()
if err != nil {
t.Errorf("%v: first Inc() + Value() unexpectedly failed: %v, want <nil> error", test.desc, err)
}
// Second Inc() + Value() should fail.
c.Inc()
_, err = c.Value()
if err != errInvalidCounter {
t.Errorf("%v: second Inc() + Value() unexpectedly succeeded: want %v", test.desc, errInvalidCounter)
}
// Third Inc() + Value() should also fail because the counter is
// already in an invalid state.
c.Inc()
_, err = c.Value()
if err != errInvalidCounter {
t.Errorf("%v: Third Inc() + Value() unexpectedly succeeded: want %v", test.desc, errInvalidCounter)
}
}
}

View File

@@ -0,0 +1,380 @@
/*
*
* Copyright 2018 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 conn contains an implementation of a secure channel created by gRPC
// handshakers.
package conn
import (
"encoding/binary"
"fmt"
"math"
"net"
core "google.golang.org/grpc/credentials/alts/internal"
imem "google.golang.org/grpc/internal/mem"
"google.golang.org/grpc/internal/transport/readyreader"
"google.golang.org/grpc/mem"
)
// ALTSRecordCrypto is the interface for gRPC ALTS record protocol.
type ALTSRecordCrypto interface {
// Encrypt encrypts the plaintext, computes the tag (if any) of dst and
// plaintext, and appends the result to dst, returning the updated slice.
// dst and plaintext may fully overlap or not at all.
Encrypt(dst, plaintext []byte) ([]byte, error)
// EncryptionOverhead returns the tag size (if any) in bytes.
EncryptionOverhead() int
// Decrypt decrypts ciphertext and verifies the tag (if any). If successful,
// this function appends the resulting plaintext to dst, returning the
// updated slice. dst and ciphertext may alias exactly or not at all. To
// reuse ciphertext's storage for the decrypted output, use ciphertext[:0]
// as dst. Even if the function fails, the contents of dst, up to its
// capacity, may be overwritten.
Decrypt(dst, ciphertext []byte) ([]byte, error)
}
// ALTSRecordFunc is a function type for factory functions that create
// ALTSRecordCrypto instances.
type ALTSRecordFunc func(s core.Side, keyData []byte) (ALTSRecordCrypto, error)
const (
// MsgLenFieldSize is the byte size of the frame length field of a
// framed message.
MsgLenFieldSize = 4
// The byte size of the message type field of a framed message.
msgTypeFieldSize = 4
// The bytes size limit for a ALTS record message.
altsRecordLengthLimit = 1024 * 1024 // 1 MiB
// The default bytes size of a ALTS record message.
altsRecordDefaultLength = 4 * 1024 // 4KiB
// Message type value included in ALTS record framing.
altsRecordMsgType = uint32(0x06)
// The maximum write buffer size, which can hold multiple ALTS frames.
altsWriteBufferMaxSize = 512 * 1024 // 512KiB
// The initial buffer used to read from the network.
// It includes an additional 512 Bytes to hold two 16KiB records plus
// small framing overheads.
altsReadBufferInitialSize = 32*1024 + 512 // 32.5KiB
)
var (
protocols = make(map[string]ALTSRecordFunc)
writeBufPool *imem.BinaryTieredBufferPool
// readBufPool pools buffers of at least `altsReadBufferInitialSize` size.
// Since the read buffer size is slightly larger than 32KB, using a regular
// BinaryTieredBufferPool results in allocating buffers of almost double the
// required length.
readBufPool = imem.NewDirtySimplePool()
// Compile-time check to ensure conn implements ReadyReader.
_ readyreader.Reader = &conn{}
)
func init() {
pool, err := imem.NewDirtyBinaryTieredBufferPool(
8,
12, // Go page size, 4KB
14, // 16KB (max HTTP/2 frame size used by gRPC)
15, // 32KB (default buffer size for gRPC)
16, // 64KB
17, // 128KB
19, // 512KB, max write buffer size
)
if err != nil {
panic(fmt.Sprintf("Failed to create write buffer pool: %v", err))
}
writeBufPool = pool
}
// RegisterProtocol register a ALTS record encryption protocol.
func RegisterProtocol(protocol string, f ALTSRecordFunc) error {
if _, ok := protocols[protocol]; ok {
return fmt.Errorf("protocol %v is already registered", protocol)
}
protocols[protocol] = f
return nil
}
// conn represents a secured connection. It implements the net.Conn interface.
type conn struct {
net.Conn
reader readyreader.Reader
crypto ALTSRecordCrypto
// buf holds data that has been read from the connection and decrypted,
// but has not yet been returned by Read. It is a sub-slice of protected.
buf []byte
payloadLengthLimit int
// protectedHandle buffer holds data read from the network but have not yet
// been decrypted. This data might not compose a complete frame.
//
// The buffer pointer to points to a buffer from the readBufPool. The handle
// should only be returned to the pool once nextFrame and buf are empty.
protectedHandle *[]byte
// nextFrame stores the next frame (in protected buffer) info.
nextFrame []byte
// overhead is the calculated overhead of each frame.
overhead int
constPool constBufferPool // stored as a field to avoid heap allocations.
}
// NewConn creates a new secure channel instance given the other party role and
// handshaking result.
func NewConn(c net.Conn, side core.Side, recordProtocol string, key []byte, protected []byte) (net.Conn, error) {
return NewConnWithMaxFrameSize(c, side, recordProtocol, key, protected, 0)
}
// NewConnWithMaxFrameSize creates a new secure channel instance given the
// other party role, handshaking result, and negotiated maximum frame size.
func NewConnWithMaxFrameSize(c net.Conn, side core.Side, recordProtocol string, key []byte, protected []byte, negotiatedMaxFrameSize int) (net.Conn, error) {
newCrypto := protocols[recordProtocol]
if newCrypto == nil {
return nil, fmt.Errorf("negotiated unknown next_protocol %q", recordProtocol)
}
crypto, err := newCrypto(side, key)
if err != nil {
return nil, fmt.Errorf("protocol %q: %v", recordProtocol, err)
}
overhead := MsgLenFieldSize + msgTypeFieldSize + crypto.EncryptionOverhead()
// Clamp maxRecordLen to be at least altsRecordDefaultLength.
maxRecordLen := max(altsRecordDefaultLength, negotiatedMaxFrameSize)
payloadLengthLimit := maxRecordLen - overhead
// We pre-allocate protected to be of size 32KB during initialization.
// We increase the size of the buffer by the required amount if it can't
// hold a complete encrypted record.
protectedHandle := readBufPool.Get(max(altsReadBufferInitialSize, len(protected)))
protectedBuf := *protectedHandle
// Copy additional data from hanshaker service.
copy(protectedBuf, protected)
protectedBuf = protectedBuf[:len(protected)]
altsConn := &conn{
Conn: c,
reader: readyreader.New(c),
crypto: crypto,
payloadLengthLimit: payloadLengthLimit,
protectedHandle: protectedHandle,
nextFrame: protectedBuf,
overhead: overhead,
}
return altsConn, nil
}
type constBufferPool struct {
buffer []byte
}
func (p *constBufferPool) Get(int) *[]byte {
return &p.buffer
}
func (p *constBufferPool) Put(*[]byte) {}
// Read reads and decrypts a frame from the underlying connection, and copies the
// decrypted payload into b. If the size of the payload is greater than len(b),
// Read retains the remaining bytes in an internal buffer, and subsequent calls
// to Read will read from this buffer until it is exhausted.
func (p *conn) Read(b []byte) (n int, err error) {
p.constPool.buffer = b
_, n, err = p.ReadOnReady(len(b), &p.constPool)
return n, err
}
func (p *conn) ReadOnReady(bufSize int, pool mem.BufferPool) (*[]byte, int, error) {
if len(p.buf) == 0 {
var framedMsg []byte
var protected []byte
if p.protectedHandle != nil {
protected = *p.protectedHandle
protected = protected[:cap(protected)]
}
var err error
framedMsg, p.nextFrame, err = ParseFramedMsg(p.nextFrame, altsRecordLengthLimit)
if err != nil {
return nil, 0, err
}
// Check whether the next frame to be decrypted has been
// completely received yet.
if len(framedMsg) == 0 {
copy(protected, p.nextFrame)
protected = protected[:len(p.nextFrame)]
// Always copy next incomplete frame to the beginning of
// the protected buffer and reset nextFrame to it.
p.nextFrame = protected
}
// Check whether a complete frame has been received yet.
for len(framedMsg) == 0 {
if p.protectedHandle != nil && len(protected) == cap(protected) {
// We can parse the length header to know exactly how large
// the buffer needs to be to hold the entire frame.
length, didParse := parseMessageLength(protected)
if !didParse {
// The protected buffer is initialized with a capacity of
// larger than 4B. It should always hold the message length
// header.
panic(fmt.Sprintf("protected buffer length shorter than expected: %d vs %d", len(protected), MsgLenFieldSize))
}
oldProtectedBuf := protected
oldBufHandle := p.protectedHandle
// The new buffer must be able to hold the message length header
// and the entire message.
requiredCapacity := int(length) + MsgLenFieldSize
p.protectedHandle = readBufPool.Get(requiredCapacity)
protected = *p.protectedHandle
// Copy the contents of the old buffer and set the length of the
// new buffer to the number of bytes already read.
copy(protected, oldProtectedBuf)
protected = protected[:len(oldProtectedBuf)]
readBufPool.Put(oldBufHandle)
}
if p.protectedHandle == nil {
// Connection was idle, need to re-allocate the read buffer.
newBuf, nRead, err := p.reader.ReadOnReady(altsReadBufferInitialSize, readBufPool)
if err != nil {
return nil, 0, err
}
p.protectedHandle = newBuf
protected = (*newBuf)[:nRead]
} else {
nRead, err := p.Conn.Read(protected[len(protected):cap(protected)])
if err != nil {
return nil, 0, err
}
protected = protected[:len(protected)+nRead]
}
framedMsg, p.nextFrame, err = ParseFramedMsg(protected, altsRecordLengthLimit)
if err != nil {
return nil, 0, err
}
}
// Now we have a complete frame, decrypted it.
msg := framedMsg[MsgLenFieldSize:]
if len(msg) < msgTypeFieldSize {
return nil, 0, fmt.Errorf("received frame with size %v which is shorter than message type field size %v", len(msg), msgTypeFieldSize)
}
msgType := binary.LittleEndian.Uint32(msg[:msgTypeFieldSize])
if msgType&0xff != altsRecordMsgType {
return nil, 0, fmt.Errorf("received frame with incorrect message type %v, expected lower byte %v",
msgType, altsRecordMsgType)
}
ciphertext := msg[msgTypeFieldSize:]
// Decrypt directly into the buffer, avoiding a copy from p.buf if
// possible.
if bufSize >= len(ciphertext) {
allocatedBuf := pool.Get(bufSize)
dec, err := p.crypto.Decrypt((*allocatedBuf)[:0], ciphertext)
if err != nil {
pool.Put(allocatedBuf)
return nil, 0, err
}
p.dropProtectedIfEmtpy()
return allocatedBuf, len(dec), nil
}
// Decrypt requires that if the dst and ciphertext alias, they
// must alias exactly. Code here used to use msg[:0], but msg
// starts MsgLenFieldSize+msgTypeFieldSize bytes earlier than
// ciphertext, so they alias inexactly. Using ciphertext[:0]
// arranges the appropriate aliasing without needing to copy
// ciphertext or use a separate destination buffer. For more info
// check: https://golang.org/pkg/crypto/cipher/#AEAD.
p.buf, err = p.crypto.Decrypt(ciphertext[:0], ciphertext)
if err != nil {
return nil, 0, err
}
}
allocatedBuf := pool.Get(bufSize)
n := copy(*allocatedBuf, p.buf)
p.buf = p.buf[n:]
p.dropProtectedIfEmtpy()
return allocatedBuf, n, nil
}
func (p *conn) dropProtectedIfEmtpy() {
if len(p.buf) > 0 || len(p.nextFrame) > 0 {
return
}
// Potentially idle connection, release the read buffer.
p.nextFrame = nil
p.buf = nil
if p.protectedHandle != nil {
readBufPool.Put(p.protectedHandle)
p.protectedHandle = nil
}
}
// Write encrypts, frames, and writes bytes from b to the underlying connection.
func (p *conn) Write(b []byte) (n int, err error) {
n = len(b)
// Calculate the output buffer size with framing and encryption overhead.
numOfFrames := int(math.Ceil(float64(len(b)) / float64(p.payloadLengthLimit)))
size := len(b) + numOfFrames*p.overhead
partialBSize := len(b)
if size > altsWriteBufferMaxSize {
size = altsWriteBufferMaxSize
frameLen := p.payloadLengthLimit + p.overhead
numOfFramesInMaxWriteBuf := altsWriteBufferMaxSize / frameLen
partialBSize = numOfFramesInMaxWriteBuf * p.payloadLengthLimit
}
// Get a writeBuf of the required length.
bufHandle := writeBufPool.Get(size)
defer writeBufPool.Put(bufHandle)
writeBuf := *bufHandle
for partialBStart := 0; partialBStart < len(b); partialBStart += partialBSize {
partialBEnd := min(partialBStart+partialBSize, len(b))
partialB := b[partialBStart:partialBEnd]
writeBufIndex := 0
for len(partialB) > 0 {
payloadLen := min(len(partialB), p.payloadLengthLimit)
buf := partialB[:payloadLen]
partialB = partialB[payloadLen:]
// Write buffer contains: length, type, payload, and tag
// if any.
// 1. Fill in type field.
msg := writeBuf[writeBufIndex+MsgLenFieldSize:]
binary.LittleEndian.PutUint32(msg, altsRecordMsgType)
// 2. Encrypt the payload and create a tag if any.
msg, err = p.crypto.Encrypt(msg[:msgTypeFieldSize], buf)
if err != nil {
return n, err
}
// 3. Fill in the size field.
binary.LittleEndian.PutUint32(writeBuf[writeBufIndex:], uint32(len(msg)))
// 4. Increase writeBufIndex.
writeBufIndex += len(buf) + p.overhead
}
nn, err := p.Conn.Write(writeBuf[:writeBufIndex])
if err != nil {
// We need to calculate the actual data size that was
// written. This means we need to remove header,
// encryption overheads, and any partially-written
// frame data.
numOfWrittenFrames := int(math.Floor(float64(nn) / float64(p.payloadLengthLimit+p.overhead)))
return partialBStart + numOfWrittenFrames*p.payloadLengthLimit, err
}
}
return n, nil
}

View File

@@ -0,0 +1,563 @@
/*
*
* Copyright 2018 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 conn
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"math"
"net"
"reflect"
"strings"
"testing"
core "google.golang.org/grpc/credentials/alts/internal"
"google.golang.org/grpc/internal/grpctest"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
const (
rekeyRecordProtocol = "ALTSRP_GCM_AES128_REKEY"
)
var (
recordProtocols = []string{rekeyRecordProtocol}
altsRecordFuncs = map[string]ALTSRecordFunc{
// ALTS handshaker protocols.
rekeyRecordProtocol: func(s core.Side, keyData []byte) (ALTSRecordCrypto, error) {
return NewAES128GCM(s, keyData)
},
}
)
func init() {
for protocol, f := range altsRecordFuncs {
if err := RegisterProtocol(protocol, f); err != nil {
panic(err)
}
}
}
// testConn mimics a net.Conn to the peer.
type testConn struct {
net.Conn
in *bytes.Buffer
out *bytes.Buffer
}
func (c *testConn) Read(b []byte) (n int, err error) {
return c.in.Read(b)
}
func (c *testConn) Write(b []byte) (n int, err error) {
return c.out.Write(b)
}
func (c *testConn) Close() error {
return nil
}
func newTestALTSRecordConn(in, out *bytes.Buffer, side core.Side, rp string, protected []byte) *conn {
key := []byte{
// 16 arbitrary bytes.
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xd2, 0x4c, 0xce, 0x4f, 0x49}
tc := testConn{
in: in,
out: out,
}
c, err := NewConn(&tc, side, rp, key, protected)
if err != nil {
panic(fmt.Sprintf("Unexpected error creating test ALTS record connection: %v", err))
}
return c.(*conn)
}
func newConnPair(rp string, clientProtected []byte, serverProtected []byte) (client, server *conn) {
clientBuf := new(bytes.Buffer)
serverBuf := new(bytes.Buffer)
clientConn := newTestALTSRecordConn(clientBuf, serverBuf, core.ClientSide, rp, clientProtected)
serverConn := newTestALTSRecordConn(serverBuf, clientBuf, core.ServerSide, rp, serverProtected)
return clientConn, serverConn
}
func testPingPong(t *testing.T, rp string) {
clientConn, serverConn := newConnPair(rp, nil, nil)
clientMsg := []byte("Client Message")
if n, err := clientConn.Write(clientMsg); n != len(clientMsg) || err != nil {
t.Fatalf("Client Write() = %v, %v; want %v, <nil>", n, err, len(clientMsg))
}
rcvClientMsg := make([]byte, len(clientMsg))
if n, err := serverConn.Read(rcvClientMsg); n != len(rcvClientMsg) || err != nil {
t.Fatalf("Server Read() = %v, %v; want %v, <nil>", n, err, len(rcvClientMsg))
}
if !reflect.DeepEqual(clientMsg, rcvClientMsg) {
t.Fatalf("Client Write()/Server Read() = %v, want %v", rcvClientMsg, clientMsg)
}
serverMsg := []byte("Server Message")
if n, err := serverConn.Write(serverMsg); n != len(serverMsg) || err != nil {
t.Fatalf("Server Write() = %v, %v; want %v, <nil>", n, err, len(serverMsg))
}
rcvServerMsg := make([]byte, len(serverMsg))
if n, err := clientConn.Read(rcvServerMsg); n != len(rcvServerMsg) || err != nil {
t.Fatalf("Client Read() = %v, %v; want %v, <nil>", n, err, len(rcvServerMsg))
}
if !reflect.DeepEqual(serverMsg, rcvServerMsg) {
t.Fatalf("Server Write()/Client Read() = %v, want %v", rcvServerMsg, serverMsg)
}
}
func (s) TestPingPong(t *testing.T) {
for _, rp := range recordProtocols {
testPingPong(t, rp)
}
}
func testSmallReadBuffer(t *testing.T, rp string) {
clientConn, serverConn := newConnPair(rp, nil, nil)
msg := []byte("Very Important Message")
if n, err := clientConn.Write(msg); err != nil {
t.Fatalf("Write() = %v, %v; want %v, <nil>", n, err, len(msg))
}
rcvMsg := make([]byte, len(msg))
n := 2 // Arbitrary index to break rcvMsg in two.
rcvMsg1 := rcvMsg[:n]
rcvMsg2 := rcvMsg[n:]
if n, err := serverConn.Read(rcvMsg1); n != len(rcvMsg1) || err != nil {
t.Fatalf("Read() = %v, %v; want %v, <nil>", n, err, len(rcvMsg1))
}
if n, err := serverConn.Read(rcvMsg2); n != len(rcvMsg2) || err != nil {
t.Fatalf("Read() = %v, %v; want %v, <nil>", n, err, len(rcvMsg2))
}
if !reflect.DeepEqual(msg, rcvMsg) {
t.Fatalf("Write()/Read() = %v, want %v", rcvMsg, msg)
}
}
func (s) TestSmallReadBuffer(t *testing.T) {
for _, rp := range recordProtocols {
testSmallReadBuffer(t, rp)
}
}
func testLargeMsg(t *testing.T, rp string) {
clientConn, serverConn := newConnPair(rp, nil, nil)
// msgLen is such that the length in the framing is larger than the
// default size of one frame.
msgLen := altsRecordDefaultLength - msgTypeFieldSize - clientConn.crypto.EncryptionOverhead() + 1
msg := make([]byte, msgLen)
if n, err := clientConn.Write(msg); n != len(msg) || err != nil {
t.Fatalf("Write() = %v, %v; want %v, <nil>", n, err, len(msg))
}
rcvMsg := make([]byte, len(msg))
if n, err := io.ReadFull(serverConn, rcvMsg); n != len(rcvMsg) || err != nil {
t.Fatalf("Read() = %v, %v; want %v, <nil>", n, err, len(rcvMsg))
}
if !reflect.DeepEqual(msg, rcvMsg) {
t.Fatalf("Write()/Server Read() = %v, want %v", rcvMsg, msg)
}
}
func (s) TestLargeMsg(t *testing.T) {
for _, rp := range recordProtocols {
testLargeMsg(t, rp)
}
}
// TestLargeRecord writes a very large ALTS record and verifies that the server
// receives it correctly. The large ALTS record should cause the reader to
// expand it's read buffer to hold the entire record and store the decrypted
// message until the receiver reads all of the bytes.
func (s) TestLargeRecord(t *testing.T) {
clientConn, serverConn := newConnPair(rekeyRecordProtocol, nil, nil)
msg := []byte(strings.Repeat("a", 2*altsReadBufferInitialSize))
// Increase the size of ALTS records written by the client.
clientConn.payloadLengthLimit = math.MaxInt32
if n, err := clientConn.Write(msg); n != len(msg) || err != nil {
t.Fatalf("Write() = %v, %v; want %v, <nil>", n, err, len(msg))
}
rcvMsg := make([]byte, len(msg))
if n, err := io.ReadFull(serverConn, rcvMsg); n != len(rcvMsg) || err != nil {
t.Fatalf("Read() = %v, %v; want %v, <nil>", n, err, len(rcvMsg))
}
if !reflect.DeepEqual(msg, rcvMsg) {
t.Fatalf("Write()/Server Read() = %v, want %v", rcvMsg, msg)
}
}
// BenchmarkLargeMessage measures the performance of ALTS conns for sending and
// receiving a large message.
func BenchmarkLargeMessage(b *testing.B) {
msgLen := 20 * 1024 * 1024 // 20 MiB
msg := make([]byte, msgLen)
rcvMsg := make([]byte, len(msg))
b.ResetTimer()
clientConn, serverConn := newConnPair(rekeyRecordProtocol, nil, nil)
for range b.N {
// Write 20 MiB 5 times to transfer a total of 100 MiB.
for range 5 {
if n, err := clientConn.Write(msg); n != len(msg) || err != nil {
b.Fatalf("Write() = %v, %v; want %v, <nil>", n, err, len(msg))
}
if n, err := io.ReadFull(serverConn, rcvMsg); n != len(rcvMsg) || err != nil {
b.Fatalf("Read() = %v, %v; want %v, <nil>", n, err, len(rcvMsg))
}
}
}
}
func testIncorrectMsgType(t *testing.T, rp string) {
// framedMsg is an empty ciphertext with correct framing but wrong
// message type.
framedMsg := make([]byte, MsgLenFieldSize+msgTypeFieldSize)
binary.LittleEndian.PutUint32(framedMsg[:MsgLenFieldSize], msgTypeFieldSize)
wrongMsgType := uint32(0x22)
binary.LittleEndian.PutUint32(framedMsg[MsgLenFieldSize:], wrongMsgType)
in := bytes.NewBuffer(framedMsg)
c := newTestALTSRecordConn(in, nil, core.ClientSide, rp, nil)
b := make([]byte, 1)
if n, err := c.Read(b); n != 0 || err == nil {
t.Fatalf("Read() = <nil>, want %v", fmt.Errorf("received frame with incorrect message type %v", wrongMsgType))
}
}
func (s) TestIncorrectMsgType(t *testing.T) {
for _, rp := range recordProtocols {
testIncorrectMsgType(t, rp)
}
}
func testFrameTooLarge(t *testing.T, rp string) {
buf := new(bytes.Buffer)
clientConn := newTestALTSRecordConn(nil, buf, core.ClientSide, rp, nil)
serverConn := newTestALTSRecordConn(buf, nil, core.ServerSide, rp, nil)
// payloadLen is such that the length in the framing is larger than
// allowed in one frame.
payloadLen := altsRecordLengthLimit - msgTypeFieldSize - clientConn.crypto.EncryptionOverhead() + 1
payload := make([]byte, payloadLen)
c, err := clientConn.crypto.Encrypt(nil, payload)
if err != nil {
t.Fatalf("Error encrypting message: %v", err)
}
msgLen := msgTypeFieldSize + len(c)
framedMsg := make([]byte, MsgLenFieldSize+msgLen)
binary.LittleEndian.PutUint32(framedMsg[:MsgLenFieldSize], uint32(msgTypeFieldSize+len(c)))
msg := framedMsg[MsgLenFieldSize:]
binary.LittleEndian.PutUint32(msg[:msgTypeFieldSize], altsRecordMsgType)
copy(msg[msgTypeFieldSize:], c)
if _, err = buf.Write(framedMsg); err != nil {
t.Fatalf("Unexpected error writing to buffer: %v", err)
}
b := make([]byte, 1)
if n, err := serverConn.Read(b); n != 0 || err == nil {
t.Fatalf("Read() = <nil>, want %v", fmt.Errorf("received the frame length %d larger than the limit %d", altsRecordLengthLimit+1, altsRecordLengthLimit))
}
}
func (s) TestFrameTooLarge(t *testing.T) {
for _, rp := range recordProtocols {
testFrameTooLarge(t, rp)
}
}
func testWriteLargeData(t *testing.T, rp string) {
// Test sending and receiving messages larger than the maximum write
// buffer size.
clientConn, serverConn := newConnPair(rp, nil, nil)
// Message size is intentionally chosen to not be multiple of
// payloadLengthLimit.
msgSize := altsWriteBufferMaxSize + (100 * 1024)
clientMsg := make([]byte, msgSize)
for i := 0; i < msgSize; i++ {
clientMsg[i] = 0xAA
}
if n, err := clientConn.Write(clientMsg); n != len(clientMsg) || err != nil {
t.Fatalf("Client Write() = %v, %v; want %v, <nil>", n, err, len(clientMsg))
}
// We need to keep reading until the entire message is received. The
// reason we set all bytes of the message to a value other than zero is
// to avoid ambiguous zero-init value of rcvClientMsg buffer and the
// actual received data.
rcvClientMsg := make([]byte, 0, msgSize)
numberOfExpectedFrames := int(math.Ceil(float64(msgSize) / float64(serverConn.payloadLengthLimit)))
for i := 0; i < numberOfExpectedFrames; i++ {
expectedRcvSize := serverConn.payloadLengthLimit
if i == numberOfExpectedFrames-1 {
// Last frame might be smaller.
expectedRcvSize = msgSize % serverConn.payloadLengthLimit
}
tmpBuf := make([]byte, expectedRcvSize)
if n, err := serverConn.Read(tmpBuf); n != len(tmpBuf) || err != nil {
t.Fatalf("Server Read() = %v, %v; want %v, <nil>", n, err, len(tmpBuf))
}
rcvClientMsg = append(rcvClientMsg, tmpBuf...)
}
if !reflect.DeepEqual(clientMsg, rcvClientMsg) {
t.Fatalf("Client Write()/Server Read() = %v, want %v", rcvClientMsg, clientMsg)
}
}
func (s) TestWriteLargeData(t *testing.T) {
for _, rp := range recordProtocols {
testWriteLargeData(t, rp)
}
}
func testProtectedBuffer(t *testing.T, rp string) {
key := []byte{
// 16 arbitrary bytes.
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xd2, 0x4c, 0xce, 0x4f, 0x49}
// Encrypt a message to be passed to NewConn as a client-side protected
// buffer.
newCrypto := protocols[rp]
if newCrypto == nil {
t.Fatalf("Unknown record protocol %q", rp)
}
crypto, err := newCrypto(core.ClientSide, key)
if err != nil {
t.Fatalf("Failed to create a crypter for protocol %q: %v", rp, err)
}
msg := []byte("Client Protected Message")
encryptedMsg, err := crypto.Encrypt(nil, msg)
if err != nil {
t.Fatalf("Failed to encrypt the client protected message: %v", err)
}
protectedMsg := make([]byte, 8) // 8 bytes = 4 length + 4 type
binary.LittleEndian.PutUint32(protectedMsg, uint32(len(encryptedMsg))+4) // 4 bytes for the type
binary.LittleEndian.PutUint32(protectedMsg[4:], altsRecordMsgType)
protectedMsg = append(protectedMsg, encryptedMsg...)
_, serverConn := newConnPair(rp, nil, protectedMsg)
rcvClientMsg := make([]byte, len(msg))
if n, err := serverConn.Read(rcvClientMsg); n != len(rcvClientMsg) || err != nil {
t.Fatalf("Server Read() = %v, %v; want %v, <nil>", n, err, len(rcvClientMsg))
}
if !reflect.DeepEqual(msg, rcvClientMsg) {
t.Fatalf("Client protected/Server Read() = %v, want %v", rcvClientMsg, msg)
}
}
func (s) TestProtectedBuffer(t *testing.T) {
for _, rp := range recordProtocols {
testProtectedBuffer(t, rp)
}
}
// BenchmarkWriteMemoryUsage measures the allocations per ALTS connection.
// Run this with: go test -bench=BenchmarkWriteMemoryUsage -benchmem
func BenchmarkWriteMemoryUsage(b *testing.B) {
b.ReportAllocs()
data := []byte(strings.Repeat("d", 256))
key := []byte{
// 16 arbitrary bytes.
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xd2, 0x4c, 0xce, 0x4f, 0x49}
conn := &noopConn{}
for b.Loop() {
c, err := NewConn(conn, core.ClientSide, rekeyRecordProtocol, key, nil)
if err != nil {
b.Fatal(err)
}
if _, err := c.Write(data); err != nil {
b.Fatalf("Write failed: %v", err)
}
}
}
// BenchmarkReadMemoryUsage measures the allocations per ALTS connection.
// Run this with: go test -bench=BenchmarkReadMemoryUsage -benchmem
func BenchmarkReadMemoryUsage(b *testing.B) {
b.ReportAllocs()
data := []byte(strings.Repeat("d", 257))
readBuf := make([]byte, 1024)
for b.Loop() {
clientConn, serverConn := newConnPair(rekeyRecordProtocol, nil, nil)
b.StopTimer()
clientConn.Write(data)
b.StartTimer()
if _, err := serverConn.Read(readBuf); err != nil {
b.Fatalf("Write failed: %v", err)
}
}
}
type noopConn struct {
net.Conn
}
func (c *noopConn) Write(b []byte) (n int, err error) {
return len(b), nil
}
func (c *noopConn) Close() error {
return nil
}
func (s) TestParseFramedMsgVulnerability(t *testing.T) {
key := []byte{0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xd2, 0x4c, 0xce, 0x4f, 0x49}
tc := testConn{
in: new(bytes.Buffer),
out: new(bytes.Buffer),
}
// We pass a protected buffer representing a framed message length of 0,
// followed by bytes that will act as a dirty buffer capacity. If the capacity
// bytes happen to start with `0x06` (altsRecordMsgType), the vulnerable
// code will try to slice `msg[4:]` which panics because len(msg) is 0.
malformedProtected := []byte{0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00}
c, err := NewConn(&tc, core.ServerSide, rekeyRecordProtocol, key, malformedProtected)
if err != nil {
t.Fatalf("NewConn failed: %v", err)
}
buf := make([]byte, 1024)
const wantErr = "shorter than message type field size"
if _, err := c.Read(buf); err == nil || !strings.Contains(err.Error(), wantErr) {
t.Fatalf("c.Read(buf) returned error: %v, want error containing %q", err, wantErr)
}
}
// countALTSFrames parses raw ALTS wire bytes and returns the number of complete frames.
func countALTSFrames(data []byte) int {
count := 0
for len(data) >= MsgLenFieldSize {
frameLen := int(binary.LittleEndian.Uint32(data[:MsgLenFieldSize]))
total := MsgLenFieldSize + frameLen
if total > len(data) {
break
}
data = data[total:]
count++
}
return count
}
func (s) TestNewConnNegotiatedMaxFrameSize(t *testing.T) {
key := make([]byte, 32)
for _, tc := range []struct {
name string
negotiatedMaxFrameSize int
wantMaxRecordLen int
}{
{"NotNegotiated", 0, altsRecordDefaultLength},
{"SmallerThanDefault_10B", 10, altsRecordDefaultLength},
{"SmallerThanDefault_1024B", 1024, altsRecordDefaultLength},
{"Negotiated_4KB", 4 * 1024, 4 * 1024},
{"Negotiated_16KB", 16 * 1024, 16 * 1024},
{"Negotiated_64KB", 64 * 1024, 64 * 1024},
} {
t.Run(tc.name, func(t *testing.T) {
tcConn := testConn{}
c, err := NewConnWithMaxFrameSize(&tcConn, core.ClientSide, rekeyRecordProtocol, key, nil, tc.negotiatedMaxFrameSize)
if err != nil {
t.Fatalf("NewConn() err = %v, want nil", err)
}
altsC := c.(*conn)
wantPayloadLimit := tc.wantMaxRecordLen - altsC.overhead
if altsC.payloadLengthLimit != wantPayloadLimit {
t.Errorf("payloadLengthLimit = %v, want %v", altsC.payloadLengthLimit, wantPayloadLimit)
}
})
}
}
// TestWriteHonorsNegotiatedMaxFrameSize verifies that Write uses the negotiated
// max_frame_size so we never send a frame larger than negotiated.
func (s) TestWriteHonorsNegotiatedMaxFrameSize(t *testing.T) {
key := make([]byte, 16)
const negotiatedMaxFrameSize = 16 * 1024
out := new(bytes.Buffer)
c, err := NewConnWithMaxFrameSize(&testConn{in: new(bytes.Buffer), out: out}, core.ClientSide, rekeyRecordProtocol, key, nil, negotiatedMaxFrameSize)
if err != nil {
t.Fatalf("NewConn failed: %v", err)
}
altsC := c.(*conn)
if got, want := altsC.payloadLengthLimit+altsC.overhead, negotiatedMaxFrameSize; got != want {
t.Fatalf("frame size = %v, want %v", got, want)
}
// Two negotiatedMaxFrameSize-worth of plaintext should split into exactly two frames.
payload := make([]byte, 2*altsC.payloadLengthLimit)
if _, err := c.Write(payload); err != nil {
t.Fatalf("Write failed: %v", err)
}
if got, want := countALTSFrames(out.Bytes()), 2; got != want {
t.Errorf("Write produced %d frames, want %d", got, want)
}
}
type partialWriteTestConn struct {
net.Conn
maxWriteBytes int
err error
out bytes.Buffer
}
func (c *partialWriteTestConn) Write(b []byte) (int, error) {
if len(b) > c.maxWriteBytes {
c.out.Write(b[:c.maxWriteBytes])
return c.maxWriteBytes, c.err
}
c.out.Write(b)
return len(b), nil
}
// TestWritePartialWriteErrorWithNegotiatedMaxFrameSize verifies that when a partial Write error occurs,
// the number of written bytes returned correctly accounts for the negotiated max_frame_size.
func (s) TestWritePartialWriteErrorWithNegotiatedMaxFrameSize(t *testing.T) {
key := make([]byte, 16)
const negotiatedMaxFrameSize = 16 * 1024
// Allow writing 1.5 frames (16384 + 8192 = 24576 bytes) before returning io.ErrUnexpectedEOF.
writeLimit := negotiatedMaxFrameSize + negotiatedMaxFrameSize/2
errConn := &partialWriteTestConn{
maxWriteBytes: writeLimit,
err: io.ErrUnexpectedEOF,
}
c, err := NewConnWithMaxFrameSize(errConn, core.ClientSide, rekeyRecordProtocol, key, nil, negotiatedMaxFrameSize)
if err != nil {
t.Fatalf("NewConn failed: %v", err)
}
altsC := c.(*conn)
// Send 3 frames worth of payload
payload := make([]byte, 3*altsC.payloadLengthLimit)
n, writeErr := c.Write(payload)
if writeErr != io.ErrUnexpectedEOF {
t.Fatalf("Write err = %v, want %v", writeErr, io.ErrUnexpectedEOF)
}
// Exactly 1 complete frame was written before the partial write error.
wantWrittenBytes := altsC.payloadLengthLimit
if n != wantWrittenBytes {
t.Errorf("Write() returned n = %d, want %d", n, wantWrittenBytes)
}
}

View File

@@ -0,0 +1,63 @@
/*
*
* Copyright 2018 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 conn
import core "google.golang.org/grpc/credentials/alts/internal"
// NewOutCounter returns an outgoing counter initialized to the starting sequence
// number for the client/server side of a connection.
func NewOutCounter(s core.Side, overflowLen int) (c Counter) {
c.overflowLen = overflowLen
if s == core.ServerSide {
// Server counters in ALTS record have the little-endian high bit
// set.
c.value[counterLen-1] = 0x80
}
return
}
// NewInCounter returns an incoming counter initialized to the starting sequence
// number for the client/server side of a connection. This is used in ALTS record
// to check that incoming counters are as expected, since ALTS record guarantees
// that messages are unwrapped in the same order that the peer wrapped them.
func NewInCounter(s core.Side, overflowLen int) (c Counter) {
c.overflowLen = overflowLen
if s == core.ClientSide {
// Server counters in ALTS record have the little-endian high bit
// set.
c.value[counterLen-1] = 0x80
}
return
}
// CounterFromValue creates a new counter given an initial value.
func CounterFromValue(value []byte, overflowLen int) (c Counter) {
c.overflowLen = overflowLen
copy(c.value[:], value)
return
}
// CounterSide returns the connection side (client/server) a sequence counter is
// associated with.
func CounterSide(c []byte) core.Side {
if c[counterLen-1]&0x80 == 0x80 {
return core.ServerSide
}
return core.ClientSide
}

View File

@@ -0,0 +1,381 @@
/*
*
* Copyright 2018 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 handshaker provides ALTS handshaking functionality for GCP.
package handshaker
import (
"context"
"errors"
"fmt"
"io"
"net"
"time"
"golang.org/x/sync/semaphore"
grpc "google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
core "google.golang.org/grpc/credentials/alts/internal"
"google.golang.org/grpc/credentials/alts/internal/authinfo"
"google.golang.org/grpc/credentials/alts/internal/conn"
altsgrpc "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"
altspb "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"
"google.golang.org/grpc/internal/envconfig"
)
const (
// The maximum byte size of receive frames.
frameLimit = 64 * 1024 // 64 KB
rekeyRecordProtocolName = "ALTSRP_GCM_AES128_REKEY"
)
var (
hsProtocol = altspb.HandshakeProtocol_ALTS
appProtocols = []string{"grpc"}
recordProtocols = []string{rekeyRecordProtocolName}
keyLength = map[string]int{
rekeyRecordProtocolName: 44,
}
altsRecordFuncs = map[string]conn.ALTSRecordFunc{
// ALTS handshaker protocols.
rekeyRecordProtocolName: func(s core.Side, keyData []byte) (conn.ALTSRecordCrypto, error) {
return conn.NewAES128GCMRekey(s, keyData)
},
}
// control number of concurrent created (but not closed) handshakes.
clientHandshakes = semaphore.NewWeighted(int64(envconfig.ALTSMaxConcurrentHandshakes))
serverHandshakes = semaphore.NewWeighted(int64(envconfig.ALTSMaxConcurrentHandshakes))
// errOutOfBound occurs when the handshake service returns a consumed
// bytes value larger than the buffer that was passed to it originally.
errOutOfBound = errors.New("handshaker service consumed bytes value is out-of-bound")
)
func init() {
for protocol, f := range altsRecordFuncs {
if err := conn.RegisterProtocol(protocol, f); err != nil {
panic(err)
}
}
}
// ClientHandshakerOptions contains the client handshaker options that can
// provided by the caller.
type ClientHandshakerOptions struct {
// ClientIdentity is the handshaker client local identity.
ClientIdentity *altspb.Identity
// TargetName is the server service account name for secure name
// checking.
TargetName string
// TargetServiceAccounts contains a list of expected target service
// accounts. One of these accounts should match one of the accounts in
// the handshaker results. Otherwise, the handshake fails.
TargetServiceAccounts []string
// RPCVersions specifies the gRPC versions accepted by the client.
RPCVersions *altspb.RpcProtocolVersions
// BoundAccessToken is a bound access token to be sent to the server for authentication.
BoundAccessToken string
}
// ServerHandshakerOptions contains the server handshaker options that can
// provided by the caller.
type ServerHandshakerOptions struct {
// RPCVersions specifies the gRPC versions accepted by the server.
RPCVersions *altspb.RpcProtocolVersions
}
// DefaultClientHandshakerOptions returns the default client handshaker options.
func DefaultClientHandshakerOptions() *ClientHandshakerOptions {
return &ClientHandshakerOptions{}
}
// DefaultServerHandshakerOptions returns the default client handshaker options.
func DefaultServerHandshakerOptions() *ServerHandshakerOptions {
return &ServerHandshakerOptions{}
}
// altsHandshaker is used to complete an ALTS handshake between client and
// server. This handshaker talks to the ALTS handshaker service in the metadata
// server.
type altsHandshaker struct {
// RPC stream used to access the ALTS Handshaker service.
stream altsgrpc.HandshakerService_DoHandshakeClient
// the connection to the peer.
conn net.Conn
// a virtual connection to the ALTS handshaker service.
clientConn *grpc.ClientConn
// client handshake options.
clientOpts *ClientHandshakerOptions
// server handshake options.
serverOpts *ServerHandshakerOptions
// defines the side doing the handshake, client or server.
side core.Side
}
// NewClientHandshaker creates a core.Handshaker that performs a client-side
// ALTS handshake by acting as a proxy between the peer and the ALTS handshaker
// service in the metadata server.
func NewClientHandshaker(_ context.Context, conn *grpc.ClientConn, c net.Conn, opts *ClientHandshakerOptions) (core.Handshaker, error) {
return &altsHandshaker{
stream: nil,
conn: c,
clientConn: conn,
clientOpts: opts,
side: core.ClientSide,
}, nil
}
// NewServerHandshaker creates a core.Handshaker that performs a server-side
// ALTS handshake by acting as a proxy between the peer and the ALTS handshaker
// service in the metadata server.
func NewServerHandshaker(_ context.Context, conn *grpc.ClientConn, c net.Conn, opts *ServerHandshakerOptions) (core.Handshaker, error) {
return &altsHandshaker{
stream: nil,
conn: c,
clientConn: conn,
serverOpts: opts,
side: core.ServerSide,
}, nil
}
// ClientHandshake starts and completes a client ALTS handshake for GCP. Once
// done, ClientHandshake returns a secure connection.
func (h *altsHandshaker) ClientHandshake(ctx context.Context) (net.Conn, credentials.AuthInfo, error) {
if err := clientHandshakes.Acquire(ctx, 1); err != nil {
return nil, nil, err
}
defer clientHandshakes.Release(1)
if h.side != core.ClientSide {
return nil, nil, errors.New("only handshakers created using NewClientHandshaker can perform a client handshaker")
}
// TODO(matthewstevenson88): Change unit tests to use public APIs so
// that h.stream can unconditionally be set based on h.clientConn.
if h.stream == nil {
stream, err := altsgrpc.NewHandshakerServiceClient(h.clientConn).DoHandshake(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to establish stream to ALTS handshaker service: %v", err)
}
h.stream = stream
}
// Create target identities from service account list.
targetIdentities := make([]*altspb.Identity, 0, len(h.clientOpts.TargetServiceAccounts))
for _, account := range h.clientOpts.TargetServiceAccounts {
targetIdentities = append(targetIdentities, &altspb.Identity{
IdentityOneof: &altspb.Identity_ServiceAccount{
ServiceAccount: account,
},
})
}
req := &altspb.HandshakerReq{
ReqOneof: &altspb.HandshakerReq_ClientStart{
ClientStart: &altspb.StartClientHandshakeReq{
HandshakeSecurityProtocol: hsProtocol,
ApplicationProtocols: appProtocols,
RecordProtocols: recordProtocols,
TargetIdentities: targetIdentities,
LocalIdentity: h.clientOpts.ClientIdentity,
TargetName: h.clientOpts.TargetName,
RpcVersions: h.clientOpts.RPCVersions,
},
},
}
if h.clientOpts.BoundAccessToken != "" {
req.GetClientStart().AccessToken = h.clientOpts.BoundAccessToken
}
conn, result, err := h.doHandshake(req)
if err != nil {
return nil, nil, err
}
authInfo := authinfo.New(result)
return conn, authInfo, nil
}
// ServerHandshake starts and completes a server ALTS handshake for GCP. Once
// done, ServerHandshake returns a secure connection.
func (h *altsHandshaker) ServerHandshake(ctx context.Context) (net.Conn, credentials.AuthInfo, error) {
if err := serverHandshakes.Acquire(ctx, 1); err != nil {
return nil, nil, err
}
defer serverHandshakes.Release(1)
if h.side != core.ServerSide {
return nil, nil, errors.New("only handshakers created using NewServerHandshaker can perform a server handshaker")
}
// TODO(matthewstevenson88): Change unit tests to use public APIs so
// that h.stream can unconditionally be set based on h.clientConn.
if h.stream == nil {
stream, err := altsgrpc.NewHandshakerServiceClient(h.clientConn).DoHandshake(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to establish stream to ALTS handshaker service: %v", err)
}
h.stream = stream
}
p := make([]byte, frameLimit)
n, err := h.conn.Read(p)
if err != nil {
return nil, nil, err
}
// Prepare server parameters.
params := make(map[int32]*altspb.ServerHandshakeParameters)
params[int32(altspb.HandshakeProtocol_ALTS)] = &altspb.ServerHandshakeParameters{
RecordProtocols: recordProtocols,
}
req := &altspb.HandshakerReq{
ReqOneof: &altspb.HandshakerReq_ServerStart{
ServerStart: &altspb.StartServerHandshakeReq{
ApplicationProtocols: appProtocols,
HandshakeParameters: params,
InBytes: p[:n],
RpcVersions: h.serverOpts.RPCVersions,
},
},
}
conn, result, err := h.doHandshake(req)
if err != nil {
return nil, nil, err
}
authInfo := authinfo.New(result)
return conn, authInfo, nil
}
func (h *altsHandshaker) doHandshake(req *altspb.HandshakerReq) (net.Conn, *altspb.HandshakerResult, error) {
resp, err := h.accessHandshakerService(req)
if err != nil {
return nil, nil, err
}
// Check of the returned status is an error.
if resp.GetStatus() != nil {
if got, want := resp.GetStatus().Code, uint32(codes.OK); got != want {
return nil, nil, fmt.Errorf("%v", resp.GetStatus().Details)
}
}
var extra []byte
if req.GetServerStart() != nil {
if resp.GetBytesConsumed() > uint32(len(req.GetServerStart().GetInBytes())) {
return nil, nil, errOutOfBound
}
extra = req.GetServerStart().GetInBytes()[resp.GetBytesConsumed():]
}
result, extra, err := h.processUntilDone(resp, extra)
if err != nil {
return nil, nil, err
}
// The handshaker returns a 128 bytes key. It should be truncated based
// on the returned record protocol.
keyLen, ok := keyLength[result.RecordProtocol]
if !ok {
return nil, nil, fmt.Errorf("unknown resulted record protocol %v", result.RecordProtocol)
}
maxFrameSize := int(envconfig.ALTSMaxFrameSize)
if peerMax := int(result.GetMaxFrameSize()); peerMax > 0 {
maxFrameSize = min(peerMax, maxFrameSize)
}
sc, err := conn.NewConnWithMaxFrameSize(h.conn, h.side, result.GetRecordProtocol(), result.KeyData[:keyLen], extra, maxFrameSize)
if err != nil {
return nil, nil, err
}
return sc, result, nil
}
func (h *altsHandshaker) accessHandshakerService(req *altspb.HandshakerReq) (*altspb.HandshakerResp, error) {
if err := h.stream.Send(req); err != nil {
return nil, fmt.Errorf("failed to send ALTS handshaker request: %w", err)
}
resp, err := h.stream.Recv()
if err != nil {
return nil, fmt.Errorf("failed to receive ALTS handshaker response: %w", err)
}
return resp, nil
}
// processUntilDone processes the handshake until the handshaker service returns
// the results. Handshaker service takes care of frame parsing, so we read
// whatever received from the network and send it to the handshaker service.
func (h *altsHandshaker) processUntilDone(resp *altspb.HandshakerResp, extra []byte) (*altspb.HandshakerResult, []byte, error) {
var lastWriteTime time.Time
buf := make([]byte, frameLimit)
for {
if len(resp.OutFrames) > 0 {
lastWriteTime = time.Now()
if _, err := h.conn.Write(resp.OutFrames); err != nil {
return nil, nil, err
}
}
if resp.Result != nil {
return resp.Result, extra, nil
}
n, err := h.conn.Read(buf)
if err != nil && err != io.EOF {
return nil, nil, err
}
// If there is nothing to send to the handshaker service, and
// nothing is received from the peer, then we are stuck.
// This covers the case when the peer is not responding. Note
// that handshaker service connection issues are caught in
// accessHandshakerService before we even get here.
if len(resp.OutFrames) == 0 && n == 0 {
return nil, nil, core.PeerNotRespondingError
}
// Append extra bytes from the previous interaction with the
// handshaker service with the current buffer read from conn.
p := append(extra, buf[:n]...)
// Compute the time elapsed since the last write to the peer.
timeElapsed := time.Since(lastWriteTime)
timeElapsedMs := uint32(timeElapsed.Milliseconds())
// From here on, p and extra point to the same slice.
resp, err = h.accessHandshakerService(&altspb.HandshakerReq{
ReqOneof: &altspb.HandshakerReq_Next{
Next: &altspb.NextHandshakeMessageReq{
InBytes: p,
NetworkLatencyMs: timeElapsedMs,
},
},
})
if err != nil {
return nil, nil, err
}
// Set extra based on handshaker service response.
if resp.GetBytesConsumed() > uint32(len(p)) {
return nil, nil, errOutOfBound
}
extra = p[resp.GetBytesConsumed():]
}
}
// Close terminates the Handshaker. It should be called when the caller obtains
// the secure connection.
func (h *altsHandshaker) Close() {
if h.stream != nil {
h.stream.CloseSend()
}
}
// ResetConcurrentHandshakeSemaphoreForTesting resets the handshake semaphores
// to allow numberOfAllowedHandshakes concurrent handshakes each.
func ResetConcurrentHandshakeSemaphoreForTesting(numberOfAllowedHandshakes int64) {
clientHandshakes = semaphore.NewWeighted(numberOfAllowedHandshakes)
serverHandshakes = semaphore.NewWeighted(numberOfAllowedHandshakes)
}

View File

@@ -0,0 +1,437 @@
/*
*
* Copyright 2018 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 handshaker
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
grpc "google.golang.org/grpc"
core "google.golang.org/grpc/credentials/alts/internal"
altspb "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"
"google.golang.org/grpc/credentials/alts/internal/testutil"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpctest"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
var (
testRecordProtocol = rekeyRecordProtocolName
testKey = []byte{
// 44 arbitrary bytes.
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xd2, 0x4c, 0xce, 0x4f, 0x49,
0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xd2, 0x4c, 0xce, 0x4f, 0x49, 0x1f, 0x8b,
0xd2, 0x4c, 0xce, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2,
}
testServiceAccount = "test_service_account"
testTargetServiceAccounts = []string{testServiceAccount}
testClientIdentity = &altspb.Identity{
IdentityOneof: &altspb.Identity_Hostname{
Hostname: "i_am_a_client",
},
}
)
const defaultTestTimeout = 10 * time.Second
// testRPCStream mimics a altspb.HandshakerService_DoHandshakeClient object.
type testRPCStream struct {
grpc.ClientStream
t *testing.T
isClient bool
// The resp expected to be returned by Recv(). Make sure this is set to
// the content the test requires before Recv() is invoked.
recvBuf *altspb.HandshakerResp
// false if it is the first access to Handshaker service on Envelope.
first bool
// useful for testing concurrent calls.
delay time.Duration
// The minimum expected value of the network_latency_ms field in a
// NextHandshakeMessageReq.
minExpectedNetworkLatency time.Duration
// peerMaxFrameSize is the max_frame_size in the HandshakerResult.
peerMaxFrameSize uint32
}
func (t *testRPCStream) Recv() (*altspb.HandshakerResp, error) {
resp := t.recvBuf
t.recvBuf = nil
return resp, nil
}
func (t *testRPCStream) Send(req *altspb.HandshakerReq) error {
var resp *altspb.HandshakerResp
if !t.first {
// Generate the bytes to be returned by Recv() for the initial
// handshaking.
t.first = true
if t.isClient {
resp = &altspb.HandshakerResp{
OutFrames: testutil.MakeFrame("ClientInit"),
// Simulate consuming ServerInit.
BytesConsumed: 14,
}
} else {
resp = &altspb.HandshakerResp{
OutFrames: testutil.MakeFrame("ServerInit"),
// Simulate consuming ClientInit.
BytesConsumed: 14,
}
}
} else {
switch req := req.ReqOneof.(type) {
case *altspb.HandshakerReq_Next:
// Compare the network_latency_ms field to the minimum expected network
// latency.
if nl := time.Duration(req.Next.NetworkLatencyMs) * time.Millisecond; nl < t.minExpectedNetworkLatency {
return fmt.Errorf("networkLatency (%v) is smaller than expected min network latency (%v)", nl, t.minExpectedNetworkLatency)
}
default:
return fmt.Errorf("handshake request has unexpected type: %v", req)
}
// Add delay to test concurrent calls.
cleanup := stat.Update()
defer cleanup()
time.Sleep(t.delay)
// Generate the response to be returned by Recv() for the
// follow-up handshaking.
result := &altspb.HandshakerResult{
RecordProtocol: testRecordProtocol,
KeyData: testKey,
MaxFrameSize: t.peerMaxFrameSize,
}
resp = &altspb.HandshakerResp{
Result: result,
// Simulate consuming ClientFinished or ServerFinished.
BytesConsumed: 18,
}
}
t.recvBuf = resp
return nil
}
func (t *testRPCStream) CloseSend() error {
return nil
}
var stat testutil.Stats
func (s) TestClientHandshake(t *testing.T) {
for _, testCase := range []struct {
delay time.Duration
numberOfHandshakes int
readLatency time.Duration
}{
{0 * time.Millisecond, 1, time.Duration(0)},
{0 * time.Millisecond, 1, 2 * time.Millisecond},
{100 * time.Millisecond, 10 * int(envconfig.ALTSMaxConcurrentHandshakes), time.Duration(0)},
} {
errc := make(chan error)
stat.Reset()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
for i := 0; i < testCase.numberOfHandshakes; i++ {
stream := &testRPCStream{
t: t,
isClient: true,
minExpectedNetworkLatency: testCase.readLatency,
}
// Preload the inbound frames.
f1 := testutil.MakeFrame("ServerInit")
f2 := testutil.MakeFrame("ServerFinished")
in := bytes.NewBuffer(f1)
in.Write(f2)
out := new(bytes.Buffer)
tc := testutil.NewTestConnWithReadLatency(in, out, testCase.readLatency)
chs := &altsHandshaker{
stream: stream,
conn: tc,
clientOpts: &ClientHandshakerOptions{
TargetServiceAccounts: testTargetServiceAccounts,
ClientIdentity: testClientIdentity,
},
side: core.ClientSide,
}
go func() {
_, context, err := chs.ClientHandshake(ctx)
if err == nil && context == nil {
errc <- errors.New("expected non-nil ALTS context")
return
}
errc <- err
chs.Close()
}()
}
// Ensure that there are no errors.
for i := 0; i < testCase.numberOfHandshakes; i++ {
if err := <-errc; err != nil {
t.Errorf("ClientHandshake() = _, %v, want _, <nil>", err)
}
}
// Ensure that there are no concurrent calls more than the limit.
if stat.MaxConcurrentCalls > int(envconfig.ALTSMaxConcurrentHandshakes) {
t.Errorf("Observed %d concurrent handshakes; want <= %d", stat.MaxConcurrentCalls, envconfig.ALTSMaxConcurrentHandshakes)
}
}
}
func (s) TestServerHandshake(t *testing.T) {
for _, testCase := range []struct {
delay time.Duration
numberOfHandshakes int
}{
{0 * time.Millisecond, 1},
{100 * time.Millisecond, 10 * int(envconfig.ALTSMaxConcurrentHandshakes)},
} {
errc := make(chan error)
stat.Reset()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
for i := 0; i < testCase.numberOfHandshakes; i++ {
stream := &testRPCStream{
t: t,
isClient: false,
}
// Preload the inbound frames.
f1 := testutil.MakeFrame("ClientInit")
f2 := testutil.MakeFrame("ClientFinished")
in := bytes.NewBuffer(f1)
in.Write(f2)
out := new(bytes.Buffer)
tc := testutil.NewTestConn(in, out)
shs := &altsHandshaker{
stream: stream,
conn: tc,
serverOpts: DefaultServerHandshakerOptions(),
side: core.ServerSide,
}
go func() {
_, context, err := shs.ServerHandshake(ctx)
if err == nil && context == nil {
errc <- errors.New("expected non-nil ALTS context")
return
}
errc <- err
shs.Close()
}()
}
// Ensure that there are no errors.
for i := 0; i < testCase.numberOfHandshakes; i++ {
if err := <-errc; err != nil {
t.Errorf("ServerHandshake() = _, %v, want _, <nil>", err)
}
}
// Ensure that there are no concurrent calls more than the limit.
if stat.MaxConcurrentCalls > int(envconfig.ALTSMaxConcurrentHandshakes) {
t.Errorf("Observed %d concurrent handshakes; want <= %d", stat.MaxConcurrentCalls, envconfig.ALTSMaxConcurrentHandshakes)
}
}
}
// testUnresponsiveRPCStream is used for testing the PeerNotResponding case.
type testUnresponsiveRPCStream struct {
grpc.ClientStream
}
func (t *testUnresponsiveRPCStream) Recv() (*altspb.HandshakerResp, error) {
return &altspb.HandshakerResp{}, nil
}
func (t *testUnresponsiveRPCStream) Send(*altspb.HandshakerReq) error {
return nil
}
func (t *testUnresponsiveRPCStream) CloseSend() error {
return nil
}
func (s) TestPeerNotResponding(t *testing.T) {
stream := &testUnresponsiveRPCStream{}
chs := &altsHandshaker{
stream: stream,
conn: testutil.NewUnresponsiveTestConn(),
clientOpts: &ClientHandshakerOptions{
TargetServiceAccounts: testTargetServiceAccounts,
ClientIdentity: testClientIdentity,
},
side: core.ClientSide,
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
_, context, err := chs.ClientHandshake(ctx)
chs.Close()
if context != nil {
t.Error("expected non-nil ALTS context")
}
if got, want := err, core.PeerNotRespondingError; got != want {
t.Errorf("ClientHandshake() = %v, want %v", got, want)
}
}
func (s) TestNewClientHandshaker(t *testing.T) {
conn := testutil.NewTestConn(nil, nil)
clientConn := &grpc.ClientConn{}
opts := &ClientHandshakerOptions{}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
hs, err := NewClientHandshaker(ctx, clientConn, conn, opts)
if err != nil {
t.Errorf("NewClientHandshaker returned unexpected error: %v", err)
}
expectedHs := &altsHandshaker{
stream: nil,
conn: conn,
clientConn: clientConn,
clientOpts: opts,
serverOpts: nil,
side: core.ClientSide,
}
cmpOpts := []cmp.Option{
cmp.AllowUnexported(altsHandshaker{}),
cmpopts.IgnoreFields(altsHandshaker{}, "conn", "clientConn"),
}
if got, want := hs.(*altsHandshaker), expectedHs; !cmp.Equal(got, want, cmpOpts...) {
t.Errorf("NewClientHandshaker() returned unexpected handshaker: got: %v, want: %v", got, want)
}
if hs.(*altsHandshaker).stream != nil {
t.Errorf("NewClientHandshaker() returned handshaker with non-nil stream")
}
if hs.(*altsHandshaker).clientConn != clientConn {
t.Errorf("NewClientHandshaker() returned handshaker with unexpected clientConn")
}
hs.Close()
}
func (s) TestNewServerHandshaker(t *testing.T) {
conn := testutil.NewTestConn(nil, nil)
clientConn := &grpc.ClientConn{}
opts := &ServerHandshakerOptions{}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
hs, err := NewServerHandshaker(ctx, clientConn, conn, opts)
if err != nil {
t.Errorf("NewServerHandshaker returned unexpected error: %v", err)
}
expectedHs := &altsHandshaker{
stream: nil,
conn: conn,
clientConn: clientConn,
clientOpts: nil,
serverOpts: opts,
side: core.ServerSide,
}
cmpOpts := []cmp.Option{
cmp.AllowUnexported(altsHandshaker{}),
cmpopts.IgnoreFields(altsHandshaker{}, "conn", "clientConn"),
}
if got, want := hs.(*altsHandshaker), expectedHs; !cmp.Equal(got, want, cmpOpts...) {
t.Errorf("NewServerHandshaker() returned unexpected handshaker: got: %v, want: %v", got, want)
}
if hs.(*altsHandshaker).stream != nil {
t.Errorf("NewServerHandshaker() returned handshaker with non-nil stream")
}
if hs.(*altsHandshaker).clientConn != clientConn {
t.Errorf("NewServerHandshaker() returned handshaker with unexpected clientConn")
}
hs.Close()
}
func (s) TestHandshakeMaxFrameSizeNegotiation(t *testing.T) {
old := envconfig.ALTSMaxFrameSize
envconfig.ALTSMaxFrameSize = 64 * 1024
defer func() { envconfig.ALTSMaxFrameSize = old }()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
stream := &testRPCStream{
t: t,
isClient: true,
peerMaxFrameSize: 1024 * 1024,
}
f1 := testutil.MakeFrame("ServerInit")
f2 := testutil.MakeFrame("ServerFinished")
in := bytes.NewBuffer(f1)
in.Write(f2)
out := new(bytes.Buffer)
tc := testutil.NewTestConn(in, out)
chs := &altsHandshaker{
stream: stream,
conn: tc,
clientOpts: &ClientHandshakerOptions{
TargetServiceAccounts: testTargetServiceAccounts,
ClientIdentity: testClientIdentity,
},
side: core.ClientSide,
}
defer chs.Close()
sc, _, err := chs.ClientHandshake(ctx)
if err != nil {
t.Fatalf("ClientHandshake() failed: %v", err)
}
if sc == nil {
t.Fatal("ClientHandshake() returned nil conn, want non-nil")
}
// Clear any handshake frames written to out during ClientHandshake.
out.Reset()
// Write a payload large enough to produce a full ALTS frame.
payload := make([]byte, 128*1024)
if _, err := sc.Write(payload); err != nil {
t.Fatalf("Secure conn Write() failed: %v", err)
}
// In ALTS wire format, the first 4 bytes of a frame contain the little-endian
// frame length (frameLen). Writing a large payload splits it into frames of
// the maximum calculated size. We verify the connection's max frame size by
// asserting that the total wire frame length (4 + frameLen) equals 64 * 1024.
if len(out.Bytes()) < 4 {
t.Fatalf("Output buffer too small: %d bytes", len(out.Bytes()))
}
frameLen := int(binary.LittleEndian.Uint32(out.Bytes()[:4]))
if got, want := 4+frameLen, 64*1024; got != want {
t.Errorf("Wire frame size = %v, want %v", got, want)
}
}

View File

@@ -0,0 +1,91 @@
/*
*
* Copyright 2018 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 service manages connections between the VM application and the ALTS
// handshaker service.
package service
import (
"sync"
"time"
grpc "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/keepalive"
)
var (
// mu guards hsConnMap and hsDialer.
mu sync.Mutex
// hsConn represents a mapping from a hypervisor handshaker service address
// to a corresponding connection to a hypervisor handshaker service
// instance.
hsConnMap = make(map[string]*grpc.ClientConn)
)
// Dial dials the handshake service in the hypervisor. If a connection has
// already been established, this function returns it. Otherwise, a new
// connection is created.
func Dial(hsAddress string) (*grpc.ClientConn, error) {
mu.Lock()
defer mu.Unlock()
hsConn, ok := hsConnMap[hsAddress]
if !ok {
// Create a new connection to the handshaker service. Note that
// this connection stays open until the application is closed.
// Disable the service config to avoid unnecessary TXT record lookups that
// cause timeouts with some versions of systemd-resolved.
var err error
opts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDisableServiceConfig(),
}
if envconfig.ALTSHandshakerKeepaliveParams {
opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
Timeout: 10 * time.Second,
Time: 10 * time.Minute,
}))
}
hsConn, err = grpc.NewClient(hsAddress, opts...)
if err != nil {
return nil, err
}
hsConnMap[hsAddress] = hsConn
}
return hsConn, nil
}
// CloseForTesting closes all open connections to the handshaker service.
//
// For testing purposes only.
func CloseForTesting() error {
for _, hsConn := range hsConnMap {
if hsConn == nil {
continue
}
if err := hsConn.Close(); err != nil {
return err
}
}
// Reset the connection map.
hsConnMap = make(map[string]*grpc.ClientConn)
return nil
}

View File

@@ -0,0 +1,80 @@
/*
*
* Copyright 2018 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 service
import (
"testing"
"google.golang.org/grpc/internal/grpctest"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
const (
testAddress1 = "some_address_1"
testAddress2 = "some_address_2"
)
// TestDial verifies the behaviour of alts handshake when there are multiple Dials.
// If a connection has already been established, this function returns it.
// Otherwise, a new connection is created.
func (s) TestDial(t *testing.T) {
// First call to Dial, it should create a connection to the server running
// at the given address.
conn1, err := Dial(testAddress1)
if err != nil {
t.Fatalf("first call to Dial(%v) failed: %v", testAddress1, err)
}
defer conn1.Close()
if got, want := hsConnMap[testAddress1], conn1; got != want {
t.Fatalf("hsConnMap[%v]=%v, want %v", testAddress1, got, want)
}
// Second call to Dial should return conn1 above.
conn2, err := Dial(testAddress1)
if err != nil {
t.Fatalf("second call to Dial(%v) failed: %v", testAddress1, err)
}
defer conn2.Close()
if got, want := conn2, conn1; got != want {
t.Fatalf("second call to Dial(%v)=(%v, _), want (%v,. _)", testAddress1, got, want)
}
if got, want := hsConnMap[testAddress1], conn1; got != want {
t.Fatalf("hsConnMap[%v]=%v, want %v", testAddress1, got, want)
}
// Third call to Dial using a different address should create a new connection.
conn3, err := Dial(testAddress2)
if err != nil {
t.Fatalf("third call to Dial(%v) failed: %v", testAddress2, err)
}
defer conn3.Close()
if got, want := hsConnMap[testAddress2], conn3; got != want {
t.Fatalf("hsConnMap[%v]=%v, want %v", testAddress2, got, want)
}
if got, want := conn2 == conn3, false; got != want {
t.Fatalf("(conn2==conn3)=%v, want %v", got, want)
}
}

View File

@@ -0,0 +1,211 @@
// Copyright 2018 The 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.
// The canonical version of this proto can be found at
// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/altscontext.proto
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.12
// protoc v5.27.1
// source: grpc/gcp/altscontext.proto
package grpc_gcp
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type AltsContext struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The application protocol negotiated for this connection.
ApplicationProtocol string `protobuf:"bytes,1,opt,name=application_protocol,json=applicationProtocol,proto3" json:"application_protocol,omitempty"`
// The record protocol negotiated for this connection.
RecordProtocol string `protobuf:"bytes,2,opt,name=record_protocol,json=recordProtocol,proto3" json:"record_protocol,omitempty"`
// The security level of the created secure channel.
SecurityLevel SecurityLevel `protobuf:"varint,3,opt,name=security_level,json=securityLevel,proto3,enum=grpc.gcp.SecurityLevel" json:"security_level,omitempty"`
// The peer service account.
PeerServiceAccount string `protobuf:"bytes,4,opt,name=peer_service_account,json=peerServiceAccount,proto3" json:"peer_service_account,omitempty"`
// The local service account.
LocalServiceAccount string `protobuf:"bytes,5,opt,name=local_service_account,json=localServiceAccount,proto3" json:"local_service_account,omitempty"`
// The RPC protocol versions supported by the peer.
PeerRpcVersions *RpcProtocolVersions `protobuf:"bytes,6,opt,name=peer_rpc_versions,json=peerRpcVersions,proto3" json:"peer_rpc_versions,omitempty"`
// Additional attributes of the peer.
PeerAttributes map[string]string `protobuf:"bytes,7,rep,name=peer_attributes,json=peerAttributes,proto3" json:"peer_attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AltsContext) Reset() {
*x = AltsContext{}
mi := &file_grpc_gcp_altscontext_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AltsContext) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AltsContext) ProtoMessage() {}
func (x *AltsContext) ProtoReflect() protoreflect.Message {
mi := &file_grpc_gcp_altscontext_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AltsContext.ProtoReflect.Descriptor instead.
func (*AltsContext) Descriptor() ([]byte, []int) {
return file_grpc_gcp_altscontext_proto_rawDescGZIP(), []int{0}
}
func (x *AltsContext) GetApplicationProtocol() string {
if x != nil {
return x.ApplicationProtocol
}
return ""
}
func (x *AltsContext) GetRecordProtocol() string {
if x != nil {
return x.RecordProtocol
}
return ""
}
func (x *AltsContext) GetSecurityLevel() SecurityLevel {
if x != nil {
return x.SecurityLevel
}
return SecurityLevel_SECURITY_NONE
}
func (x *AltsContext) GetPeerServiceAccount() string {
if x != nil {
return x.PeerServiceAccount
}
return ""
}
func (x *AltsContext) GetLocalServiceAccount() string {
if x != nil {
return x.LocalServiceAccount
}
return ""
}
func (x *AltsContext) GetPeerRpcVersions() *RpcProtocolVersions {
if x != nil {
return x.PeerRpcVersions
}
return nil
}
func (x *AltsContext) GetPeerAttributes() map[string]string {
if x != nil {
return x.PeerAttributes
}
return nil
}
var File_grpc_gcp_altscontext_proto protoreflect.FileDescriptor
const file_grpc_gcp_altscontext_proto_rawDesc = "" +
"\n" +
"\x1agrpc/gcp/altscontext.proto\x12\bgrpc.gcp\x1a(grpc/gcp/transport_security_common.proto\"\xf1\x03\n" +
"\vAltsContext\x121\n" +
"\x14application_protocol\x18\x01 \x01(\tR\x13applicationProtocol\x12'\n" +
"\x0frecord_protocol\x18\x02 \x01(\tR\x0erecordProtocol\x12>\n" +
"\x0esecurity_level\x18\x03 \x01(\x0e2\x17.grpc.gcp.SecurityLevelR\rsecurityLevel\x120\n" +
"\x14peer_service_account\x18\x04 \x01(\tR\x12peerServiceAccount\x122\n" +
"\x15local_service_account\x18\x05 \x01(\tR\x13localServiceAccount\x12I\n" +
"\x11peer_rpc_versions\x18\x06 \x01(\v2\x1d.grpc.gcp.RpcProtocolVersionsR\x0fpeerRpcVersions\x12R\n" +
"\x0fpeer_attributes\x18\a \x03(\v2).grpc.gcp.AltsContext.PeerAttributesEntryR\x0epeerAttributes\x1aA\n" +
"\x13PeerAttributesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01Bl\n" +
"\x15io.grpc.alts.internalB\x10AltsContextProtoP\x01Z?google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcpb\x06proto3"
var (
file_grpc_gcp_altscontext_proto_rawDescOnce sync.Once
file_grpc_gcp_altscontext_proto_rawDescData []byte
)
func file_grpc_gcp_altscontext_proto_rawDescGZIP() []byte {
file_grpc_gcp_altscontext_proto_rawDescOnce.Do(func() {
file_grpc_gcp_altscontext_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_grpc_gcp_altscontext_proto_rawDesc), len(file_grpc_gcp_altscontext_proto_rawDesc)))
})
return file_grpc_gcp_altscontext_proto_rawDescData
}
var file_grpc_gcp_altscontext_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_grpc_gcp_altscontext_proto_goTypes = []any{
(*AltsContext)(nil), // 0: grpc.gcp.AltsContext
nil, // 1: grpc.gcp.AltsContext.PeerAttributesEntry
(SecurityLevel)(0), // 2: grpc.gcp.SecurityLevel
(*RpcProtocolVersions)(nil), // 3: grpc.gcp.RpcProtocolVersions
}
var file_grpc_gcp_altscontext_proto_depIdxs = []int32{
2, // 0: grpc.gcp.AltsContext.security_level:type_name -> grpc.gcp.SecurityLevel
3, // 1: grpc.gcp.AltsContext.peer_rpc_versions:type_name -> grpc.gcp.RpcProtocolVersions
1, // 2: grpc.gcp.AltsContext.peer_attributes:type_name -> grpc.gcp.AltsContext.PeerAttributesEntry
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_grpc_gcp_altscontext_proto_init() }
func file_grpc_gcp_altscontext_proto_init() {
if File_grpc_gcp_altscontext_proto != nil {
return
}
file_grpc_gcp_transport_security_common_proto_init()
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_grpc_gcp_altscontext_proto_rawDesc), len(file_grpc_gcp_altscontext_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_grpc_gcp_altscontext_proto_goTypes,
DependencyIndexes: file_grpc_gcp_altscontext_proto_depIdxs,
MessageInfos: file_grpc_gcp_altscontext_proto_msgTypes,
}.Build()
File_grpc_gcp_altscontext_proto = out.File
file_grpc_gcp_altscontext_proto_goTypes = nil
file_grpc_gcp_altscontext_proto_depIdxs = nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
// Copyright 2018 The 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.
// The canonical version of this proto can be found at
// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/handshaker.proto
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc v5.27.1
// source: grpc/gcp/handshaker.proto
package grpc_gcp
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
HandshakerService_DoHandshake_FullMethodName = "/grpc.gcp.HandshakerService/DoHandshake"
)
// HandshakerServiceClient is the client API for HandshakerService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HandshakerServiceClient interface {
// Handshaker service accepts a stream of handshaker request, returning a
// stream of handshaker response. Client is expected to send exactly one
// message with either client_start or server_start followed by one or more
// messages with next. Each time client sends a request, the handshaker
// service expects to respond. Client does not have to wait for service's
// response before sending next request.
DoHandshake(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[HandshakerReq, HandshakerResp], error)
}
type handshakerServiceClient struct {
cc grpc.ClientConnInterface
}
func NewHandshakerServiceClient(cc grpc.ClientConnInterface) HandshakerServiceClient {
return &handshakerServiceClient{cc}
}
func (c *handshakerServiceClient) DoHandshake(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[HandshakerReq, HandshakerResp], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &HandshakerService_ServiceDesc.Streams[0], HandshakerService_DoHandshake_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[HandshakerReq, HandshakerResp]{ClientStream: stream}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type HandshakerService_DoHandshakeClient = grpc.BidiStreamingClient[HandshakerReq, HandshakerResp]
// HandshakerServiceServer is the server API for HandshakerService service.
// All implementations must embed UnimplementedHandshakerServiceServer
// for forward compatibility.
type HandshakerServiceServer interface {
// Handshaker service accepts a stream of handshaker request, returning a
// stream of handshaker response. Client is expected to send exactly one
// message with either client_start or server_start followed by one or more
// messages with next. Each time client sends a request, the handshaker
// service expects to respond. Client does not have to wait for service's
// response before sending next request.
DoHandshake(grpc.BidiStreamingServer[HandshakerReq, HandshakerResp]) error
mustEmbedUnimplementedHandshakerServiceServer()
}
// UnimplementedHandshakerServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedHandshakerServiceServer struct{}
func (UnimplementedHandshakerServiceServer) DoHandshake(grpc.BidiStreamingServer[HandshakerReq, HandshakerResp]) error {
return status.Error(codes.Unimplemented, "method DoHandshake not implemented")
}
func (UnimplementedHandshakerServiceServer) mustEmbedUnimplementedHandshakerServiceServer() {}
func (UnimplementedHandshakerServiceServer) testEmbeddedByValue() {}
// UnsafeHandshakerServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to HandshakerServiceServer will
// result in compilation errors.
type UnsafeHandshakerServiceServer interface {
mustEmbedUnimplementedHandshakerServiceServer()
}
func RegisterHandshakerServiceServer(s grpc.ServiceRegistrar, srv HandshakerServiceServer) {
// If the following call panics, it indicates UnimplementedHandshakerServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&HandshakerService_ServiceDesc, srv)
}
func _HandshakerService_DoHandshake_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(HandshakerServiceServer).DoHandshake(&grpc.GenericServerStream[HandshakerReq, HandshakerResp]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type HandshakerService_DoHandshakeServer = grpc.BidiStreamingServer[HandshakerReq, HandshakerResp]
// HandshakerService_ServiceDesc is the grpc.ServiceDesc for HandshakerService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var HandshakerService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "grpc.gcp.HandshakerService",
HandlerType: (*HandshakerServiceServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "DoHandshake",
Handler: _HandshakerService_DoHandshake_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "grpc/gcp/handshaker.proto",
}

View File

@@ -0,0 +1,366 @@
// Copyright 2018 The 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.
// The canonical version of this proto can be found at
// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/transport_security_common.proto
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.12
// protoc v5.27.1
// source: grpc/gcp/transport_security_common.proto
package grpc_gcp
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// The security level of the created channel. The list is sorted in increasing
// level of security. This order must always be maintained.
type SecurityLevel int32
const (
SecurityLevel_SECURITY_NONE SecurityLevel = 0
SecurityLevel_INTEGRITY_ONLY SecurityLevel = 1
SecurityLevel_INTEGRITY_AND_PRIVACY SecurityLevel = 2
)
// Enum value maps for SecurityLevel.
var (
SecurityLevel_name = map[int32]string{
0: "SECURITY_NONE",
1: "INTEGRITY_ONLY",
2: "INTEGRITY_AND_PRIVACY",
}
SecurityLevel_value = map[string]int32{
"SECURITY_NONE": 0,
"INTEGRITY_ONLY": 1,
"INTEGRITY_AND_PRIVACY": 2,
}
)
func (x SecurityLevel) Enum() *SecurityLevel {
p := new(SecurityLevel)
*p = x
return p
}
func (x SecurityLevel) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (SecurityLevel) Descriptor() protoreflect.EnumDescriptor {
return file_grpc_gcp_transport_security_common_proto_enumTypes[0].Descriptor()
}
func (SecurityLevel) Type() protoreflect.EnumType {
return &file_grpc_gcp_transport_security_common_proto_enumTypes[0]
}
func (x SecurityLevel) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use SecurityLevel.Descriptor instead.
func (SecurityLevel) EnumDescriptor() ([]byte, []int) {
return file_grpc_gcp_transport_security_common_proto_rawDescGZIP(), []int{0}
}
// Max and min supported RPC protocol versions.
type RpcProtocolVersions struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Maximum supported RPC version.
MaxRpcVersion *RpcProtocolVersions_Version `protobuf:"bytes,1,opt,name=max_rpc_version,json=maxRpcVersion,proto3" json:"max_rpc_version,omitempty"`
// Minimum supported RPC version.
MinRpcVersion *RpcProtocolVersions_Version `protobuf:"bytes,2,opt,name=min_rpc_version,json=minRpcVersion,proto3" json:"min_rpc_version,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RpcProtocolVersions) Reset() {
*x = RpcProtocolVersions{}
mi := &file_grpc_gcp_transport_security_common_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RpcProtocolVersions) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RpcProtocolVersions) ProtoMessage() {}
func (x *RpcProtocolVersions) ProtoReflect() protoreflect.Message {
mi := &file_grpc_gcp_transport_security_common_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RpcProtocolVersions.ProtoReflect.Descriptor instead.
func (*RpcProtocolVersions) Descriptor() ([]byte, []int) {
return file_grpc_gcp_transport_security_common_proto_rawDescGZIP(), []int{0}
}
func (x *RpcProtocolVersions) GetMaxRpcVersion() *RpcProtocolVersions_Version {
if x != nil {
return x.MaxRpcVersion
}
return nil
}
func (x *RpcProtocolVersions) GetMinRpcVersion() *RpcProtocolVersions_Version {
if x != nil {
return x.MinRpcVersion
}
return nil
}
// The ordered list of protocols that the client wishes to use, or the set
// that the server supports.
type TransportProtocolPreferences struct {
state protoimpl.MessageState `protogen:"open.v1"`
TransportProtocol []string `protobuf:"bytes,1,rep,name=transport_protocol,json=transportProtocol,proto3" json:"transport_protocol,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *TransportProtocolPreferences) Reset() {
*x = TransportProtocolPreferences{}
mi := &file_grpc_gcp_transport_security_common_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *TransportProtocolPreferences) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TransportProtocolPreferences) ProtoMessage() {}
func (x *TransportProtocolPreferences) ProtoReflect() protoreflect.Message {
mi := &file_grpc_gcp_transport_security_common_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TransportProtocolPreferences.ProtoReflect.Descriptor instead.
func (*TransportProtocolPreferences) Descriptor() ([]byte, []int) {
return file_grpc_gcp_transport_security_common_proto_rawDescGZIP(), []int{1}
}
func (x *TransportProtocolPreferences) GetTransportProtocol() []string {
if x != nil {
return x.TransportProtocol
}
return nil
}
// The negotiated transport protocol.
type NegotiatedTransportProtocol struct {
state protoimpl.MessageState `protogen:"open.v1"`
TransportProtocol string `protobuf:"bytes,1,opt,name=transport_protocol,json=transportProtocol,proto3" json:"transport_protocol,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NegotiatedTransportProtocol) Reset() {
*x = NegotiatedTransportProtocol{}
mi := &file_grpc_gcp_transport_security_common_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *NegotiatedTransportProtocol) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NegotiatedTransportProtocol) ProtoMessage() {}
func (x *NegotiatedTransportProtocol) ProtoReflect() protoreflect.Message {
mi := &file_grpc_gcp_transport_security_common_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NegotiatedTransportProtocol.ProtoReflect.Descriptor instead.
func (*NegotiatedTransportProtocol) Descriptor() ([]byte, []int) {
return file_grpc_gcp_transport_security_common_proto_rawDescGZIP(), []int{2}
}
func (x *NegotiatedTransportProtocol) GetTransportProtocol() string {
if x != nil {
return x.TransportProtocol
}
return ""
}
// RPC version contains a major version and a minor version.
type RpcProtocolVersions_Version struct {
state protoimpl.MessageState `protogen:"open.v1"`
Major uint32 `protobuf:"varint,1,opt,name=major,proto3" json:"major,omitempty"`
Minor uint32 `protobuf:"varint,2,opt,name=minor,proto3" json:"minor,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RpcProtocolVersions_Version) Reset() {
*x = RpcProtocolVersions_Version{}
mi := &file_grpc_gcp_transport_security_common_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RpcProtocolVersions_Version) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RpcProtocolVersions_Version) ProtoMessage() {}
func (x *RpcProtocolVersions_Version) ProtoReflect() protoreflect.Message {
mi := &file_grpc_gcp_transport_security_common_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RpcProtocolVersions_Version.ProtoReflect.Descriptor instead.
func (*RpcProtocolVersions_Version) Descriptor() ([]byte, []int) {
return file_grpc_gcp_transport_security_common_proto_rawDescGZIP(), []int{0, 0}
}
func (x *RpcProtocolVersions_Version) GetMajor() uint32 {
if x != nil {
return x.Major
}
return 0
}
func (x *RpcProtocolVersions_Version) GetMinor() uint32 {
if x != nil {
return x.Minor
}
return 0
}
var File_grpc_gcp_transport_security_common_proto protoreflect.FileDescriptor
const file_grpc_gcp_transport_security_common_proto_rawDesc = "" +
"\n" +
"(grpc/gcp/transport_security_common.proto\x12\bgrpc.gcp\"\xea\x01\n" +
"\x13RpcProtocolVersions\x12M\n" +
"\x0fmax_rpc_version\x18\x01 \x01(\v2%.grpc.gcp.RpcProtocolVersions.VersionR\rmaxRpcVersion\x12M\n" +
"\x0fmin_rpc_version\x18\x02 \x01(\v2%.grpc.gcp.RpcProtocolVersions.VersionR\rminRpcVersion\x1a5\n" +
"\aVersion\x12\x14\n" +
"\x05major\x18\x01 \x01(\rR\x05major\x12\x14\n" +
"\x05minor\x18\x02 \x01(\rR\x05minor\"M\n" +
"\x1cTransportProtocolPreferences\x12-\n" +
"\x12transport_protocol\x18\x01 \x03(\tR\x11transportProtocol\"L\n" +
"\x1bNegotiatedTransportProtocol\x12-\n" +
"\x12transport_protocol\x18\x01 \x01(\tR\x11transportProtocol*Q\n" +
"\rSecurityLevel\x12\x11\n" +
"\rSECURITY_NONE\x10\x00\x12\x12\n" +
"\x0eINTEGRITY_ONLY\x10\x01\x12\x19\n" +
"\x15INTEGRITY_AND_PRIVACY\x10\x02Bx\n" +
"\x15io.grpc.alts.internalB\x1cTransportSecurityCommonProtoP\x01Z?google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcpb\x06proto3"
var (
file_grpc_gcp_transport_security_common_proto_rawDescOnce sync.Once
file_grpc_gcp_transport_security_common_proto_rawDescData []byte
)
func file_grpc_gcp_transport_security_common_proto_rawDescGZIP() []byte {
file_grpc_gcp_transport_security_common_proto_rawDescOnce.Do(func() {
file_grpc_gcp_transport_security_common_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_grpc_gcp_transport_security_common_proto_rawDesc), len(file_grpc_gcp_transport_security_common_proto_rawDesc)))
})
return file_grpc_gcp_transport_security_common_proto_rawDescData
}
var file_grpc_gcp_transport_security_common_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_grpc_gcp_transport_security_common_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_grpc_gcp_transport_security_common_proto_goTypes = []any{
(SecurityLevel)(0), // 0: grpc.gcp.SecurityLevel
(*RpcProtocolVersions)(nil), // 1: grpc.gcp.RpcProtocolVersions
(*TransportProtocolPreferences)(nil), // 2: grpc.gcp.TransportProtocolPreferences
(*NegotiatedTransportProtocol)(nil), // 3: grpc.gcp.NegotiatedTransportProtocol
(*RpcProtocolVersions_Version)(nil), // 4: grpc.gcp.RpcProtocolVersions.Version
}
var file_grpc_gcp_transport_security_common_proto_depIdxs = []int32{
4, // 0: grpc.gcp.RpcProtocolVersions.max_rpc_version:type_name -> grpc.gcp.RpcProtocolVersions.Version
4, // 1: grpc.gcp.RpcProtocolVersions.min_rpc_version:type_name -> grpc.gcp.RpcProtocolVersions.Version
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_grpc_gcp_transport_security_common_proto_init() }
func file_grpc_gcp_transport_security_common_proto_init() {
if File_grpc_gcp_transport_security_common_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_grpc_gcp_transport_security_common_proto_rawDesc), len(file_grpc_gcp_transport_security_common_proto_rawDesc)),
NumEnums: 1,
NumMessages: 4,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_grpc_gcp_transport_security_common_proto_goTypes,
DependencyIndexes: file_grpc_gcp_transport_security_common_proto_depIdxs,
EnumInfos: file_grpc_gcp_transport_security_common_proto_enumTypes,
MessageInfos: file_grpc_gcp_transport_security_common_proto_msgTypes,
}.Build()
File_grpc_gcp_transport_security_common_proto = out.File
file_grpc_gcp_transport_security_common_proto_goTypes = nil
file_grpc_gcp_transport_security_common_proto_depIdxs = nil
}

View File

@@ -0,0 +1,323 @@
/*
*
* Copyright 2018 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 testutil include useful test utilities for the handshaker.
package testutil
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
"sync"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/alts/internal/conn"
altsgrpc "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"
altspb "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"
)
// Stats is used to collect statistics about concurrent handshake calls.
type Stats struct {
mu sync.Mutex
calls int
MaxConcurrentCalls int
}
// Update updates the statistics by adding one call.
func (s *Stats) Update() func() {
s.mu.Lock()
s.calls++
if s.calls > s.MaxConcurrentCalls {
s.MaxConcurrentCalls = s.calls
}
s.mu.Unlock()
return func() {
s.mu.Lock()
s.calls--
s.mu.Unlock()
}
}
// Reset resets the statistics.
func (s *Stats) Reset() {
s.mu.Lock()
defer s.mu.Unlock()
s.calls = 0
s.MaxConcurrentCalls = 0
}
// testConn mimics a net.Conn to the peer.
type testConn struct {
net.Conn
in *bytes.Buffer
out *bytes.Buffer
readLatency time.Duration
}
// NewTestConn creates a new instance of testConn object.
func NewTestConn(in *bytes.Buffer, out *bytes.Buffer) net.Conn {
return &testConn{
in: in,
out: out,
readLatency: time.Duration(0),
}
}
// NewTestConnWithReadLatency creates a new instance of testConn object that
// pauses for readLatency before any call to Read() returns.
func NewTestConnWithReadLatency(in *bytes.Buffer, out *bytes.Buffer, readLatency time.Duration) net.Conn {
return &testConn{
in: in,
out: out,
readLatency: readLatency,
}
}
// Read reads from the in buffer.
func (c *testConn) Read(b []byte) (n int, err error) {
time.Sleep(c.readLatency)
return c.in.Read(b)
}
// Write writes to the out buffer.
func (c *testConn) Write(b []byte) (n int, err error) {
return c.out.Write(b)
}
// Close closes the testConn object.
func (c *testConn) Close() error {
return nil
}
// unresponsiveTestConn mimics a net.Conn for an unresponsive peer. It is used
// for testing the PeerNotResponding case.
type unresponsiveTestConn struct {
net.Conn
}
// NewUnresponsiveTestConn creates a new instance of unresponsiveTestConn object.
func NewUnresponsiveTestConn() net.Conn {
return &unresponsiveTestConn{}
}
// Read reads from the in buffer.
func (c *unresponsiveTestConn) Read([]byte) (n int, err error) {
return 0, io.EOF
}
// Write writes to the out buffer.
func (c *unresponsiveTestConn) Write([]byte) (n int, err error) {
return 0, nil
}
// Close closes the TestConn object.
func (c *unresponsiveTestConn) Close() error {
return nil
}
// MakeFrame creates a handshake frame.
func MakeFrame(pl string) []byte {
f := make([]byte, len(pl)+conn.MsgLenFieldSize)
binary.LittleEndian.PutUint32(f, uint32(len(pl)))
copy(f[conn.MsgLenFieldSize:], []byte(pl))
return f
}
// FakeHandshaker is a fake implementation of the ALTS handshaker service.
type FakeHandshaker struct {
altsgrpc.HandshakerServiceServer
// ExpectedBoundAccessToken is the expected bound access token in the ClientStart request.
ExpectedBoundAccessToken string
}
// DoHandshake performs a fake ALTS handshake.
func (h *FakeHandshaker) DoHandshake(stream altsgrpc.HandshakerService_DoHandshakeServer) error {
var isAssistingClient bool
var handshakeFramesReceivedSoFar []byte
for {
req, err := stream.Recv()
if err != nil {
if err == io.EOF {
return nil
}
return fmt.Errorf("stream recv failure: %v", err)
}
var resp *altspb.HandshakerResp
switch req := req.ReqOneof.(type) {
case *altspb.HandshakerReq_ClientStart:
isAssistingClient = true
resp, err = h.processStartClient(req.ClientStart)
if err != nil {
return fmt.Errorf("processStartClient failure: %v", err)
}
case *altspb.HandshakerReq_ServerStart:
// If we have received the full ClientInit, send the ServerInit and
// ServerFinished. Otherwise, wait for more bytes to arrive from the client.
isAssistingClient = false
handshakeFramesReceivedSoFar = append(handshakeFramesReceivedSoFar, req.ServerStart.InBytes...)
sendHandshakeFrame := bytes.Equal(handshakeFramesReceivedSoFar, []byte("ClientInit"))
resp, err = h.processServerStart(req.ServerStart, sendHandshakeFrame)
if err != nil {
return fmt.Errorf("processServerStart failure: %v", err)
}
case *altspb.HandshakerReq_Next:
// If we have received all handshake frames, send the handshake result.
// Otherwise, wait for more bytes to arrive from the peer.
oldHandshakesBytes := len(handshakeFramesReceivedSoFar)
handshakeFramesReceivedSoFar = append(handshakeFramesReceivedSoFar, req.Next.InBytes...)
isHandshakeComplete := false
if isAssistingClient {
isHandshakeComplete = bytes.HasPrefix(handshakeFramesReceivedSoFar, []byte("ServerInitServerFinished"))
} else {
isHandshakeComplete = bytes.HasPrefix(handshakeFramesReceivedSoFar, []byte("ClientInitClientFinished"))
}
if !isHandshakeComplete {
resp = &altspb.HandshakerResp{
BytesConsumed: uint32(len(handshakeFramesReceivedSoFar) - oldHandshakesBytes),
Status: &altspb.HandshakerStatus{
Code: uint32(codes.OK),
},
}
break
}
resp, err = h.getHandshakeResult(isAssistingClient)
if err != nil {
return fmt.Errorf("getHandshakeResult failure: %v", err)
}
default:
return fmt.Errorf("handshake request has unexpected type: %v", req)
}
if err = stream.Send(resp); err != nil {
return fmt.Errorf("stream send failure: %v", err)
}
}
}
func (h *FakeHandshaker) processStartClient(req *altspb.StartClientHandshakeReq) (*altspb.HandshakerResp, error) {
if req.HandshakeSecurityProtocol != altspb.HandshakeProtocol_ALTS {
return nil, fmt.Errorf("unexpected handshake security protocol: %v", req.HandshakeSecurityProtocol)
}
if len(req.ApplicationProtocols) != 1 || req.ApplicationProtocols[0] != "grpc" {
return nil, fmt.Errorf("unexpected application protocols: %v", req.ApplicationProtocols)
}
if len(req.RecordProtocols) != 1 || req.RecordProtocols[0] != "ALTSRP_GCM_AES128_REKEY" {
return nil, fmt.Errorf("unexpected record protocols: %v", req.RecordProtocols)
}
if h.ExpectedBoundAccessToken != req.GetAccessToken() {
return nil, fmt.Errorf("unexpected access token: %v", req.GetAccessToken())
}
return &altspb.HandshakerResp{
OutFrames: []byte("ClientInit"),
BytesConsumed: 0,
Status: &altspb.HandshakerStatus{
Code: uint32(codes.OK),
},
}, nil
}
func (h *FakeHandshaker) processServerStart(req *altspb.StartServerHandshakeReq, sendHandshakeFrame bool) (*altspb.HandshakerResp, error) {
if len(req.ApplicationProtocols) != 1 || req.ApplicationProtocols[0] != "grpc" {
return nil, fmt.Errorf("unexpected application protocols: %v", req.ApplicationProtocols)
}
parameters, ok := req.GetHandshakeParameters()[int32(altspb.HandshakeProtocol_ALTS)]
if !ok {
return nil, fmt.Errorf("missing ALTS handshake parameters")
}
if len(parameters.RecordProtocols) != 1 || parameters.RecordProtocols[0] != "ALTSRP_GCM_AES128_REKEY" {
return nil, fmt.Errorf("unexpected record protocols: %v", parameters.RecordProtocols)
}
if sendHandshakeFrame {
return &altspb.HandshakerResp{
OutFrames: []byte("ServerInitServerFinished"),
BytesConsumed: uint32(len(req.InBytes)),
Status: &altspb.HandshakerStatus{
Code: uint32(codes.OK),
},
}, nil
}
return &altspb.HandshakerResp{
OutFrames: []byte("ServerInitServerFinished"),
BytesConsumed: 10,
Status: &altspb.HandshakerStatus{
Code: uint32(codes.OK),
},
}, nil
}
func (h *FakeHandshaker) getHandshakeResult(isAssistingClient bool) (*altspb.HandshakerResp, error) {
if isAssistingClient {
return &altspb.HandshakerResp{
OutFrames: []byte("ClientFinished"),
BytesConsumed: 24,
Result: &altspb.HandshakerResult{
ApplicationProtocol: "grpc",
RecordProtocol: "ALTSRP_GCM_AES128_REKEY",
KeyData: []byte("negotiated-key-data-for-altsrp-gcm-aes128-rekey"),
PeerIdentity: &altspb.Identity{
IdentityOneof: &altspb.Identity_ServiceAccount{
ServiceAccount: "server@bar.com",
},
},
PeerRpcVersions: &altspb.RpcProtocolVersions{
MaxRpcVersion: &altspb.RpcProtocolVersions_Version{
Minor: 1,
Major: 2,
},
MinRpcVersion: &altspb.RpcProtocolVersions_Version{
Minor: 1,
Major: 2,
},
},
},
Status: &altspb.HandshakerStatus{
Code: uint32(codes.OK),
},
}, nil
}
return &altspb.HandshakerResp{
BytesConsumed: 14,
Result: &altspb.HandshakerResult{
ApplicationProtocol: "grpc",
RecordProtocol: "ALTSRP_GCM_AES128_REKEY",
KeyData: []byte("negotiated-key-data-for-altsrp-gcm-aes128-rekey"),
PeerIdentity: &altspb.Identity{
IdentityOneof: &altspb.Identity_ServiceAccount{
ServiceAccount: "client@baz.com",
},
},
PeerRpcVersions: &altspb.RpcProtocolVersions{
MaxRpcVersion: &altspb.RpcProtocolVersions_Version{
Minor: 1,
Major: 2,
},
MinRpcVersion: &altspb.RpcProtocolVersions_Version{
Minor: 1,
Major: 2,
},
},
},
Status: &altspb.HandshakerStatus{
Code: uint32(codes.OK),
},
}, nil
}

70
credentials/alts/utils.go Normal file
View File

@@ -0,0 +1,70 @@
/*
*
* Copyright 2018 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 alts
import (
"context"
"errors"
"strings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)
// AuthInfoFromContext extracts the alts.AuthInfo object from the given context,
// if it exists. This API should be used by gRPC server RPC handlers to get
// information about the communicating peer. For client-side, use grpc.Peer()
// CallOption.
func AuthInfoFromContext(ctx context.Context) (AuthInfo, error) {
p, ok := peer.FromContext(ctx)
if !ok {
return nil, errors.New("no Peer found in Context")
}
return AuthInfoFromPeer(p)
}
// AuthInfoFromPeer extracts the alts.AuthInfo object from the given peer, if it
// exists. This API should be used by gRPC clients after obtaining a peer object
// using the grpc.Peer() CallOption.
func AuthInfoFromPeer(p *peer.Peer) (AuthInfo, error) {
altsAuthInfo, ok := p.AuthInfo.(AuthInfo)
if !ok {
return nil, errors.New("no alts.AuthInfo found in Peer")
}
return altsAuthInfo, nil
}
// ClientAuthorizationCheck checks whether the client is authorized to access
// the requested resources based on the given expected client service accounts.
// This API should be used by gRPC server RPC handlers. This API should not be
// used by clients.
func ClientAuthorizationCheck(ctx context.Context, expectedServiceAccounts []string) error {
authInfo, err := AuthInfoFromContext(ctx)
if err != nil {
return status.Errorf(codes.PermissionDenied, "The context is not an ALTS-compatible context: %v", err)
}
peer := authInfo.PeerServiceAccount()
for _, sa := range expectedServiceAccounts {
if strings.EqualFold(peer, sa) {
return nil
}
}
return status.Errorf(codes.PermissionDenied, "Client %v is not authorized", peer)
}

View File

@@ -0,0 +1,167 @@
//go:build linux || windows
// +build linux windows
/*
*
* Copyright 2018 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 alts
import (
"context"
"strings"
"testing"
"time"
"google.golang.org/grpc/codes"
altspb "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)
const (
testServiceAccount1 = "service_account1"
testServiceAccount2 = "service_account2"
testServiceAccount3 = "service_account3"
defaultTestTimeout = 10 * time.Second
)
func (s) TestAuthInfoFromContext(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
altsAuthInfo := &fakeALTSAuthInfo{}
p := &peer.Peer{
AuthInfo: altsAuthInfo,
}
for _, tc := range []struct {
desc string
ctx context.Context
success bool
out AuthInfo
}{
{
"working case",
peer.NewContext(ctx, p),
true,
altsAuthInfo,
},
} {
authInfo, err := AuthInfoFromContext(tc.ctx)
if got, want := (err == nil), tc.success; got != want {
t.Errorf("%v: AuthInfoFromContext(_)=(err=nil)=%v, want %v", tc.desc, got, want)
}
if got, want := authInfo, tc.out; got != want {
t.Errorf("%v:, AuthInfoFromContext(_)=(%v, _), want (%v, _)", tc.desc, got, want)
}
}
}
func (s) TestAuthInfoFromPeer(t *testing.T) {
altsAuthInfo := &fakeALTSAuthInfo{}
p := &peer.Peer{
AuthInfo: altsAuthInfo,
}
for _, tc := range []struct {
desc string
p *peer.Peer
success bool
out AuthInfo
}{
{
"working case",
p,
true,
altsAuthInfo,
},
} {
authInfo, err := AuthInfoFromPeer(tc.p)
if got, want := (err == nil), tc.success; got != want {
t.Errorf("%v: AuthInfoFromPeer(_)=(err=nil)=%v, want %v", tc.desc, got, want)
}
if got, want := authInfo, tc.out; got != want {
t.Errorf("%v:, AuthInfoFromPeer(_)=(%v, _), want (%v, _)", tc.desc, got, want)
}
}
}
func (s) TestClientAuthorizationCheck(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
altsAuthInfo := &fakeALTSAuthInfo{testServiceAccount1}
p := &peer.Peer{
AuthInfo: altsAuthInfo,
}
for _, tc := range []struct {
desc string
ctx context.Context
expectedServiceAccounts []string
success bool
code codes.Code
}{
{
"working case",
peer.NewContext(ctx, p),
[]string{testServiceAccount1, testServiceAccount2},
true,
codes.OK, // err is nil, code is OK.
},
{
"working case (case ignored)",
peer.NewContext(ctx, p),
[]string{strings.ToUpper(testServiceAccount1), testServiceAccount2},
true,
codes.OK, // err is nil, code is OK.
},
{
"context does not have AuthInfo",
ctx,
[]string{testServiceAccount1, testServiceAccount2},
false,
codes.PermissionDenied,
},
{
"unauthorized client",
peer.NewContext(ctx, p),
[]string{testServiceAccount2, testServiceAccount3},
false,
codes.PermissionDenied,
},
} {
err := ClientAuthorizationCheck(tc.ctx, tc.expectedServiceAccounts)
if got, want := (err == nil), tc.success; got != want {
t.Errorf("%v: ClientAuthorizationCheck(_, %v)=(err=nil)=%v, want %v", tc.desc, tc.expectedServiceAccounts, got, want)
}
if got, want := status.Code(err), tc.code; got != want {
t.Errorf("%v: ClientAuthorizationCheck(_, %v).Code=%v, want %v", tc.desc, tc.expectedServiceAccounts, got, want)
}
}
}
type fakeALTSAuthInfo struct {
peerServiceAccount string
}
func (*fakeALTSAuthInfo) AuthType() string { return "" }
func (*fakeALTSAuthInfo) ApplicationProtocol() string { return "" }
func (*fakeALTSAuthInfo) RecordProtocol() string { return "" }
func (*fakeALTSAuthInfo) SecurityLevel() altspb.SecurityLevel {
return altspb.SecurityLevel_SECURITY_NONE
}
func (f *fakeALTSAuthInfo) PeerServiceAccount() string { return f.peerServiceAccount }
func (*fakeALTSAuthInfo) LocalServiceAccount() string { return "" }
func (*fakeALTSAuthInfo) PeerRPCVersions() *altspb.RpcProtocolVersions { return nil }

337
credentials/credentials.go Normal file
View File

@@ -0,0 +1,337 @@
/*
*
* Copyright 2014 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 credentials implements various credentials supported by gRPC library,
// which encapsulate all the state needed by a client to authenticate with a
// server and make various assertions, e.g., about the client's identity, role,
// or whether it is authorized to make a particular call.
package credentials // import "google.golang.org/grpc/credentials"
import (
"context"
"errors"
"fmt"
"net"
"google.golang.org/grpc/attributes"
icredentials "google.golang.org/grpc/internal/credentials"
"google.golang.org/protobuf/proto"
)
// PerRPCCredentials defines the common interface for the credentials which need to
// attach security information to every RPC (e.g., oauth2).
type PerRPCCredentials interface {
// GetRequestMetadata gets the current request metadata, refreshing tokens
// if required. This should be called by the transport layer on each
// request, and the data should be populated in headers or other
// context. If a status code is returned, it will be used as the status for
// the RPC (restricted to an allowable set of codes as defined by gRFC
// A54). uri is the URI of the entry point for the request. When supported
// by the underlying implementation, ctx can be used for timeout and
// cancellation. Additionally, RequestInfo data will be available via ctx
// to this call.
GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
// RequireTransportSecurity indicates whether the credentials requires
// transport security.
RequireTransportSecurity() bool
}
// SecurityLevel defines the protection level on an established connection.
//
// This API is experimental.
type SecurityLevel int
const (
// InvalidSecurityLevel indicates an invalid security level.
// The zero SecurityLevel value is invalid for backward compatibility.
InvalidSecurityLevel SecurityLevel = iota
// NoSecurity indicates a connection is insecure.
NoSecurity
// IntegrityOnly indicates a connection only provides integrity protection.
IntegrityOnly
// PrivacyAndIntegrity indicates a connection provides both privacy and integrity protection.
PrivacyAndIntegrity
)
// String returns SecurityLevel in a string format.
func (s SecurityLevel) String() string {
switch s {
case NoSecurity:
return "NoSecurity"
case IntegrityOnly:
return "IntegrityOnly"
case PrivacyAndIntegrity:
return "PrivacyAndIntegrity"
}
return fmt.Sprintf("invalid SecurityLevel: %v", int(s))
}
// CommonAuthInfo contains authenticated information common to AuthInfo implementations.
// It should be embedded in a struct implementing AuthInfo to provide additional information
// about the credentials.
//
// This API is experimental.
type CommonAuthInfo struct {
SecurityLevel SecurityLevel
}
// GetCommonAuthInfo returns the pointer to CommonAuthInfo struct.
func (c CommonAuthInfo) GetCommonAuthInfo() CommonAuthInfo {
return c
}
// ProtocolInfo provides static information regarding transport credentials.
type ProtocolInfo struct {
// ProtocolVersion is the gRPC wire protocol version.
//
// Deprecated: this is unused by gRPC.
ProtocolVersion string
// SecurityProtocol is the security protocol in use.
SecurityProtocol string
// SecurityVersion is the security protocol version. It is a static version string from the
// credentials, not a value that reflects per-connection protocol negotiation. To retrieve
// details about the credentials used for a connection, use the Peer's AuthInfo field instead.
//
// Deprecated: please use Peer.AuthInfo.
SecurityVersion string
// ServerName is the user-configured server name. If set, this overrides
// the default :authority header used for all RPCs on the channel using the
// containing credentials, unless grpc.WithAuthority is set on the channel,
// in which case that setting will take precedence.
//
// This must be a valid `:authority` header according to
// [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2).
//
// Deprecated: Users should use grpc.WithAuthority to override the authority
// on a channel instead of configuring the credentials.
ServerName string
}
// AuthInfo defines the common interface for the auth information the users are interested in.
// A struct that implements AuthInfo should embed CommonAuthInfo by including additional
// information about the credentials in it.
type AuthInfo interface {
AuthType() string
}
// AuthorityValidator validates the authority used to override the `:authority`
// header. This is an optional interface that implementations of AuthInfo can
// implement if they support per-RPC authority overrides. It is invoked when the
// application attempts to override the HTTP/2 `:authority` header using the
// CallAuthority call option.
type AuthorityValidator interface {
// ValidateAuthority checks the authority value used to override the
// `:authority` header. The authority parameter is the override value
// provided by the application via the CallAuthority option. This value
// typically corresponds to the server hostname or endpoint the RPC is
// targeting. It returns non-nil error if the validation fails.
ValidateAuthority(authority string) error
}
// ErrConnDispatched indicates that rawConn has been dispatched out of gRPC
// and the caller should not close rawConn.
var ErrConnDispatched = errors.New("credentials: rawConn is dispatched out of gRPC")
// TransportCredentials defines the common interface for all the live gRPC wire
// protocols and supported transport security protocols (e.g., TLS, SSL).
type TransportCredentials interface {
// ClientHandshake does the authentication handshake specified by the
// corresponding authentication protocol on rawConn for clients. It returns
// the authenticated connection and the corresponding auth information
// about the connection. The auth information should embed CommonAuthInfo
// to return additional information about the credentials. Implementations
// must use the provided context to implement timely cancellation. gRPC
// will try to reconnect if the error returned is a temporary error
// (io.EOF, context.DeadlineExceeded or err.Temporary() == true). If the
// returned error is a wrapper error, implementations should make sure that
// the error implements Temporary() to have the correct retry behaviors.
// Additionally, ClientHandshakeInfo data will be available via the context
// passed to this call.
//
// The second argument to this method is the `:authority` header value used
// while creating new streams on this connection after authentication
// succeeds. Implementations must use this as the server name during the
// authentication handshake.
//
// If the returned net.Conn is closed, it MUST close the net.Conn provided.
ClientHandshake(context.Context, string, net.Conn) (net.Conn, AuthInfo, error)
// ServerHandshake does the authentication handshake for servers. It returns
// the authenticated connection and the corresponding auth information about
// the connection. The auth information should embed CommonAuthInfo to return additional information
// about the credentials.
//
// If the returned net.Conn is closed, it MUST close the net.Conn provided.
ServerHandshake(net.Conn) (net.Conn, AuthInfo, error)
// Info provides the ProtocolInfo of this TransportCredentials.
Info() ProtocolInfo
// Clone makes a copy of this TransportCredentials.
Clone() TransportCredentials
// OverrideServerName specifies the value used for the following:
//
// - verifying the hostname on the returned certificates
// - as SNI in the client's handshake to support virtual hosting
// - as the value for `:authority` header at stream creation time
//
// The provided string should be a valid `:authority` header according to
// [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2).
//
// Deprecated: this method is unused by gRPC. Users should use
// grpc.WithAuthority to override the authority on a channel instead of
// configuring the credentials.
OverrideServerName(string) error
}
// Bundle is a combination of TransportCredentials and PerRPCCredentials.
//
// It also contains a mode switching method, so it can be used as a combination
// of different credential policies.
//
// Bundle cannot be used together with individual TransportCredentials.
// PerRPCCredentials from Bundle will be appended to other PerRPCCredentials.
//
// This API is experimental.
type Bundle interface {
// TransportCredentials returns the transport credentials from the Bundle.
//
// Implementations must return non-nil transport credentials. If transport
// security is not needed by the Bundle, implementations may choose to
// return insecure.NewCredentials().
TransportCredentials() TransportCredentials
// PerRPCCredentials returns the per-RPC credentials from the Bundle.
//
// May be nil if per-RPC credentials are not needed.
PerRPCCredentials() PerRPCCredentials
// NewWithMode should make a copy of Bundle, and switch mode. Modifying the
// existing Bundle may cause races.
//
// NewWithMode returns nil if the requested mode is not supported.
NewWithMode(mode string) (Bundle, error)
}
// RequestInfo contains request data attached to the context passed to GetRequestMetadata calls.
//
// This API is experimental.
type RequestInfo struct {
// The method passed to Invoke or NewStream for this RPC. (For proto methods, this has the format "/some.Service/Method")
Method string
// AuthInfo contains the information from a security handshake (TransportCredentials.ClientHandshake, TransportCredentials.ServerHandshake)
AuthInfo AuthInfo
}
// requestInfoKey is a struct to be used as the key to store RequestInfo in a
// context.
type requestInfoKey struct{}
// RequestInfoFromContext extracts the RequestInfo from the context if it exists.
//
// This API is experimental.
func RequestInfoFromContext(ctx context.Context) (ri RequestInfo, ok bool) {
ri, ok = ctx.Value(requestInfoKey{}).(RequestInfo)
return ri, ok
}
// NewContextWithRequestInfo creates a new context from ctx and attaches ri to it.
//
// This RequestInfo will be accessible via RequestInfoFromContext.
//
// Intended to be used from tests for PerRPCCredentials implementations (that
// often need to check connection's SecurityLevel). Should not be used from
// non-test code: the gRPC client already prepares a context with the correct
// RequestInfo attached when calling PerRPCCredentials.GetRequestMetadata.
//
// This API is experimental.
func NewContextWithRequestInfo(ctx context.Context, ri RequestInfo) context.Context {
return context.WithValue(ctx, requestInfoKey{}, ri)
}
// ClientHandshakeInfo holds data to be passed to ClientHandshake. This makes
// it possible to pass arbitrary data to the handshaker from gRPC, resolver,
// balancer etc. Individual credential implementations control the actual
// format of the data that they are willing to receive.
//
// This API is experimental.
type ClientHandshakeInfo struct {
// Attributes contains the attributes for the address. It could be provided
// by the gRPC, resolver, balancer etc.
Attributes *attributes.Attributes
}
// ClientHandshakeInfoFromContext returns the ClientHandshakeInfo struct stored
// in ctx.
//
// This API is experimental.
func ClientHandshakeInfoFromContext(ctx context.Context) ClientHandshakeInfo {
chi, _ := icredentials.ClientHandshakeInfoFromContext(ctx).(ClientHandshakeInfo)
return chi
}
// CheckSecurityLevel checks if a connection's security level is greater than or equal to the specified one.
// It returns success if 1) the condition is satisfied or 2) AuthInfo struct does not implement GetCommonAuthInfo() method
// or 3) CommonAuthInfo.SecurityLevel has an invalid zero value. For 2) and 3), it is for the purpose of backward-compatibility.
//
// This API is experimental.
func CheckSecurityLevel(ai AuthInfo, level SecurityLevel) error {
type internalInfo interface {
GetCommonAuthInfo() CommonAuthInfo
}
if ai == nil {
return errors.New("AuthInfo is nil")
}
if ci, ok := ai.(internalInfo); ok {
// CommonAuthInfo.SecurityLevel has an invalid value.
if ci.GetCommonAuthInfo().SecurityLevel == InvalidSecurityLevel {
return nil
}
if ci.GetCommonAuthInfo().SecurityLevel < level {
return fmt.Errorf("requires SecurityLevel %v; connection has %v", level, ci.GetCommonAuthInfo().SecurityLevel)
}
}
// The condition is satisfied or AuthInfo struct does not implement GetCommonAuthInfo() method.
return nil
}
// ChannelzSecurityInfo defines the interface that security protocols should implement
// in order to provide security info to channelz.
//
// This API is experimental.
type ChannelzSecurityInfo interface {
GetSecurityValue() ChannelzSecurityValue
}
// ChannelzSecurityValue defines the interface that GetSecurityValue() return value
// should satisfy. This interface should only be satisfied by *TLSChannelzSecurityValue
// and *OtherChannelzSecurityValue.
//
// This API is experimental.
type ChannelzSecurityValue interface {
isChannelzSecurityValue()
}
// OtherChannelzSecurityValue defines the struct that non-TLS protocol should return
// from GetSecurityValue(), which contains protocol specific security info. Note
// the Value field will be sent to users of channelz requesting channel info, and
// thus sensitive info should better be avoided.
//
// This API is experimental.
type OtherChannelzSecurityValue struct {
ChannelzSecurityValue
Name string
Value proto.Message
}

View File

@@ -0,0 +1,538 @@
/*
*
* Copyright 2025 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 credentials_test
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"net"
"strings"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/credentials/local"
"google.golang.org/grpc/internal/stubserver"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/grpc/testdata"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
)
func authorityChecker(ctx context.Context, wantAuthority string) error {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return status.Error(codes.InvalidArgument, "failed to parse metadata")
}
auths, ok := md[":authority"]
if !ok {
return status.Error(codes.InvalidArgument, "no authority header")
}
if len(auths) != 1 {
return status.Errorf(codes.InvalidArgument, "expected exactly one authority header, got %v", auths)
}
if auths[0] != wantAuthority {
return status.Errorf(codes.InvalidArgument, "invalid authority header %q, want %q", auths[0], wantAuthority)
}
return nil
}
func loadTLSCreds(t *testing.T) (grpc.ServerOption, grpc.DialOption) {
t.Helper()
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
if err != nil {
t.Fatalf("Failed to load key pair: %v", err)
return nil, nil
}
serverCreds := grpc.Creds(credentials.NewServerTLSFromCert(&cert))
clientCreds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), "x.test.example.com")
if err != nil {
t.Fatalf("Failed to create client credentials: %v", err)
}
return serverCreds, grpc.WithTransportCredentials(clientCreds)
}
// Tests the scenario where the `grpc.CallAuthority` call option is used with
// different transport credentials. The test verifies that the specified
// authority is correctly propagated to the serve when a correct authority is
// used.
func (s) TestCorrectAuthorityWithCreds(t *testing.T) {
const authority = "auth.test.example.com"
const authorityWithPort = "auth.test.example.com:8010"
tests := []struct {
name string
creds func(t *testing.T) (grpc.ServerOption, grpc.DialOption)
expectedAuth string
}{
{
name: "Insecure",
creds: func(*testing.T) (grpc.ServerOption, grpc.DialOption) {
c := insecure.NewCredentials()
return grpc.Creds(c), grpc.WithTransportCredentials(c)
},
expectedAuth: authority,
},
{
name: "Local",
creds: func(*testing.T) (grpc.ServerOption, grpc.DialOption) {
c := local.NewCredentials()
return grpc.Creds(c), grpc.WithTransportCredentials(c)
},
expectedAuth: authority,
},
{
name: "TLS",
creds: func(t *testing.T) (grpc.ServerOption, grpc.DialOption) {
return loadTLSCreds(t)
},
expectedAuth: authority,
},
{
name: "TLSAuthorityWithPort",
creds: func(t *testing.T) (grpc.ServerOption, grpc.DialOption) {
return loadTLSCreds(t)
},
expectedAuth: authorityWithPort,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ss := &stubserver.StubServer{
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
if err := authorityChecker(ctx, tt.expectedAuth); err != nil {
return nil, err
}
return &testpb.Empty{}, nil
},
}
serverOpt, dialOpt := tt.creds(t)
if err := ss.StartServer(serverOpt); err != nil {
t.Fatalf("Error starting endpoint server: %v", err)
}
defer ss.Stop()
cc, err := grpc.NewClient(ss.Address, dialOpt)
if err != nil {
t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err)
}
defer cc.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(tt.expectedAuth)); err != nil {
t.Fatalf("EmptyCall() rpc failed: %v", err)
}
})
}
}
// Tests the `grpc.CallAuthority` option with TLS credentials. This test verifies
// that the RPC fails with `UNAVAILABLE` status code and doesn't reach the server
// when an incorrect authority is used.
func (s) TestIncorrectAuthorityWithTLS(t *testing.T) {
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
if err != nil {
t.Fatalf("Failed to load key pair: %s", err)
}
creds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), "x.test.example.com")
if err != nil {
t.Fatalf("Failed to create credentials %v", err)
}
tests := []struct {
name string
authority string
}{
{
name: "IncorrectAuthority",
authority: "auth.example.com",
},
{
name: "IncorrectAuthorityWithPort",
authority: "auth.example.com:8443",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
serverCalled := make(chan struct{})
ss := &stubserver.StubServer{
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
close(serverCalled)
return nil, nil
},
}
if err := ss.StartServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert))); err != nil {
t.Fatalf("Error starting endpoint server: %v", err)
}
defer ss.Stop()
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(creds))
if err != nil {
t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err)
}
defer cc.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(tt.authority)); status.Code(err) != codes.Unavailable {
t.Fatalf("EmptyCall() returned status %v, want %v", status.Code(err), codes.Unavailable)
}
select {
case <-serverCalled:
t.Fatalf("Server handler should not have been called")
case <-time.After(defaultTestShortTimeout):
}
})
}
}
// testAuthInfoNoValidator implements only credentials.AuthInfo and not
// credentials.AuthorityValidator.
type testAuthInfoNoValidator struct{}
// AuthType returns the authentication type.
func (testAuthInfoNoValidator) AuthType() string {
return "test"
}
// testAuthInfoWithValidator implements both credentials.AuthInfo and
// credentials.AuthorityValidator.
type testAuthInfoWithValidator struct {
validAuthority string
}
// AuthType returns the authentication type.
func (testAuthInfoWithValidator) AuthType() string {
return "test"
}
// ValidateAuthority implements credentials.AuthorityValidator.
func (v testAuthInfoWithValidator) ValidateAuthority(authority string) error {
if authority == v.validAuthority {
return nil
}
return fmt.Errorf("invalid authority %q, want %q", authority, v.validAuthority)
}
// testCreds is a test TransportCredentials that can optionally support
// authority validation.
type testCreds struct {
authority string
}
// ClientHandshake performs the client-side handshake.
func (c *testCreds) ClientHandshake(_ context.Context, _ string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
if c.authority != "" {
return rawConn, testAuthInfoWithValidator{validAuthority: c.authority}, nil
}
return rawConn, testAuthInfoNoValidator{}, nil
}
// ServerHandshake performs the server-side handshake.
func (c *testCreds) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
if c.authority != "" {
return rawConn, testAuthInfoWithValidator{validAuthority: c.authority}, nil
}
return rawConn, testAuthInfoNoValidator{}, nil
}
// Clone creates a copy of testCreds.
func (c *testCreds) Clone() credentials.TransportCredentials {
return &testCreds{authority: c.authority}
}
// Info provides protocol information.
func (c *testCreds) Info() credentials.ProtocolInfo {
return credentials.ProtocolInfo{}
}
// OverrideServerName overrides the server name used for verification.
func (c *testCreds) OverrideServerName(string) error {
return nil
}
// TestAuthorityValidationFailureWithCustomCreds tests the `grpc.CallAuthority`
// call option using custom credentials. It covers two failure scenarios:
// - The credentials implement AuthorityValidator but authority used to override
// is not valid.
// - The credentials do not implement AuthorityValidator, but an authority
// override is specified.
// In both cases, the RPC is expected to fail with an `UNAVAILABLE` status code.
func (s) TestAuthorityValidationFailureWithCustomCreds(t *testing.T) {
tests := []struct {
name string
creds credentials.TransportCredentials
authority string
}{
{
name: "IncorrectAuthorityWithFakeCreds",
authority: "auth.example.com",
creds: &testCreds{authority: "auth.test.example.com"},
},
{
name: "FakeCredsWithNoAuthValidator",
creds: &testCreds{},
authority: "auth.test.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
serverCalled := make(chan struct{})
ss := stubserver.StubServer{
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
close(serverCalled)
return nil, nil
},
}
if err := ss.StartServer(); err != nil {
t.Fatalf("Failed to start stub server: %v", err)
}
defer ss.Stop()
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(tt.creds))
if err != nil {
t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err)
}
defer cc.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(tt.authority)); status.Code(err) != codes.Unavailable {
t.Fatalf("EmptyCall() returned status %v, want %v", status.Code(err), codes.Unavailable)
}
select {
case <-serverCalled:
t.Fatalf("Server should not have been called")
case <-time.After(defaultTestShortTimeout):
}
})
}
}
// TestCorrectAuthorityWithCustomCreds tests the `grpc.CallAuthority` call
// option using custom credentials. It verifies that the provided authority is
// correctly propagated to the server when a correct authority is used.
func (s) TestCorrectAuthorityWithCustomCreds(t *testing.T) {
const authority = "auth.test.example.com"
creds := &testCreds{authority: "auth.test.example.com"}
ss := stubserver.StubServer{
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
if err := authorityChecker(ctx, authority); err != nil {
return nil, err
}
return &testpb.Empty{}, nil
},
}
if err := ss.StartServer(); err != nil {
t.Fatalf("Failed to start stub server: %v", err)
}
defer ss.Stop()
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(creds))
if err != nil {
t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err)
}
defer cc.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(authority)); status.Code(err) != codes.OK {
t.Fatalf("EmptyCall() returned status %v, want %v", status.Code(err), codes.OK)
}
}
// TestAuthorityOverrideWithCertChain tests that the authority being used to
// override per-RPC authority is validated against the leaf certificate only
// and not against the intermediate certificates.
func (s) TestAuthorityOverrideWithCertChain(t *testing.T) {
rootCert, certChain, leafKey := generateCertChain(t, "root.example.com", "intermediate.example.com", "*.leaf.example.com")
// Construct server credentials from leaf and intermediate certificates.
serverCert := tls.Certificate{
Certificate: [][]byte{certChain[0].Raw, certChain[1].Raw},
PrivateKey: leafKey,
}
serverCreds := credentials.NewServerTLSFromCert(&serverCert)
// Create client credentials trusting the Root CA.
certPool := x509.NewCertPool()
certPool.AddCert(rootCert)
clientCreds := credentials.NewTLS(&tls.Config{
RootCAs: certPool,
ServerName: "test1.leaf.example.com",
})
tests := []struct {
name string
authority string
wantCode codes.Code
wantErr string
}{
{
name: "AuthorityMatchesIntermediate",
authority: "intermediate.example.com",
wantCode: codes.Unavailable,
wantErr: "failed to validate authority",
},
{
name: "AuthorityMatchesLeaf",
authority: "test2.leaf.example.com",
wantCode: codes.OK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup and start the stub server.
ss := &stubserver.StubServer{
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
if err := authorityChecker(ctx, tt.authority); err != nil {
return nil, err
}
return &testpb.Empty{}, nil
},
}
if err := ss.StartServer(grpc.Creds(serverCreds)); err != nil {
t.Fatalf("failed to start server: %v", err)
}
defer ss.Stop()
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(clientCreds))
if err != nil {
t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err)
}
defer cc.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
_, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(tt.authority))
if got := status.Code(err); got != tt.wantCode {
t.Fatalf("EmptyCall() with authority %q: got code %v, want %v", tt.authority, got, tt.wantCode)
}
if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
t.Fatalf("EmptyCall() with authority %q: expected error to contain %q, got %v", tt.authority, tt.wantErr, err)
}
})
}
}
// certConfig defines the configuration for generating a certificate.
type certConfig struct {
commonName string
dnsNames []string
isCA bool
serial int64
parentCert *x509.Certificate
parentKey *rsa.PrivateKey
}
// createCertificate generates a certificate based on the provided certConfig.
// It creates self-signed certificates if parentCert is nil otherwise it creates
// certificates signed by a parent certificate.
func createCertificate(t *testing.T, cfg certConfig) (*x509.Certificate, *rsa.PrivateKey) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
now := time.Now()
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(cfg.serial),
Subject: pkix.Name{CommonName: cfg.commonName},
DNSNames: cfg.dnsNames,
NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(time.Hour),
BasicConstraintsValid: true,
IsCA: cfg.isCA,
}
// If no parent is provided, the certificate is self-signed
signingCert := cfg.parentCert
signingKey := cfg.parentKey
if signingCert == nil {
signingCert = tmpl
signingKey = key
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, signingCert, key.Public(), signingKey)
if err != nil {
t.Fatal(err)
}
cert, err := x509.ParseCertificate(der)
if err != nil {
t.Fatal(err)
}
return cert, key
}
// generateCertChain creates a 3 certificate chain (Root -> Intermediate ->
// Leaf). It returns the root certificate, a slice containing the leaf and
// intermediate certificates in the order [leaf, intermediate], and the private
// key for the leaf certificate.
func generateCertChain(t *testing.T, rootName, interName, leafName string) (root *x509.Certificate, chain []*x509.Certificate, leafKey *rsa.PrivateKey) {
t.Helper()
rootCfg := certConfig{
commonName: rootName,
isCA: true,
}
root, rootKey := createCertificate(t, rootCfg)
interCfg := certConfig{
commonName: interName,
dnsNames: []string{interName},
isCA: true,
serial: 2,
parentCert: root,
parentKey: rootKey,
}
intermediate, interKey := createCertificate(t, interCfg)
leafCfg := certConfig{
commonName: leafName,
dnsNames: []string{leafName},
isCA: false,
serial: 3,
parentCert: intermediate,
parentKey: interKey,
}
leaf, leafKey := createCertificate(t, leafCfg)
return root, []*x509.Certificate{leaf, intermediate}, leafKey
}

View File

@@ -0,0 +1,322 @@
/*
*
* Copyright 2016 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 credentials
import (
"context"
"crypto/tls"
"net"
"strings"
"testing"
"time"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/testdata"
)
const defaultTestTimeout = 10 * time.Second
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
// A struct that implements AuthInfo interface but does not implement GetCommonAuthInfo() method.
type testAuthInfoNoGetCommonAuthInfoMethod struct{}
func (ta testAuthInfoNoGetCommonAuthInfoMethod) AuthType() string {
return "testAuthInfoNoGetCommonAuthInfoMethod"
}
// A struct that implements AuthInfo interface and implements CommonAuthInfo() method.
type testAuthInfo struct {
CommonAuthInfo
}
func (ta testAuthInfo) AuthType() string {
return "testAuthInfo"
}
func (s) TestCheckSecurityLevel(t *testing.T) {
testCases := []struct {
authLevel SecurityLevel
testLevel SecurityLevel
want bool
}{
{
authLevel: PrivacyAndIntegrity,
testLevel: PrivacyAndIntegrity,
want: true,
},
{
authLevel: IntegrityOnly,
testLevel: PrivacyAndIntegrity,
want: false,
},
{
authLevel: IntegrityOnly,
testLevel: NoSecurity,
want: true,
},
{
authLevel: InvalidSecurityLevel,
testLevel: IntegrityOnly,
want: true,
},
{
authLevel: InvalidSecurityLevel,
testLevel: PrivacyAndIntegrity,
want: true,
},
}
for _, tc := range testCases {
err := CheckSecurityLevel(testAuthInfo{CommonAuthInfo: CommonAuthInfo{SecurityLevel: tc.authLevel}}, tc.testLevel)
if tc.want && (err != nil) {
t.Fatalf("CheckSeurityLevel(%s, %s) returned failure but want success", tc.authLevel.String(), tc.testLevel.String())
} else if !tc.want && (err == nil) {
t.Fatalf("CheckSeurityLevel(%s, %s) returned success but want failure", tc.authLevel.String(), tc.testLevel.String())
}
}
}
func (s) TestCheckSecurityLevelNoGetCommonAuthInfoMethod(t *testing.T) {
if err := CheckSecurityLevel(testAuthInfoNoGetCommonAuthInfoMethod{}, PrivacyAndIntegrity); err != nil {
t.Fatalf("CheckSeurityLevel() returned failure but want success")
}
}
func (s) TestTLSOverrideServerName(t *testing.T) {
expectedServerName := "server.name"
c := NewTLS(nil)
c.OverrideServerName(expectedServerName)
if c.Info().ServerName != expectedServerName {
t.Fatalf("c.Info().ServerName = %v, want %v", c.Info().ServerName, expectedServerName)
}
}
func (s) TestTLSClone(t *testing.T) {
expectedServerName := "server.name"
c := NewTLS(nil)
c.OverrideServerName(expectedServerName)
cc := c.Clone()
if cc.Info().ServerName != expectedServerName {
t.Fatalf("cc.Info().ServerName = %v, want %v", cc.Info().ServerName, expectedServerName)
}
cc.OverrideServerName("")
if c.Info().ServerName != expectedServerName {
t.Fatalf("Change in clone should not affect the original, c.Info().ServerName = %v, want %v", c.Info().ServerName, expectedServerName)
}
}
type serverHandshake func(net.Conn) (AuthInfo, error)
func (s) TestClientHandshakeReturnsAuthInfo(t *testing.T) {
tcs := []struct {
name string
address string
}{
{
name: "localhost",
address: "localhost:0",
},
{
name: "ipv4",
address: "127.0.0.1:0",
},
{
name: "ipv6",
address: "[::1]:0",
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
done := make(chan AuthInfo, 1)
lis := launchServerOnListenAddress(t, tlsServerHandshake, done, tc.address)
defer lis.Close()
lisAddr := lis.Addr().String()
clientAuthInfo := clientHandle(t, gRPCClientHandshake, lisAddr)
// wait until server sends serverAuthInfo or fails.
serverAuthInfo, ok := <-done
if !ok {
t.Fatalf("Error at server-side")
}
if !compare(clientAuthInfo, serverAuthInfo) {
t.Fatalf("c.ClientHandshake(_, %v, _) = %v, want %v.", lisAddr, clientAuthInfo, serverAuthInfo)
}
})
}
}
func (s) TestServerHandshakeReturnsAuthInfo(t *testing.T) {
done := make(chan AuthInfo, 1)
lis := launchServer(t, gRPCServerHandshake, done)
defer lis.Close()
clientAuthInfo := clientHandle(t, tlsClientHandshake, lis.Addr().String())
// wait until server sends serverAuthInfo or fails.
serverAuthInfo, ok := <-done
if !ok {
t.Fatalf("Error at server-side")
}
if !compare(clientAuthInfo, serverAuthInfo) {
t.Fatalf("ServerHandshake(_) = %v, want %v.", serverAuthInfo, clientAuthInfo)
}
}
func (s) TestServerAndClientHandshake(t *testing.T) {
done := make(chan AuthInfo, 1)
lis := launchServer(t, gRPCServerHandshake, done)
defer lis.Close()
clientAuthInfo := clientHandle(t, gRPCClientHandshake, lis.Addr().String())
// wait until server sends serverAuthInfo or fails.
serverAuthInfo, ok := <-done
if !ok {
t.Fatalf("Error at server-side")
}
if !compare(clientAuthInfo, serverAuthInfo) {
t.Fatalf("AuthInfo returned by server: %v and client: %v aren't same", serverAuthInfo, clientAuthInfo)
}
}
func compare(a1, a2 AuthInfo) bool {
if a1.AuthType() != a2.AuthType() {
return false
}
switch a1.AuthType() {
case "tls":
state1 := a1.(TLSInfo).State
state2 := a2.(TLSInfo).State
if state1.Version == state2.Version &&
state1.HandshakeComplete == state2.HandshakeComplete &&
state1.CipherSuite == state2.CipherSuite &&
state1.NegotiatedProtocol == state2.NegotiatedProtocol {
return true
}
return false
default:
return false
}
}
func launchServer(t *testing.T, hs serverHandshake, done chan AuthInfo) net.Listener {
return launchServerOnListenAddress(t, hs, done, "localhost:0")
}
func launchServerOnListenAddress(t *testing.T, hs serverHandshake, done chan AuthInfo, address string) net.Listener {
lis, err := net.Listen("tcp", address)
if err != nil {
if strings.Contains(err.Error(), "bind: cannot assign requested address") ||
strings.Contains(err.Error(), "socket: address family not supported by protocol") {
t.Skipf("no support for address %v", address)
}
t.Fatalf("Failed to listen: %v", err)
}
go serverHandle(t, hs, done, lis)
return lis
}
// Is run in a separate goroutine.
func serverHandle(t *testing.T, hs serverHandshake, done chan AuthInfo, lis net.Listener) {
serverRawConn, err := lis.Accept()
if err != nil {
t.Errorf("Server failed to accept connection: %v", err)
close(done)
return
}
serverAuthInfo, err := hs(serverRawConn)
if err != nil {
t.Errorf("Server failed while handshake. Error: %v", err)
serverRawConn.Close()
close(done)
return
}
done <- serverAuthInfo
}
func clientHandle(t *testing.T, hs func(net.Conn, string) (AuthInfo, error), lisAddr string) AuthInfo {
conn, err := net.Dial("tcp", lisAddr)
if err != nil {
t.Fatalf("Client failed to connect to %s. Error: %v", lisAddr, err)
}
defer conn.Close()
clientAuthInfo, err := hs(conn, lisAddr)
if err != nil {
t.Fatalf("Error on client while handshake. Error: %v", err)
}
return clientAuthInfo
}
// Server handshake implementation in gRPC.
func gRPCServerHandshake(conn net.Conn) (AuthInfo, error) {
serverTLS, err := NewServerTLSFromFile(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
if err != nil {
return nil, err
}
_, serverAuthInfo, err := serverTLS.ServerHandshake(conn)
if err != nil {
return nil, err
}
return serverAuthInfo, nil
}
// Client handshake implementation in gRPC.
func gRPCClientHandshake(conn net.Conn, lisAddr string) (AuthInfo, error) {
clientTLS := NewTLS(&tls.Config{InsecureSkipVerify: true})
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
_, authInfo, err := clientTLS.ClientHandshake(ctx, lisAddr, conn)
if err != nil {
return nil, err
}
return authInfo, nil
}
func tlsServerHandshake(conn net.Conn) (AuthInfo, error) {
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
if err != nil {
return nil, err
}
serverTLSConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
NextProtos: []string{"h2"},
}
serverConn := tls.Server(conn, serverTLSConfig)
err = serverConn.Handshake()
if err != nil {
return nil, err
}
return TLSInfo{State: serverConn.ConnectionState(), CommonAuthInfo: CommonAuthInfo{SecurityLevel: PrivacyAndIntegrity}}, nil
}
func tlsClientHandshake(conn net.Conn, _ string) (AuthInfo, error) {
clientTLSConfig := &tls.Config{
InsecureSkipVerify: true, // NOLINT
NextProtos: []string{"h2"},
}
clientConn := tls.Client(conn, clientTLSConfig)
if err := clientConn.Handshake(); err != nil {
return nil, err
}
return TLSInfo{State: clientConn.ConnectionState(), CommonAuthInfo: CommonAuthInfo{SecurityLevel: PrivacyAndIntegrity}}, nil
}

View File

@@ -0,0 +1,278 @@
/*
*
* Copyright 2026 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 google
import (
"context"
"errors"
"fmt"
"sync"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/idtoken"
"cloud.google.com/go/compute/metadata"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/google/internal"
"google.golang.org/grpc/internal/backoff"
"google.golang.org/grpc/internal/transport"
"google.golang.org/grpc/status"
)
const (
// preemptiveRefresh is the window before a token's actual expiration during
// which the token is considered stale. Requests using a stale but still
// valid token will trigger a background asynchronous refresh. This avoids
// blocking the current RPC, preventing periodic latency spikes during token
// refresh.
preemptiveRefresh = 1 * time.Minute
// metadataTimeout is the timeout for the call to the metadata server to
// prevent the background token fetch from hanging indefinitely in case
// of connection issues.
metadataTimeout = 60 * time.Second
)
type gcpServiceAccountIdentityCallCreds struct {
// The following fields are initialized at creation time and are read-only
// after that.
ctx context.Context
audience string
creds *auth.Credentials
backoff backoff.Strategy
// The following fields are protected by mu.
mu sync.Mutex
token *auth.Token
tokenExpiry time.Time // timestamp after which the cached token is considered invalid
preemptiveTokenRefresh time.Time // timestamp after which background preemptive refresh is triggered
fetching chan struct{} // used to ensure a single in-progress background token fetch
nextRetryTime time.Time // timestamp after which we can attempt the next token fetch
retryAttempt int // consecutive fetch failure count used to compute backoff delay
lastErr error // cached error returned from the most recent token fetch attempt
}
func init() {
internal.BackoffStrategy = backoff.DefaultExponential
internal.NewIDTokenCredentials = func(opts *idtoken.Options) (*auth.Credentials, error) {
return idtoken.NewCredentials(opts)
}
}
// NewServiceAccountIdentityCredentials creates a PerRPCCredentials that
// authenticates using a GCP Service Account Identity JWT token for the given
// audience.
//
// This credential fetches the ID token from the GCE metadata server and is
// only valid for use in environments running on GCP. The ctx and audience
// parameters cannot be empty.
//
// The credentials object starts asynchronous background token fetches to
// refresh expired tokens. The provided context propagates cancellation to
// these background tasks. Users should not pass an RPC-scoped context here,
// but rather a context that is valid for the entire lifetime of the
// credentials and should cancel the context when they are done.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func NewServiceAccountIdentityCredentials(ctx context.Context, audience string) (credentials.PerRPCCredentials, error) {
if ctx == nil {
return nil, fmt.Errorf("credentials: ctx cannot be nil")
}
if audience == "" {
return nil, fmt.Errorf("credentials: audience cannot be empty")
}
creds, err := internal.NewIDTokenCredentials(&idtoken.Options{Audience: audience})
if err != nil {
return nil, fmt.Errorf("credentials: failed to create ID token credentials: %v", err)
}
return &gcpServiceAccountIdentityCallCreds{
ctx: ctx,
audience: audience,
creds: creds,
backoff: internal.BackoffStrategy,
}, nil
}
// GetRequestMetadata gets the current request metadata, refreshing tokens if
// required. This implementation follows the PerRPCCredentials interface.
//
// It guarantees that only one underlying token fetch will be executed
// concurrently. If a valid token is cached, it is returned immediately. If
// a fetch recently failed, the cached error is returned until the backoff
// interval expires. Otherwise, it initiates a new token fetch or blocks
// waiting for an already-in-progress fetch to complete.
func (c *gcpServiceAccountIdentityCallCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
ri, _ := credentials.RequestInfoFromContext(ctx)
if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
return nil, fmt.Errorf("credentials: cannot send secure credentials on an insecure connection: %v", err)
}
if md, err := c.cachedRequestMetadata(true); md != nil || err != nil {
return md, err
}
c.mu.Lock()
// Now that we have the lock, did someone else finish the fetch while we
// were waiting for the lock?
md, err := c.cachedRequestMetadataLocked(false)
if md != nil || err != nil {
c.mu.Unlock()
return md, err
}
// If no one is fetching, start it.
if c.fetching == nil {
c.fetching = make(chan struct{})
go c.startFetch()
}
wait := c.fetching
c.mu.Unlock()
select {
case <-wait:
// Return the new cached result from the most recent fetch.
md, err := c.cachedRequestMetadata(false)
if md == nil && err == nil {
return nil, status.Error(codes.Unavailable, "credentials: fetched token is expired")
}
return md, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
// cachedRequestMetadata handles its own locking for the "fast path".
func (c *gcpServiceAccountIdentityCallCreds) cachedRequestMetadata(attemptPreemptiveRefresh bool) (map[string]string, error) {
c.mu.Lock()
defer c.mu.Unlock()
return c.cachedRequestMetadataLocked(attemptPreemptiveRefresh)
}
// cachedRequestMetadataLocked returns the cached token if it is valid, or nil
// if it is not. If attemptPreemptiveRefresh is true, it will also trigger a
// background refresh if the token is stale but still valid. It returns the
// cached error if the last fetch failed and the backoff interval has not
// expired.
//
// Returns (nil, nil) if the token is not valid and a new fetch is required. The
// caller is responsible for initiating the fetch or waiting for an in-progress
// fetch to complete.
//
// It must be called with mu locked.
func (c *gcpServiceAccountIdentityCallCreds) cachedRequestMetadataLocked(attemptPreemptiveRefresh bool) (map[string]string, error) {
if c.token != nil && c.isTokenValidLocked() {
if attemptPreemptiveRefresh && c.isTokenStaleLocked() && c.fetching == nil {
c.fetching = make(chan struct{})
go c.startFetch()
}
return map[string]string{"authorization": "Bearer " + c.token.Value}, nil
}
if c.lastErr != nil && time.Now().Before(c.nextRetryTime) {
return nil, c.lastErr
}
return nil, nil
}
// RequireTransportSecurity indicates whether the credentials requires
// transport security.
func (c *gcpServiceAccountIdentityCallCreds) RequireTransportSecurity() bool {
return true
}
// isTokenStaleLocked checks if the token falls within the
// preemptiveRefresh window. It must be called with mu locked.
func (c *gcpServiceAccountIdentityCallCreds) isTokenStaleLocked() bool {
return c.preemptiveTokenRefresh.Before(time.Now())
}
// isTokenValidLocked checks if the token is not expired yet. It must be called
// with mu locked.
func (c *gcpServiceAccountIdentityCallCreds) isTokenValidLocked() bool {
return c.tokenExpiry.After(time.Now())
}
// startFetch initiates a token fetch and updates the credential
// state upon completion.
func (c *gcpServiceAccountIdentityCallCreds) startFetch() {
ctx, cancel := context.WithTimeout(c.ctx, metadataTimeout)
defer cancel()
token, err := c.creds.TokenProvider.Token(ctx)
c.mu.Lock()
defer c.mu.Unlock()
close(c.fetching)
c.fetching = nil
c.updateStateLocked(token, err)
}
// updateStateLocked updates the credentials local token cache and
// backoff state based on the outcome of a background fetch attempt.
//
// If the fetch succeeded, the cached token is updated, and the backoff timers
// and error are reset.
//
// If the fetch failed, backoff attempts are calculated and the error is mapped
// to a gRPC status.
// - If the HTTP request fails with a status that maps to gRPC UNAVAILABLE
// according to HTTP to gRPC status code mappings, it returns UNAVAILABLE.
// - All other HTTP error status codes map to UNAUTHENTICATED.
// - Non-HTTP request failures are mapped to UNAVAILABLE.
//
// It must be called with mu locked.
func (c *gcpServiceAccountIdentityCallCreds) updateStateLocked(token *auth.Token, err error) {
if err != nil {
var mappedErr error
var metadataErr *metadata.Error
if errors.As(err, &metadataErr) {
switch transport.HTTPStatusConvTab[metadataErr.Code] {
case codes.Unavailable:
mappedErr = status.Errorf(codes.Unavailable, "credentials: failed to fetch token from metadata server: %v", err)
default:
mappedErr = status.Errorf(codes.Unauthenticated, "credentials: failed to fetch token from metadata server: %v", err)
}
} else {
mappedErr = status.Errorf(codes.Unavailable, "credentials: failed to fetch ID token: %v", err)
}
c.lastErr = mappedErr
backoffDelay := c.backoff.Backoff(c.retryAttempt)
c.retryAttempt++
c.nextRetryTime = time.Now().Add(backoffDelay)
return
}
c.lastErr = nil
c.retryAttempt = 0
c.nextRetryTime = time.Time{}
c.token = token
// Per gRFC A83, the cached token is considered invalid 30 seconds before its
// actual expiration time to accommodate for clock skew.
c.tokenExpiry = token.Expiry.Add(-30 * time.Second)
c.preemptiveTokenRefresh = c.tokenExpiry.Add(-preemptiveRefresh)
}

View File

@@ -0,0 +1,545 @@
/*
*
* Copyright 2026 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 google_test
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"testing"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/idtoken"
"cloud.google.com/go/compute/metadata"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/google"
"google.golang.org/grpc/credentials/google/internal"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/status"
)
const defaultTestTimeout = 10 * time.Second
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
// gcpTestAuthInfo implements credentials.AuthInfo for testing.
type gcpTestAuthInfo struct {
credentials.CommonAuthInfo
}
func (t *gcpTestAuthInfo) AuthType() string {
return "test"
}
// stubBackoff implements a backoff strategy interface for testing.
type stubBackoff struct {
backoffDelay time.Duration
}
func (m *stubBackoff) Backoff(int) time.Duration {
return m.backoffDelay
}
// stubTokenProvider implements auth.TokenProvider for unit testing.
// It supplies mocked token payloads, simulates processing delay, and tracks
// invocation frequency to verify caching and backoff behaviors.
type stubTokenProvider struct {
mu sync.Mutex
err error
token *auth.Token
callCount int // tracks fetch attempts to verify caching and backoff behaviors.
}
func (c *stubTokenProvider) Token(context.Context) (*auth.Token, error) {
c.mu.Lock()
c.callCount++
token := c.token
err := c.err
c.mu.Unlock()
return token, err
}
func (c *stubTokenProvider) setErr(err error) {
c.mu.Lock()
defer c.mu.Unlock()
c.err = err
}
func (c *stubTokenProvider) setToken(token *auth.Token) {
c.mu.Lock()
defer c.mu.Unlock()
c.token = token
}
func (c *stubTokenProvider) getCallCount() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.callCount
}
// setupStubTokenProvider initializes and returns a stubTokenProvider
// configured with the specified token value, token expiry, error and delay.
func setupStubTokenProvider(token string, err error) *stubTokenProvider {
return &stubTokenProvider{
err: err,
token: &auth.Token{
Value: token,
Expiry: time.Now().Add(1 * time.Hour), // idtoken package works with hardcoded 1 hour token expiry.
},
}
}
// setupTestGCPServiceAccountIdentityCreds constructs a GCP service account
// identity credentials instance with an injected stub token provider.
//
// It overrides internal.NewIDTokenCredentials to use the stubTokenProvider,
// and registers a cleanup function to restore original hook after the test.
func setupTestGCPServiceAccountIdentityCreds(ctx context.Context, t *testing.T, stubToken *stubTokenProvider) credentials.PerRPCCredentials {
// Override the ID token credentials to use stub token provider.
origNewIDTokenCredentials := internal.NewIDTokenCredentials
internal.NewIDTokenCredentials = func(*idtoken.Options) (*auth.Credentials, error) {
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: auth.NewCachedTokenProvider(stubToken, &auth.CachedTokenProviderOptions{})}), nil
}
t.Cleanup(func() { internal.NewIDTokenCredentials = origNewIDTokenCredentials })
creds, err := google.NewServiceAccountIdentityCredentials(ctx, "audience")
if err != nil {
t.Fatalf("NewServiceAccountIdentityCredentials() failed: %v", err)
}
return creds
}
// Test verifies that NewServiceAccountIdentityCredentials returns an error
// when called with empty audience.
func (s) TestNewServiceAccountIdentityCredentials_EmptyAudience(t *testing.T) {
const wantErr = "credentials: audience cannot be empty"
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
if _, err := google.NewServiceAccountIdentityCredentials(ctx, ""); err == nil || !strings.Contains(err.Error(), wantErr) {
t.Fatalf("NewServiceAccountIdentityCredentials() returned error = %v, want error containing %q", err, wantErr)
}
}
// Test verifies the successful retrieval of an ID token from the underlying
// token provider. It ensures that a valid token is correctly retrieved and
// formatted into the required "authorization" metadata header with the
// "Bearer " prefix.
func (s) TestGCPServiceAccountIdentityCallCreds_GetRequestMetadata(t *testing.T) {
const token = "token"
stubToken := setupStubTokenProvider(token, nil)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
creds := setupTestGCPServiceAccountIdentityCreds(ctx, t, stubToken)
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &gcpTestAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}},
})
md, err := creds.GetRequestMetadata(ctx)
if err != nil {
t.Fatalf("GetRequestMetadata() failed: %v", err)
}
want := "Bearer " + token
if got := md["authorization"]; got != want {
t.Errorf("Unexpected token from GetRequestMetadata(), got %q, want %q", got, want)
}
}
// Test verifies that when a backoff delay is set, a token fetch failure caches
// the failure error, and subsequent calls to GetRequestMetadata before the
// backoff window expires return the cached error immediately without
// initiating another fetch request to the provider.
func (s) TestGCPServiceAccountIdentityCallCreds_Backoff(t *testing.T) {
const wantErr = "failed while fetching idToken"
// Override the backoff strategy with a very large value to guarantee
// it doesn't expire during this test.
origBackoff := internal.BackoffStrategy
internal.BackoffStrategy = &stubBackoff{backoffDelay: 2 * defaultTestTimeout}
t.Cleanup(func() { internal.BackoffStrategy = origBackoff })
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
stubToken := setupStubTokenProvider("token", errors.New(wantErr))
creds := setupTestGCPServiceAccountIdentityCreds(ctx, t, stubToken)
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &gcpTestAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}},
})
// First call triggers initial fetch, which fails.
if _, err := creds.GetRequestMetadata(ctx); err == nil || !strings.Contains(err.Error(), wantErr) {
t.Fatalf("GetRequestMetadata() failed with err = %v, want error %q", err, wantErr)
}
// Verify that calls within the backoff window fail immediately with
// the cached error without attempting a new token fetch.
const numCalls = 3
for i := 0; i < numCalls; i++ {
if _, err := creds.GetRequestMetadata(ctx); err == nil || !strings.Contains(err.Error(), wantErr) {
t.Fatalf("GetRequestMetadata() attempt %d failed with err = %v, want error %q", i, err, wantErr)
}
}
// Verify that no subsequent fetch attempt made.
if got := stubToken.getCallCount(); got != 1 {
t.Fatalf("Unexpected call count to token provider: got %d, want 1", got)
}
}
// Test verifies that when backoff expired, a subsequent request after an
// initial failure triggers a new fetch attempt and returns the new error.
func (s) TestGCPServiceAccountIdentityCallCreds_BackoffExpired(t *testing.T) {
const (
wantErr = "failed while fetching idToken"
wantErr2 = "second attempt to fetch token"
)
// Override the backoff strategy with a short delay.
const backoffDelay = 10 * time.Millisecond
origBackoff := internal.BackoffStrategy
internal.BackoffStrategy = &stubBackoff{backoffDelay: backoffDelay}
t.Cleanup(func() { internal.BackoffStrategy = origBackoff })
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
stubToken := setupStubTokenProvider("token", errors.New(wantErr))
creds := setupTestGCPServiceAccountIdentityCreds(ctx, t, stubToken)
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &gcpTestAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}},
})
// First call triggers token fetch and fails.
if _, err := creds.GetRequestMetadata(ctx); err == nil || !strings.Contains(err.Error(), wantErr) {
t.Fatalf("GetRequestMetadata() failed with err = %v, want error %q", err, wantErr)
}
if got := stubToken.getCallCount(); got != 1 {
t.Fatalf("Unexpected call count to token provider: got %d, want 1", got)
}
// Wait for backoff to expire.
select {
case <-time.After(2 * backoffDelay):
case <-ctx.Done():
t.Fatal("Timed out waiting for backoff to expire")
}
// Update token provider with second failure error to distinguish the
// new attempt.
stubToken.setErr(errors.New(wantErr2))
// Since backoff is 0s (already expired), the next request
// immediately triggers a new fetch attempt.
if _, err := creds.GetRequestMetadata(ctx); err == nil || !strings.Contains(err.Error(), wantErr2) {
t.Fatalf("GetRequestMetadata() failed with err = %v, want error %q", err, wantErr2)
}
// Verify that a second token fetch actually happened.
if got := stubToken.getCallCount(); got != 2 {
t.Fatalf("Unexpected call count to token provider: got %d, want 2", got)
}
}
// Test verifies that when backoff expires, a subsequent request after an
// initial failure triggers a new fetch attempt and returns the new token.
func (s) TestGCPServiceAccountIdentityCallCreds_BackoffExpiredRecovery(t *testing.T) {
const (
wantErr = "failed while fetching idToken"
token = "token"
)
// Override the backoff strategy with a short delay.
const backoffDelay = 10 * time.Millisecond
origBackoff := internal.BackoffStrategy
internal.BackoffStrategy = &stubBackoff{backoffDelay: backoffDelay}
t.Cleanup(func() { internal.BackoffStrategy = origBackoff })
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
stubToken := setupStubTokenProvider(token, errors.New(wantErr))
creds := setupTestGCPServiceAccountIdentityCreds(ctx, t, stubToken)
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &gcpTestAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}},
})
// First call triggers token fetch and fails.
if _, err := creds.GetRequestMetadata(ctx); err == nil || !strings.Contains(err.Error(), wantErr) {
t.Fatalf("GetRequestMetadata() failed with err = %v, want error %q", err, wantErr)
}
if got := stubToken.getCallCount(); got != 1 {
t.Fatalf("Unexpected call count to token provider: got %d, want 1", got)
}
// Wait for backoff to expire.
select {
case <-time.After(2 * backoffDelay):
case <-ctx.Done():
t.Fatal("Timed out waiting for backoff to expire")
}
// Update token provider to return a valid token and nil error.
stubToken.setErr(nil)
md, err := creds.GetRequestMetadata(ctx)
if err != nil {
t.Fatalf("GetRequestMetadata() failed unexpectedly: %v", err)
}
want := "Bearer " + token
if got := md["authorization"]; got != want {
t.Errorf("GetRequestMetadata() got %q, want %q", got, want)
}
// Verify that the second fetch happened successfully.
if got := stubToken.getCallCount(); got != 2 {
t.Fatalf("Unexpected call count to token provider: got %d, want 2", got)
}
}
// Test verifies that when multiple goroutines request a token concurrently,
// only a single fetch is executed and all blocked requests successfully
// receive the same valid token.
func (s) TestGCPServiceAccountIdentityCallCreds_ConcurrentCalls_Success(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
stubToken := setupStubTokenProvider("token", nil)
creds := setupTestGCPServiceAccountIdentityCreds(ctx, t, stubToken)
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &gcpTestAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}},
})
const numCalls = 5
errs := make([]error, numCalls)
var wg sync.WaitGroup
for i := range numCalls {
wg.Add(1)
go func(idx int) {
defer wg.Done()
_, errs[idx] = creds.GetRequestMetadata(ctx)
}(i)
}
wg.Wait()
for _, err := range errs {
if err != nil {
t.Fatalf("Concurrent call to GetRequestMetadata() failed with err = %v", err)
}
}
if got := stubToken.getCallCount(); got != 1 {
t.Fatalf("Unexpected call count to token provider: got %d, want 1", got)
}
}
// Test verifies that when multiple goroutines request a token concurrently,
// and the first fetch fails, all blocked requests receives the exact same
// error after the fetch.
func (s) TestGCPServiceAccountIdentityCallCreds_ConcurrentCalls_Failure(t *testing.T) {
const wantErr = "failed while fetching idToken"
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
stubToken := setupStubTokenProvider("token", errors.New(wantErr))
creds := setupTestGCPServiceAccountIdentityCreds(ctx, t, stubToken)
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &gcpTestAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}},
})
const numCalls = 5
errs := make([]error, numCalls)
var wg sync.WaitGroup
for i := range numCalls {
wg.Add(1)
go func(idx int) {
defer wg.Done()
_, errs[idx] = creds.GetRequestMetadata(ctx)
}(i)
}
wg.Wait()
for _, err := range errs {
if err == nil || !strings.Contains(err.Error(), wantErr) {
t.Fatalf("Concurrent call to GetRequestMetadata() failed with err = %v, want error containing %q", err, wantErr)
}
}
if got := stubToken.getCallCount(); got != 1 {
t.Fatalf("Unexpected call count to token provider: got %d, want 1", got)
}
}
// Test verifies that credentials fail to return metadata when the security
// level of the connection is not secure.
func (s) TestGCPServiceAccountIdentityCallCreds_SecurityLevelFailure(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
stubToken := setupStubTokenProvider("token", nil)
creds := setupTestGCPServiceAccountIdentityCreds(ctx, t, stubToken)
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &gcpTestAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}},
})
const wantErr = "cannot send secure credentials on an insecure connection"
_, err := creds.GetRequestMetadata(ctx)
if err == nil || !strings.Contains(err.Error(), wantErr) {
t.Errorf("GetRequestMetadata() failed with error = %v, want error %q", err, wantErr)
}
}
// Test verifies that when a cached token is within the early expiration window
// (stale but still valid), the second call to GetRequestMetadata immediately
// returns the existing token and triggers an asynchronous background fetch for
// a new token. A subsequent call, after waiting for the background fetch to
// complete, returns the newly acquired token.
func (s) TestGCPServiceAccountIdentityCallCreds_EarlyExpiry(t *testing.T) {
const (
firstToken = "token-A"
secondToken = "token-B"
)
stubToken := setupStubTokenProvider(firstToken, nil)
stubToken.setToken(
&auth.Token{
Value: firstToken,
// Keeping it 1 minute as we subtract 30 seconds from the token expiry.
Expiry: time.Now().Add(1 * time.Minute),
},
)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
creds := setupTestGCPServiceAccountIdentityCreds(ctx, t, stubToken)
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &gcpTestAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}},
})
md, err := creds.GetRequestMetadata(ctx)
if err != nil {
t.Fatalf("GetRequestMetadata() failed: %v", err)
}
wantfirstToken := "Bearer " + firstToken
if got := md["authorization"]; got != wantfirstToken {
t.Errorf("GetRequestMetadata() returned %q, want %q", got, wantfirstToken)
}
if got := stubToken.getCallCount(); got != 1 {
t.Fatalf("Unexpected call count to token provider: got %d, want 1", got)
}
// Update stub token provider to return a new token
stubToken.setToken(&auth.Token{Value: secondToken, Expiry: time.Now().Add(1 * time.Hour)})
// The cached token has not expired yet but is stale (the current time is
// past the preemptiveTokenRefresh window). GetRequestMetadata should
// immediately return the cached first token to avoid blocking the RPC, and
// asynchronously trigger a background fetch for the new token.
md, err = creds.GetRequestMetadata(ctx)
if err != nil {
t.Fatalf("GetRequestMetadata() failed: %v", err)
}
if got := md["authorization"]; got != wantfirstToken {
t.Errorf("GetRequestMetadata() returned %q, want %q", got, wantfirstToken)
}
wantSecondToken := "Bearer " + secondToken
for ; ctx.Err() == nil; <-time.After(10 * time.Millisecond) {
md, err = creds.GetRequestMetadata(ctx)
if err != nil {
t.Fatalf("GetRequestMetadata() failed: %v", err)
}
if md["authorization"] == wantSecondToken {
// Verify that only 2 fetch calls are made.
if got := stubToken.getCallCount(); got != 2 {
t.Fatalf("Unexpected call count to token provider: got %d, want 2", got)
}
break
}
}
if ctx.Err() != nil {
t.Fatal("timed out waiting for background fetch to update token")
}
}
// Test verifies that different types of errors from the metadata server are
// mapped to the correct gRPC status codes when returned through
// GetRequestMetadata.
func (s) TestGCPServiceAccountIdentityCallCreds_ErrorMapping(t *testing.T) {
tests := []struct {
name string
err error
want codes.Code
}{
{
name: "429_too_many_requests",
err: &metadata.Error{Code: 429, Message: "too many requests"},
want: codes.Unavailable,
},
{
name: "503_service_unavailable",
err: &metadata.Error{Code: 503, Message: "unavailable"},
want: codes.Unavailable,
},
{
name: "403_forbidden",
err: &metadata.Error{Code: 403, Message: "forbidden"},
want: codes.Unauthenticated,
},
{
name: "generic_protocol_error",
err: fmt.Errorf("generic connection error"),
want: codes.Unavailable,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
stubToken := setupStubTokenProvider("token", nil)
creds := setupTestGCPServiceAccountIdentityCreds(ctx, t, stubToken)
stubToken.setErr(tc.err)
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &gcpTestAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}},
})
if _, err := creds.GetRequestMetadata(ctx); status.Code(err) != tc.want {
t.Errorf("GetRequestMetadata() failed with gRPC status code %v, want %v for error %v", status.Code(err), tc.want, tc.err)
}
})
}
}

View File

@@ -0,0 +1,178 @@
/*
*
* Copyright 2018 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 google defines credentials for google cloud services.
package google
import (
"context"
"fmt"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/alts"
"google.golang.org/grpc/credentials/oauth"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/internal"
)
const defaultCloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"
var logger = grpclog.Component("credentials")
// DefaultCredentialsOptions constructs options to build DefaultCredentials.
type DefaultCredentialsOptions struct {
// PerRPCCreds is a per RPC credentials that is passed to a bundle.
PerRPCCreds credentials.PerRPCCredentials
// ALTSPerRPCCreds is a per RPC credentials that, if specified, will
// supercede PerRPCCreds above for and only for ALTS connections.
ALTSPerRPCCreds credentials.PerRPCCredentials
}
// NewDefaultCredentialsWithOptions returns a credentials bundle that is
// configured to work with google services.
//
// This API is experimental.
func NewDefaultCredentialsWithOptions(opts DefaultCredentialsOptions) credentials.Bundle {
if opts.PerRPCCreds == nil {
var err error
// If the ADC ends up being Compute Engine Credentials, this context
// won't be used. Otherwise, the context dictates all the subsequent
// token requests via HTTP. So we cannot have any deadline or timeout.
opts.PerRPCCreds, err = newADC(context.TODO())
if err != nil {
logger.Warningf("NewDefaultCredentialsWithOptions: failed to create application oauth: %v", err)
}
}
if opts.ALTSPerRPCCreds != nil {
opts.PerRPCCreds = &dualPerRPCCreds{
perRPCCreds: opts.PerRPCCreds,
altsPerRPCCreds: opts.ALTSPerRPCCreds,
}
}
c := &creds{opts: opts}
bundle, err := c.NewWithMode(internal.CredsBundleModeFallback)
if err != nil {
logger.Warningf("NewDefaultCredentialsWithOptions: failed to create new creds: %v", err)
}
return bundle
}
// NewDefaultCredentials returns a credentials bundle that is configured to work
// with google services.
//
// This API is experimental.
func NewDefaultCredentials() credentials.Bundle {
return NewDefaultCredentialsWithOptions(DefaultCredentialsOptions{})
}
// NewComputeEngineCredentials returns a credentials bundle that is configured to work
// with google services. This API must only be used when running on GCE. Authentication configured
// by this API represents the GCE VM's default service account.
//
// This API is experimental.
func NewComputeEngineCredentials() credentials.Bundle {
return NewDefaultCredentialsWithOptions(DefaultCredentialsOptions{
PerRPCCreds: oauth.NewComputeEngine(),
})
}
// creds implements credentials.Bundle.
type creds struct {
opts DefaultCredentialsOptions
// Supported modes are defined in internal/internal.go.
mode string
// The active transport credentials associated with this bundle.
transportCreds credentials.TransportCredentials
// The active per RPC credentials associated with this bundle.
perRPCCreds credentials.PerRPCCredentials
}
func (c *creds) TransportCredentials() credentials.TransportCredentials {
return c.transportCreds
}
func (c *creds) PerRPCCredentials() credentials.PerRPCCredentials {
if c == nil {
return nil
}
return c.perRPCCreds
}
var (
newTLS = func() credentials.TransportCredentials {
return credentials.NewTLS(nil)
}
newALTS = func() credentials.TransportCredentials {
return alts.NewClientCreds(alts.DefaultClientOptions())
}
newADC = func(ctx context.Context) (credentials.PerRPCCredentials, error) {
return oauth.NewApplicationDefault(ctx, defaultCloudPlatformScope)
}
)
// NewWithMode should make a copy of Bundle, and switch mode. Modifying the
// existing Bundle may cause races.
func (c *creds) NewWithMode(mode string) (credentials.Bundle, error) {
newCreds := &creds{
opts: c.opts,
mode: mode,
}
// Create transport credentials.
switch mode {
case internal.CredsBundleModeFallback:
newCreds.transportCreds = newClusterTransportCreds(newTLS(), newALTS())
case internal.CredsBundleModeBackendFromBalancer, internal.CredsBundleModeBalancer:
// Only the clients can use google default credentials, so we only need
// to create new ALTS client creds here.
newCreds.transportCreds = newALTS()
default:
return nil, fmt.Errorf("unsupported mode: %v", mode)
}
if mode == internal.CredsBundleModeFallback || mode == internal.CredsBundleModeBackendFromBalancer {
newCreds.perRPCCreds = newCreds.opts.PerRPCCreds
}
return newCreds, nil
}
// dualPerRPCCreds implements credentials.PerRPCCredentials by embedding the
// fallback PerRPCCredentials and the ALTS one. It pickes one of them based on
// the channel type.
type dualPerRPCCreds struct {
perRPCCreds credentials.PerRPCCredentials
altsPerRPCCreds credentials.PerRPCCredentials
}
func (d *dualPerRPCCreds) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
ri, ok := credentials.RequestInfoFromContext(ctx)
if !ok {
return nil, fmt.Errorf("request info not found from context")
}
if authType := ri.AuthInfo.AuthType(); authType == "alts" {
return d.altsPerRPCCreds.GetRequestMetadata(ctx, uri...)
}
// This ensures backward compatibility even if authType is not "tls".
return d.perRPCCreds.GetRequestMetadata(ctx, uri...)
}
func (d *dualPerRPCCreds) RequireTransportSecurity() bool {
return d.altsPerRPCCreds.RequireTransportSecurity() || d.perRPCCreds.RequireTransportSecurity()
}

View File

@@ -0,0 +1,268 @@
/*
*
* Copyright 2021 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 google
import (
"context"
"net"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/credentials"
icredentials "google.golang.org/grpc/internal/credentials"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/xds"
"google.golang.org/grpc/resolver"
)
var defaultTestTimeout = 10 * time.Second
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
type testCreds struct {
credentials.TransportCredentials
typ string
}
func (c *testCreds) ClientHandshake(context.Context, string, net.Conn) (net.Conn, credentials.AuthInfo, error) {
return nil, &testAuthInfo{typ: c.typ}, nil
}
func (c *testCreds) ServerHandshake(net.Conn) (net.Conn, credentials.AuthInfo, error) {
return nil, &testAuthInfo{typ: c.typ}, nil
}
type testAuthInfo struct {
typ string
}
func (t *testAuthInfo) AuthType() string {
return t.typ
}
type testPerRPCCreds struct {
md map[string]string
}
func (c *testPerRPCCreds) RequireTransportSecurity() bool {
return true
}
func (c *testPerRPCCreds) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
return c.md, nil
}
var (
testTLS = &testCreds{typ: "tls"}
testALTS = &testCreds{typ: "alts"}
)
func overrideNewCredsFuncs() func() {
origNewTLS := newTLS
newTLS = func() credentials.TransportCredentials {
return testTLS
}
origNewALTS := newALTS
newALTS = func() credentials.TransportCredentials {
return testALTS
}
origNewADC := newADC
newADC = func(context.Context) (credentials.PerRPCCredentials, error) {
// We do not use perRPC creds in this test. It is safe to return nil here.
return nil, nil
}
return func() {
newTLS = origNewTLS
newALTS = origNewALTS
newADC = origNewADC
}
}
// TestClientHandshakeBasedOnClusterName that by default (without switching
// modes), ClientHandshake does either tls or alts base on the cluster name in
// attributes.
func (s) TestClientHandshakeBasedOnClusterName(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
defer overrideNewCredsFuncs()()
for bundleTyp, tc := range map[string]credentials.Bundle{
"defaultCredsWithOptions": NewDefaultCredentialsWithOptions(DefaultCredentialsOptions{}),
"defaultCreds": NewDefaultCredentials(),
"computeCreds": NewComputeEngineCredentials(),
} {
tests := []struct {
name string
ctx context.Context
wantTyp string
}{
{
name: "no cluster name",
ctx: ctx,
wantTyp: "tls",
},
{
name: "with non-CFE cluster name",
ctx: icredentials.NewClientHandshakeInfoContext(ctx, credentials.ClientHandshakeInfo{
Attributes: xds.SetXDSHandshakeClusterName(resolver.Address{}, "lalala").Attributes,
}),
// non-CFE backends should use alts.
wantTyp: "alts",
},
{
name: "with CFE cluster name",
ctx: icredentials.NewClientHandshakeInfoContext(ctx, credentials.ClientHandshakeInfo{
Attributes: xds.SetXDSHandshakeClusterName(resolver.Address{}, "google_cfe_bigtable.googleapis.com").Attributes,
}),
// CFE should use tls.
wantTyp: "tls",
},
{
name: "with xdstp CFE cluster name",
ctx: icredentials.NewClientHandshakeInfoContext(ctx, credentials.ClientHandshakeInfo{
Attributes: xds.SetXDSHandshakeClusterName(resolver.Address{}, "xdstp://traffic-director-c2p.xds.googleapis.com/envoy.config.cluster.v3.Cluster/google_cfe_bigtable.googleapis.com").Attributes,
}),
// CFE should use tls.
wantTyp: "tls",
},
{
name: "with xdstp non-CFE cluster name",
ctx: icredentials.NewClientHandshakeInfoContext(ctx, credentials.ClientHandshakeInfo{
Attributes: xds.SetXDSHandshakeClusterName(resolver.Address{}, "xdstp://other.com/envoy.config.cluster.v3.Cluster/google_cfe_bigtable.googleapis.com").Attributes,
}),
// non-CFE should use atls.
wantTyp: "alts",
},
}
for _, tt := range tests {
t.Run(bundleTyp+" "+tt.name, func(t *testing.T) {
_, info, err := tc.TransportCredentials().ClientHandshake(tt.ctx, "", nil)
if err != nil {
t.Fatalf("ClientHandshake failed: %v", err)
}
if gotType := info.AuthType(); gotType != tt.wantTyp {
t.Fatalf("unexpected authtype: %v, want: %v", gotType, tt.wantTyp)
}
_, infoServer, err := tc.TransportCredentials().ServerHandshake(nil)
if err != nil {
t.Fatalf("ClientHandshake failed: %v", err)
}
// ServerHandshake should always do TLS.
if gotType := infoServer.AuthType(); gotType != "tls" {
t.Fatalf("unexpected server authtype: %v, want: %v", gotType, "tls")
}
})
}
}
}
func TestDefaultCredentialsWithOptions(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
md1 := map[string]string{"foo": "tls"}
md2 := map[string]string{"foo": "alts"}
tests := []struct {
desc string
defaultCredsOpts DefaultCredentialsOptions
authInfo credentials.AuthInfo
wantedMetadata map[string]string
}{
{
desc: "no ALTSPerRPCCreds with tls channel",
defaultCredsOpts: DefaultCredentialsOptions{
PerRPCCreds: &testPerRPCCreds{
md: md1,
},
},
authInfo: &testAuthInfo{typ: "tls"},
wantedMetadata: md1,
},
{
desc: "no ALTSPerRPCCreds with alts channel",
defaultCredsOpts: DefaultCredentialsOptions{
PerRPCCreds: &testPerRPCCreds{
md: md1,
},
},
authInfo: &testAuthInfo{typ: "alts"},
wantedMetadata: md1,
},
{
desc: "ALTSPerRPCCreds specified with tls channel",
defaultCredsOpts: DefaultCredentialsOptions{
PerRPCCreds: &testPerRPCCreds{
md: md1,
},
ALTSPerRPCCreds: &testPerRPCCreds{
md: md2,
},
},
authInfo: &testAuthInfo{typ: "tls"},
wantedMetadata: md1,
},
{
desc: "ALTSPerRPCCreds specified with alts channel",
defaultCredsOpts: DefaultCredentialsOptions{
PerRPCCreds: &testPerRPCCreds{
md: md1,
},
ALTSPerRPCCreds: &testPerRPCCreds{
md: md2,
},
},
authInfo: &testAuthInfo{typ: "alts"},
wantedMetadata: md2,
},
{
desc: "ALTSPerRPCCreds specified with unknown channel",
defaultCredsOpts: DefaultCredentialsOptions{
PerRPCCreds: &testPerRPCCreds{
md: md1,
},
ALTSPerRPCCreds: &testPerRPCCreds{
md: md2,
},
},
authInfo: &testAuthInfo{typ: "foo"},
wantedMetadata: md1,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
bundle := NewDefaultCredentialsWithOptions(tc.defaultCredsOpts)
ri := credentials.RequestInfo{AuthInfo: tc.authInfo}
ctx := credentials.NewContextWithRequestInfo(ctx, ri)
got, err := bundle.PerRPCCredentials().GetRequestMetadata(ctx, "uri")
if err != nil {
t.Fatalf("Bundle's PerRPCCredentials().GetRequestMetadata() unexpected error = %v", err)
}
if diff := cmp.Diff(got, tc.wantedMetadata); diff != "" {
t.Errorf("Unexpected request metadata from bundle's PerRPCCredentials. Diff (-got +want):\n%v", diff)
}
})
}
}

View File

@@ -0,0 +1,35 @@
/*
*
* Copyright 2026 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 internal contains functionality internal to the google package.
package internal
import (
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/idtoken"
"google.golang.org/grpc/internal/backoff"
)
// The following variables are overridden in tests.
var (
// BackoffStrategy is the backoff strategy to use when token fetch fails.
BackoffStrategy backoff.Strategy
// NewIDTokenCredentials builds idtoken credentials using specified options.
NewIDTokenCredentials func(opts *idtoken.Options) (*auth.Credentials, error)
)

128
credentials/google/xds.go Normal file
View File

@@ -0,0 +1,128 @@
/*
*
* Copyright 2021 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 google
import (
"context"
"net"
"net/url"
"strings"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/xds"
)
const cfeClusterNamePrefix = "google_cfe_"
const cfeClusterResourceNamePrefix = "/envoy.config.cluster.v3.Cluster/google_cfe_"
const cfeClusterAuthorityName = "traffic-director-c2p.xds.googleapis.com"
// clusterTransportCreds is a combo of TLS + ALTS.
//
// On the client, ClientHandshake picks TLS or ALTS based on address attributes.
// - if attributes has cluster name
// - if cluster name has prefix "google_cfe_", or
// "xdstp://traffic-director-c2p.xds.googleapis.com/envoy.config.cluster.v3.Cluster/google_cfe_",
// use TLS
// - otherwise, use ALTS
//
// - else, do TLS
//
// On the server, ServerHandshake always does TLS.
type clusterTransportCreds struct {
tls credentials.TransportCredentials
alts credentials.TransportCredentials
}
func newClusterTransportCreds(tls, alts credentials.TransportCredentials) *clusterTransportCreds {
return &clusterTransportCreds{
tls: tls,
alts: alts,
}
}
// clusterName returns the xDS cluster name stored in the attributes in the
// context.
func clusterName(ctx context.Context) string {
chi := credentials.ClientHandshakeInfoFromContext(ctx)
if chi.Attributes == nil {
return ""
}
cluster, _ := xds.GetXDSHandshakeClusterName(chi.Attributes)
return cluster
}
// isDirectPathCluster returns true if the cluster in the context is a
// directpath cluster, meaning ALTS should be used.
func isDirectPathCluster(ctx context.Context) bool {
cluster := clusterName(ctx)
if cluster == "" {
// No cluster; not xDS; use TLS.
return false
}
if strings.HasPrefix(cluster, cfeClusterNamePrefix) {
// xDS cluster prefixed by "google_cfe_"; use TLS.
return false
}
if !strings.HasPrefix(cluster, "xdstp:") {
// Other xDS cluster name; use ALTS.
return true
}
u, err := url.Parse(cluster)
if err != nil {
// Shouldn't happen, but assume ALTS.
return true
}
// If authority AND path match our CFE checks, use TLS; otherwise use ALTS.
return u.Host != cfeClusterAuthorityName || !strings.HasPrefix(u.Path, cfeClusterResourceNamePrefix)
}
func (c *clusterTransportCreds) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
if isDirectPathCluster(ctx) {
// If attributes have cluster name, and cluster name is not cfe, it's a
// backend address, use ALTS.
return c.alts.ClientHandshake(ctx, authority, rawConn)
}
return c.tls.ClientHandshake(ctx, authority, rawConn)
}
func (c *clusterTransportCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
return c.tls.ServerHandshake(conn)
}
func (c *clusterTransportCreds) Info() credentials.ProtocolInfo {
// TODO: this always returns tls.Info now, because we don't have a cluster
// name to check when this method is called. This method doesn't affect
// anything important now. We may want to revisit this if it becomes more
// important later.
return c.tls.Info()
}
func (c *clusterTransportCreds) Clone() credentials.TransportCredentials {
return &clusterTransportCreds{
tls: c.tls.Clone(),
alts: c.alts.Clone(),
}
}
func (c *clusterTransportCreds) OverrideServerName(s string) error {
if err := c.tls.OverrideServerName(s); err != nil {
return err
}
return c.alts.OverrideServerName(s)
}

View File

@@ -0,0 +1,60 @@
/*
*
* Copyright 2021 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 google
import (
"context"
"testing"
"google.golang.org/grpc/credentials"
icredentials "google.golang.org/grpc/internal/credentials"
"google.golang.org/grpc/internal/xds"
"google.golang.org/grpc/resolver"
)
func (s) TestIsDirectPathCluster(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
c := func(cluster string) context.Context {
return icredentials.NewClientHandshakeInfoContext(ctx, credentials.ClientHandshakeInfo{
Attributes: xds.SetXDSHandshakeClusterName(resolver.Address{}, cluster).Attributes,
})
}
testCases := []struct {
name string
ctx context.Context
want bool
}{
{"not an xDS cluster", ctx, false},
{"cfe", c("google_cfe_bigtable.googleapis.com"), false},
{"non-cfe", c("google_bigtable.googleapis.com"), true},
{"starts with xdstp but not cfe format", c("xdstp:google_cfe_bigtable.googleapis.com"), true},
{"no authority", c("xdstp:///envoy.config.cluster.v3.Cluster/google_cfe_"), true},
{"wrong authority", c("xdstp://foo.bar/envoy.config.cluster.v3.Cluster/google_cfe_"), true},
{"xdstp CFE", c("xdstp://traffic-director-c2p.xds.googleapis.com/envoy.config.cluster.v3.Cluster/google_cfe_"), false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := isDirectPathCluster(tc.ctx); got != tc.want {
t.Errorf("isDirectPathCluster(_) = %v; want %v", got, tc.want)
}
})
}
}

View File

@@ -0,0 +1,104 @@
/*
*
* 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 insecure provides an implementation of the
// credentials.TransportCredentials interface which disables transport security.
package insecure
import (
"context"
"net"
"google.golang.org/grpc/credentials"
)
// NewCredentials returns a credentials which disables transport security.
//
// Note that using this credentials with per-RPC credentials which require
// transport security is incompatible and will cause RPCs to fail.
func NewCredentials() credentials.TransportCredentials {
return insecureTC{}
}
// insecureTC implements the insecure transport credentials. The handshake
// methods simply return the passed in net.Conn and set the security level to
// NoSecurity.
type insecureTC struct{}
func (insecureTC) ClientHandshake(_ context.Context, _ string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
return conn, info{credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}}, nil
}
func (insecureTC) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
return conn, info{credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}}, nil
}
func (insecureTC) Info() credentials.ProtocolInfo {
return credentials.ProtocolInfo{SecurityProtocol: "insecure"}
}
func (insecureTC) Clone() credentials.TransportCredentials {
return insecureTC{}
}
func (insecureTC) OverrideServerName(string) error {
return nil
}
// info contains the auth information for an insecure connection.
// It implements the AuthInfo interface.
type info struct {
credentials.CommonAuthInfo
}
// AuthType returns the type of info as a string.
func (info) AuthType() string {
return "insecure"
}
// ValidateAuthority allows any value to be overridden for the :authority
// header.
func (info) ValidateAuthority(string) error {
return nil
}
// insecureBundle implements an insecure bundle.
// An insecure bundle provides a thin wrapper around insecureTC to support
// the credentials.Bundle interface.
type insecureBundle struct{}
// NewBundle returns a bundle with disabled transport security and no per rpc credential.
func NewBundle() credentials.Bundle {
return insecureBundle{}
}
// NewWithMode returns a new insecure Bundle. The mode is ignored.
func (insecureBundle) NewWithMode(string) (credentials.Bundle, error) {
return insecureBundle{}, nil
}
// PerRPCCredentials returns an nil implementation as insecure
// bundle does not support a per rpc credential.
func (insecureBundle) PerRPCCredentials() credentials.PerRPCCredentials {
return nil
}
// TransportCredentials returns the underlying insecure transport credential.
func (insecureBundle) TransportCredentials() credentials.TransportCredentials {
return NewCredentials()
}

50
credentials/jwt/doc.go Normal file
View File

@@ -0,0 +1,50 @@
/*
*
* Copyright 2025 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 jwt implements JWT token file-based call credentials.
//
// This package provides support for A97 JWT Call Credentials, allowing gRPC
// clients to authenticate using JWT tokens read from files. While originally
// designed for xDS environments, these credentials are general-purpose.
//
// The credentials can be used directly in gRPC clients or configured via xDS.
//
// # Token Requirements
//
// JWT tokens must:
// - Be valid, well-formed JWT tokens with header, payload, and signature
// - Include an "exp" (expiration) claim
// - Be readable from the specified file path
//
// # Considerations
//
// - Tokens are cached until expiration to avoid excessive file I/O
// - Transport security is required (RequireTransportSecurity returns true)
// - Errors in reading tokens or parsing JWTs will result in RPC UNAVAILALBE or
// UNAUTHENTICATED errors. The errors are cached and retried with exponential
// backoff.
//
// This implementation is originally intended for use in service mesh
// environments like Istio where JWT tokens are provisioned and rotated by the
// infrastructure.
//
// # Experimental
//
// Notice: All APIs in this package are experimental and may be removed in a
// later release.
package jwt

View File

@@ -0,0 +1,118 @@
/*
*
* Copyright 2025 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 jwt
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"
)
var (
errTokenFileAccess = errors.New("token file access error")
errJWTValidation = errors.New("invalid JWT")
)
// jwtClaims represents the JWT claims structure for extracting expiration time.
type jwtClaims struct {
Exp int64 `json:"exp"`
}
// jwtFileReader handles reading and parsing JWT tokens from files.
// It is safe to call methods on this type concurrently as no state is stored.
type jwtFileReader struct {
tokenFilePath string
}
// readToken reads and parses a JWT token from the configured file.
// Returns the token string, expiration time, and any error encountered.
func (r *jwtFileReader) readToken() (string, time.Time, error) {
tokenBytes, err := os.ReadFile(r.tokenFilePath)
if err != nil {
return "", time.Time{}, fmt.Errorf("%v: %w", err, errTokenFileAccess)
}
token := strings.TrimSpace(string(tokenBytes))
if token == "" {
return "", time.Time{}, fmt.Errorf("token file %q is empty: %w", r.tokenFilePath, errJWTValidation)
}
exp, err := r.extractExpiration(token)
if err != nil {
return "", time.Time{}, fmt.Errorf("token file %q: %v: %w", r.tokenFilePath, err, errJWTValidation)
}
return token, exp, nil
}
const tokenDelim = "."
// extractClaimsRaw returns the JWT's claims part as raw string. Even though the
// header and signature are not used, it still expects that the input string to
// be well-formed (ie comprised of exactly three parts, separated by a dot
// character).
func extractClaimsRaw(s string) (string, bool) {
_, s, ok := strings.Cut(s, tokenDelim)
if !ok { // no period found
return "", false
}
claims, s, ok := strings.Cut(s, tokenDelim)
if !ok { // only one period found
return "", false
}
_, _, ok = strings.Cut(s, tokenDelim)
if ok { // three periods found
return "", false
}
return claims, true
}
// extractExpiration parses the JWT token to extract the expiration time.
func (r *jwtFileReader) extractExpiration(token string) (time.Time, error) {
claimsRaw, ok := extractClaimsRaw(token)
if !ok {
return time.Time{}, fmt.Errorf("expected 3 parts in token")
}
payloadBytes, err := base64.RawURLEncoding.DecodeString(claimsRaw)
if err != nil {
return time.Time{}, fmt.Errorf("decode error: %v", err)
}
var claims jwtClaims
if err := json.Unmarshal(payloadBytes, &claims); err != nil {
return time.Time{}, fmt.Errorf("unmarshal error: %v", err)
}
if claims.Exp == 0 {
return time.Time{}, fmt.Errorf("no expiration claims")
}
expTime := time.Unix(claims.Exp, 0)
// Check if token is already expired.
if expTime.Before(time.Now()) {
return time.Time{}, fmt.Errorf("expired token")
}
return expTime, nil
}

View File

@@ -0,0 +1,172 @@
/*
*
* Copyright 2025 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 jwt
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
)
func (s) TestJWTFileReader_ReadToken_FileErrors(t *testing.T) {
tests := []struct {
name string
create bool
contents string
wantErr error
}{
{
name: "nonexistent_file",
create: false,
contents: "",
wantErr: errTokenFileAccess,
},
{
name: "empty_file",
create: true,
contents: "",
wantErr: errJWTValidation,
},
{
name: "file_with_whitespace_only",
create: true,
contents: " \n\t ",
wantErr: errJWTValidation,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var tokenFile string
if !tt.create {
tokenFile = "/does-not-exist"
} else {
tokenFile = writeTempFile(t, "token", tt.contents)
}
reader := jwtFileReader{tokenFilePath: tokenFile}
if _, _, err := reader.readToken(); err == nil {
t.Fatal("ReadToken() expected error, got nil")
} else if !errors.Is(err, tt.wantErr) {
t.Fatalf("ReadToken() error = %v, want error %v", err, tt.wantErr)
}
})
}
}
func (s) TestJWTFileReader_ReadToken_InvalidJWT(t *testing.T) {
now := time.Now().Truncate(time.Second)
tests := []struct {
name string
tokenContent string
wantErr error
}{
{
name: "valid_token_without_expiration",
tokenContent: createTestJWT(t, time.Time{}),
wantErr: errJWTValidation,
},
{
name: "expired_token",
tokenContent: createTestJWT(t, now.Add(-time.Hour)),
wantErr: errJWTValidation,
},
{
name: "malformed_JWT_not_enough_parts",
tokenContent: "invalid.jwt",
wantErr: errJWTValidation,
},
{
name: "malformed_JWT_invalid_base64",
tokenContent: "header.invalid_base64!@#.signature",
wantErr: errJWTValidation,
},
{
name: "malformed_JWT_invalid_JSON",
tokenContent: createInvalidJWT(t),
wantErr: errJWTValidation,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tokenFile := writeTempFile(t, "token", tt.tokenContent)
reader := jwtFileReader{tokenFilePath: tokenFile}
if _, _, err := reader.readToken(); err == nil {
t.Fatal("ReadToken() expected error, got nil")
} else if !errors.Is(err, tt.wantErr) {
t.Fatalf("ReadToken() error = %v, want error %v", err, tt.wantErr)
}
})
}
}
func (s) TestJWTFileReader_ReadToken_ValidToken(t *testing.T) {
now := time.Now().Truncate(time.Second)
tokenExp := now.Add(time.Hour)
token := createTestJWT(t, tokenExp)
tokenFile := writeTempFile(t, "token", token)
reader := jwtFileReader{tokenFilePath: tokenFile}
readToken, expiry, err := reader.readToken()
if err != nil {
t.Fatalf("ReadToken() unexpected error: %v", err)
}
if readToken != token {
t.Errorf("ReadToken() token = %q, want %q", readToken, token)
}
if !expiry.Equal(tokenExp) {
t.Errorf("ReadToken() expiry = %v, want %v", expiry, tokenExp)
}
}
// createInvalidJWT creates a JWT with invalid JSON in the payload.
func createInvalidJWT(t *testing.T) string {
t.Helper()
header := map[string]any{
"typ": "JWT",
"alg": "HS256",
}
headerBytes, err := json.Marshal(header)
if err != nil {
t.Fatalf("Failed to marshal header: %v", err)
}
headerB64 := base64.URLEncoding.EncodeToString(headerBytes)
headerB64 = strings.TrimRight(headerB64, "=")
// Create invalid JSON payload
invalidJSON := "invalid json content"
payloadB64 := base64.URLEncoding.EncodeToString([]byte(invalidJSON))
payloadB64 = strings.TrimRight(payloadB64, "=")
signature := base64.URLEncoding.EncodeToString([]byte("fake_signature"))
signature = strings.TrimRight(signature, "=")
return fmt.Sprintf("%s.%s.%s", headerB64, payloadB64, signature)
}

View File

@@ -0,0 +1,180 @@
/*
*
* Copyright 2025 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 jwt
import (
"context"
"errors"
"fmt"
"sync"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/backoff"
"google.golang.org/grpc/status"
)
const preemptiveRefreshThreshold = time.Minute
// jwtTokenFileCallCreds provides JWT token-based PerRPCCredentials that reads
// tokens from a file.
// This implementation follows the A97 JWT Call Credentials specification.
type jwtTokenFileCallCreds struct {
fileReader *jwtFileReader
backoffStrategy backoff.Strategy
// cached data protected by mu
mu sync.Mutex
cachedAuthHeader string // "Bearer " + token
cachedExpiry time.Time // Slightly less than actual expiration time
cachedError error // Error from last failed attempt
retryAttempt int // Current retry attempt number
nextRetryTime time.Time // When next retry is allowed
pendingRefresh bool // Whether a refresh is currently in progress
}
// NewTokenFileCallCredentials creates PerRPCCredentials that reads JWT tokens
// from the specified file path.
func NewTokenFileCallCredentials(tokenFilePath string) (credentials.PerRPCCredentials, error) {
if tokenFilePath == "" {
return nil, fmt.Errorf("tokenFilePath cannot be empty")
}
creds := &jwtTokenFileCallCreds{
fileReader: &jwtFileReader{tokenFilePath: tokenFilePath},
backoffStrategy: backoff.DefaultExponential,
}
return creds, nil
}
// GetRequestMetadata gets the current request metadata, refreshing tokens if
// required. This implementation follows the PerRPCCredentials interface. The
// tokens will get automatically refreshed if they are about to expire or if
// they haven't been loaded successfully yet.
// If it's not possible to extract a token from the file, UNAVAILABLE is
// returned.
// If the token is extracted but invalid, then UNAUTHENTICATED is returned.
// If errors are encoutered, a backoff is applied before retrying.
func (c *jwtTokenFileCallCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
ri, _ := credentials.RequestInfoFromContext(ctx)
if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
return nil, fmt.Errorf("cannot send secure credentials on an insecure connection: %v", err)
}
c.mu.Lock()
defer c.mu.Unlock()
if c.isTokenValidLocked() {
needsPreemptiveRefresh := time.Until(c.cachedExpiry) < preemptiveRefreshThreshold
if needsPreemptiveRefresh && !c.pendingRefresh {
// Start refresh if not pending (handling the prior RPC may have
// just spawned a goroutine).
c.pendingRefresh = true
go c.refreshToken()
}
return map[string]string{
"authorization": c.cachedAuthHeader,
}, nil
}
// If in backoff state, just return the cached error.
if c.cachedError != nil && time.Now().Before(c.nextRetryTime) {
return nil, c.cachedError
}
// At this point, the token is either invalid or expired and we are no
// longer backing off from any encountered errors. So refresh it.
// NB: We are holding the lock while reading the token from file. This will
// cause other RPCs to block until the read completes (sucecssfully or not)
// and the cache is updated. Subsequent RPCs will end up using the cache.
// This is per A97.
token, expiry, err := c.fileReader.readToken()
c.updateCacheLocked(token, expiry, err)
if c.cachedError != nil {
return nil, c.cachedError
}
return map[string]string{
"authorization": c.cachedAuthHeader,
}, nil
}
// RequireTransportSecurity indicates whether the credentials requires
// transport security.
func (c *jwtTokenFileCallCreds) RequireTransportSecurity() bool {
return true
}
// isTokenValidLocked checks if the cached token is still valid.
// Caller must hold c.mu lock.
func (c *jwtTokenFileCallCreds) isTokenValidLocked() bool {
if c.cachedAuthHeader == "" {
return false
}
return c.cachedExpiry.After(time.Now())
}
// refreshToken reads the token from file and updates the cached data.
func (c *jwtTokenFileCallCreds) refreshToken() {
// Deliberately not locking c.mu here. This way other RPCs can proceed
// while we read the token. This is per gRFC A97.
token, expiry, err := c.fileReader.readToken()
c.mu.Lock()
defer c.mu.Unlock()
c.updateCacheLocked(token, expiry, err)
c.pendingRefresh = false
}
// updateCacheLocked updates the cached token, expiry, and error state.
// If an error is provided, it determines whether to set it as an UNAVAILABLE
// or UNAUTHENTICATED error based on the error type.
// NOTE: This method (and its callers) do not queue up a token refresh/retry if
// the expiration is soon / an error was encountered. Instead, this is done when
// handling RPCs. This is as per gRFC A97, which states that it is
// undesirable to retry loading the token if the channel is idle.
// Caller must hold c.mu lock.
func (c *jwtTokenFileCallCreds) updateCacheLocked(token string, expiry time.Time, err error) {
if err != nil {
// Convert to gRPC status codes
if errors.Is(err, errTokenFileAccess) {
c.cachedError = status.Error(codes.Unavailable, err.Error())
} else if errors.Is(err, errJWTValidation) {
c.cachedError = status.Error(codes.Unauthenticated, err.Error())
} else {
// Should not happen. Treat unknown errors as UNAUTHENTICATED.
c.cachedError = status.Error(codes.Unauthenticated, err.Error())
}
c.retryAttempt++
backoffDelay := c.backoffStrategy.Backoff(c.retryAttempt - 1)
c.nextRetryTime = time.Now().Add(backoffDelay)
return
}
// Success - clear any cached error and update token cache
c.cachedError = nil
c.retryAttempt = 0
c.nextRetryTime = time.Time{}
c.cachedAuthHeader = "Bearer " + token
// Per gRFC A97: consider token invalid if it expires within the next 30
// seconds to accommodate for clock skew and server processing time.
c.cachedExpiry = expiry.Add(-30 * time.Second)
}

View File

@@ -0,0 +1,265 @@
/*
*
* Copyright 2026 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 jwt_test
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/credentials/jwt"
"google.golang.org/grpc/internal/stubserver"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/testdata"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
)
const defaultTestTimeout = 5 * time.Second
const wantErr = "cannot send secure credentials on an insecure connection"
// TestJWTCallCredentials_InsecureTransport_AsCallOption verifies that when JWT
// call credentials are passed as a per-RPC call option over an insecure
// transport, the RPC fails with a meaningful error.
func TestJWTCallCredentials_InsecureTransport_AsCallOption(t *testing.T) {
token := createTestJWT(t, time.Now().Add(time.Hour))
tokenFile := writeTempTokenFile(t, token)
jwtCreds, err := jwt.NewTokenFileCallCredentials(tokenFile)
if err != nil {
t.Fatalf("NewTokenFileCallCredentials(%q) failed: %v", tokenFile, err)
}
ss := &stubserver.StubServer{}
if err := ss.StartServer(grpc.Creds(insecure.NewCredentials())); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer ss.Stop()
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatalf("grpc.NewClient(%q) failed: %v", ss.Address, err)
}
defer cc.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
client := testgrpc.NewTestServiceClient(cc)
_, err = client.EmptyCall(ctx, &testpb.Empty{}, grpc.PerRPCCredentials(jwtCreds))
if err == nil || !strings.Contains(err.Error(), wantErr) {
t.Fatalf("EmptyCall() error = %v; want error containing %q", err, wantErr)
}
}
// TestJWTCallCredentials_InsecureTransport_AsDialOption verifies that when JWT
// call credentials are passed as a dial option over an insecure transport, the
// client creation fails with a meaningful error.
func TestJWTCallCredentials_InsecureTransport_AsDialOption(t *testing.T) {
token := createTestJWT(t, time.Now().Add(time.Hour))
tokenFile := writeTempTokenFile(t, token)
jwtCreds, err := jwt.NewTokenFileCallCredentials(tokenFile)
if err != nil {
t.Fatalf("NewTokenFileCallCredentials(%q) failed: %v", tokenFile, err)
}
ss := &stubserver.StubServer{}
if err := ss.StartServer(grpc.Creds(insecure.NewCredentials())); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer ss.Stop()
_, err = grpc.NewClient(ss.Address, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials(jwtCreds))
if err == nil || !strings.Contains(err.Error(), "the credentials require transport level security") {
t.Fatalf("grpc.NewClient() error = %v; want error containing %q", err, "the credentials require transport level security")
}
}
// TestJWTCallCredentials_SecureTransport_AsDialOption verifies that JWT call
// credentials work correctly when passed as a dial option over a secure TLS
// transport.
func TestJWTCallCredentials_SecureTransport_AsDialOption(t *testing.T) {
token := createTestJWT(t, time.Now().Add(time.Hour))
tokenFile := writeTempTokenFile(t, token)
jwtCreds, err := jwt.NewTokenFileCallCredentials(tokenFile)
if err != nil {
t.Fatalf("NewTokenFileCallCredentials(%q) failed: %v", tokenFile, err)
}
wantAuth := "Bearer " + token
ss := &stubserver.StubServer{
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, fmt.Errorf("no metadata received")
}
authHeaders := md.Get("authorization")
if len(authHeaders) != 1 || authHeaders[0] != wantAuth {
return nil, fmt.Errorf("authorization header mismatch: got %v, want %q", authHeaders, wantAuth)
}
return &testpb.Empty{}, nil
},
}
serverCert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
if err != nil {
t.Fatalf("Failed to load server cert: %v", err)
}
if err := ss.StartServer(grpc.Creds(credentials.NewServerTLSFromCert(&serverCert))); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer ss.Stop()
clientCreds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), "x.test.example.com")
if err != nil {
t.Fatalf("Failed to create client TLS credentials: %v", err)
}
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(clientCreds), grpc.WithPerRPCCredentials(jwtCreds))
if err != nil {
t.Fatalf("grpc.NewClient(%q) failed: %v", ss.Address, err)
}
defer cc.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
client := testgrpc.NewTestServiceClient(cc)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
}
// TestJWTCallCredentials_SecureTransport_AsCallOption verifies that JWT call
// credentials work correctly when passed as a per-RPC call option over a secure
// TLS transport.
func TestJWTCallCredentials_SecureTransport_AsCallOption(t *testing.T) {
token := createTestJWT(t, time.Now().Add(time.Hour))
tokenFile := writeTempTokenFile(t, token)
jwtCreds, err := jwt.NewTokenFileCallCredentials(tokenFile)
if err != nil {
t.Fatalf("NewTokenFileCallCredentials(%q) failed: %v", tokenFile, err)
}
wantAuth := "Bearer " + token
ss := &stubserver.StubServer{
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, fmt.Errorf("no metadata received")
}
authHeaders := md.Get("authorization")
if len(authHeaders) != 1 || authHeaders[0] != wantAuth {
return nil, fmt.Errorf("authorization header mismatch: got %v, want %q", authHeaders, wantAuth)
}
return &testpb.Empty{}, nil
},
}
serverCert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
if err != nil {
t.Fatalf("Failed to load server cert: %v", err)
}
if err := ss.StartServer(grpc.Creds(credentials.NewServerTLSFromCert(&serverCert))); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
defer ss.Stop()
clientCreds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), "x.test.example.com")
if err != nil {
t.Fatalf("Failed to create client TLS credentials: %v", err)
}
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(clientCreds))
if err != nil {
t.Fatalf("grpc.NewClient(%q) failed: %v", ss.Address, err)
}
defer cc.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
client := testgrpc.NewTestServiceClient(cc)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.PerRPCCredentials(jwtCreds)); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
}
// createTestJWT creates a test JWT token with the specified expiration.
func createTestJWT(t *testing.T, expiration time.Time) string {
t.Helper()
claims := map[string]any{}
if !expiration.IsZero() {
claims["exp"] = expiration.Unix()
}
header := map[string]any{
"typ": "JWT",
"alg": "HS256",
}
headerBytes, err := json.Marshal(header)
if err != nil {
t.Fatalf("Failed to marshal header: %v", err)
}
claimsBytes, err := json.Marshal(claims)
if err != nil {
t.Fatalf("Failed to marshal claims: %v", err)
}
headerB64 := base64.URLEncoding.EncodeToString(headerBytes)
claimsB64 := base64.URLEncoding.EncodeToString(claimsBytes)
// Remove padding for URL-safe base64.
headerB64 = strings.TrimRight(headerB64, "=")
claimsB64 = strings.TrimRight(claimsB64, "=")
// For testing, we use a fake signature.
signature := base64.URLEncoding.EncodeToString([]byte("fake_signature"))
signature = strings.TrimRight(signature, "=")
return fmt.Sprintf("%s.%s.%s", headerB64, claimsB64, signature)
}
// writeTempTokenFile writes the token to a temporary file and returns the path.
func writeTempTokenFile(t *testing.T, token string) string {
t.Helper()
tempDir := t.TempDir()
filePath := filepath.Join(tempDir, "token")
if err := os.WriteFile(filePath, []byte(token), 0600); err != nil {
t.Fatalf("Failed to write temp token file: %v", err)
}
return filePath
}

View File

@@ -0,0 +1,555 @@
/*
*
* Copyright 2025 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 jwt
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/status"
)
const defaultTestTimeout = 5 * time.Second
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
func (s) TestNewTokenFileCallCredentialsValidFilepath(t *testing.T) {
creds, err := NewTokenFileCallCredentials("/path/to/token")
if err != nil {
t.Fatalf("NewTokenFileCallCredentials() unexpected error: %v", err)
}
if creds == nil {
t.Fatal("NewTokenFileCallCredentials() returned nil credentials")
}
}
func (s) TestNewTokenFileCallCredentialsMissingFilepath(t *testing.T) {
if _, err := NewTokenFileCallCredentials(""); err == nil {
t.Fatalf("NewTokenFileCallCredentials() expected error, got nil")
}
}
func (s) TestTokenFileCallCreds_RequireTransportSecurity(t *testing.T) {
creds, err := NewTokenFileCallCredentials("/path/to/token")
if err != nil {
t.Fatalf("NewTokenFileCallCredentials() failed: %v", err)
}
if !creds.RequireTransportSecurity() {
t.Error("RequireTransportSecurity() = false, want true")
}
}
func (s) TestTokenFileCallCreds_GetRequestMetadata(t *testing.T) {
now := time.Now().Truncate(time.Second)
tests := []struct {
name string
invalidTokenPath bool
tokenContent string
authInfo credentials.AuthInfo
wantCode codes.Code
wantMetadata map[string]string
}{
{
name: "valid_token_with_future_expiration",
tokenContent: createTestJWT(t, now.Add(time.Hour)),
authInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
wantCode: codes.OK,
wantMetadata: map[string]string{"authorization": "Bearer " + createTestJWT(t, now.Add(time.Hour))},
},
{
name: "insufficient_security_level",
tokenContent: createTestJWT(t, now.Add(time.Hour)),
authInfo: &testAuthInfo{secLevel: credentials.NoSecurity},
wantCode: codes.Unknown, // http2Client.getCallAuthData actually transforms such errors into into Unauthenticated
},
{
name: "unreachable_token_file",
invalidTokenPath: true,
authInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
wantCode: codes.Unavailable,
},
{
name: "empty_file",
tokenContent: "",
authInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
wantCode: codes.Unauthenticated,
},
{
name: "malformed_JWT_token",
tokenContent: "bad contents",
authInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
wantCode: codes.Unauthenticated,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var tokenFile string
if tt.invalidTokenPath {
tokenFile = "/does-not-exist"
} else {
tokenFile = writeTempFile(t, "token", tt.tokenContent)
}
creds, err := NewTokenFileCallCredentials(tokenFile)
if err != nil {
t.Fatalf("NewTokenFileCallCredentials() failed: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: tt.authInfo,
})
metadata, err := creds.GetRequestMetadata(ctx)
if gotCode := status.Code(err); gotCode != tt.wantCode {
t.Fatalf("GetRequestMetadata() = %v, want %v", gotCode, tt.wantCode)
}
if diff := cmp.Diff(tt.wantMetadata, metadata); diff != "" {
t.Errorf("GetRequestMetadata() returned unexpected metadata (-want +got):\n%s", diff)
}
})
}
}
func (s) TestTokenFileCallCreds_TokenCaching(t *testing.T) {
token := createTestJWT(t, time.Now().Add(time.Hour))
tokenFile := writeTempFile(t, "token", token)
creds, err := NewTokenFileCallCredentials(tokenFile)
if err != nil {
t.Fatalf("NewTokenFileCallCredentials() failed: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
})
// First call should read from file.
metadata1, err := creds.GetRequestMetadata(ctx)
if err != nil {
t.Fatalf("First GetRequestMetadata() failed: %v", err)
}
wantMetadata := map[string]string{"authorization": "Bearer " + token}
if diff := cmp.Diff(wantMetadata, metadata1); diff != "" {
t.Errorf("First GetRequestMetadata() returned unexpected metadata (-want +got):\n%s", diff)
}
// Update the file with a different token.
newToken := createTestJWT(t, time.Now().Add(2*time.Hour))
if err := os.WriteFile(tokenFile, []byte(newToken), 0600); err != nil {
t.Fatalf("Failed to update token file: %v", err)
}
// Second call should return cached token (not the updated one).
metadata2, err := creds.GetRequestMetadata(ctx)
if err != nil {
t.Fatalf("Second GetRequestMetadata() failed: %v", err)
}
if diff := cmp.Diff(metadata1, metadata2); diff != "" {
t.Errorf("Second GetRequestMetadata() returned unexpected metadata (-want +got):\n%s", diff)
}
}
// testAuthInfo implements credentials.AuthInfo for testing.
type testAuthInfo struct {
secLevel credentials.SecurityLevel
}
func (t *testAuthInfo) AuthType() string {
return "test"
}
func (t *testAuthInfo) GetCommonAuthInfo() credentials.CommonAuthInfo {
return credentials.CommonAuthInfo{SecurityLevel: t.secLevel}
}
// Tests that cached token expiration is set to 30 seconds before actual token
// expiration.
// TODO: Refactor the test to avoid inspecting and mutating internal state.
func (s) TestTokenFileCallCreds_CacheExpirationIsBeforeTokenExpiration(t *testing.T) {
// Create token that expires in 2 hours.
tokenExp := time.Now().Truncate(time.Second).Add(2 * time.Hour)
token := createTestJWT(t, tokenExp)
tokenFile := writeTempFile(t, "token", token)
creds, err := NewTokenFileCallCredentials(tokenFile)
if err != nil {
t.Fatalf("NewTokenFileCallCredentials() failed: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
})
// Get token to trigger caching.
if _, err = creds.GetRequestMetadata(ctx); err != nil {
t.Fatalf("GetRequestMetadata() failed: %v", err)
}
// Verify cached expiration is 30 seconds before actual token expiration.
impl := creds.(*jwtTokenFileCallCreds)
impl.mu.Lock()
cachedExp := impl.cachedExpiry
impl.mu.Unlock()
wantExp := tokenExp.Add(-30 * time.Second)
if !cachedExp.Equal(wantExp) {
t.Errorf("Cache expiration = %v, want %v", cachedExp, wantExp)
}
}
// Tests that pre-emptive refresh is triggered within 1 minute of expiration.
// This is tested as follows:
// * A token which expires "soon" is created.
// * On the first call to GetRequestMetadata, the token will get loaded and returned.
// * Another token is created and overwrites the file.
// * On the second call we will still return the (valid) first token but also
// detect that a refresh needs to happen and trigger it.
// * On the third call we confirm the new token has been loaded and returned.
func (s) TestTokenFileCallCreds_PreemptiveRefreshIsTriggered(t *testing.T) {
// Create token that expires in 80 seconds (=> cache expires in ~50s).
// This ensures pre-emptive refresh triggers since 50s < the 1 minute check.
tokenExp := time.Now().Add(80 * time.Second)
expiringToken := createTestJWT(t, tokenExp)
tokenFile := writeTempFile(t, "token", expiringToken)
creds, err := NewTokenFileCallCredentials(tokenFile)
if err != nil {
t.Fatalf("NewTokenFileCallCredentials() failed: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
})
// First call should read from file synchronously.
metadata1, err := creds.GetRequestMetadata(ctx)
if err != nil {
t.Fatalf("GetRequestMetadata() failed: %v", err)
}
wantAuth1 := "Bearer " + expiringToken
gotAuth1 := metadata1["authorization"]
if gotAuth1 != wantAuth1 {
t.Fatalf("First call should return original token: got %q, want %q", gotAuth1, wantAuth1)
}
// Verify token was cached and confirm expectation that refresh should be
// triggered.
impl := creds.(*jwtTokenFileCallCreds)
impl.mu.Lock()
cacheExp := impl.cachedExpiry
tokenCached := impl.cachedAuthHeader != ""
shouldTriggerRefresh := time.Until(cacheExp) < preemptiveRefreshThreshold
impl.mu.Unlock()
if !tokenCached {
t.Fatal("Token should be cached after successful GetRequestMetadata")
}
if !shouldTriggerRefresh {
timeUntilExp := time.Until(cacheExp)
t.Fatalf("Cache expires in %v; test precondition requires that this triggers preemptive refresh", timeUntilExp)
}
// Create new token file with different expiration while refresh is
// happening.
newToken := createTestJWT(t, time.Now().Add(2*time.Hour))
if err := os.WriteFile(tokenFile, []byte(newToken), 0600); err != nil {
t.Fatalf("Failed to write updated token file: %v", err)
}
// Get token again - this call should trigger a refresh given that the first
// one was cached but expiring soon.
// However, the function should have returned right away with the current
// cached token because it is still valid and the preemptive refresh is
// meant to happen without blocking the RPC.
metadata2, err := creds.GetRequestMetadata(ctx)
if err != nil {
t.Fatalf("Second GetRequestMetadata() failed: %v", err)
}
wantAuth2 := wantAuth1
gotAuth2 := metadata2["authorization"]
if gotAuth2 != wantAuth2 {
t.Fatalf("Second call should return the original token: got %q, want %q", gotAuth2, wantAuth2)
}
// Now should get the new token which was refreshed in the background.
wantAuth3 := "Bearer " + newToken
ctx, cancel = context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
})
for ; ; <-time.After(time.Millisecond) {
if ctx.Err() != nil {
t.Fatal("Context deadline expired before pre-emptive refresh completed")
}
// If the newly returned metadata is different to the old one, verify
// that it matches the token from the updated file. If not, fail the
// test.
metadata3, err := creds.GetRequestMetadata(ctx)
if err != nil {
t.Fatalf("Second GetRequestMetadata() failed: %v", err)
}
// Pre-emptive refresh not completed yet, try again.
gotAuth3 := metadata3["authorization"]
if gotAuth3 == gotAuth2 {
continue
}
if gotAuth3 != wantAuth3 {
t.Fatalf("Third call should return the new token: got %q, want %q", gotAuth3, wantAuth3)
}
break
}
}
// Tests that backoff behavior handles file read errors correctly.
// It has the following expectations:
// First call to GetRequestMetadata() fails with UNAVAILABLE due to a
// missing file.
// Second call to GetRequestMetadata() fails with UNAVAILABLE due backoff.
// Third call to GetRequestMetadata() fails with UNAVAILABLE due to retry.
// Fourth call to GetRequestMetadata() fails with UNAVAILABLE due to backoff
// even though file exists.
// Fifth call to GetRequestMetadata() succeeds after reading the file and
// backoff has expired.
// TODO: Refactor the test to avoid inspecting and mutating internal state.
func (s) TestTokenFileCallCreds_BackoffBehavior(t *testing.T) {
tempDir := t.TempDir()
nonExistentFile := filepath.Join(tempDir, "nonexistent")
creds, err := NewTokenFileCallCredentials(nonExistentFile)
if err != nil {
t.Fatalf("NewTokenFileCallCredentials() failed: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ctx = credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
AuthInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
})
// First call should fail with UNAVAILABLE.
beforeCallRetryTime := time.Now()
_, err = creds.GetRequestMetadata(ctx)
if err == nil {
t.Fatal("Expected error from nonexistent file")
}
if status.Code(err) != codes.Unavailable {
t.Fatalf("GetRequestMetadata() = %v, want UNAVAILABLE", status.Code(err))
}
// Verify error is cached internally.
impl := creds.(*jwtTokenFileCallCreds)
impl.mu.Lock()
retryAttempt := impl.retryAttempt
nextRetryTime := impl.nextRetryTime
impl.mu.Unlock()
if retryAttempt != 1 {
t.Errorf("Expected retry attempt to be 1, got %d", retryAttempt)
}
if !nextRetryTime.After(beforeCallRetryTime) {
t.Error("Next retry time should be set to a time after the first call")
}
// Second call should still return cached error and not retry.
// Set nextRetryTime far enough in the future to ensure that's the case.
impl.mu.Lock()
impl.nextRetryTime = time.Now().Add(1 * time.Minute)
wantNextRetryTime := impl.nextRetryTime
impl.mu.Unlock()
_, err = creds.GetRequestMetadata(ctx)
if err == nil {
t.Fatalf("creds.GetRequestMetadata() = %v, want non-nil", err)
}
if status.Code(err) != codes.Unavailable {
t.Fatalf("GetRequestMetadata() = %v, want cached UNAVAILABLE", status.Code(err))
}
impl.mu.Lock()
retryAttempt2 := impl.retryAttempt
nextRetryTime2 := impl.nextRetryTime
impl.mu.Unlock()
if !nextRetryTime2.Equal(wantNextRetryTime) {
t.Errorf("nextRetryTime should not change due to backoff. Got: %v, Want: %v", nextRetryTime2, wantNextRetryTime)
}
if retryAttempt2 != 1 {
t.Error("Retry attempt should not change due to backoff")
}
// Third call should retry but still fail with UNAVAILABLE.
// Set the backoff retry time in the past to allow next retry attempt.
impl.mu.Lock()
impl.nextRetryTime = time.Now().Add(-1 * time.Minute)
beforeCallRetryTime = impl.nextRetryTime
impl.mu.Unlock()
_, err = creds.GetRequestMetadata(ctx)
if err == nil {
t.Fatalf("creds.GetRequestMetadata() = %v, want non-nil", err)
}
if status.Code(err) != codes.Unavailable {
t.Fatalf("GetRequestMetadata() = %v, want cached UNAVAILABLE", status.Code(err))
}
impl.mu.Lock()
retryAttempt3 := impl.retryAttempt
nextRetryTime3 := impl.nextRetryTime
impl.mu.Unlock()
if !nextRetryTime3.After(beforeCallRetryTime) {
t.Error("nextRetryTime3 should have been updated after third call")
}
if retryAttempt3 != 2 {
t.Error("Expected retry attempt to increase after retry")
}
// Create valid token file.
validToken := createTestJWT(t, time.Now().Add(time.Hour))
if err := os.WriteFile(nonExistentFile, []byte(validToken), 0600); err != nil {
t.Fatalf("Failed to create valid token file: %v", err)
}
// Fourth call should still fail even though the file now exists due to backoff.
// Set nextRetryTime far enough in the future to ensure that's the case.
_, err = creds.GetRequestMetadata(ctx)
impl.mu.Lock()
impl.nextRetryTime = time.Now().Add(1 * time.Minute)
wantNextRetryTime = impl.nextRetryTime
impl.mu.Unlock()
if err == nil {
t.Fatalf("creds.GetRequestMetadata() = %v, want non-nil", err)
}
if status.Code(err) != codes.Unavailable {
t.Fatalf("GetRequestMetadata() = %v, want cached UNAVAILABLE", status.Code(err))
}
impl.mu.Lock()
retryAttempt4 := impl.retryAttempt
nextRetryTime4 := impl.nextRetryTime
impl.mu.Unlock()
if !nextRetryTime4.Equal(wantNextRetryTime) {
t.Errorf("nextRetryTime should not change due to backoff. Got: %v, Want: %v", nextRetryTime4, wantNextRetryTime)
}
if retryAttempt4 != retryAttempt3 {
t.Error("Retry attempt should not change due to backoff")
}
// Fifth call should succeed since the file now exists and the backoff has
// expired.
// Set the backoff retry time in the past to allow next retry attempt.
impl.mu.Lock()
impl.nextRetryTime = time.Now().Add(-1 * time.Minute)
impl.mu.Unlock()
_, err = creds.GetRequestMetadata(ctx)
if err != nil {
t.Fatalf("After creating valid token file, backoff should expire and trigger a token reload on the next RPC. GetRequestMetadata() should eventually succeed, but got: %v", err)
}
// If successful, verify error cache and backoff state were cleared.
impl.mu.Lock()
clearedErr := impl.cachedError
retryAttempt = impl.retryAttempt
nextRetryTime = impl.nextRetryTime
impl.mu.Unlock()
if clearedErr != nil {
t.Errorf("After successful retry, cached error should be cleared, got: %v", clearedErr)
}
if retryAttempt != 0 {
t.Errorf("After successful retry, retry attempt should be reset, got: %d", retryAttempt)
}
if !nextRetryTime.IsZero() {
t.Error("After successful retry, next retry time should be cleared")
}
}
// createTestJWT creates a test JWT token with the specified expiration.
func createTestJWT(t *testing.T, expiration time.Time) string {
t.Helper()
claims := map[string]any{}
if !expiration.IsZero() {
claims["exp"] = expiration.Unix()
}
header := map[string]any{
"typ": "JWT",
"alg": "HS256",
}
headerBytes, err := json.Marshal(header)
if err != nil {
t.Fatalf("Failed to marshal header: %v", err)
}
claimsBytes, err := json.Marshal(claims)
if err != nil {
t.Fatalf("Failed to marshal claims: %v", err)
}
headerB64 := base64.URLEncoding.EncodeToString(headerBytes)
claimsB64 := base64.URLEncoding.EncodeToString(claimsBytes)
// Remove padding for URL-safe base64
headerB64 = strings.TrimRight(headerB64, "=")
claimsB64 = strings.TrimRight(claimsB64, "=")
// For testing, we'll use a fake signature
signature := base64.URLEncoding.EncodeToString([]byte("fake_signature"))
signature = strings.TrimRight(signature, "=")
return fmt.Sprintf("%s.%s.%s", headerB64, claimsB64, signature)
}
func writeTempFile(t *testing.T, name, content string) string {
t.Helper()
tempDir := t.TempDir()
filePath := filepath.Join(tempDir, name)
if err := os.WriteFile(filePath, []byte(content), 0600); err != nil {
t.Fatalf("Failed to write temp file: %v", err)
}
return filePath
}

121
credentials/local/local.go Normal file
View File

@@ -0,0 +1,121 @@
/*
*
* 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 local implements local transport credentials.
// Local credentials reports the security level based on the type
// of connection. If the connection is local TCP, NoSecurity will be
// reported, and if the connection is UDS, PrivacyAndIntegrity will be
// reported. If local credentials is not used in local connections
// (local TCP or UDS), it will fail.
//
// # Experimental
//
// Notice: This package is EXPERIMENTAL and may be changed or removed in a
// later release.
package local
import (
"context"
"fmt"
"net"
"strings"
"google.golang.org/grpc/credentials"
)
// info contains the auth information for a local connection.
// It implements the AuthInfo interface.
type info struct {
credentials.CommonAuthInfo
}
// AuthType returns the type of info as a string.
func (info) AuthType() string {
return "local"
}
// ValidateAuthority allows any value to be overridden for the :authority
// header.
func (info) ValidateAuthority(string) error {
return nil
}
// localTC is the credentials required to establish a local connection.
type localTC struct {
info credentials.ProtocolInfo
}
func (c *localTC) Info() credentials.ProtocolInfo {
return c.info
}
// getSecurityLevel returns the security level for a local connection.
// It returns an error if a connection is not local.
func getSecurityLevel(network, addr string) (credentials.SecurityLevel, error) {
switch {
// Local TCP connection
case strings.HasPrefix(addr, "127."), strings.HasPrefix(addr, "[::1]:"):
return credentials.NoSecurity, nil
// Windows named pipe connection
case network == "pipe" && strings.HasPrefix(addr, `\\.\pipe\`):
return credentials.NoSecurity, nil
// UDS connection
case network == "unix":
return credentials.PrivacyAndIntegrity, nil
// Not a local connection and should fail
default:
return credentials.InvalidSecurityLevel, fmt.Errorf("local credentials rejected connection to non-local address %q", addr)
}
}
func (*localTC) ClientHandshake(_ context.Context, _ string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
secLevel, err := getSecurityLevel(conn.RemoteAddr().Network(), conn.RemoteAddr().String())
if err != nil {
return nil, nil, err
}
return conn, info{credentials.CommonAuthInfo{SecurityLevel: secLevel}}, nil
}
func (*localTC) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
secLevel, err := getSecurityLevel(conn.RemoteAddr().Network(), conn.RemoteAddr().String())
if err != nil {
return nil, nil, err
}
return conn, info{credentials.CommonAuthInfo{SecurityLevel: secLevel}}, nil
}
// NewCredentials returns a local credential implementing credentials.TransportCredentials.
func NewCredentials() credentials.TransportCredentials {
return &localTC{
info: credentials.ProtocolInfo{
SecurityProtocol: "local",
},
}
}
// Clone makes a copy of Local credentials.
func (c *localTC) Clone() credentials.TransportCredentials {
return &localTC{info: c.info}
}
// OverrideServerName overrides the server name used to verify the hostname on the returned certificates from the server.
// Since this feature is specific to TLS (SNI + hostname verification check), it does not take any effect for local credentials.
func (c *localTC) OverrideServerName(serverNameOverride string) error {
c.info.ServerName = serverNameOverride
return nil
}

View File

@@ -0,0 +1,225 @@
/*
*
* 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 local
import (
"context"
"fmt"
"net"
"runtime"
"strings"
"testing"
"time"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/grpctest"
)
const defaultTestTimeout = 10 * time.Second
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
func (s) TestGetSecurityLevel(t *testing.T) {
testCases := []struct {
testNetwork string
testAddr string
want credentials.SecurityLevel
}{
{
testNetwork: "tcp",
testAddr: "127.0.0.1:10000",
want: credentials.NoSecurity,
},
{
testNetwork: "tcp",
testAddr: "[::1]:10000",
want: credentials.NoSecurity,
},
{
testNetwork: "unix",
testAddr: "/tmp/grpc_fullstack_test",
want: credentials.PrivacyAndIntegrity,
},
{
testNetwork: "tcp",
testAddr: "192.168.0.1:10000",
want: credentials.InvalidSecurityLevel,
},
}
for _, tc := range testCases {
got, _ := getSecurityLevel(tc.testNetwork, tc.testAddr)
if got != tc.want {
t.Fatalf("GetSeurityLevel(%s, %s) returned %s but want %s", tc.testNetwork, tc.testAddr, got.String(), tc.want.String())
}
}
}
type serverHandshake func(net.Conn) (credentials.AuthInfo, error)
func getSecurityLevelFromAuthInfo(ai credentials.AuthInfo) credentials.SecurityLevel {
if c, ok := ai.(interface {
GetCommonAuthInfo() credentials.CommonAuthInfo
}); ok {
return c.GetCommonAuthInfo().SecurityLevel
}
return credentials.InvalidSecurityLevel
}
// Server local handshake implementation.
func serverLocalHandshake(conn net.Conn) (credentials.AuthInfo, error) {
cred := NewCredentials()
_, authInfo, err := cred.ServerHandshake(conn)
if err != nil {
return nil, err
}
return authInfo, nil
}
// Client local handshake implementation.
func clientLocalHandshake(conn net.Conn, lisAddr string) (credentials.AuthInfo, error) {
cred := NewCredentials()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
_, authInfo, err := cred.ClientHandshake(ctx, lisAddr, conn)
if err != nil {
return nil, err
}
return authInfo, nil
}
// Client connects to a server with local credentials.
func clientHandle(hs func(net.Conn, string) (credentials.AuthInfo, error), network, lisAddr string) (credentials.AuthInfo, error) {
conn, _ := net.Dial(network, lisAddr)
defer conn.Close()
clientAuthInfo, err := hs(conn, lisAddr)
if err != nil {
return nil, fmt.Errorf("Error on client while handshake")
}
return clientAuthInfo, nil
}
type testServerHandleResult struct {
authInfo credentials.AuthInfo
err error
}
// Server accepts a client's connection with local credentials.
func serverHandle(hs serverHandshake, done chan testServerHandleResult, lis net.Listener) {
serverRawConn, err := lis.Accept()
if err != nil {
done <- testServerHandleResult{authInfo: nil, err: fmt.Errorf("Server failed to accept connection. Error: %v", err)}
return
}
serverAuthInfo, err := hs(serverRawConn)
if err != nil {
serverRawConn.Close()
done <- testServerHandleResult{authInfo: nil, err: fmt.Errorf("Server failed while handshake. Error: %v", err)}
return
}
done <- testServerHandleResult{authInfo: serverAuthInfo, err: nil}
}
func serverAndClientHandshake(lis net.Listener) (credentials.SecurityLevel, error) {
done := make(chan testServerHandleResult, 1)
const timeout = 5 * time.Second
timer := time.NewTimer(timeout)
defer timer.Stop()
go serverHandle(serverLocalHandshake, done, lis)
defer lis.Close()
clientAuthInfo, err := clientHandle(clientLocalHandshake, lis.Addr().Network(), lis.Addr().String())
if err != nil {
return credentials.InvalidSecurityLevel, fmt.Errorf("Error at client-side: %v", err)
}
select {
case <-timer.C:
return credentials.InvalidSecurityLevel, fmt.Errorf("Test didn't finish in time")
case serverHandleResult := <-done:
if serverHandleResult.err != nil {
return credentials.InvalidSecurityLevel, fmt.Errorf("Error at server-side: %v", serverHandleResult.err)
}
clientSecLevel := getSecurityLevelFromAuthInfo(clientAuthInfo)
serverSecLevel := getSecurityLevelFromAuthInfo(serverHandleResult.authInfo)
if clientSecLevel == credentials.InvalidSecurityLevel {
return credentials.InvalidSecurityLevel, fmt.Errorf("Error at client-side: client's AuthInfo does not implement GetCommonAuthInfo()")
}
if serverSecLevel == credentials.InvalidSecurityLevel {
return credentials.InvalidSecurityLevel, fmt.Errorf("Error at server-side: server's AuthInfo does not implement GetCommonAuthInfo()")
}
if clientSecLevel != serverSecLevel {
return credentials.InvalidSecurityLevel, fmt.Errorf("client's AuthInfo contains %s but server's AuthInfo contains %s", clientSecLevel.String(), serverSecLevel.String())
}
return clientSecLevel, nil
}
}
func (s) TestServerAndClientHandshake(t *testing.T) {
testCases := []struct {
testNetwork string
testAddr string
want credentials.SecurityLevel
}{
{
testNetwork: "tcp",
testAddr: "127.0.0.1:0",
want: credentials.NoSecurity,
},
{
testNetwork: "tcp",
testAddr: "[::1]:0",
want: credentials.NoSecurity,
},
{
testNetwork: "tcp",
testAddr: "localhost:0",
want: credentials.NoSecurity,
},
{
testNetwork: "unix",
testAddr: fmt.Sprintf("/tmp/grpc_fullstck_test%d", time.Now().UnixNano()),
want: credentials.PrivacyAndIntegrity,
},
}
for _, tc := range testCases {
if runtime.GOOS == "windows" && tc.testNetwork == "unix" {
t.Skip("skipping tests for unix connections on Windows")
}
t.Run("serverAndClientHandshakeResult", func(t *testing.T) {
lis, err := net.Listen(tc.testNetwork, tc.testAddr)
if err != nil {
if strings.Contains(err.Error(), "bind: cannot assign requested address") ||
strings.Contains(err.Error(), "socket: address family not supported by protocol") {
t.Skipf("no support for address %v", tc.testAddr)
}
t.Fatalf("Failed to listen: %v", err)
}
got, err := serverAndClientHandshake(lis)
if got != tc.want {
t.Fatalf("serverAndClientHandshake(%s, %s) = %v, %v; want %v, nil", tc.testNetwork, tc.testAddr, got, err, tc.want)
}
})
}
}

244
credentials/oauth/oauth.go Normal file
View File

@@ -0,0 +1,244 @@
/*
*
* Copyright 2015 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 oauth implements gRPC credentials using OAuth.
package oauth
import (
"context"
"fmt"
"net/url"
"os"
"sync"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
"google.golang.org/grpc/credentials"
)
// TokenSource supplies PerRPCCredentials from an oauth2.TokenSource.
type TokenSource struct {
oauth2.TokenSource
}
// GetRequestMetadata gets the request metadata as a map from a TokenSource.
func (ts TokenSource) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
token, err := ts.Token()
if err != nil {
return nil, err
}
ri, _ := credentials.RequestInfoFromContext(ctx)
if err = credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
return nil, fmt.Errorf("unable to transfer TokenSource PerRPCCredentials: %v", err)
}
return map[string]string{
"authorization": token.Type() + " " + token.AccessToken,
}, nil
}
// RequireTransportSecurity indicates whether the credentials requires transport security.
func (ts TokenSource) RequireTransportSecurity() bool {
return true
}
// removeServiceNameFromJWTURI removes RPC service name from URI.
func removeServiceNameFromJWTURI(uri string) (string, error) {
parsed, err := url.Parse(uri)
if err != nil {
return "", err
}
parsed.Path = "/"
return parsed.String(), nil
}
type jwtAccess struct {
jsonKey []byte
}
// NewJWTAccessFromFile creates PerRPCCredentials from the given keyFile.
func NewJWTAccessFromFile(keyFile string) (credentials.PerRPCCredentials, error) {
jsonKey, err := os.ReadFile(keyFile)
if err != nil {
return nil, fmt.Errorf("credentials: failed to read the service account key file: %v", err)
}
return NewJWTAccessFromKey(jsonKey)
}
// NewJWTAccessFromKey creates PerRPCCredentials from the given jsonKey.
func NewJWTAccessFromKey(jsonKey []byte) (credentials.PerRPCCredentials, error) {
return jwtAccess{jsonKey}, nil
}
func (j jwtAccess) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
// Remove RPC service name from URI that will be used as audience
// in a self-signed JWT token. It follows https://google.aip.dev/auth/4111.
aud, err := removeServiceNameFromJWTURI(uri[0])
if err != nil {
return nil, err
}
// TODO: the returned TokenSource is reusable. Store it in a sync.Map, with
// uri as the key, to avoid recreating for every RPC.
ts, err := google.JWTAccessTokenSourceFromJSON(j.jsonKey, aud)
if err != nil {
return nil, err
}
token, err := ts.Token()
if err != nil {
return nil, err
}
ri, _ := credentials.RequestInfoFromContext(ctx)
if err = credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
return nil, fmt.Errorf("unable to transfer jwtAccess PerRPCCredentials: %v", err)
}
return map[string]string{
"authorization": token.Type() + " " + token.AccessToken,
}, nil
}
func (j jwtAccess) RequireTransportSecurity() bool {
return true
}
// oauthAccess supplies PerRPCCredentials from a given token.
type oauthAccess struct {
token oauth2.Token
}
// NewOauthAccess constructs the PerRPCCredentials using a given token.
//
// Deprecated: use oauth.TokenSource instead.
func NewOauthAccess(token *oauth2.Token) credentials.PerRPCCredentials {
return oauthAccess{token: *token}
}
func (oa oauthAccess) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
ri, _ := credentials.RequestInfoFromContext(ctx)
if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
return nil, fmt.Errorf("unable to transfer oauthAccess PerRPCCredentials: %v", err)
}
return map[string]string{
"authorization": oa.token.Type() + " " + oa.token.AccessToken,
}, nil
}
func (oa oauthAccess) RequireTransportSecurity() bool {
return true
}
// NewComputeEngine constructs the PerRPCCredentials that fetches access tokens from
// Google Compute Engine (GCE)'s metadata server. It is only valid to use this
// if your program is running on a GCE instance.
// TODO(dsymonds): Deprecate and remove this.
func NewComputeEngine() credentials.PerRPCCredentials {
return TokenSource{google.ComputeTokenSource("")}
}
// serviceAccount represents PerRPCCredentials via JWT signing key.
type serviceAccount struct {
mu sync.Mutex
config *jwt.Config
t *oauth2.Token
}
func (s *serviceAccount) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.t.Valid() {
var err error
s.t, err = s.config.TokenSource(ctx).Token()
if err != nil {
return nil, err
}
}
ri, _ := credentials.RequestInfoFromContext(ctx)
if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
return nil, fmt.Errorf("unable to transfer serviceAccount PerRPCCredentials: %v", err)
}
return map[string]string{
"authorization": s.t.Type() + " " + s.t.AccessToken,
}, nil
}
func (s *serviceAccount) RequireTransportSecurity() bool {
return true
}
// NewServiceAccountFromKey constructs the PerRPCCredentials using the JSON key slice
// from a Google Developers service account.
func NewServiceAccountFromKey(jsonKey []byte, scope ...string) (credentials.PerRPCCredentials, error) {
config, err := google.JWTConfigFromJSON(jsonKey, scope...)
if err != nil {
return nil, err
}
return &serviceAccount{config: config}, nil
}
// NewServiceAccountFromFile constructs the PerRPCCredentials using the JSON key file
// of a Google Developers service account.
func NewServiceAccountFromFile(keyFile string, scope ...string) (credentials.PerRPCCredentials, error) {
jsonKey, err := os.ReadFile(keyFile)
if err != nil {
return nil, fmt.Errorf("credentials: failed to read the service account key file: %v", err)
}
return NewServiceAccountFromKey(jsonKey, scope...)
}
// NewApplicationDefault returns "Application Default Credentials". For more
// detail, see https://developers.google.com/accounts/docs/application-default-credentials.
func NewApplicationDefault(ctx context.Context, scope ...string) (credentials.PerRPCCredentials, error) {
creds, err := google.FindDefaultCredentials(ctx, scope...)
if err != nil {
return nil, err
}
// If JSON is nil, the authentication is provided by the environment and not
// with a credentials file, e.g. when code is running on Google Cloud
// Platform. Use the returned token source.
if creds.JSON == nil {
return TokenSource{creds.TokenSource}, nil
}
// If auth is provided by env variable or creds file, the behavior will be
// different based on whether scope is set. Because the returned
// creds.TokenSource does oauth with jwt by default, and it requires scope.
// We can only use it if scope is not empty, otherwise it will fail with
// missing scope error.
//
// If scope is set, use it, it should just work.
//
// If scope is not set, we try to use jwt directly without oauth (this only
// works if it's a service account).
if len(scope) != 0 {
return TokenSource{creds.TokenSource}, nil
}
// Try to convert JSON to a jwt config without setting the optional scope
// parameter to check if it's a service account (the function errors if it's
// not). This is necessary because the returned config doesn't show the type
// of the account.
if _, err := google.JWTConfigFromJSON(creds.JSON); err != nil {
// If this fails, it's not a service account, return the original
// TokenSource from above.
return TokenSource{creds.TokenSource}, nil
}
// If it's a service account, create a JWT only access with the key.
return NewJWTAccessFromKey(creds.JSON)
}

View File

@@ -0,0 +1,60 @@
/*
*
* Copyright 2021 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 oauth
import (
"strings"
"testing"
)
func checkErrorMsg(err error, msg string) bool {
if err == nil && msg == "" {
return true
} else if err != nil {
return strings.Contains(err.Error(), msg)
}
return false
}
func TestRemoveServiceNameFromJWTURI(t *testing.T) {
tests := []struct {
name string
uri string
wantedURI string
wantedErrMsg string
}{
{
name: "invalid URI",
uri: "ht tp://foo.com",
wantedErrMsg: "first path segment in URL cannot contain colon",
},
{
name: "valid URI",
uri: "https://foo.com/go/",
wantedURI: "https://foo.com/",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, err := removeServiceNameFromJWTURI(tt.uri); got != tt.wantedURI || !checkErrorMsg(err, tt.wantedErrMsg) {
t.Errorf("RemoveServiceNameFromJWTURI() = %s, %v, want %s, %v", got, err, tt.wantedURI, tt.wantedErrMsg)
}
})
}
}

407
credentials/sts/sts.go Normal file
View File

@@ -0,0 +1,407 @@
/*
*
* 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 sts implements call credentials using STS (Security Token Service) as
// defined in https://tools.ietf.org/html/rfc8693.
//
// # Experimental
//
// Notice: All APIs in this package are experimental and may be changed or
// removed in a later release.
package sts
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"sync"
"time"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
)
const (
// HTTP request timeout set on the http.Client used to make STS requests.
stsRequestTimeout = 5 * time.Second
// If lifetime left in a cached token is lesser than this value, we fetch a
// new one instead of returning the current one.
minCachedTokenLifetime = 300 * time.Second
tokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
defaultCloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"
)
// For overriding in tests.
var (
loadSystemCertPool = x509.SystemCertPool
makeHTTPDoer = makeHTTPClient
readSubjectTokenFrom = os.ReadFile
readActorTokenFrom = os.ReadFile
logger = grpclog.Component("credentials")
)
// Options configures the parameters used for an STS based token exchange.
type Options struct {
// TokenExchangeServiceURI is the address of the server which implements STS
// token exchange functionality.
TokenExchangeServiceURI string // Required.
// Resource is a URI that indicates the target service or resource where the
// client intends to use the requested security token.
Resource string // Optional.
// Audience is the logical name of the target service where the client
// intends to use the requested security token
Audience string // Optional.
// Scope is a list of space-delimited, case-sensitive strings, that allow
// the client to specify the desired scope of the requested security token
// in the context of the service or resource where the token will be used.
// If this field is left unspecified, a default value of
// https://www.googleapis.com/auth/cloud-platform will be used.
Scope string // Optional.
// RequestedTokenType is an identifier, as described in
// https://tools.ietf.org/html/rfc8693#section-3, that indicates the type of
// the requested security token.
RequestedTokenType string // Optional.
// SubjectTokenPath is a filesystem path which contains the security token
// that represents the identity of the party on behalf of whom the request
// is being made.
SubjectTokenPath string // Required.
// SubjectTokenType is an identifier, as described in
// https://tools.ietf.org/html/rfc8693#section-3, that indicates the type of
// the security token in the "subject_token_path" parameter.
SubjectTokenType string // Required.
// ActorTokenPath is a security token that represents the identity of the
// acting party.
ActorTokenPath string // Optional.
// ActorTokenType is an identifier, as described in
// https://tools.ietf.org/html/rfc8693#section-3, that indicates the type of
// the security token in the "actor_token_path" parameter.
ActorTokenType string // Optional.
}
func (o Options) String() string {
return fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s:%s:%s", o.TokenExchangeServiceURI, o.Resource, o.Audience, o.Scope, o.RequestedTokenType, o.SubjectTokenPath, o.SubjectTokenType, o.ActorTokenPath, o.ActorTokenType)
}
// NewCredentials returns a new PerRPCCredentials implementation, configured
// using opts, which performs token exchange using STS.
func NewCredentials(opts Options) (credentials.PerRPCCredentials, error) {
if err := validateOptions(opts); err != nil {
return nil, err
}
// Load the system roots to validate the certificate presented by the STS
// endpoint during the TLS handshake.
roots, err := loadSystemCertPool()
if err != nil {
return nil, err
}
return &callCreds{
opts: opts,
client: makeHTTPDoer(roots),
}, nil
}
// callCreds provides the implementation of call credentials based on an STS
// token exchange.
type callCreds struct {
opts Options
client httpDoer
// Cached accessToken to avoid an STS token exchange for every call to
// GetRequestMetadata.
mu sync.Mutex
tokenMetadata map[string]string
tokenExpiry time.Time
}
// GetRequestMetadata returns the cached accessToken, if available and valid, or
// fetches a new one by performing an STS token exchange.
func (c *callCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
ri, _ := credentials.RequestInfoFromContext(ctx)
if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
return nil, fmt.Errorf("unable to transfer STS PerRPCCredentials: %v", err)
}
// Holding the lock for the whole duration of the STS request and response
// processing ensures that concurrent RPCs don't end up in multiple
// requests being made.
c.mu.Lock()
defer c.mu.Unlock()
if md := c.cachedMetadata(); md != nil {
return md, nil
}
req, err := constructRequest(ctx, c.opts)
if err != nil {
return nil, err
}
respBody, err := sendRequest(c.client, req)
if err != nil {
return nil, err
}
ti, err := tokenInfoFromResponse(respBody)
if err != nil {
return nil, err
}
c.tokenMetadata = map[string]string{"Authorization": fmt.Sprintf("%s %s", ti.tokenType, ti.token)}
c.tokenExpiry = ti.expiryTime
return c.tokenMetadata, nil
}
// RequireTransportSecurity indicates whether the credentials requires
// transport security.
func (c *callCreds) RequireTransportSecurity() bool {
return true
}
// httpDoer wraps the single method on the http.Client type that we use. This
// helps with overriding in unittests.
type httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}
func makeHTTPClient(roots *x509.CertPool) httpDoer {
return &http.Client{
// The token exchange request carries the subject token (and an optional
// actor token) in its body. Following a redirect replays that body to
// the redirect target, so refuse to follow redirects and let sendRequest
// surface the 3xx as a non-2xx status instead.
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: stsRequestTimeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: roots,
},
},
}
}
// validateOptions performs the following validation checks on opts:
// - tokenExchangeServiceURI is not empty
// - tokenExchangeServiceURI is a valid URI with a http(s) scheme
// - subjectTokenPath and subjectTokenType are not empty.
func validateOptions(opts Options) error {
if opts.TokenExchangeServiceURI == "" {
return errors.New("empty token_exchange_service_uri in options")
}
u, err := url.Parse(opts.TokenExchangeServiceURI)
if err != nil {
return err
}
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("scheme is not supported: %q. Only http(s) is supported", u.Scheme)
}
if opts.SubjectTokenPath == "" {
return errors.New("required field SubjectTokenPath is not specified")
}
if opts.SubjectTokenType == "" {
return errors.New("required field SubjectTokenType is not specified")
}
return nil
}
// cachedMetadata returns the cached metadata provided it is not going to
// expire anytime soon.
//
// Caller must hold c.mu.
func (c *callCreds) cachedMetadata() map[string]string {
now := time.Now()
// If the cached token has not expired and the lifetime remaining on that
// token is greater than the minimum value we are willing to accept, go
// ahead and use it.
if c.tokenExpiry.After(now) && c.tokenExpiry.Sub(now) > minCachedTokenLifetime {
return c.tokenMetadata
}
return nil
}
// constructRequest creates the STS request body in JSON based on the provided
// options.
// - Contents of the subjectToken are read from the file specified in
// options. If we encounter an error here, we bail out.
// - Contents of the actorToken are read from the file specified in options.
// If we encounter an error here, we ignore this field because this is
// optional.
// - Most of the other fields in the request come directly from options.
//
// A new HTTP request is created by calling http.NewRequestWithContext() and
// passing the provided context, thereby enforcing any timeouts specified in
// the latter.
func constructRequest(ctx context.Context, opts Options) (*http.Request, error) {
subToken, err := readSubjectTokenFrom(opts.SubjectTokenPath)
if err != nil {
return nil, err
}
reqScope := opts.Scope
if reqScope == "" {
reqScope = defaultCloudPlatformScope
}
reqParams := &requestParameters{
GrantType: tokenExchangeGrantType,
Resource: opts.Resource,
Audience: opts.Audience,
Scope: reqScope,
RequestedTokenType: opts.RequestedTokenType,
SubjectToken: string(subToken),
SubjectTokenType: opts.SubjectTokenType,
}
if opts.ActorTokenPath != "" {
actorToken, err := readActorTokenFrom(opts.ActorTokenPath)
if err != nil {
return nil, err
}
reqParams.ActorToken = string(actorToken)
reqParams.ActorTokenType = opts.ActorTokenType
}
jsonBody, err := json.Marshal(reqParams)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", opts.TokenExchangeServiceURI, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create http request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
return req, nil
}
func sendRequest(client httpDoer, req *http.Request) ([]byte, error) {
// http.Client returns a non-nil error only if it encounters an error
// caused by client policy (such as CheckRedirect), or failure to speak
// HTTP (such as a network connectivity problem). A non-2xx status code
// doesn't cause an error.
resp, err := client.Do(req)
if err != nil {
return nil, err
}
// When the http.Client returns a non-nil error, it is the
// responsibility of the caller to read the response body till an EOF is
// encountered and to close it.
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusOK {
return body, nil
}
logger.Warningf("http status %d, body: %s", resp.StatusCode, string(body))
return nil, fmt.Errorf("http status %d, body: %s", resp.StatusCode, string(body))
}
func tokenInfoFromResponse(respBody []byte) (*tokenInfo, error) {
respData := &responseParameters{}
if err := json.Unmarshal(respBody, respData); err != nil {
return nil, fmt.Errorf("json.Unmarshal(%v): %v", respBody, err)
}
if respData.AccessToken == "" {
return nil, fmt.Errorf("empty accessToken in response (%v)", string(respBody))
}
return &tokenInfo{
tokenType: respData.TokenType,
token: respData.AccessToken,
expiryTime: time.Now().Add(time.Duration(respData.ExpiresIn) * time.Second),
}, nil
}
// requestParameters stores all STS request attributes defined in
// https://tools.ietf.org/html/rfc8693#section-2.1.
type requestParameters struct {
// REQUIRED. The value "urn:ietf:params:oauth:grant-type:token-exchange"
// indicates that a token exchange is being performed.
GrantType string `json:"grant_type"`
// OPTIONAL. Indicates the location of the target service or resource where
// the client intends to use the requested security token.
Resource string `json:"resource,omitempty"`
// OPTIONAL. The logical name of the target service where the client intends
// to use the requested security token.
Audience string `json:"audience,omitempty"`
// OPTIONAL. A list of space-delimited, case-sensitive strings, that allow
// the client to specify the desired scope of the requested security token
// in the context of the service or Resource where the token will be used.
Scope string `json:"scope,omitempty"`
// OPTIONAL. An identifier, for the type of the requested security token.
RequestedTokenType string `json:"requested_token_type,omitempty"`
// REQUIRED. A security token that represents the identity of the party on
// behalf of whom the request is being made.
SubjectToken string `json:"subject_token"`
// REQUIRED. An identifier, that indicates the type of the security token in
// the "subject_token" parameter.
SubjectTokenType string `json:"subject_token_type"`
// OPTIONAL. A security token that represents the identity of the acting
// party.
ActorToken string `json:"actor_token,omitempty"`
// An identifier, that indicates the type of the security token in the
// "actor_token" parameter.
ActorTokenType string `json:"actor_token_type,omitempty"`
}
// responseParameters stores all attributes sent as JSON in a successful STS
// response. These attributes are defined in
// https://tools.ietf.org/html/rfc8693#section-2.2.1.
type responseParameters struct {
// REQUIRED. The security token issued by the authorization server
// in response to the token exchange request.
AccessToken string `json:"access_token"`
// REQUIRED. An identifier, representation of the issued security token.
IssuedTokenType string `json:"issued_token_type"`
// REQUIRED. A case-insensitive value specifying the method of using the access
// token issued. It provides the client with information about how to utilize the
// access token to access protected resources.
TokenType string `json:"token_type"`
// RECOMMENDED. The validity lifetime, in seconds, of the token issued by the
// authorization server.
ExpiresIn int64 `json:"expires_in"`
// OPTIONAL, if the Scope of the issued security token is identical to the
// Scope requested by the client; otherwise, REQUIRED.
Scope string `json:"scope"`
// OPTIONAL. A refresh token will typically not be issued when the exchange is
// of one temporary credential (the subject_token) for a different temporary
// credential (the issued token) for use in some other context.
RefreshToken string `json:"refresh_token"`
}
// tokenInfo wraps the information received in a successful STS response.
type tokenInfo struct {
tokenType string
token string
expiryTime time.Time
}

803
credentials/sts/sts_test.go Normal file
View File

@@ -0,0 +1,803 @@
/*
*
* 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 sts
import (
"bytes"
"context"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/http/httputil"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/testutils"
)
const (
requestedTokenType = "urn:ietf:params:oauth:token-type:access-token"
actorTokenPath = "/var/run/secrets/token.jwt"
actorTokenType = "urn:ietf:params:oauth:token-type:refresh_token"
actorTokenContents = "actorToken.jwt.contents"
accessTokenContents = "access_token"
subjectTokenPath = "/var/run/secrets/token.jwt"
subjectTokenType = "urn:ietf:params:oauth:token-type:id_token"
subjectTokenContents = "subjectToken.jwt.contents"
serviceURI = "http://localhost"
exampleResource = "https://backend.example.com/api"
exampleAudience = "example-backend-service"
testScope = "https://www.googleapis.com/auth/monitoring"
defaultTestTimeout = 1 * time.Second
defaultTestShortTimeout = 10 * time.Millisecond
)
var (
goodOptions = Options{
TokenExchangeServiceURI: serviceURI,
Audience: exampleAudience,
RequestedTokenType: requestedTokenType,
SubjectTokenPath: subjectTokenPath,
SubjectTokenType: subjectTokenType,
}
goodRequestParams = &requestParameters{
GrantType: tokenExchangeGrantType,
Audience: exampleAudience,
Scope: defaultCloudPlatformScope,
RequestedTokenType: requestedTokenType,
SubjectToken: subjectTokenContents,
SubjectTokenType: subjectTokenType,
}
goodMetadata = map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", accessTokenContents),
}
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
// A struct that implements AuthInfo interface and added to the context passed
// to GetRequestMetadata from tests.
type testAuthInfo struct {
credentials.CommonAuthInfo
}
func (ta testAuthInfo) AuthType() string {
return "testAuthInfo"
}
func createTestContext(ctx context.Context, s credentials.SecurityLevel) context.Context {
auth := &testAuthInfo{CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: s}}
ri := credentials.RequestInfo{
Method: "testInfo",
AuthInfo: auth,
}
return credentials.NewContextWithRequestInfo(ctx, ri)
}
// errReader implements the io.Reader interface and returns an error from the
// Read method.
type errReader struct{}
func (r errReader) Read([]byte) (n int, err error) {
return 0, errors.New("read error")
}
// We need a function to construct the response instead of simply declaring it
// as a variable since the response body will be consumed by the
// credentials, and therefore we will need a new one everytime.
func makeGoodResponse() *http.Response {
respJSON, _ := json.Marshal(responseParameters{
AccessToken: accessTokenContents,
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
TokenType: "Bearer",
ExpiresIn: 3600,
})
respBody := io.NopCloser(bytes.NewReader(respJSON))
return &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
Body: respBody,
}
}
// Overrides the http.Client with a fakeClient which sends a good response.
func overrideHTTPClientGood() (*testutils.FakeHTTPClient, func()) {
fc := &testutils.FakeHTTPClient{
ReqChan: testutils.NewChannel(),
RespChan: testutils.NewChannel(),
}
fc.RespChan.Send(makeGoodResponse())
origMakeHTTPDoer := makeHTTPDoer
makeHTTPDoer = func(_ *x509.CertPool) httpDoer { return fc }
return fc, func() { makeHTTPDoer = origMakeHTTPDoer }
}
// Overrides the http.Client with the provided fakeClient.
func overrideHTTPClient(fc *testutils.FakeHTTPClient) func() {
origMakeHTTPDoer := makeHTTPDoer
makeHTTPDoer = func(_ *x509.CertPool) httpDoer { return fc }
return func() { makeHTTPDoer = origMakeHTTPDoer }
}
// Overrides the subject token read to return a const which we can compare in
// our tests.
func overrideSubjectTokenGood() func() {
origReadSubjectTokenFrom := readSubjectTokenFrom
readSubjectTokenFrom = func(string) ([]byte, error) {
return []byte(subjectTokenContents), nil
}
return func() { readSubjectTokenFrom = origReadSubjectTokenFrom }
}
// Overrides the subject token read to always return an error.
func overrideSubjectTokenError() func() {
origReadSubjectTokenFrom := readSubjectTokenFrom
readSubjectTokenFrom = func(string) ([]byte, error) {
return nil, errors.New("error reading subject token")
}
return func() { readSubjectTokenFrom = origReadSubjectTokenFrom }
}
// Overrides the actor token read to return a const which we can compare in
// our tests.
func overrideActorTokenGood() func() {
origReadActorTokenFrom := readActorTokenFrom
readActorTokenFrom = func(string) ([]byte, error) {
return []byte(actorTokenContents), nil
}
return func() { readActorTokenFrom = origReadActorTokenFrom }
}
// Overrides the actor token read to always return an error.
func overrideActorTokenError() func() {
origReadActorTokenFrom := readActorTokenFrom
readActorTokenFrom = func(string) ([]byte, error) {
return nil, errors.New("error reading actor token")
}
return func() { readActorTokenFrom = origReadActorTokenFrom }
}
// compareRequest compares the http.Request received in the test with the
// expected requestParameters specified in wantReqParams.
func compareRequest(gotRequest *http.Request, wantReqParams *requestParameters) error {
jsonBody, err := json.Marshal(wantReqParams)
if err != nil {
return err
}
wantReq, err := http.NewRequest("POST", serviceURI, bytes.NewBuffer(jsonBody))
if err != nil {
return fmt.Errorf("failed to create http request: %v", err)
}
wantReq.Header.Set("Content-Type", "application/json")
wantR, err := httputil.DumpRequestOut(wantReq, true)
if err != nil {
return err
}
gotR, err := httputil.DumpRequestOut(gotRequest, true)
if err != nil {
return err
}
if diff := cmp.Diff(string(wantR), string(gotR)); diff != "" {
return fmt.Errorf("sts request diff (-want +got):\n%s", diff)
}
return nil
}
// receiveAndCompareRequest waits for a request to be sent out by the
// credentials implementation using the fakeHTTPClient and compares it to an
// expected goodRequest. This is expected to be called in a separate goroutine
// by the tests. So, any errors encountered are pushed to an error channel
// which is monitored by the test.
func receiveAndCompareRequest(ReqChan *testutils.Channel, errCh chan error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
val, err := ReqChan.Receive(ctx)
if err != nil {
errCh <- err
return
}
req := val.(*http.Request)
if err := compareRequest(req, goodRequestParams); err != nil {
errCh <- err
return
}
errCh <- nil
}
// TestGetRequestMetadataSuccess verifies the successful case of sending an
// token exchange request and processing the response.
func (s) TestGetRequestMetadataSuccess(t *testing.T) {
defer overrideSubjectTokenGood()()
fc, cancel := overrideHTTPClientGood()
defer cancel()
creds, err := NewCredentials(goodOptions)
if err != nil {
t.Fatalf("NewCredentials(%v) = %v", goodOptions, err)
}
errCh := make(chan error, 1)
go receiveAndCompareRequest(fc.ReqChan, errCh)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
gotMetadata, err := creds.GetRequestMetadata(createTestContext(ctx, credentials.PrivacyAndIntegrity), "")
if err != nil {
t.Fatalf("creds.GetRequestMetadata() = %v", err)
}
if !cmp.Equal(gotMetadata, goodMetadata) {
t.Fatalf("creds.GetRequestMetadata() = %v, want %v", gotMetadata, goodMetadata)
}
if err := <-errCh; err != nil {
t.Fatal(err)
}
// Make another call to get request metadata and this should return contents
// from the cache. This will fail if the credentials tries to send a fresh
// request here since we have not configured our fakeClient to return any
// response on retries.
gotMetadata, err = creds.GetRequestMetadata(createTestContext(ctx, credentials.PrivacyAndIntegrity), "")
if err != nil {
t.Fatalf("creds.GetRequestMetadata() = %v", err)
}
if !cmp.Equal(gotMetadata, goodMetadata) {
t.Fatalf("creds.GetRequestMetadata() = %v, want %v", gotMetadata, goodMetadata)
}
}
// TestGetRequestMetadataBadSecurityLevel verifies the case where the
// securityLevel specified in the context passed to GetRequestMetadata is not
// sufficient.
func (s) TestGetRequestMetadataBadSecurityLevel(t *testing.T) {
defer overrideSubjectTokenGood()()
creds, err := NewCredentials(goodOptions)
if err != nil {
t.Fatalf("NewCredentials(%v) = %v", goodOptions, err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
gotMetadata, err := creds.GetRequestMetadata(createTestContext(ctx, credentials.IntegrityOnly), "")
if err == nil {
t.Fatalf("creds.GetRequestMetadata() succeeded with metadata %v, expected to fail", gotMetadata)
}
}
// TestGetRequestMetadataCacheExpiry verifies the case where the cached access
// token has expired, and the credentials implementation will have to send a
// fresh token exchange request.
func (s) TestGetRequestMetadataCacheExpiry(t *testing.T) {
const expiresInSecs = 1
defer overrideSubjectTokenGood()()
fc := &testutils.FakeHTTPClient{
ReqChan: testutils.NewChannel(),
RespChan: testutils.NewChannel(),
}
defer overrideHTTPClient(fc)()
creds, err := NewCredentials(goodOptions)
if err != nil {
t.Fatalf("NewCredentials(%v) = %v", goodOptions, err)
}
// The fakeClient is configured to return an access_token with a one second
// expiry. So, in the second iteration, the credentials will find the cache
// entry, but that would have expired, and therefore we expect it to send
// out a fresh request.
for i := 0; i < 2; i++ {
errCh := make(chan error, 1)
go receiveAndCompareRequest(fc.ReqChan, errCh)
respJSON, _ := json.Marshal(responseParameters{
AccessToken: accessTokenContents,
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
TokenType: "Bearer",
ExpiresIn: expiresInSecs,
})
respBody := io.NopCloser(bytes.NewReader(respJSON))
resp := &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
Body: respBody,
}
fc.RespChan.Send(resp)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
gotMetadata, err := creds.GetRequestMetadata(createTestContext(ctx, credentials.PrivacyAndIntegrity), "")
if err != nil {
t.Fatalf("creds.GetRequestMetadata() = %v", err)
}
if !cmp.Equal(gotMetadata, goodMetadata) {
t.Fatalf("creds.GetRequestMetadata() = %v, want %v", gotMetadata, goodMetadata)
}
if err := <-errCh; err != nil {
t.Fatal(err)
}
time.Sleep(expiresInSecs * time.Second)
}
}
// TestGetRequestMetadataBadResponses verifies the scenario where the token
// exchange server returns bad responses.
func (s) TestGetRequestMetadataBadResponses(t *testing.T) {
tests := []struct {
name string
response *http.Response
}{
{
name: "bad JSON",
response: &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("not JSON")),
},
},
{
name: "no access token",
response: &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("{}")),
},
},
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
defer overrideSubjectTokenGood()()
fc := &testutils.FakeHTTPClient{
ReqChan: testutils.NewChannel(),
RespChan: testutils.NewChannel(),
}
defer overrideHTTPClient(fc)()
creds, err := NewCredentials(goodOptions)
if err != nil {
t.Fatalf("NewCredentials(%v) = %v", goodOptions, err)
}
errCh := make(chan error, 1)
go receiveAndCompareRequest(fc.ReqChan, errCh)
fc.RespChan.Send(test.response)
if _, err := creds.GetRequestMetadata(createTestContext(ctx, credentials.PrivacyAndIntegrity), ""); err == nil {
t.Fatal("creds.GetRequestMetadata() succeeded when expected to fail")
}
if err := <-errCh; err != nil {
t.Fatal(err)
}
})
}
}
// TestGetRequestMetadataBadSubjectTokenRead verifies the scenario where the
// attempt to read the subjectToken fails.
func (s) TestGetRequestMetadataBadSubjectTokenRead(t *testing.T) {
defer overrideSubjectTokenError()()
fc, cancel := overrideHTTPClientGood()
defer cancel()
creds, err := NewCredentials(goodOptions)
if err != nil {
t.Fatalf("NewCredentials(%v) = %v", goodOptions, err)
}
errCh := make(chan error, 1)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestShortTimeout)
defer cancel()
if _, err := fc.ReqChan.Receive(ctx); err != context.DeadlineExceeded {
errCh <- err
return
}
errCh <- nil
}()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err := creds.GetRequestMetadata(createTestContext(ctx, credentials.PrivacyAndIntegrity), ""); err == nil {
t.Fatal("creds.GetRequestMetadata() succeeded when expected to fail")
}
if err := <-errCh; err != nil {
t.Fatal(err)
}
}
func (s) TestNewCredentials(t *testing.T) {
tests := []struct {
name string
opts Options
errSystemRoots bool
wantErr bool
}{
{
name: "invalid options - empty subjectTokenPath",
opts: Options{
TokenExchangeServiceURI: serviceURI,
},
wantErr: true,
},
{
name: "invalid system root certs",
opts: goodOptions,
errSystemRoots: true,
wantErr: true,
},
{
name: "good case",
opts: goodOptions,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.errSystemRoots {
oldSystemRoots := loadSystemCertPool
loadSystemCertPool = func() (*x509.CertPool, error) {
return nil, errors.New("failed to load system cert pool")
}
defer func() {
loadSystemCertPool = oldSystemRoots
}()
}
creds, err := NewCredentials(test.opts)
if (err != nil) != test.wantErr {
t.Fatalf("NewCredentials(%v) = %v, want %v", test.opts, err, test.wantErr)
}
if err == nil {
if !creds.RequireTransportSecurity() {
t.Errorf("creds.RequireTransportSecurity() returned false")
}
}
})
}
}
func (s) TestValidateOptions(t *testing.T) {
tests := []struct {
name string
opts Options
wantErrPrefix string
}{
{
name: "empty token exchange service URI",
opts: Options{},
wantErrPrefix: "empty token_exchange_service_uri in options",
},
{
name: "invalid URI",
opts: Options{
TokenExchangeServiceURI: "\tI'm a bad URI\n",
},
wantErrPrefix: "invalid control character in URL",
},
{
name: "unsupported scheme",
opts: Options{
TokenExchangeServiceURI: "unix:///path/to/socket",
},
wantErrPrefix: "scheme is not supported",
},
{
name: "empty subjectTokenPath",
opts: Options{
TokenExchangeServiceURI: serviceURI,
},
wantErrPrefix: "required field SubjectTokenPath is not specified",
},
{
name: "empty subjectTokenType",
opts: Options{
TokenExchangeServiceURI: serviceURI,
SubjectTokenPath: subjectTokenPath,
},
wantErrPrefix: "required field SubjectTokenType is not specified",
},
{
name: "good options",
opts: goodOptions,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := validateOptions(test.opts)
if (err != nil) != (test.wantErrPrefix != "") {
t.Errorf("validateOptions(%v) = %v, want %v", test.opts, err, test.wantErrPrefix)
}
if err != nil && !strings.Contains(err.Error(), test.wantErrPrefix) {
t.Errorf("validateOptions(%v) = %v, want %v", test.opts, err, test.wantErrPrefix)
}
})
}
}
func (s) TestConstructRequest(t *testing.T) {
tests := []struct {
name string
opts Options
subjectTokenReadErr bool
actorTokenReadErr bool
wantReqParams *requestParameters
wantErr bool
}{
{
name: "subject token read failure",
subjectTokenReadErr: true,
opts: goodOptions,
wantErr: true,
},
{
name: "actor token read failure",
actorTokenReadErr: true,
opts: Options{
TokenExchangeServiceURI: serviceURI,
Audience: exampleAudience,
RequestedTokenType: requestedTokenType,
SubjectTokenPath: subjectTokenPath,
SubjectTokenType: subjectTokenType,
ActorTokenPath: actorTokenPath,
ActorTokenType: actorTokenType,
},
wantErr: true,
},
{
name: "default cloud platform scope",
opts: goodOptions,
wantReqParams: goodRequestParams,
},
{
name: "all good",
opts: Options{
TokenExchangeServiceURI: serviceURI,
Resource: exampleResource,
Audience: exampleAudience,
Scope: testScope,
RequestedTokenType: requestedTokenType,
SubjectTokenPath: subjectTokenPath,
SubjectTokenType: subjectTokenType,
ActorTokenPath: actorTokenPath,
ActorTokenType: actorTokenType,
},
wantReqParams: &requestParameters{
GrantType: tokenExchangeGrantType,
Resource: exampleResource,
Audience: exampleAudience,
Scope: testScope,
RequestedTokenType: requestedTokenType,
SubjectToken: subjectTokenContents,
SubjectTokenType: subjectTokenType,
ActorToken: actorTokenContents,
ActorTokenType: actorTokenType,
},
},
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.subjectTokenReadErr {
defer overrideSubjectTokenError()()
} else {
defer overrideSubjectTokenGood()()
}
if test.actorTokenReadErr {
defer overrideActorTokenError()()
} else {
defer overrideActorTokenGood()()
}
gotRequest, err := constructRequest(ctx, test.opts)
if (err != nil) != test.wantErr {
t.Fatalf("constructRequest(%v) = %v, wantErr: %v", test.opts, err, test.wantErr)
}
if test.wantErr {
return
}
if err := compareRequest(gotRequest, test.wantReqParams); err != nil {
t.Fatal(err)
}
})
}
}
func (s) TestSendRequest(t *testing.T) {
defer overrideSubjectTokenGood()()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
req, err := constructRequest(ctx, goodOptions)
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
resp *http.Response
respErr error
wantErr bool
}{
{
name: "client error",
respErr: errors.New("http.Client.Do failed"),
wantErr: true,
},
{
name: "bad response body",
resp: &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
Body: io.NopCloser(errReader{}),
},
wantErr: true,
},
{
name: "nonOK status code",
resp: &http.Response{
Status: "400 BadRequest",
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(strings.NewReader("")),
},
wantErr: true,
},
{
name: "good case",
resp: makeGoodResponse(),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
client := &testutils.FakeHTTPClient{
ReqChan: testutils.NewChannel(),
RespChan: testutils.NewChannel(),
Err: test.respErr,
}
client.RespChan.Send(test.resp)
_, err := sendRequest(client, req)
if (err != nil) != test.wantErr {
t.Errorf("sendRequest(%v) = %v, wantErr: %v", req, err, test.wantErr)
}
})
}
}
func (s) TestSendRequestDoesNotFollowRedirects(t *testing.T) {
// Stands in for a redirect target that must never receive the token
// exchange request. It records the body of any request it does get.
leaked := make(chan string, 1)
redirectTarget := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
select {
case leaked <- string(body):
default:
}
}))
defer redirectTarget.Close()
// The configured token exchange endpoint redirects the POST, whose body
// carries the subject token, to another host.
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, redirectTarget.URL, http.StatusTemporaryRedirect)
}))
defer server.Close()
req, err := http.NewRequest(http.MethodPost, server.URL, bytes.NewBufferString(`{"subject_token":"secret"}`))
if err != nil {
t.Fatalf("http.NewRequest() failed: %v", err)
}
req.Header.Set("Content-Type", "application/json")
if _, err := sendRequest(makeHTTPClient(nil), req); err == nil {
t.Error("sendRequest() succeeded across a redirect; want a non-nil error")
}
select {
case body := <-leaked:
t.Errorf("Redirect target received request body %q; the subject token must not be replayed to another host", body)
default:
}
}
func (s) TestTokenInfoFromResponse(t *testing.T) {
noAccessToken, _ := json.Marshal(responseParameters{
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
TokenType: "Bearer",
ExpiresIn: 3600,
})
goodResponse, _ := json.Marshal(responseParameters{
IssuedTokenType: requestedTokenType,
AccessToken: accessTokenContents,
TokenType: "Bearer",
ExpiresIn: 3600,
})
tests := []struct {
name string
respBody []byte
wantTokenInfo *tokenInfo
wantErr bool
}{
{
name: "bad JSON",
respBody: []byte("not JSON"),
wantErr: true,
},
{
name: "empty response",
respBody: []byte(""),
wantErr: true,
},
{
name: "non-empty response with no access token",
respBody: noAccessToken,
wantErr: true,
},
{
name: "good response",
respBody: goodResponse,
wantTokenInfo: &tokenInfo{
tokenType: "Bearer",
token: accessTokenContents,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotTokenInfo, err := tokenInfoFromResponse(test.respBody)
if (err != nil) != test.wantErr {
t.Fatalf("tokenInfoFromResponse(%+v) = %v, wantErr: %v", test.respBody, err, test.wantErr)
}
if test.wantErr {
return
}
// Can't do a cmp.Equal on the whole struct since the expiryField
// is populated based on time.Now().
if gotTokenInfo.tokenType != test.wantTokenInfo.tokenType || gotTokenInfo.token != test.wantTokenInfo.token {
t.Errorf("tokenInfoFromResponse(%+v) = %+v, want: %+v", test.respBody, gotTokenInfo, test.wantTokenInfo)
}
})
}
}

322
credentials/tls.go Normal file
View File

@@ -0,0 +1,322 @@
/*
*
* Copyright 2014 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 credentials
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/url"
"os"
"google.golang.org/grpc/grpclog"
credinternal "google.golang.org/grpc/internal/credentials"
"google.golang.org/grpc/internal/envconfig"
)
const alpnFailureHelpMessage = "If you upgraded from a grpc-go version earlier than 1.67, your TLS connections may have stopped working due to ALPN enforcement. For more details, see: https://github.com/grpc/grpc-go/issues/434"
var logger = grpclog.Component("credentials")
// TLSInfo contains the auth information for a TLS authenticated connection.
// It implements the AuthInfo interface.
type TLSInfo struct {
State tls.ConnectionState
CommonAuthInfo
// This API is experimental.
SPIFFEID *url.URL
}
// AuthType returns the type of TLSInfo as a string.
func (t TLSInfo) AuthType() string {
return "tls"
}
// ValidateAuthority validates the provided authority being used to override the
// :authority header by verifying it against the peer certificate. It returns a
// non-nil error if the validation fails.
func (t TLSInfo) ValidateAuthority(authority string) error {
host, _, err := net.SplitHostPort(authority)
if err != nil {
host = authority
}
// Verify authority against the leaf certificate.
if len(t.State.PeerCertificates) == 0 {
// This is not expected to happen as the TLS handshake has already
// completed and should have populated PeerCertificates.
return fmt.Errorf("credentials: no peer certificates found to verify authority %q", host)
}
return t.State.PeerCertificates[0].VerifyHostname(host)
}
// cipherSuiteLookup returns the string version of a TLS cipher suite ID.
func cipherSuiteLookup(cipherSuiteID uint16) string {
for _, s := range tls.CipherSuites() {
if s.ID == cipherSuiteID {
return s.Name
}
}
for _, s := range tls.InsecureCipherSuites() {
if s.ID == cipherSuiteID {
return s.Name
}
}
return fmt.Sprintf("unknown ID: %v", cipherSuiteID)
}
// GetSecurityValue returns security info requested by channelz.
func (t TLSInfo) GetSecurityValue() ChannelzSecurityValue {
v := &TLSChannelzSecurityValue{
StandardName: cipherSuiteLookup(t.State.CipherSuite),
}
// Currently there's no way to get LocalCertificate info from tls package.
if len(t.State.PeerCertificates) > 0 {
v.RemoteCertificate = t.State.PeerCertificates[0].Raw
}
return v
}
// tlsCreds is the credentials required for authenticating a connection using TLS.
type tlsCreds struct {
// TLS configuration
config *tls.Config
}
func (c tlsCreds) Info() ProtocolInfo {
return ProtocolInfo{
SecurityProtocol: "tls",
SecurityVersion: "1.2",
ServerName: c.config.ServerName,
}
}
func (c *tlsCreds) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (_ net.Conn, _ AuthInfo, err error) {
// use local cfg to avoid clobbering ServerName if using multiple endpoints
cfg := credinternal.CloneTLSConfig(c.config)
serverName, _, err := net.SplitHostPort(authority)
if err != nil {
// If the authority had no host port or if the authority cannot be parsed, use it as-is.
serverName = authority
}
cfg.ServerName = serverName
conn := tls.Client(rawConn, cfg)
errChannel := make(chan error, 1)
go func() {
errChannel <- conn.Handshake()
close(errChannel)
}()
select {
case err := <-errChannel:
if err != nil {
conn.Close()
return nil, nil, err
}
case <-ctx.Done():
conn.Close()
return nil, nil, ctx.Err()
}
// The negotiated protocol can be either of the following:
// 1. h2: When the server supports ALPN. Only HTTP/2 can be negotiated since
// it is the only protocol advertised by the client during the handshake.
// The tls library ensures that the server chooses a protocol advertised
// by the client.
// 2. "" (empty string): If the server doesn't support ALPN. ALPN is a requirement
// for using HTTP/2 over TLS. We can terminate the connection immediately.
np := conn.ConnectionState().NegotiatedProtocol
if np == "" {
if envconfig.EnforceALPNEnabled {
conn.Close()
return nil, nil, fmt.Errorf("credentials: cannot check peer: missing selected ALPN property. %s", alpnFailureHelpMessage)
}
logger.Warningf("Allowing TLS connection to server %q with ALPN disabled. TLS connections to servers with ALPN disabled will be disallowed in future grpc-go releases", cfg.ServerName)
}
tlsInfo := TLSInfo{
State: conn.ConnectionState(),
CommonAuthInfo: CommonAuthInfo{
SecurityLevel: PrivacyAndIntegrity,
},
}
id := credinternal.SPIFFEIDFromState(conn.ConnectionState())
if id != nil {
tlsInfo.SPIFFEID = id
}
return credinternal.WrapSyscallConn(rawConn, conn), tlsInfo, nil
}
func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, AuthInfo, error) {
conn := tls.Server(rawConn, c.config)
if err := conn.Handshake(); err != nil {
conn.Close()
return nil, nil, err
}
cs := conn.ConnectionState()
// The negotiated application protocol can be empty only if the client doesn't
// support ALPN. In such cases, we can close the connection since ALPN is required
// for using HTTP/2 over TLS.
if cs.NegotiatedProtocol == "" {
if envconfig.EnforceALPNEnabled {
conn.Close()
return nil, nil, fmt.Errorf("credentials: cannot check peer: missing selected ALPN property. %s", alpnFailureHelpMessage)
} else if logger.V(2) {
logger.Info("Allowing TLS connection from client with ALPN disabled. TLS connections with ALPN disabled will be disallowed in future grpc-go releases")
}
}
tlsInfo := TLSInfo{
State: cs,
CommonAuthInfo: CommonAuthInfo{
SecurityLevel: PrivacyAndIntegrity,
},
}
id := credinternal.SPIFFEIDFromState(conn.ConnectionState())
if id != nil {
tlsInfo.SPIFFEID = id
}
return credinternal.WrapSyscallConn(rawConn, conn), tlsInfo, nil
}
func (c *tlsCreds) Clone() TransportCredentials {
return NewTLS(c.config)
}
func (c *tlsCreds) OverrideServerName(serverNameOverride string) error {
c.config.ServerName = serverNameOverride
return nil
}
// The following cipher suites are forbidden for use with HTTP/2 by
// https://datatracker.ietf.org/doc/html/rfc7540#appendix-A
var tls12ForbiddenCipherSuites = map[uint16]struct{}{
tls.TLS_RSA_WITH_AES_128_CBC_SHA: {},
tls.TLS_RSA_WITH_AES_256_CBC_SHA: {},
tls.TLS_RSA_WITH_AES_128_GCM_SHA256: {},
tls.TLS_RSA_WITH_AES_256_GCM_SHA384: {},
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: {},
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: {},
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: {},
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: {},
}
// NewTLS uses c to construct a TransportCredentials based on TLS.
func NewTLS(c *tls.Config) TransportCredentials {
config := applyDefaults(c)
if config.GetConfigForClient != nil {
oldFn := config.GetConfigForClient
config.GetConfigForClient = func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
cfgForClient, err := oldFn(hello)
if err != nil || cfgForClient == nil {
return cfgForClient, err
}
return applyDefaults(cfgForClient), nil
}
}
return &tlsCreds{config: config}
}
func applyDefaults(c *tls.Config) *tls.Config {
config := credinternal.CloneTLSConfig(c)
config.NextProtos = credinternal.AppendH2ToNextProtos(config.NextProtos)
// If the user did not configure a MinVersion and did not configure a
// MaxVersion < 1.2, use MinVersion=1.2, which is required by
// https://datatracker.ietf.org/doc/html/rfc7540#section-9.2
if config.MinVersion == 0 && (config.MaxVersion == 0 || config.MaxVersion >= tls.VersionTLS12) {
config.MinVersion = tls.VersionTLS12
}
// If the user did not configure CipherSuites, use all "secure" cipher
// suites reported by the TLS package, but remove some explicitly forbidden
// by https://datatracker.ietf.org/doc/html/rfc7540#appendix-A
if config.CipherSuites == nil {
for _, cs := range tls.CipherSuites() {
if _, ok := tls12ForbiddenCipherSuites[cs.ID]; !ok {
config.CipherSuites = append(config.CipherSuites, cs.ID)
}
}
}
return config
}
// NewClientTLSFromCert constructs TLS credentials from the provided root
// certificate authority certificate(s) to validate server connections. If
// certificates to establish the identity of the client need to be included in
// the credentials (eg: for mTLS), use NewTLS instead, where a complete
// tls.Config can be specified.
//
// serverNameOverride is for testing only. If set to a non empty string, it will
// override the virtual host name of authority (e.g. :authority header field) in
// requests. Users should use grpc.WithAuthority passed to grpc.NewClient to
// override the authority of the client instead.
func NewClientTLSFromCert(cp *x509.CertPool, serverNameOverride string) TransportCredentials {
return NewTLS(&tls.Config{ServerName: serverNameOverride, RootCAs: cp})
}
// NewClientTLSFromFile constructs TLS credentials from the provided root
// certificate authority certificate file(s) to validate server connections. If
// certificates to establish the identity of the client need to be included in
// the credentials (eg: for mTLS), use NewTLS instead, where a complete
// tls.Config can be specified.
//
// serverNameOverride is for testing only. If set to a non empty string, it will
// override the virtual host name of authority (e.g. :authority header field) in
// requests. Users should use grpc.WithAuthority passed to grpc.NewClient to
// override the authority of the client instead.
func NewClientTLSFromFile(certFile, serverNameOverride string) (TransportCredentials, error) {
b, err := os.ReadFile(certFile)
if err != nil {
return nil, err
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(b) {
return nil, fmt.Errorf("credentials: failed to append certificates")
}
return NewTLS(&tls.Config{ServerName: serverNameOverride, RootCAs: cp}), nil
}
// NewServerTLSFromCert constructs TLS credentials from the input certificate for server.
func NewServerTLSFromCert(cert *tls.Certificate) TransportCredentials {
return NewTLS(&tls.Config{Certificates: []tls.Certificate{*cert}})
}
// NewServerTLSFromFile constructs TLS credentials from the input certificate file and key
// file for server.
func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil
}
// TLSChannelzSecurityValue defines the struct that TLS protocol should return
// from GetSecurityValue(), containing security info like cipher and certificate used.
//
// # Experimental
//
// Notice: This type is EXPERIMENTAL and may be changed or removed in a
// later release.
type TLSChannelzSecurityValue struct {
ChannelzSecurityValue
StandardName string
LocalCertificate []byte
RemoteCertificate []byte
}

View File

@@ -0,0 +1,114 @@
/*
*
* 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 certprovider
import (
"context"
"sync"
"google.golang.org/grpc/internal/grpcsync"
)
// Distributor makes it easy for provider implementations to furnish new key
// materials by handling synchronization between the producer and consumers of
// the key material.
//
// Provider implementations which choose to use a Distributor should do the
// following:
// - create a new Distributor using the NewDistributor() function.
// - invoke the Set() method whenever they have new key material or errors to
// report.
// - delegate to the distributor when handing calls to KeyMaterial().
// - invoke the Stop() method when they are done using the distributor.
type Distributor struct {
// mu protects the underlying key material.
mu sync.Mutex
km *KeyMaterial
pErr error
// ready channel to unblock KeyMaterial() invocations blocked on
// availability of key material.
ready *grpcsync.Event
// done channel to notify provider implementations and unblock any
// KeyMaterial() calls, once the Distributor is closed.
closed *grpcsync.Event
}
// NewDistributor returns a new Distributor.
func NewDistributor() *Distributor {
return &Distributor{
ready: grpcsync.NewEvent(),
closed: grpcsync.NewEvent(),
}
}
// Set updates the key material in the distributor with km.
//
// Provider implementations which use the distributor must not modify the
// contents of the KeyMaterial struct pointed to by km.
//
// A non-nil err value indicates the error that the provider implementation ran
// into when trying to fetch key material, and makes it possible to surface the
// error to the user. A non-nil error value passed here causes distributor's
// KeyMaterial() method to return nil key material.
func (d *Distributor) Set(km *KeyMaterial, err error) {
d.mu.Lock()
d.km = km
d.pErr = err
if err != nil {
// If a non-nil err is passed, we ignore the key material being passed.
d.km = nil
}
d.ready.Fire()
d.mu.Unlock()
}
// KeyMaterial returns the most recent key material provided to the Distributor.
// If no key material was provided at the time of this call, it will block until
// the deadline on the context expires or fresh key material arrives.
func (d *Distributor) KeyMaterial(ctx context.Context) (*KeyMaterial, error) {
if d.closed.HasFired() {
return nil, errProviderClosed
}
if d.ready.HasFired() {
return d.keyMaterial()
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-d.closed.Done():
return nil, errProviderClosed
case <-d.ready.Done():
return d.keyMaterial()
}
}
func (d *Distributor) keyMaterial() (*KeyMaterial, error) {
d.mu.Lock()
defer d.mu.Unlock()
return d.km, d.pErr
}
// Stop turns down the distributor, releases allocated resources and fails any
// active KeyMaterial() call waiting for new key material.
func (d *Distributor) Stop() {
d.closed.Fire()
}

View File

@@ -0,0 +1,110 @@
/*
*
* 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 certprovider
import (
"context"
"errors"
"testing"
"time"
)
var errProviderTestInternal = errors.New("provider internal error")
// TestDistributorEmpty tries to read key material from an empty distributor and
// expects the call to timeout.
func (s) TestDistributorEmpty(t *testing.T) {
dist := NewDistributor()
// This call to KeyMaterial() should timeout because no key material has
// been set on the distributor as yet.
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
if err := readAndVerifyKeyMaterial(ctx, dist, nil); !errors.Is(err, context.DeadlineExceeded) {
t.Fatal(err)
}
}
// TestDistributor invokes the different methods on the Distributor type and
// verifies the results.
func (s) TestDistributor(t *testing.T) {
dist := NewDistributor()
// Read cert/key files from testdata.
km1 := loadKeyMaterials(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem")
km2 := loadKeyMaterials(t, "x509/server2_cert.pem", "x509/server2_key.pem", "x509/client_ca_cert.pem")
// Push key material into the distributor and make sure that a call to
// KeyMaterial() returns the expected key material, with both the local
// certs and root certs.
dist.Set(km1, nil)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := readAndVerifyKeyMaterial(ctx, dist, km1); err != nil {
t.Fatal(err)
}
// Push new key material into the distributor and make sure that a call to
// KeyMaterial() returns the expected key material, with only root certs.
dist.Set(km2, nil)
if err := readAndVerifyKeyMaterial(ctx, dist, km2); err != nil {
t.Fatal(err)
}
// Push an error into the distributor and make sure that a call to
// KeyMaterial() returns that error and nil keyMaterial.
dist.Set(km2, errProviderTestInternal)
if gotKM, err := dist.KeyMaterial(ctx); gotKM != nil || !errors.Is(err, errProviderTestInternal) {
t.Fatalf("KeyMaterial() = {%v, %v}, want {nil, %v}", gotKM, err, errProviderTestInternal)
}
// Stop the distributor and KeyMaterial() should return errProviderClosed.
dist.Stop()
if km, err := dist.KeyMaterial(ctx); !errors.Is(err, errProviderClosed) {
t.Fatalf("KeyMaterial() = {%v, %v}, want {nil, %v}", km, err, errProviderClosed)
}
}
// TestDistributorConcurrency invokes methods on the distributor in parallel. It
// exercises that the scenario where a distributor's KeyMaterial() method is
// blocked waiting for keyMaterial, while the Set() method is called from
// another goroutine. It verifies that the KeyMaterial() method eventually
// returns with expected keyMaterial.
func (s) TestDistributorConcurrency(t *testing.T) {
dist := NewDistributor()
// Read cert/key files from testdata.
km := loadKeyMaterials(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem")
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
// Push key material into the distributor from a goroutine and read from
// here to verify that the distributor returns the expected keyMaterial.
go func() {
// Add a small sleep here to make sure that the call to KeyMaterial()
// happens before the call to Set(), thereby the former is blocked till
// the latter happens.
time.Sleep(100 * time.Microsecond)
dist.Set(km, nil)
}()
if err := readAndVerifyKeyMaterial(ctx, dist, km); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,103 @@
/*
*
* 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 pemfile
import (
"encoding/json"
"fmt"
"time"
"google.golang.org/grpc/credentials/tls/certprovider"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/durationpb"
)
const (
// PluginName is the name of the PEM file watcher plugin.
PluginName = "file_watcher"
defaultRefreshInterval = 10 * time.Minute
)
func init() {
certprovider.Register(&pluginBuilder{})
}
type pluginBuilder struct{}
func (p *pluginBuilder) ParseConfig(c any) (*certprovider.BuildableConfig, error) {
data, ok := c.(json.RawMessage)
if !ok {
return nil, fmt.Errorf("meshca: unsupported config type: %T", c)
}
opts, err := pluginConfigFromJSON(data)
if err != nil {
return nil, err
}
return certprovider.NewBuildableConfig(PluginName, opts.canonical(), func(certprovider.BuildOptions) certprovider.Provider {
return newProvider(opts)
}), nil
}
func (p *pluginBuilder) Name() string {
return PluginName
}
func pluginConfigFromJSON(jd json.RawMessage) (Options, error) {
// The only difference between this anonymous struct and the Options struct
// is that the refresh_interval is represented here as a duration proto,
// while in the latter a time.Duration is used.
cfg := &struct {
CertificateFile string `json:"certificate_file,omitempty"`
PrivateKeyFile string `json:"private_key_file,omitempty"`
CACertificateFile string `json:"ca_certificate_file,omitempty"`
SPIFFETrustBundleMapFile string `json:"spiffe_trust_bundle_map_file,omitempty"`
RefreshInterval json.RawMessage `json:"refresh_interval,omitempty"`
}{}
if err := json.Unmarshal(jd, cfg); err != nil {
return Options{}, fmt.Errorf("pemfile: json.Unmarshal(%s) failed: %v", string(jd), err)
}
if !envconfig.XDSSPIFFEEnabled {
cfg.SPIFFETrustBundleMapFile = ""
}
opts := Options{
CertFile: cfg.CertificateFile,
KeyFile: cfg.PrivateKeyFile,
RootFile: cfg.CACertificateFile,
SPIFFEBundleMapFile: cfg.SPIFFETrustBundleMapFile,
// Refresh interval is the only field in the configuration for which we
// support a default value. We cannot possibly have valid defaults for
// file paths to watch. Also, it is valid to specify an empty path for
// some of those fields if the user does not want to watch them.
RefreshDuration: defaultRefreshInterval,
}
if cfg.RefreshInterval != nil {
dur := &durationpb.Duration{}
if err := protojson.Unmarshal(cfg.RefreshInterval, dur); err != nil {
return Options{}, fmt.Errorf("pemfile: protojson.Unmarshal(%+v) failed: %v", cfg.RefreshInterval, err)
}
opts.RefreshDuration = dur.AsDuration()
}
if err := opts.validate(); err != nil {
return Options{}, err
}
return opts, nil
}

View File

@@ -0,0 +1,162 @@
/*
*
* 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 pemfile
import (
"encoding/json"
"testing"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/testutils"
)
func TestParseConfig(t *testing.T) {
tests := []struct {
desc string
input any
wantOutput string
wantErr bool
enabledSpiffe bool
}{
{
desc: "non JSON input",
input: new(int),
wantErr: true,
},
{
desc: "invalid JSON",
input: json.RawMessage(`bad bad json`),
wantErr: true,
},
{
desc: "JSON input does not match expected",
input: json.RawMessage(`["foo": "bar"]`),
wantErr: true,
},
{
desc: "no credential files",
input: json.RawMessage(`{}`),
wantErr: true,
},
{
desc: "only cert file",
input: json.RawMessage(`
{
"certificate_file": "/a/b/cert.pem"
}`),
wantErr: true,
},
{
desc: "only key file",
input: json.RawMessage(`
{
"private_key_file": "/a/b/key.pem"
}`),
wantErr: true,
},
{
desc: "cert and key in different directories",
input: json.RawMessage(`
{
"certificate_file": "/b/a/cert.pem",
"private_key_file": "/a/b/key.pem"
}`),
wantErr: true,
},
{
desc: "bad refresh duration",
input: json.RawMessage(`
{
"certificate_file": "/a/b/cert.pem",
"private_key_file": "/a/b/key.pem",
"ca_certificate_file": "/a/b/ca.pem",
"refresh_interval": "duration"
}`),
wantErr: true,
},
{
desc: "good config with default refresh interval",
input: json.RawMessage(`
{
"certificate_file": "/a/b/cert.pem",
"private_key_file": "/a/b/key.pem",
"ca_certificate_file": "/a/b/ca.pem"
}`),
wantOutput: "file_watcher:/a/b/cert.pem:/a/b/key.pem:/a/b/ca.pem::10m0s",
},
{
desc: "good config",
input: json.RawMessage(`
{
"certificate_file": "/a/b/cert.pem",
"private_key_file": "/a/b/key.pem",
"ca_certificate_file": "/a/b/ca.pem",
"refresh_interval": "200s"
}`),
wantOutput: "file_watcher:/a/b/cert.pem:/a/b/key.pem:/a/b/ca.pem::3m20s",
},
{
desc: "good config with spiffe disabled",
input: json.RawMessage(`
{
"certificate_file": "/a/b/cert.pem",
"private_key_file": "/a/b/key.pem",
"ca_certificate_file": "/a/b/ca.pem",
"spiffe_trust_bundle_map_file": "/a/b/spiffe_bundle.json",
"refresh_interval": "200s"
}`),
wantOutput: "file_watcher:/a/b/cert.pem:/a/b/key.pem:/a/b/ca.pem::3m20s",
},
{
desc: "good config with spiffe enabled",
input: json.RawMessage(`
{
"certificate_file": "/a/b/cert.pem",
"private_key_file": "/a/b/key.pem",
"ca_certificate_file": "/a/b/ca.pem",
"spiffe_trust_bundle_map_file": "/a/b/spiffe_bundle.json",
"refresh_interval": "200s"
}`),
wantOutput: "file_watcher:/a/b/cert.pem:/a/b/key.pem:/a/b/ca.pem:/a/b/spiffe_bundle.json:3m20s",
enabledSpiffe: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
if test.enabledSpiffe {
testutils.SetEnvConfig(t, &envconfig.XDSSPIFFEEnabled, true)
}
builder := &pluginBuilder{}
bc, err := builder.ParseConfig(test.input)
if (err != nil) != test.wantErr {
t.Fatalf("ParseConfig(%+v) failed: %v", test.input, err)
}
if test.wantErr {
return
}
gotConfig := bc.String()
if gotConfig != test.wantOutput {
t.Fatalf("ParseConfig(%v) = %s, want %s", test.input, gotConfig, test.wantOutput)
}
})
}
}

View File

@@ -0,0 +1,297 @@
/*
*
* 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 pemfile provides a file watching certificate provider plugin
// implementation which works for files with PEM contents.
//
// # Experimental
//
// Notice: All APIs in this package are experimental and may be removed in a
// later release.
package pemfile
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"google.golang.org/grpc/credentials/tls/certprovider"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/internal/credentials/spiffe"
)
const defaultCertRefreshDuration = 1 * time.Hour
var (
// For overriding from unit tests.
newDistributor = func() distributor { return certprovider.NewDistributor() }
logger = grpclog.Component("pemfile")
)
// Options configures a certificate provider plugin that watches a specified set
// of files that contain certificates and keys in PEM format.
type Options struct {
// CertFile is the file that holds the identity certificate.
// Optional. If this is set, KeyFile must also be set.
CertFile string
// KeyFile is the file that holds identity private key.
// Optional. If this is set, CertFile must also be set.
KeyFile string
// RootFile is the file that holds trusted root certificate(s).
// Optional.
RootFile string
// SPIFFEBundleMapFile is the file that holds the spiffe bundle map.
// If a given provider configures both the RootFile and the
// SPIFFEBundleMapFile, the SPIFFEBundleMapFile will be preferred.
// Optional.
SPIFFEBundleMapFile string
// RefreshDuration is the amount of time the plugin waits before checking
// for updates in the specified files.
// Optional. If not set, a default value (1 hour) will be used.
RefreshDuration time.Duration
}
func (o Options) canonical() []byte {
return []byte(fmt.Sprintf("%s:%s:%s:%s:%s", o.CertFile, o.KeyFile, o.RootFile, o.SPIFFEBundleMapFile, o.RefreshDuration))
}
func (o Options) validate() error {
if o.CertFile == "" && o.KeyFile == "" && o.RootFile == "" && o.SPIFFEBundleMapFile == "" {
return fmt.Errorf("pemfile: at least one credential file needs to be specified")
}
if keySpecified, certSpecified := o.KeyFile != "", o.CertFile != ""; keySpecified != certSpecified {
return fmt.Errorf("pemfile: private key file and identity cert file should be both specified or not specified")
}
// C-core has a limitation that they cannot verify that a certificate file
// matches a key file. So, the only way to get around this is to make sure
// that both files are in the same directory and that they do an atomic
// read. Even though Java/Go do not have this limitation, we want the
// overall plugin behavior to be consistent across languages.
if certDir, keyDir := filepath.Dir(o.CertFile), filepath.Dir(o.KeyFile); certDir != keyDir {
return errors.New("pemfile: certificate and key file must be in the same directory")
}
return nil
}
// NewProvider returns a new certificate provider plugin that is configured to
// watch the PEM files specified in the passed in options.
func NewProvider(o Options) (certprovider.Provider, error) {
if err := o.validate(); err != nil {
return nil, err
}
return newProvider(o), nil
}
// newProvider is used to create a new certificate provider plugin after
// validating the options, and hence does not return an error.
func newProvider(o Options) certprovider.Provider {
if o.RefreshDuration == 0 {
o.RefreshDuration = defaultCertRefreshDuration
}
provider := &watcher{opts: o}
if o.CertFile != "" && o.KeyFile != "" {
provider.identityDistributor = newDistributor()
}
if o.RootFile != "" || o.SPIFFEBundleMapFile != "" {
provider.rootDistributor = newDistributor()
}
ctx, cancel := context.WithCancel(context.Background())
provider.cancel = cancel
go provider.run(ctx)
return provider
}
// watcher is a certificate provider plugin that implements the
// certprovider.Provider interface. It watches a set of certificate and key
// files and provides the most up-to-date key material for consumption by
// credentials implementation.
type watcher struct {
identityDistributor distributor
rootDistributor distributor
opts Options
certFileContents []byte
keyFileContents []byte
rootFileContents []byte
spiffeBundleMapFileContents []byte
cancel context.CancelFunc
}
// distributor wraps the methods on certprovider.Distributor which are used by
// the plugin. This is very useful in tests which need to know exactly when the
// plugin updates its key material.
type distributor interface {
KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error)
Set(km *certprovider.KeyMaterial, err error)
Stop()
}
// updateIdentityDistributor checks if the cert/key files that the plugin is
// watching have changed, and if so, reads the new contents and updates the
// identityDistributor with the new key material.
//
// Skips updates when file reading or parsing fails.
// TODO(easwars): Retry with limit (on the number of retries or the amount of
// time) upon failures.
func (w *watcher) updateIdentityDistributor() {
if w.identityDistributor == nil {
return
}
certFileContents, err := os.ReadFile(w.opts.CertFile)
if err != nil {
logger.Warningf("certFile (%s) read failed: %v", w.opts.CertFile, err)
return
}
keyFileContents, err := os.ReadFile(w.opts.KeyFile)
if err != nil {
logger.Warningf("keyFile (%s) read failed: %v", w.opts.KeyFile, err)
return
}
// If the file contents have not changed, skip updating the distributor.
if bytes.Equal(w.certFileContents, certFileContents) && bytes.Equal(w.keyFileContents, keyFileContents) {
return
}
cert, err := tls.X509KeyPair(certFileContents, keyFileContents)
if err != nil {
logger.Warningf("tls.X509KeyPair(%q, %q) failed: %v", certFileContents, keyFileContents, err)
return
}
w.certFileContents = certFileContents
w.keyFileContents = keyFileContents
w.identityDistributor.Set(&certprovider.KeyMaterial{Certs: []tls.Certificate{cert}}, nil)
}
// updateRootDistributor checks if the root cert file that the plugin is
// watching hs changed, and if so, updates the rootDistributor with the new key
// material.
//
// Skips updates when root cert reading or parsing fails.
// TODO(easwars): Retry with limit (on the number of retries or the amount of
// time) upon failures.
func (w *watcher) updateRootDistributor() {
if w.rootDistributor == nil {
return
}
// If SPIFFEBundleMap is set, use it and DON'T use the RootFile, even if it
// fails
if w.opts.SPIFFEBundleMapFile != "" {
w.maybeUpdateSPIFFEBundleMap()
} else {
w.maybeUpdateRootFile()
}
}
func (w *watcher) maybeUpdateSPIFFEBundleMap() {
spiffeBundleMapContents, err := os.ReadFile(w.opts.SPIFFEBundleMapFile)
if err != nil {
logger.Warningf("spiffeBundleMapFile (%s) read failed: %v", w.opts.SPIFFEBundleMapFile, err)
return
}
// If the file contents have not changed, skip updating the distributor.
if bytes.Equal(w.spiffeBundleMapFileContents, spiffeBundleMapContents) {
return
}
bundleMap, err := spiffe.BundleMapFromBytes(spiffeBundleMapContents)
if err != nil {
logger.Warning("Failed to parse spiffe bundle map")
return
}
w.spiffeBundleMapFileContents = spiffeBundleMapContents
w.rootDistributor.Set(&certprovider.KeyMaterial{SPIFFEBundleMap: bundleMap}, nil)
}
func (w *watcher) maybeUpdateRootFile() {
rootFileContents, err := os.ReadFile(w.opts.RootFile)
if err != nil {
logger.Warningf("rootFile (%s) read failed: %v", w.opts.RootFile, err)
return
}
trustPool := x509.NewCertPool()
if !trustPool.AppendCertsFromPEM(rootFileContents) {
logger.Warning("Failed to parse root certificate")
return
}
// If the file contents have not changed, skip updating the distributor.
if bytes.Equal(w.rootFileContents, rootFileContents) {
return
}
w.rootFileContents = rootFileContents
w.rootDistributor.Set(&certprovider.KeyMaterial{Roots: trustPool}, nil)
}
// run is a long running goroutine which watches the configured files for
// changes, and pushes new key material into the appropriate distributors which
// is returned from calls to KeyMaterial().
func (w *watcher) run(ctx context.Context) {
ticker := time.NewTicker(w.opts.RefreshDuration)
for {
w.updateIdentityDistributor()
w.updateRootDistributor()
select {
case <-ctx.Done():
ticker.Stop()
if w.identityDistributor != nil {
w.identityDistributor.Stop()
}
if w.rootDistributor != nil {
w.rootDistributor.Stop()
}
return
case <-ticker.C:
}
}
}
// KeyMaterial returns the key material sourced by the watcher.
// Callers are expected to use the returned value as read-only.
func (w *watcher) KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error) {
km := &certprovider.KeyMaterial{}
if w.identityDistributor != nil {
identityKM, err := w.identityDistributor.KeyMaterial(ctx)
if err != nil {
return nil, err
}
km.Certs = identityKM.Certs
}
if w.rootDistributor != nil {
rootKM, err := w.rootDistributor.KeyMaterial(ctx)
if err != nil {
return nil, err
}
km.SPIFFEBundleMap = rootKM.SPIFFEBundleMap
km.Roots = rootKM.Roots
}
return km, nil
}
// Close cleans up resources allocated by the watcher.
func (w *watcher) Close() {
w.cancel()
}

View File

@@ -0,0 +1,629 @@
/*
*
* 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 pemfile
import (
"context"
"fmt"
"os"
"path"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/credentials/tls/certprovider"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/testdata"
)
const (
// These are the names of files inside temporary directories, which the
// plugin is asked to watch.
certFile = "cert.pem"
keyFile = "key.pem"
rootFile = "ca.pem"
spiffeBundleFile = "spiffebundle.json"
defaultTestRefreshDuration = 100 * time.Millisecond
defaultTestTimeout = 5 * time.Second
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
func compareKeyMaterial(got, want *certprovider.KeyMaterial) error {
if len(got.Certs) != len(want.Certs) {
return fmt.Errorf("keyMaterial certs = %+v, want %+v", got, want)
}
for i := 0; i < len(got.Certs); i++ {
if !got.Certs[i].Leaf.Equal(want.Certs[i].Leaf) {
return fmt.Errorf("keyMaterial certs = %+v, want %+v", got, want)
}
}
if gotR, wantR := got.Roots, want.Roots; !gotR.Equal(wantR) {
return fmt.Errorf("keyMaterial roots = %v, want %v", gotR, wantR)
}
if gotBundle, wantBundle := got.SPIFFEBundleMap, want.SPIFFEBundleMap; !cmp.Equal(gotBundle, wantBundle) {
return fmt.Errorf("keyMaterial spiffe bundle map = %v, want %v", gotBundle, wantBundle)
}
return nil
}
// TestNewProvider tests the NewProvider() function with different inputs.
func (s) TestNewProvider(t *testing.T) {
tests := []struct {
desc string
options Options
wantError bool
}{
{
desc: "No credential files specified",
options: Options{},
wantError: true,
},
{
desc: "Only identity cert is specified",
options: Options{
CertFile: testdata.Path("x509/client1_cert.pem"),
},
wantError: true,
},
{
desc: "Only identity key is specified",
options: Options{
KeyFile: testdata.Path("x509/client1_key.pem"),
},
wantError: true,
},
{
desc: "Identity cert/key pair is specified",
options: Options{
KeyFile: testdata.Path("x509/client1_key.pem"),
CertFile: testdata.Path("x509/client1_cert.pem"),
},
},
{
desc: "Only root certs are specified",
options: Options{
RootFile: testdata.Path("x509/client_ca_cert.pem"),
},
},
{
desc: "Only spiffe bundle map specified",
options: Options{
SPIFFEBundleMapFile: testdata.Path("spiffe/spiffebundle.json"),
},
},
{
desc: "Everything is specified",
options: Options{
KeyFile: testdata.Path("x509/client1_key.pem"),
CertFile: testdata.Path("x509/client1_cert.pem"),
RootFile: testdata.Path("x509/client_ca_cert.pem"),
SPIFFEBundleMapFile: testdata.Path("spiffe/spiffebundle.json"),
},
wantError: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
provider, err := NewProvider(test.options)
if (err != nil) != test.wantError {
t.Fatalf("NewProvider(%v) = %v, want %v", test.options, err, test.wantError)
}
if err != nil {
return
}
provider.Close()
})
}
}
// wrappedDistributor wraps a distributor and pushes on a channel whenever new
// key material is pushed to the distributor.
type wrappedDistributor struct {
*certprovider.Distributor
distCh *testutils.Channel
}
func newWrappedDistributor(distCh *testutils.Channel) *wrappedDistributor {
return &wrappedDistributor{
distCh: distCh,
Distributor: certprovider.NewDistributor(),
}
}
func (wd *wrappedDistributor) Set(km *certprovider.KeyMaterial, err error) {
wd.Distributor.Set(km, err)
wd.distCh.Send(nil)
}
func createTmpFile(t *testing.T, src, dst string) {
t.Helper()
data, err := os.ReadFile(src)
if err != nil {
t.Fatalf("os.ReadFile(%q) failed: %v", src, err)
}
if err := os.WriteFile(dst, data, os.ModePerm); err != nil {
t.Fatalf("os.WriteFile(%q) failed: %v", dst, err)
}
t.Logf("Wrote file at: %s", dst)
t.Logf("%s", string(data))
}
func removeTmpFile(t *testing.T, filePath string) {
t.Helper()
if err := os.Remove(filePath); err != nil {
t.Fatalf("os.RemoveFIle(%q) failed: %v", filePath, err)
}
t.Logf("Removed file at: %s", filePath)
}
// createTempDirWithFiles creates a temporary directory under the system default
// tempDir with the given dirSuffix. It also reads from certSrc, keySrc and
// rootSrc files are creates appropriate files under the newly create tempDir.
// Returns the name of the created tempDir.
func createTmpDirWithFiles(t *testing.T, dirSuffix, certSrc, keySrc, rootSrc, spiffeBundleSrc string) string {
t.Helper()
// Create a temp directory. Passing an empty string for the first argument
// uses the system temp directory.
dir, err := os.MkdirTemp("", dirSuffix)
if err != nil {
t.Fatalf("os.MkdirTemp() failed: %v", err)
}
t.Logf("Using tmpdir: %s", dir)
createTmpFile(t, testdata.Path(certSrc), path.Join(dir, certFile))
createTmpFile(t, testdata.Path(keySrc), path.Join(dir, keyFile))
createTmpFile(t, testdata.Path(rootSrc), path.Join(dir, rootFile))
createTmpFile(t, testdata.Path(spiffeBundleSrc), path.Join(dir, spiffeBundleFile))
return dir
}
// initializeProvider performs setup steps common to all tests (except the one
// which uses symlinks).
func initializeProvider(t *testing.T, testName string, useSPIFFEBundle bool) (string, certprovider.Provider, *testutils.Channel, func()) {
t.Helper()
// Override the newDistributor to one which pushes on a channel that we
// can block on.
origDistributorFunc := newDistributor
distCh := testutils.NewChannel()
d := newWrappedDistributor(distCh)
newDistributor = func() distributor { return d }
// Create a new provider to watch the files in tmpdir.
dir := createTmpDirWithFiles(t, testName+"*", "x509/client1_cert.pem", "x509/client1_key.pem", "x509/client_ca_cert.pem", "spiffe/spiffebundle.json")
opts := Options{
CertFile: path.Join(dir, certFile),
KeyFile: path.Join(dir, keyFile),
RootFile: path.Join(dir, rootFile),
RefreshDuration: defaultTestRefreshDuration,
}
if useSPIFFEBundle {
opts.SPIFFEBundleMapFile = path.Join(dir, spiffeBundleFile)
}
prov, err := NewProvider(opts)
if err != nil {
t.Fatalf("NewProvider(%+v) failed: %v", opts, err)
}
// Make sure the provider picks up the files and pushes the key material on
// to the distributors.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
for i := 0; i < 2; i++ {
// Since we have root and identity certs, we need to make sure the
// update is pushed on both of them.
if _, err := distCh.Receive(ctx); err != nil {
t.Fatalf("Timeout waiting for provider to read files and push key material to distributor: %v", err)
}
}
return dir, prov, distCh, func() {
newDistributor = origDistributorFunc
prov.Close()
}
}
// TestProvider_NoUpdate tests the case where a file watcher plugin is created
// successfully, and the underlying files do not change. Verifies that the
// plugin does not push new updates to the distributor in this case.
func (s) TestProvider_NoUpdate(t *testing.T) {
baseName := "no_update"
for _, useSPIFFEBundle := range []bool{true, false} {
testName := baseName
if useSPIFFEBundle {
testName = testName + "_" + "withSPIFFEBundle"
}
t.Run(testName, func(t *testing.T) {
_, prov, distCh, cancel := initializeProvider(t, "no_update", useSPIFFEBundle)
defer cancel()
// Make sure the provider is healthy and returns key material.
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cc()
if _, err := prov.KeyMaterial(ctx); err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
// Files haven't change. Make sure no updates are pushed by the provider.
sCtx, sc := context.WithTimeout(context.Background(), 2*defaultTestRefreshDuration)
defer sc()
if _, err := distCh.Receive(sCtx); err == nil {
t.Fatal("New key material pushed to distributor when underlying files did not change")
}
})
}
}
// TestProvider_UpdateSuccess tests the case where a file watcher plugin is
// created successfully and the underlying files change. Verifies that the
// changes are picked up by the provider.
func (s) TestProvider_UpdateSuccess(t *testing.T) {
baseName := "update_success"
for _, useSPIFFEBundle := range []bool{true, false} {
testName := baseName
if useSPIFFEBundle {
testName = testName + "_" + "withSPIFFEBundle"
}
t.Run(testName, func(t *testing.T) {
dir, prov, distCh, cancel := initializeProvider(t, "update_success", useSPIFFEBundle)
defer cancel()
// Make sure the provider is healthy and returns key material.
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cc()
km1, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
// Change only the root file.
if useSPIFFEBundle {
createTmpFile(t, testdata.Path("spiffe/spiffebundle2.json"), path.Join(dir, spiffeBundleFile))
} else {
createTmpFile(t, testdata.Path("x509/server_ca_cert.pem"), path.Join(dir, rootFile))
}
if _, err := distCh.Receive(ctx); err != nil {
t.Fatal("Timeout waiting for new key material to be pushed to the distributor")
}
// Make sure update is picked up.
km2, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
if err := compareKeyMaterial(km1, km2); err == nil {
t.Fatal("Expected provider to return new key material after update to underlying file")
}
// Change only cert/key files.
createTmpFile(t, testdata.Path("x509/client2_cert.pem"), path.Join(dir, certFile))
createTmpFile(t, testdata.Path("x509/client2_key.pem"), path.Join(dir, keyFile))
if _, err := distCh.Receive(ctx); err != nil {
t.Fatal("Timeout waiting for new key material to be pushed to the distributor")
}
// Make sure update is picked up.
km3, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
if err := compareKeyMaterial(km2, km3); err == nil {
t.Fatal("Expected provider to return new key material after update to underlying file")
}
})
}
}
// TestProvider_UpdateSuccessWithSymlink tests the case where a file watcher
// plugin is created successfully to watch files through a symlink and the
// symlink is updates to point to new files. Verifies that the changes are
// picked up by the provider.
func (s) TestProvider_UpdateSuccessWithSymlink(t *testing.T) {
baseName := "update_with_symlink"
for _, useSPIFFEBundle := range []bool{true, false} {
testName := baseName
if useSPIFFEBundle {
testName = testName + "_" + "withSPIFFEBundle"
}
t.Run(testName, func(t *testing.T) {
// Override the newDistributor to one which pushes on a channel that we
// can block on.
origDistributorFunc := newDistributor
distCh := testutils.NewChannel()
d := newWrappedDistributor(distCh)
newDistributor = func() distributor { return d }
defer func() { newDistributor = origDistributorFunc }()
// Create two tempDirs with different files.
dir1 := createTmpDirWithFiles(t, "update_with_symlink1_*", "x509/client1_cert.pem", "x509/client1_key.pem", "x509/client_ca_cert.pem", "spiffe/spiffebundle.json")
dir2 := createTmpDirWithFiles(t, "update_with_symlink2_*", "x509/server1_cert.pem", "x509/server1_key.pem", "x509/server_ca_cert.pem", "spiffe/spiffebundle2.json")
// Create a symlink under a new tempdir, and make it point to dir1.
tmpdir, err := os.MkdirTemp("", "test_symlink_*")
if err != nil {
t.Fatalf("os.MkdirTemp() failed: %v", err)
}
symLinkName := path.Join(tmpdir, "test_symlink")
if err := os.Symlink(dir1, symLinkName); err != nil {
t.Fatalf("Failed to create symlink to %q: %v", dir1, err)
}
// Create a provider which watches the files pointed to by the symlink.
opts := Options{
CertFile: path.Join(symLinkName, certFile),
KeyFile: path.Join(symLinkName, keyFile),
RootFile: path.Join(symLinkName, rootFile),
SPIFFEBundleMapFile: path.Join(symLinkName, spiffeBundleFile),
RefreshDuration: defaultTestRefreshDuration,
}
if useSPIFFEBundle {
opts.SPIFFEBundleMapFile = path.Join(symLinkName, spiffeBundleFile)
}
prov, err := NewProvider(opts)
if err != nil {
t.Fatalf("NewProvider(%+v) failed: %v", opts, err)
}
defer prov.Close()
// Make sure the provider picks up the files and pushes the key material on
// to the distributors.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
for i := 0; i < 2; i++ {
// Since we have root and identity certs, we need to make sure the
// update is pushed on both of them.
if _, err := distCh.Receive(ctx); err != nil {
t.Fatalf("Timeout waiting for provider to read files and push key material to distributor: %v", err)
}
}
km1, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
// Update the symlink to point to dir2.
symLinkTmpName := path.Join(tmpdir, "test_symlink.tmp")
if err := os.Symlink(dir2, symLinkTmpName); err != nil {
t.Fatalf("Failed to create symlink to %q: %v", dir2, err)
}
if err := os.Rename(symLinkTmpName, symLinkName); err != nil {
t.Fatalf("Failed to update symlink: %v", err)
}
// Make sure the provider picks up the new files and pushes the key material
// on to the distributors.
for i := 0; i < 2; i++ {
// Since we have root and identity certs, we need to make sure the
// update is pushed on both of them.
if _, err := distCh.Receive(ctx); err != nil {
t.Fatalf("Timeout waiting for provider to read files and push key material to distributor: %v", err)
}
}
km2, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
if err := compareKeyMaterial(km1, km2); err == nil {
t.Fatal("Expected provider to return new key material after symlink update")
}
})
}
}
// TestProvider_UpdateFailure_ThenSuccess tests the case where updating cert/key
// files fail. Verifies that the failed update does not push anything on the
// distributor. Then the update succeeds, and the test verifies that the key
// material is updated.
func (s) TestProvider_UpdateFailure_ThenSuccess(t *testing.T) {
dir, prov, distCh, cancel := initializeProvider(t, "update_failure", false)
defer cancel()
// Make sure the provider is healthy and returns key material.
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cc()
km1, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
// Update only the cert file. The key file is left unchanged. This should
// lead to these two files being not compatible with each other. This
// simulates the case where the watching goroutine might catch the files in
// the midst of an update.
createTmpFile(t, testdata.Path("x509/server1_cert.pem"), path.Join(dir, certFile))
// Since the last update left the files in an incompatible state, the update
// should not be picked up by our provider.
sCtx, sc := context.WithTimeout(context.Background(), 2*defaultTestRefreshDuration)
defer sc()
if _, err := distCh.Receive(sCtx); err == nil {
t.Fatal("New key material pushed to distributor when underlying files did not change")
}
// The provider should return key material corresponding to the old state.
km2, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
if err := compareKeyMaterial(km1, km2); err != nil {
t.Fatalf("Expected provider to not update key material: %v", err)
}
// Update the key file to match the cert file.
createTmpFile(t, testdata.Path("x509/server1_key.pem"), path.Join(dir, keyFile))
// Make sure update is picked up.
if _, err := distCh.Receive(ctx); err != nil {
t.Fatal("Timeout waiting for new key material to be pushed to the distributor")
}
km3, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
if err := compareKeyMaterial(km2, km3); err == nil {
t.Fatal("Expected provider to return new key material after update to underlying file")
}
}
// TestProvider_UpdateFailure_ThenSuccess tests the case where updating cert/key
// files fail. Verifies that the failed update does not push anything on the
// distributor. Then the update succeeds, and the test verifies that the key
// material is updated.
func (s) TestProvider_UpdateFailureSPIFFE(t *testing.T) {
tests := []struct {
name string
badFile string
}{
{
name: "malformed spiffe",
badFile: "spiffe/spiffebundle_malformed.json",
},
{
name: "invalid bundle",
badFile: "spiffe/spiffebundle_wrong_kty.json",
},
{
name: "cert in the x5c field is invalid",
badFile: "spiffe/spiffebundle_corrupted_cert.json",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
dir, prov, distCh, cancel := initializeProvider(t, tc.name, true)
defer cancel()
// Make sure the provider is healthy and returns key material.
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cc()
km1, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
// Update the file with a bad update
createTmpFile(t, testdata.Path(tc.badFile), path.Join(dir, spiffeBundleFile))
// Since the last update left the files in an incompatible state, the update
// should not be picked up by our provider.
sCtx, sc := context.WithTimeout(context.Background(), 2*defaultTestRefreshDuration)
defer sc()
if _, err := distCh.Receive(sCtx); err == nil {
t.Fatal("New key material pushed to distributor when underlying files did not change")
}
// The provider should return key material corresponding to the old state.
km2, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
if err := compareKeyMaterial(km1, km2); err != nil {
t.Fatalf("Expected provider to not update key material: %v", err)
}
})
}
}
// TestProvider_UpdateFailure_ThenSuccess tests the case where updating cert/key
// files fail. Verifies that the failed update does not push anything on the
// distributor. Then the update succeeds, and the test verifies that the key
// material is updated.
func (s) TestProvider_UpdateFailureSPIFFE_MissingFile(t *testing.T) {
dir, prov, distCh, cancel := initializeProvider(t, "Delete spiffe file being read", true)
defer cancel()
// Make sure the provider is healthy and returns key material.
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cc()
km1, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
// Remove the file that we are reading
removeTmpFile(t, path.Join(dir, spiffeBundleFile))
// Since the last update left the files in an incompatible state, the update
// should not be picked up by our provider.
sCtx, sc := context.WithTimeout(context.Background(), 2*defaultTestRefreshDuration)
defer sc()
if _, err := distCh.Receive(sCtx); err == nil {
t.Fatal("new key material pushed to distributor when underlying files did not change")
}
// The provider should return key material corresponding to the old state.
km2, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
if err := compareKeyMaterial(km1, km2); err != nil {
t.Fatalf("expected provider to not update key material: %v", err)
}
}
// TestProvider_UpdateFailure_ThenSuccess tests the case where updating cert/key
// files fail. Verifies that the failed update does not push anything on the
// distributor. Then the update succeeds, and the test verifies that the key
// material is updated.
func (s) TestProvider_UpdateFailureRoot_MissingFile(t *testing.T) {
dir, prov, distCh, cancel := initializeProvider(t, "Delete root file being read", false)
defer cancel()
// Make sure the provider is healthy and returns key material.
ctx, cc := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cc()
km1, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
// Remove the file that we are reading
removeTmpFile(t, path.Join(dir, rootFile))
// Since the last update left the files in an incompatible state, the update
// should not be picked up by our provider.
sCtx, sc := context.WithTimeout(context.Background(), 2*defaultTestRefreshDuration)
defer sc()
if _, err := distCh.Receive(sCtx); err == nil {
t.Fatal("new key material pushed to distributor when underlying files did not change")
}
// The provider should return key material corresponding to the old state.
km2, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial() failed: %v", err)
}
if err := compareKeyMaterial(km1, km2); err != nil {
t.Fatalf("expected provider to not update key material: %v", err)
}
}

View File

@@ -0,0 +1,115 @@
/*
*
* 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 certprovider defines APIs for Certificate Providers in gRPC.
//
// # Experimental
//
// Notice: All APIs in this package are experimental and may be removed in a
// later release.
package certprovider
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"github.com/spiffe/go-spiffe/v2/bundle/spiffebundle"
"google.golang.org/grpc/internal"
)
func init() {
internal.GetCertificateProviderBuilder = getBuilder
}
var (
// errProviderClosed is returned by Distributor.KeyMaterial when it is
// closed.
errProviderClosed = errors.New("provider instance is closed")
// m is a map from name to Provider builder.
m = make(map[string]Builder)
)
// Register registers the Provider builder, whose name as returned by its Name()
// method will be used as the name registered with this builder. Registered
// Builders are used by the Store to create Providers.
func Register(b Builder) {
m[b.Name()] = b
}
// getBuilder returns the Provider builder registered with the given name.
// If no builder is registered with the provided name, nil will be returned.
func getBuilder(name string) Builder {
if b, ok := m[name]; ok {
return b
}
return nil
}
// Builder creates a Provider.
type Builder interface {
// ParseConfig parses the given config, which is in a format specific to individual
// implementations, and returns a BuildableConfig on success.
ParseConfig(any) (*BuildableConfig, error)
// Name returns the name of providers built by this builder.
Name() string
}
// Provider makes it possible to keep channel credential implementations up to
// date with secrets that they rely on to secure communications on the
// underlying channel.
//
// Provider implementations are free to rely on local or remote sources to fetch
// the latest secrets, and free to share any state between different
// instantiations as they deem fit.
type Provider interface {
// KeyMaterial returns the key material sourced by the Provider.
// Callers are expected to use the returned value as read-only.
KeyMaterial(ctx context.Context) (*KeyMaterial, error)
// Close cleans up resources allocated by the Provider.
Close()
}
// KeyMaterial wraps the certificates and keys returned by a Provider instance.
type KeyMaterial struct {
// Certs contains a slice of cert/key pairs used to prove local identity.
Certs []tls.Certificate
// Roots contains the set of trusted roots to validate the peer's identity.
// This field will only be used if the `SPIFFEBundleMap` field is unset.
Roots *x509.CertPool
// SPIFFEBundleMap is an in-memory representation of a spiffe trust bundle
// map. If this value exists, it will be used to find the roots for a given
// trust domain rather than the Roots in this struct.
SPIFFEBundleMap map[string]*spiffebundle.Bundle
}
// BuildOptions contains parameters passed to a Provider at build time.
type BuildOptions struct {
// CertName holds the certificate name, whose key material is of interest to
// the caller.
CertName string
// WantRoot indicates if the caller is interested in the root certificate.
WantRoot bool
// WantIdentity indicates if the caller is interested in the identity
// certificate.
WantIdentity bool
}

View File

@@ -0,0 +1,196 @@
/*
*
* 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 certprovider
import (
"context"
"fmt"
"sync"
"sync/atomic"
)
// provStore is the global singleton certificate provider store.
var provStore = &store{
providers: make(map[storeKey]*wrappedProvider),
}
// storeKey acts as the key to the map of providers maintained by the store. A
// combination of provider name and configuration is used to uniquely identify
// every provider instance in the store. Go maps need to be indexed by
// comparable types, so the provider configuration is converted from `any` to
// `string` using the ParseConfig method while creating this key.
type storeKey struct {
// name of the certificate provider.
name string
// configuration of the certificate provider in string form.
config string
// opts contains the certificate name and other keyMaterial options.
opts BuildOptions
}
// wrappedProvider wraps a provider instance with a reference count.
type wrappedProvider struct {
Provider
refCount int
// A reference to the key and store are also kept here to override the
// Close method on the provider.
storeKey storeKey
store *store
}
// closedProvider always returns errProviderClosed error.
type closedProvider struct{}
func (c closedProvider) KeyMaterial(context.Context) (*KeyMaterial, error) {
return nil, errProviderClosed
}
func (c closedProvider) Close() {
}
// singleCloseWrappedProvider wraps a provider instance with a reference count
// to properly handle multiple calls to Close.
type singleCloseWrappedProvider struct {
provider atomic.Pointer[Provider]
}
// store is a collection of provider instances, safe for concurrent access.
type store struct {
mu sync.Mutex
providers map[storeKey]*wrappedProvider
}
// Close overrides the Close method of the embedded provider. It releases the
// reference held by the caller on the underlying provider and if the
// provider's reference count reaches zero, it is removed from the store, and
// its Close method is also invoked.
func (wp *wrappedProvider) Close() {
ps := wp.store
ps.mu.Lock()
defer ps.mu.Unlock()
wp.refCount--
if wp.refCount == 0 {
wp.Provider.Close()
delete(ps.providers, wp.storeKey)
}
}
// Close overrides the Close method of the embedded provider to avoid release the
// already released reference.
func (w *singleCloseWrappedProvider) Close() {
newProvider := Provider(closedProvider{})
oldProvider := w.provider.Swap(&newProvider)
(*oldProvider).Close()
}
// KeyMaterial returns the key material sourced by the Provider.
// Callers are expected to use the returned value as read-only.
func (w *singleCloseWrappedProvider) KeyMaterial(ctx context.Context) (*KeyMaterial, error) {
return (*w.provider.Load()).KeyMaterial(ctx)
}
// newSingleCloseWrappedProvider create wrapper a provider instance with a reference count
// to properly handle multiple calls to Close.
func newSingleCloseWrappedProvider(provider Provider) *singleCloseWrappedProvider {
w := &singleCloseWrappedProvider{}
w.provider.Store(&provider)
return w
}
// BuildableConfig wraps parsed provider configuration and functionality to
// instantiate provider instances.
type BuildableConfig struct {
name string
config []byte
starter func(BuildOptions) Provider
pStore *store
}
// NewBuildableConfig creates a new BuildableConfig with the given arguments.
// Provider implementations are expected to invoke this function after parsing
// the given configuration as part of their ParseConfig() method.
// Equivalent configurations are expected to invoke this function with the same
// config argument.
func NewBuildableConfig(name string, config []byte, starter func(BuildOptions) Provider) *BuildableConfig {
return &BuildableConfig{
name: name,
config: config,
starter: starter,
pStore: provStore,
}
}
// Build kicks off a provider instance with the wrapped configuration. Multiple
// invocations of this method with the same opts will result in provider
// instances being reused.
func (bc *BuildableConfig) Build(opts BuildOptions) (Provider, error) {
provStore.mu.Lock()
defer provStore.mu.Unlock()
sk := storeKey{
name: bc.name,
config: string(bc.config),
opts: opts,
}
if wp, ok := provStore.providers[sk]; ok {
wp.refCount++
return newSingleCloseWrappedProvider(wp), nil
}
provider := bc.starter(opts)
if provider == nil {
return nil, fmt.Errorf("provider(%q, %q).Build(%v) failed", sk.name, sk.config, opts)
}
wp := &wrappedProvider{
Provider: provider,
refCount: 1,
storeKey: sk,
store: provStore,
}
provStore.providers[sk] = wp
return newSingleCloseWrappedProvider(wp), nil
}
// String returns the provider name and config as a colon separated string.
func (bc *BuildableConfig) String() string {
return fmt.Sprintf("%s:%s", bc.name, string(bc.config))
}
// ParseConfig is a convenience function to create a BuildableConfig given a
// provider name and configuration. Returns an error if there is no registered
// builder for the given name or if the config parsing fails.
func ParseConfig(name string, config any) (*BuildableConfig, error) {
parser := getBuilder(name)
if parser == nil {
return nil, fmt.Errorf("no certificate provider builder found for %q", name)
}
return parser.ParseConfig(config)
}
// GetProvider is a convenience function to create a provider given the name,
// config and build options.
func GetProvider(name string, config any, opts BuildOptions) (Provider, error) {
bc, err := ParseConfig(name, config)
if err != nil {
return nil, err
}
return bc.Build(opts)
}

View File

@@ -0,0 +1,400 @@
/*
*
* 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 certprovider
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
"testing"
"time"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/testdata"
)
const (
fakeProvider1Name = "fake-certificate-provider-1"
fakeProvider2Name = "fake-certificate-provider-2"
fakeConfig = "my fake config"
defaultTestTimeout = 5 * time.Second
defaultTestShortTimeout = 10 * time.Millisecond
)
var fpb1, fpb2 *fakeProviderBuilder
func init() {
fpb1 = &fakeProviderBuilder{
name: fakeProvider1Name,
providerChan: testutils.NewChannel(),
}
fpb2 = &fakeProviderBuilder{
name: fakeProvider2Name,
providerChan: testutils.NewChannel(),
}
Register(fpb1)
Register(fpb2)
}
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
// fakeProviderBuilder builds new instances of fakeProvider and interprets the
// config provided to it as a string.
type fakeProviderBuilder struct {
name string
providerChan *testutils.Channel
}
func (b *fakeProviderBuilder) ParseConfig(config any) (*BuildableConfig, error) {
s, ok := config.(string)
if !ok {
return nil, fmt.Errorf("providerBuilder %s received config of type %T, want string", b.name, config)
}
return NewBuildableConfig(b.name, []byte(s), func(BuildOptions) Provider {
fp := &fakeProvider{
Distributor: NewDistributor(),
config: s,
}
b.providerChan.Send(fp)
return fp
}), nil
}
func (b *fakeProviderBuilder) Name() string {
return b.name
}
// fakeProvider is an implementation of the Provider interface which provides a
// method for tests to invoke to push new key materials.
type fakeProvider struct {
*Distributor
config string
}
func (p *fakeProvider) Start(BuildOptions) Provider {
// This is practically a no-op since this provider doesn't do any work which
// needs to be started at this point.
return p
}
// newKeyMaterial allows tests to push new key material to the fake provider
// which will be made available to users of this provider.
func (p *fakeProvider) newKeyMaterial(km *KeyMaterial, err error) {
p.Distributor.Set(km, err)
}
// Close helps implement the Provider interface.
func (p *fakeProvider) Close() {
p.Distributor.Stop()
}
// loadKeyMaterials is a helper to read cert/key files from testdata and convert
// them into a KeyMaterialReader struct.
func loadKeyMaterials(t *testing.T, cert, key, ca string) *KeyMaterial {
t.Helper()
certs, err := tls.LoadX509KeyPair(testdata.Path(cert), testdata.Path(key))
if err != nil {
t.Fatalf("Failed to load keyPair: %v", err)
}
pemData, err := os.ReadFile(testdata.Path(ca))
if err != nil {
t.Fatal(err)
}
roots := x509.NewCertPool()
roots.AppendCertsFromPEM(pemData)
return &KeyMaterial{Certs: []tls.Certificate{certs}, Roots: roots}
}
// kmReader wraps the KeyMaterial method exposed by Provider and Distributor
// implementations. Defining the interface here makes it possible to use the
// same helper from both provider and distributor tests.
type kmReader interface {
KeyMaterial(context.Context) (*KeyMaterial, error)
}
// readAndVerifyKeyMaterial attempts to read key material from the given
// provider and compares it against the expected key material.
func readAndVerifyKeyMaterial(ctx context.Context, kmr kmReader, wantKM *KeyMaterial) error {
gotKM, err := kmr.KeyMaterial(ctx)
if err != nil {
return fmt.Errorf("KeyMaterial(ctx) failed: %w", err)
}
return compareKeyMaterial(gotKM, wantKM)
}
func compareKeyMaterial(got, want *KeyMaterial) error {
if len(got.Certs) != len(want.Certs) {
return fmt.Errorf("keyMaterial certs = %+v, want %+v", got, want)
}
for i := 0; i < len(got.Certs); i++ {
if !got.Certs[i].Leaf.Equal(want.Certs[i].Leaf) {
return fmt.Errorf("keyMaterial certs = %+v, want %+v", got, want)
}
}
if gotR, wantR := got.Roots, want.Roots; !gotR.Equal(wantR) {
return fmt.Errorf("keyMaterial roots = %v, want %v", gotR, wantR)
}
return nil
}
func createProvider(t *testing.T, name, config string, opts BuildOptions) Provider {
t.Helper()
prov, err := GetProvider(name, config, opts)
if err != nil {
t.Fatalf("GetProvider(%s, %s, %v) failed: %v", name, config, opts, err)
}
return prov
}
// TestStoreSingleProvider creates a single provider through the store and calls
// methods on them.
func (s) TestStoreSingleProvider(t *testing.T) {
prov := createProvider(t, fakeProvider1Name, fakeConfig, BuildOptions{CertName: "default"})
defer prov.Close()
// Our fakeProviderBuilder pushes newly created providers on a channel. Grab
// the fake provider from that channel.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
p, err := fpb1.providerChan.Receive(ctx)
if err != nil {
t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name)
}
fakeProv := p.(*fakeProvider)
// Attempt to read from key material from the Provider returned by the
// store. This will fail because we have not pushed any key material into
// our fake provider.
sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout)
defer sCancel()
if err := readAndVerifyKeyMaterial(sCtx, prov, nil); !errors.Is(err, context.DeadlineExceeded) {
t.Fatal(err)
}
// Load key material from testdata directory, push it into out fakeProvider
// and attempt to read from the Provider returned by the store.
testKM1 := loadKeyMaterials(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem")
fakeProv.newKeyMaterial(testKM1, nil)
if err := readAndVerifyKeyMaterial(ctx, prov, testKM1); err != nil {
t.Fatal(err)
}
// Push new key material and read from the Provider. This should returned
// updated key material.
testKM2 := loadKeyMaterials(t, "x509/server2_cert.pem", "x509/server2_key.pem", "x509/client_ca_cert.pem")
fakeProv.newKeyMaterial(testKM2, nil)
if err := readAndVerifyKeyMaterial(ctx, prov, testKM2); err != nil {
t.Fatal(err)
}
}
// TestStoreSingleProviderSameConfigDifferentOpts creates multiple providers of
// same type, for same configs but different keyMaterial options through the
// store (and expects the store's sharing mechanism to kick in) and calls
// methods on them.
func (s) TestStoreSingleProviderSameConfigDifferentOpts(t *testing.T) {
// Create three readers on the same fake provider. Two of these readers use
// certName `foo`, while the third one uses certName `bar`.
optsFoo := BuildOptions{CertName: "foo"}
provFoo1 := createProvider(t, fakeProvider1Name, fakeConfig, optsFoo)
provFoo2 := createProvider(t, fakeProvider1Name, fakeConfig, optsFoo)
defer func() {
provFoo1.Close()
provFoo2.Close()
}()
// Our fakeProviderBuilder pushes newly created providers on a channel.
// Grab the fake provider for optsFoo.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
p, err := fpb1.providerChan.Receive(ctx)
if err != nil {
t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name)
}
fakeProvFoo := p.(*fakeProvider)
// Make sure only provider was created by the builder so far. The store
// should be able to share the providers.
sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout)
defer sCancel()
if _, err := fpb1.providerChan.Receive(sCtx); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("A second provider created when expected to be shared by the store")
}
optsBar := BuildOptions{CertName: "bar"}
provBar1 := createProvider(t, fakeProvider1Name, fakeConfig, optsBar)
defer provBar1.Close()
// Grab the fake provider for optsBar.
p, err = fpb1.providerChan.Receive(ctx)
if err != nil {
t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name)
}
fakeProvBar := p.(*fakeProvider)
// Push key material for optsFoo, and make sure the foo providers return
// appropriate key material and the bar provider times out.
fooKM := loadKeyMaterials(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem")
fakeProvFoo.newKeyMaterial(fooKM, nil)
if err := readAndVerifyKeyMaterial(ctx, provFoo1, fooKM); err != nil {
t.Fatal(err)
}
if err := readAndVerifyKeyMaterial(ctx, provFoo2, fooKM); err != nil {
t.Fatal(err)
}
sCtx, sCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout)
defer sCancel()
if err := readAndVerifyKeyMaterial(sCtx, provBar1, nil); !errors.Is(err, context.DeadlineExceeded) {
t.Fatal(err)
}
// Push key material for optsBar, and make sure the bar provider returns
// appropriate key material.
barKM := loadKeyMaterials(t, "x509/server2_cert.pem", "x509/server2_key.pem", "x509/client_ca_cert.pem")
fakeProvBar.newKeyMaterial(barKM, nil)
if err := readAndVerifyKeyMaterial(ctx, provBar1, barKM); err != nil {
t.Fatal(err)
}
// Make sure the above push of new key material does not affect foo readers.
if err := readAndVerifyKeyMaterial(ctx, provFoo1, fooKM); err != nil {
t.Fatal(err)
}
}
// TestStoreSingleProviderDifferentConfigs creates multiple instances of the
// same type of provider through the store with different configs. The store
// would end up creating different provider instances for these and no sharing
// would take place.
func (s) TestStoreSingleProviderDifferentConfigs(t *testing.T) {
// Create two providers of the same type, but with different configs.
opts := BuildOptions{CertName: "foo"}
cfg1 := fakeConfig + "1111"
cfg2 := fakeConfig + "2222"
prov1 := createProvider(t, fakeProvider1Name, cfg1, opts)
defer prov1.Close()
// Our fakeProviderBuilder pushes newly created providers on a channel. Grab
// the fake provider from that channel.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
p1, err := fpb1.providerChan.Receive(ctx)
if err != nil {
t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name)
}
fakeProv1 := p1.(*fakeProvider)
prov2 := createProvider(t, fakeProvider1Name, cfg2, opts)
defer prov2.Close()
// Grab the second provider from the channel.
p2, err := fpb1.providerChan.Receive(ctx)
if err != nil {
t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name)
}
fakeProv2 := p2.(*fakeProvider)
// Push the same key material into both fake providers and verify that the
// providers returned by the store return the appropriate key material.
km1 := loadKeyMaterials(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem")
fakeProv1.newKeyMaterial(km1, nil)
fakeProv2.newKeyMaterial(km1, nil)
if err := readAndVerifyKeyMaterial(ctx, prov1, km1); err != nil {
t.Fatal(err)
}
if err := readAndVerifyKeyMaterial(ctx, prov2, km1); err != nil {
t.Fatal(err)
}
// Push new key material into only one of the fake providers and verify
// that the providers returned by the store return the appropriate key
// material.
km2 := loadKeyMaterials(t, "x509/server2_cert.pem", "x509/server2_key.pem", "x509/client_ca_cert.pem")
fakeProv2.newKeyMaterial(km2, nil)
if err := readAndVerifyKeyMaterial(ctx, prov1, km1); err != nil {
t.Fatal(err)
}
if err := readAndVerifyKeyMaterial(ctx, prov2, km2); err != nil {
t.Fatal(err)
}
// Close one of the providers and verify that the other one is not affected.
prov1.Close()
if err := readAndVerifyKeyMaterial(ctx, prov2, km2); err != nil {
t.Fatal(err)
}
}
// TestStoreMultipleProviders creates providers of different types and makes
// sure closing of one does not affect the other.
func (s) TestStoreMultipleProviders(t *testing.T) {
opts := BuildOptions{CertName: "foo"}
prov1 := createProvider(t, fakeProvider1Name, fakeConfig, opts)
defer prov1.Close()
// Our fakeProviderBuilder pushes newly created providers on a channel. Grab
// the fake provider from that channel.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
p1, err := fpb1.providerChan.Receive(ctx)
if err != nil {
t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider1Name)
}
fakeProv1 := p1.(*fakeProvider)
prov2 := createProvider(t, fakeProvider2Name, fakeConfig, opts)
defer prov2.Close()
// Grab the second provider from the channel.
p2, err := fpb2.providerChan.Receive(ctx)
if err != nil {
t.Fatalf("Timeout when expecting certProvider %q to be created", fakeProvider2Name)
}
fakeProv2 := p2.(*fakeProvider)
// Push the key material into both providers and verify that the
// readers return the appropriate key material.
km1 := loadKeyMaterials(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem")
fakeProv1.newKeyMaterial(km1, nil)
km2 := loadKeyMaterials(t, "x509/server2_cert.pem", "x509/server2_key.pem", "x509/client_ca_cert.pem")
fakeProv2.newKeyMaterial(km2, nil)
if err := readAndVerifyKeyMaterial(ctx, prov1, km1); err != nil {
t.Fatal(err)
}
if err := readAndVerifyKeyMaterial(ctx, prov2, km2); err != nil {
t.Fatal(err)
}
// Close one of the providers and verify that the other one is not affected.
prov1.Close()
if err := readAndVerifyKeyMaterial(ctx, prov2, km2); err != nil {
t.Fatal(err)
}
}

608
credentials/tls_ext_test.go Normal file
View File

@@ -0,0 +1,608 @@
/*
*
* 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 credentials_test
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"os"
"strings"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/stubserver"
"google.golang.org/grpc/status"
"google.golang.org/grpc/testdata"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
)
const defaultTestTimeout = 10 * time.Second
const defaultTestShortTimeout = 10 * time.Millisecond
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
var serverCert tls.Certificate
var certPool *x509.CertPool
var serverName = "x.test.example.com"
func init() {
var err error
serverCert, err = tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
if err != nil {
panic(fmt.Sprintf("tls.LoadX509KeyPair(server1.pem, server1.key) failed: %v", err))
}
b, err := os.ReadFile(testdata.Path("x509/server_ca_cert.pem"))
if err != nil {
panic(fmt.Sprintf("Error reading CA cert file: %v", err))
}
certPool = x509.NewCertPool()
if !certPool.AppendCertsFromPEM(b) {
panic("Error appending cert from PEM")
}
}
// Tests that the MinVersion of tls.Config is set to 1.2 if it is not already
// set by the user.
func (s) TestTLS_MinVersion12(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
testCases := []struct {
name string
serverTLS func() *tls.Config
}{
{
name: "base_case",
serverTLS: func() *tls.Config {
return &tls.Config{
// MinVersion should be set to 1.2 by gRPC by default.
Certificates: []tls.Certificate{serverCert},
}
},
},
{
name: "fallback_to_base",
serverTLS: func() *tls.Config {
config := &tls.Config{
// MinVersion should be set to 1.2 by gRPC by default.
Certificates: []tls.Certificate{serverCert},
}
config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
return nil, nil
}
return config
},
},
{
name: "dynamic_using_get_config_for_client",
serverTLS: func() *tls.Config {
return &tls.Config{
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
return &tls.Config{
// MinVersion should be set to 1.2 by gRPC by default.
Certificates: []tls.Certificate{serverCert},
}, nil
},
}
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create server creds without a minimum version.
serverCreds := credentials.NewTLS(tc.serverTLS())
ss := stubserver.StubServer{
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
return &testpb.Empty{}, nil
},
}
// Create client creds that supports V1.0-V1.1.
clientCreds := credentials.NewTLS(&tls.Config{
ServerName: serverName,
RootCAs: certPool,
MinVersion: tls.VersionTLS10,
MaxVersion: tls.VersionTLS11,
})
// Start server and client separately, because Start() blocks on a
// successful connection, which we will not get.
if err := ss.StartServer(grpc.Creds(serverCreds)); err != nil {
t.Fatalf("Error starting server: %v", err)
}
defer ss.Stop()
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(clientCreds))
if err != nil {
t.Fatalf("grpc.NewClient error: %v", err)
}
defer cc.Close()
client := testgrpc.NewTestServiceClient(cc)
const wantStr = "authentication handshake failed"
if _, err = client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.Unavailable || !strings.Contains(status.Convert(err).Message(), wantStr) {
t.Fatalf("EmptyCall err = %v; want code=%v, message contains %q", err, codes.Unavailable, wantStr)
}
})
}
}
// Tests that the MinVersion of tls.Config is not changed if it is set by the
// user.
func (s) TestTLS_MinVersionOverridable(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
var allCipherSuites []uint16
for _, cs := range tls.CipherSuites() {
allCipherSuites = append(allCipherSuites, cs.ID)
}
testCases := []struct {
name string
serverTLS func() *tls.Config
}{
{
name: "base_case",
serverTLS: func() *tls.Config {
return &tls.Config{
MinVersion: tls.VersionTLS10,
Certificates: []tls.Certificate{serverCert},
CipherSuites: allCipherSuites,
}
},
},
{
name: "fallback_to_base",
serverTLS: func() *tls.Config {
config := &tls.Config{
MinVersion: tls.VersionTLS10,
Certificates: []tls.Certificate{serverCert},
CipherSuites: allCipherSuites,
}
config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
return nil, nil
}
return config
},
},
{
name: "dynamic_using_get_config_for_client",
serverTLS: func() *tls.Config {
return &tls.Config{
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
return &tls.Config{
MinVersion: tls.VersionTLS10,
Certificates: []tls.Certificate{serverCert},
CipherSuites: allCipherSuites,
}, nil
},
}
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create server creds that allow v1.0.
serverCreds := credentials.NewTLS(tc.serverTLS())
ss := stubserver.StubServer{
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
return &testpb.Empty{}, nil
},
}
// Create client creds that supports V1.0-V1.1.
clientCreds := credentials.NewTLS(&tls.Config{
ServerName: serverName,
RootCAs: certPool,
CipherSuites: allCipherSuites,
MinVersion: tls.VersionTLS10,
MaxVersion: tls.VersionTLS11,
})
if err := ss.Start([]grpc.ServerOption{grpc.Creds(serverCreds)}, grpc.WithTransportCredentials(clientCreds)); err != nil {
t.Fatalf("Error starting stub server: %v", err)
}
defer ss.Stop()
if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall err = %v; want <nil>", err)
}
})
}
}
// Tests that CipherSuites is set to exclude HTTP/2 forbidden suites by default.
func (s) TestTLS_CipherSuites(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
testCases := []struct {
name string
serverTLS func() *tls.Config
}{
{
name: "base_case",
serverTLS: func() *tls.Config {
return &tls.Config{
Certificates: []tls.Certificate{serverCert},
}
},
},
{
name: "fallback_to_base",
serverTLS: func() *tls.Config {
config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
}
config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
return nil, nil
}
return config
},
},
{
name: "dynamic_using_get_config_for_client",
serverTLS: func() *tls.Config {
return &tls.Config{
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
return &tls.Config{
Certificates: []tls.Certificate{serverCert},
}, nil
},
}
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create server creds without cipher suites.
serverCreds := credentials.NewTLS(tc.serverTLS())
ss := stubserver.StubServer{
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
return &testpb.Empty{}, nil
},
}
// Create client creds that use a forbidden suite only.
clientCreds := credentials.NewTLS(&tls.Config{
ServerName: serverName,
RootCAs: certPool,
CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
MaxVersion: tls.VersionTLS12, // TLS1.3 cipher suites are not configurable, so limit to 1.2.
})
// Start server and client separately, because Start() blocks on a
// successful connection, which we will not get.
if err := ss.StartServer(grpc.Creds(serverCreds)); err != nil {
t.Fatalf("Error starting server: %v", err)
}
defer ss.Stop()
cc, err := grpc.NewClient("dns:"+ss.Address, grpc.WithTransportCredentials(clientCreds))
if err != nil {
t.Fatalf("grpc.NewClient error: %v", err)
}
defer cc.Close()
client := testgrpc.NewTestServiceClient(cc)
const wantStr = "authentication handshake failed"
if _, err = client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.Unavailable || !strings.Contains(status.Convert(err).Message(), wantStr) {
t.Fatalf("EmptyCall err = %v; want code=%v, message contains %q", err, codes.Unavailable, wantStr)
}
})
}
}
// Tests that CipherSuites is not overridden when it is set.
func (s) TestTLS_CipherSuitesOverridable(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
testCases := []struct {
name string
serverTLS func() *tls.Config
}{
{
name: "base_case",
serverTLS: func() *tls.Config {
return &tls.Config{
Certificates: []tls.Certificate{serverCert},
CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
}
},
},
{
name: "fallback_to_base",
serverTLS: func() *tls.Config {
config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
}
config.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
return nil, nil
}
return config
},
},
{
name: "dynamic_using_get_config_for_client",
serverTLS: func() *tls.Config {
return &tls.Config{
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
return &tls.Config{
Certificates: []tls.Certificate{serverCert},
CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
}, nil
},
}
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create server that allows only a forbidden cipher suite.
serverCreds := credentials.NewTLS(tc.serverTLS())
ss := stubserver.StubServer{
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
return &testpb.Empty{}, nil
},
}
// Create server that allows only a forbidden cipher suite.
clientCreds := credentials.NewTLS(&tls.Config{
ServerName: serverName,
RootCAs: certPool,
CipherSuites: []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA},
MaxVersion: tls.VersionTLS12, // TLS1.3 cipher suites are not configurable, so limit to 1.2.
})
if err := ss.Start([]grpc.ServerOption{grpc.Creds(serverCreds)}, grpc.WithTransportCredentials(clientCreds)); err != nil {
t.Fatalf("Error starting stub server: %v", err)
}
defer ss.Stop()
if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall err = %v; want <nil>", err)
}
})
}
}
// TestTLS_ServerConfiguresALPNByDefault verifies that ALPN is configured
// correctly for a server that doesn't specify the NextProtos field and uses
// GetConfigForClient to provide the TLS config during the handshake.
func (s) TestTLS_ServerConfiguresALPNByDefault(t *testing.T) {
initialVal := envconfig.EnforceALPNEnabled
defer func() {
envconfig.EnforceALPNEnabled = initialVal
}()
envconfig.EnforceALPNEnabled = true
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
// Create a server that doesn't set the NextProtos field.
serverCreds := credentials.NewTLS(&tls.Config{
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
return &tls.Config{
Certificates: []tls.Certificate{serverCert},
}, nil
},
})
ss := stubserver.StubServer{
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
return &testpb.Empty{}, nil
},
}
clientCreds := credentials.NewTLS(&tls.Config{
ServerName: serverName,
RootCAs: certPool,
})
if err := ss.Start([]grpc.ServerOption{grpc.Creds(serverCreds)}, grpc.WithTransportCredentials(clientCreds)); err != nil {
t.Fatalf("Error starting stub server: %v", err)
}
defer ss.Stop()
if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall err = %v; want <nil>", err)
}
}
// TestTLS_DisabledALPNClient tests the behaviour of TransportCredentials when
// connecting to a server that doesn't support ALPN.
func (s) TestTLS_DisabledALPNClient(t *testing.T) {
initialVal := envconfig.EnforceALPNEnabled
defer func() {
envconfig.EnforceALPNEnabled = initialVal
}()
tests := []struct {
name string
alpnEnforced bool
wantErr bool
}{
{
name: "enforced",
alpnEnforced: true,
wantErr: true,
},
{
name: "not_enforced",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
envconfig.EnforceALPNEnabled = tc.alpnEnforced
listener, err := tls.Listen("tcp", "localhost:0", &tls.Config{
Certificates: []tls.Certificate{serverCert},
NextProtos: []string{}, // Empty list indicates ALPN is disabled.
})
if err != nil {
t.Fatalf("Error starting TLS server: %v", err)
}
errCh := make(chan error, 1)
go func() {
conn, err := listener.Accept()
if err != nil {
errCh <- fmt.Errorf("listener.Accept returned error: %v", err)
} else {
// The first write to the TLS listener initiates the TLS handshake.
conn.Write([]byte("Hello, World!"))
conn.Close()
}
close(errCh)
}()
serverAddr := listener.Addr().String()
conn, err := net.Dial("tcp", serverAddr)
if err != nil {
t.Fatalf("net.Dial(%s) failed: %v", serverAddr, err)
}
defer conn.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
clientCfg := tls.Config{
ServerName: serverName,
RootCAs: certPool,
NextProtos: []string{"h2"},
}
_, _, err = credentials.NewTLS(&clientCfg).ClientHandshake(ctx, serverName, conn)
if gotErr := (err != nil); gotErr != tc.wantErr {
t.Errorf("ClientHandshake returned unexpected error: got=%v, want=%t", err, tc.wantErr)
}
select {
case err := <-errCh:
if err != nil {
t.Fatalf("Unexpected error received from server: %v", err)
}
case <-ctx.Done():
t.Fatalf("Timeout waiting for error from server")
}
})
}
}
// TestTLS_DisabledALPNServer tests the behaviour of TransportCredentials when
// accepting a request from a client that doesn't support ALPN.
func (s) TestTLS_DisabledALPNServer(t *testing.T) {
initialVal := envconfig.EnforceALPNEnabled
defer func() {
envconfig.EnforceALPNEnabled = initialVal
}()
tests := []struct {
name string
alpnEnforced bool
wantErr bool
}{
{
name: "enforced",
alpnEnforced: true,
wantErr: true,
},
{
name: "not_enforced",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
envconfig.EnforceALPNEnabled = tc.alpnEnforced
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("Error starting server: %v", err)
}
errCh := make(chan error, 1)
go func() {
conn, err := listener.Accept()
if err != nil {
errCh <- fmt.Errorf("listener.Accept returned error: %v", err)
return
}
defer conn.Close()
serverCfg := tls.Config{
Certificates: []tls.Certificate{serverCert},
NextProtos: []string{"h2"},
}
_, _, err = credentials.NewTLS(&serverCfg).ServerHandshake(conn)
if gotErr := (err != nil); gotErr != tc.wantErr {
t.Errorf("ServerHandshake returned unexpected error: got=%v, want=%t", err, tc.wantErr)
}
close(errCh)
}()
serverAddr := listener.Addr().String()
clientCfg := &tls.Config{
Certificates: []tls.Certificate{serverCert},
NextProtos: []string{}, // Empty list indicates ALPN is disabled.
RootCAs: certPool,
ServerName: serverName,
}
conn, err := tls.Dial("tcp", serverAddr, clientCfg)
if err != nil {
t.Fatalf("tls.Dial(%s) failed: %v", serverAddr, err)
}
defer conn.Close()
select {
case <-time.After(defaultTestTimeout):
t.Fatal("Timed out waiting for completion")
case err := <-errCh:
if err != nil {
t.Fatalf("Unexpected server error: %v", err)
}
}
})
}
}

236
credentials/xds/xds.go Normal file
View File

@@ -0,0 +1,236 @@
/*
*
* 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.
package xds
import (
"context"
"crypto/tls"
"errors"
"net"
"time"
"google.golang.org/grpc/credentials"
credinternal "google.golang.org/grpc/internal/credentials"
xdsinternal "google.golang.org/grpc/internal/credentials/xds"
"google.golang.org/grpc/internal/grpcsync"
)
// 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 clusterimpl 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 clusterimpl 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)
}
hiPtr := xdsinternal.HandshakeInfoFromAttributes(chi.Attributes)
hostname := xdsinternal.Hostname(chi.Attributes)
cfg, useFallback, done, err := xdsinternal.ClientSideTLSConfig(ctx, hiPtr, hostname)
if err != nil {
return nil, nil, err
}
if useFallback {
return c.fallback.ClientHandshake(ctx, authority, rawConn)
}
defer done()
// 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() (*grpcsync.RefCounted[*xdsinternal.HandshakeInfo], error)
})
if !ok {
return c.fallback.ServerHandshake(rawConn)
}
hi, err := hiConn.XDSHandshakeInfo()
if err != nil {
return nil, nil, err
}
// 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, useFallback, done, err := xdsinternal.ServerSideTLSConfig(ctx, hi)
if err != nil {
return nil, nil, err
}
if useFallback {
return c.fallback.ServerHandshake(rawConn)
}
defer done()
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
}

View File

@@ -0,0 +1,676 @@
/*
*
* 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
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"os"
"strings"
"sync/atomic"
"testing"
"time"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/tls/certprovider"
icredentials "google.golang.org/grpc/internal/credentials"
xdsinternal "google.golang.org/grpc/internal/credentials/xds"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpcsync"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/xds/matcher"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/testdata"
)
const (
defaultTestTimeout = 1 * time.Second
defaultTestShortTimeout = 10 * time.Millisecond
defaultTestCertSAN = "abc.test.example.com"
authority = "x.test.example.com"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
// Helper function to create a real TLS client credentials which is used as
// fallback credentials from multiple tests.
func makeFallbackClientCreds(t *testing.T) credentials.TransportCredentials {
creds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), "")
if err != nil {
t.Fatal(err)
}
return creds
}
// testServer is a no-op server which listens on a local TCP port for incoming
// connections, and performs a manual TLS handshake on the received raw
// connection using a user specified handshake function. It then makes the
// result of the handshake operation available through a channel for tests to
// inspect. Tests should stop the testServer as part of their cleanup.
type testServer struct {
lis net.Listener
address string // Listening address of the test server.
handshakeFunc testHandshakeFunc // Test specified handshake function.
hsResult *testutils.Channel // Channel to deliver handshake results.
}
// handshakeResult wraps the result of the handshake operation on the test
// server. It consists of TLS connection state and an error, if the handshake
// failed. This result is delivered on the `hsResult` channel on the testServer.
type handshakeResult struct {
connState tls.ConnectionState
err error
}
// Configurable handshake function for the testServer. Tests can set this to
// simulate different conditions like handshake success, failure, timeout etc.
type testHandshakeFunc func(net.Conn) handshakeResult
// newTestServerWithHandshakeFunc starts a new testServer which listens for
// connections on a local TCP port, and uses the provided custom handshake
// function to perform TLS handshake.
func newTestServerWithHandshakeFunc(ctx context.Context, f testHandshakeFunc) *testServer {
ts := &testServer{
handshakeFunc: f,
hsResult: testutils.NewChannel(),
}
ts.start(ctx)
return ts
}
// starts actually starts listening on a local TCP port, and spawns a goroutine
// to handle new connections.
func (ts *testServer) start(ctx context.Context) error {
lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
return err
}
ts.lis = lis
ts.address = lis.Addr().String()
go ts.handleConn(ctx)
return nil
}
// handleConn accepts a new raw connection, and invokes the test provided
// handshake function to perform TLS handshake, and returns the result on the
// `hsResult` channel.
func (ts *testServer) handleConn(ctx context.Context) {
for {
rawConn, err := ts.lis.Accept()
if err != nil {
// Once the listeners closed, Accept() will return with an error.
return
}
hsr := ts.handshakeFunc(rawConn)
ts.hsResult.SendContext(ctx, hsr)
}
}
// stop closes the associated listener which causes the connection handling
// goroutine to exit.
func (ts *testServer) stop() {
ts.lis.Close()
}
// A handshake function which simulates a successful handshake without client
// authentication (server does not request for client certificate during the
// handshake here).
func testServerTLSHandshake(rawConn net.Conn) handshakeResult {
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
if err != nil {
return handshakeResult{err: err}
}
cfg := &tls.Config{
Certificates: []tls.Certificate{cert},
NextProtos: []string{"h2"},
}
conn := tls.Server(rawConn, cfg)
if err := conn.Handshake(); err != nil {
return handshakeResult{err: err}
}
return handshakeResult{connState: conn.ConnectionState()}
}
// A handshake function which simulates a successful handshake with mutual
// authentication.
func testServerMutualTLSHandshake(rawConn net.Conn) handshakeResult {
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
if err != nil {
return handshakeResult{err: err}
}
pemData, err := os.ReadFile(testdata.Path("x509/client_ca_cert.pem"))
if err != nil {
return handshakeResult{err: err}
}
roots := x509.NewCertPool()
roots.AppendCertsFromPEM(pemData)
cfg := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientCAs: roots,
}
conn := tls.Server(rawConn, cfg)
if err := conn.Handshake(); err != nil {
return handshakeResult{err: err}
}
return handshakeResult{connState: conn.ConnectionState()}
}
// fakeProvider is an implementation of the certprovider.Provider interface
// which returns the configured key material and error in calls to
// KeyMaterial().
type fakeProvider struct {
km *certprovider.KeyMaterial
err error
}
func (f *fakeProvider) KeyMaterial(context.Context) (*certprovider.KeyMaterial, error) {
return f.km, f.err
}
func (f *fakeProvider) Close() {}
// makeIdentityProvider creates a new instance of the fakeProvider returning the
// identity key material specified in the provider file paths.
func makeIdentityProvider(t *testing.T, certPath, keyPath string) certprovider.Provider {
t.Helper()
cert, err := tls.LoadX509KeyPair(testdata.Path(certPath), testdata.Path(keyPath))
if err != nil {
t.Fatal(err)
}
return &fakeProvider{km: &certprovider.KeyMaterial{Certs: []tls.Certificate{cert}}}
}
// makeRootProvider creates a new instance of the fakeProvider returning the
// root key material specified in the provider file paths.
func makeRootProvider(t *testing.T, caPath string) *fakeProvider {
pemData, err := os.ReadFile(testdata.Path(caPath))
if err != nil {
t.Fatal(err)
}
roots := x509.NewCertPool()
roots.AppendCertsFromPEM(pemData)
return &fakeProvider{km: &certprovider.KeyMaterial{Roots: roots}}
}
// newTestContextWithHandshakeInfo returns a copy of parent with HandshakeInfo
// context value added to it.
func newTestContextWithHandshakeInfo(parent context.Context, root, identity certprovider.Provider, sanExactMatch, sni string, validateSANUsingSNI bool) context.Context {
// Creating the HandshakeInfo and adding it to the attributes is very
// similar to what the CDS balancer would do when it intercepts calls to
// NewSubConn().
var sms []matcher.StringMatcher
if sanExactMatch != "" {
sms = []matcher.StringMatcher{matcher.NewExactStringMatcher(sanExactMatch, false)}
}
var hiPtr atomic.Pointer[grpcsync.RefCounted[*xdsinternal.HandshakeInfo]]
info := xdsinternal.NewHandshakeInfo(root, identity, sms, sni, false, validateSANUsingSNI, false)
hiPtr.Store(info)
addr := xdsinternal.SetHandshakeInfo(resolver.Address{}, &hiPtr)
// Moving the attributes from the resolver.Address to the context passed to
// the handshaker is done in the transport layer. Since we directly call the
// handshaker in these tests, we need to do the same here.
return icredentials.NewClientHandshakeInfoContext(parent, credentials.ClientHandshakeInfo{Attributes: addr.Attributes})
}
// compareAuthInfo compares the AuthInfo received on the client side after a
// successful handshake with the authInfo available on the testServer.
func compareAuthInfo(ctx context.Context, ts *testServer, ai credentials.AuthInfo) error {
if ai.AuthType() != "tls" {
return fmt.Errorf("ClientHandshake returned authType %q, want %q", ai.AuthType(), "tls")
}
info, ok := ai.(credentials.TLSInfo)
if !ok {
return fmt.Errorf("ClientHandshake returned authInfo of type %T, want %T", ai, credentials.TLSInfo{})
}
gotState := info.State
// Read the handshake result from the testServer which contains the TLS
// connection state and compare it with the one received on the client-side.
val, err := ts.hsResult.Receive(ctx)
if err != nil {
return fmt.Errorf("testServer failed to return handshake result: %v", err)
}
hsr := val.(handshakeResult)
if hsr.err != nil {
return fmt.Errorf("testServer handshake failure: %v", hsr.err)
}
// AuthInfo contains a variety of information. We only verify a subset here.
// This is the same subset which is verified in TLS credentials tests.
if err := compareConnState(gotState, hsr.connState); err != nil {
return err
}
return nil
}
func compareConnState(got, want tls.ConnectionState) error {
switch {
case got.Version != want.Version:
return fmt.Errorf("TLS.ConnectionState got Version: %v, want: %v", got.Version, want.Version)
case got.HandshakeComplete != want.HandshakeComplete:
return fmt.Errorf("TLS.ConnectionState got HandshakeComplete: %v, want: %v", got.HandshakeComplete, want.HandshakeComplete)
case got.CipherSuite != want.CipherSuite:
return fmt.Errorf("TLS.ConnectionState got CipherSuite: %v, want: %v", got.CipherSuite, want.CipherSuite)
case got.NegotiatedProtocol != want.NegotiatedProtocol:
return fmt.Errorf("TLS.ConnectionState got NegotiatedProtocol: %v, want: %v", got.NegotiatedProtocol, want.NegotiatedProtocol)
}
return nil
}
// TestClientCredsWithoutFallback verifies that the call to
// NewClientCredentials() fails when no fallback is specified.
func (s) TestClientCredsWithoutFallback(t *testing.T) {
if _, err := NewClientCredentials(ClientOptions{}); err == nil {
t.Fatal("NewClientCredentials() succeeded without specifying fallback")
}
}
// TestClientCredsInvalidHandshakeInfo verifies scenarios where the passed in
// HandshakeInfo is invalid because it does not contain the expected certificate
// providers.
func (s) TestClientCredsInvalidHandshakeInfo(t *testing.T) {
opts := ClientOptions{FallbackCreds: makeFallbackClientCreds(t)}
creds, err := NewClientCredentials(opts)
if err != nil {
t.Fatalf("NewClientCredentials(%v) failed: %v", opts, err)
}
pCtx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ctx := newTestContextWithHandshakeInfo(pCtx, nil, &fakeProvider{}, "", "", false)
if _, _, err := creds.ClientHandshake(ctx, authority, nil); err == nil {
t.Fatal("ClientHandshake succeeded without root certificate provider in HandshakeInfo")
}
}
// TestClientCredsProviderFailure verifies the cases where an expected
// certificate provider is missing in the HandshakeInfo value in the context.
func (s) TestClientCredsProviderFailure(t *testing.T) {
opts := ClientOptions{FallbackCreds: makeFallbackClientCreds(t)}
creds, err := NewClientCredentials(opts)
if err != nil {
t.Fatalf("NewClientCredentials(%v) failed: %v", opts, err)
}
tests := []struct {
desc string
rootProvider certprovider.Provider
identityProvider certprovider.Provider
wantErr string
}{
{
desc: "erroring root provider",
rootProvider: &fakeProvider{err: errors.New("root provider error")},
wantErr: "root provider error",
},
{
desc: "erroring identity provider",
rootProvider: &fakeProvider{km: &certprovider.KeyMaterial{}},
identityProvider: &fakeProvider{err: errors.New("identity provider error")},
wantErr: "identity provider error",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ctx = newTestContextWithHandshakeInfo(ctx, test.rootProvider, test.identityProvider, "", "", false)
if _, _, err := creds.ClientHandshake(ctx, authority, nil); err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("ClientHandshake() returned error: %q, wantErr: %q", err, test.wantErr)
}
})
}
}
// TestClientCredsSuccess verifies successful client handshake cases.
func (s) TestClientCredsSuccess(t *testing.T) {
tests := []struct {
desc string
handshakeFunc testHandshakeFunc
handshakeInfoCtx func(ctx context.Context) context.Context
enableSNIFlag bool
}{
{
desc: "fallback",
handshakeFunc: testServerTLSHandshake,
handshakeInfoCtx: func(ctx context.Context) context.Context {
// Since we don't add a HandshakeInfo to the context, the
// ClientHandshake() method will delegate to the fallback.
return ctx
},
},
{
desc: "TLS",
handshakeFunc: testServerTLSHandshake,
handshakeInfoCtx: func(ctx context.Context) context.Context {
return newTestContextWithHandshakeInfo(ctx, makeRootProvider(t, "x509/server_ca_cert.pem"), nil, defaultTestCertSAN, "", false)
},
},
{
desc: "mTLS",
handshakeFunc: testServerMutualTLSHandshake,
handshakeInfoCtx: func(ctx context.Context) context.Context {
return newTestContextWithHandshakeInfo(ctx, makeRootProvider(t, "x509/server_ca_cert.pem"), makeIdentityProvider(t, "x509/server1_cert.pem", "x509/server1_key.pem"), defaultTestCertSAN, "", false)
},
},
{
desc: "mTLS with no acceptedSANs specified",
handshakeFunc: testServerMutualTLSHandshake,
handshakeInfoCtx: func(ctx context.Context) context.Context {
return newTestContextWithHandshakeInfo(ctx, makeRootProvider(t, "x509/server_ca_cert.pem"), makeIdentityProvider(t, "x509/server1_cert.pem", "x509/server1_key.pem"), "", "", false)
},
},
{
desc: "TLS with SNI",
handshakeFunc: testServerTLSHandshake,
handshakeInfoCtx: func(ctx context.Context) context.Context {
return newTestContextWithHandshakeInfo(ctx, makeRootProvider(t, "x509/server_ca_cert.pem"), nil, "bad-match", defaultTestCertSAN, true)
},
enableSNIFlag: true,
},
{
desc: "TLS with SNI, env variable disabled, AutoSniSanValidation enabled",
handshakeFunc: testServerTLSHandshake,
handshakeInfoCtx: func(ctx context.Context) context.Context {
return newTestContextWithHandshakeInfo(ctx, makeRootProvider(t, "x509/server_ca_cert.pem"), nil, defaultTestCertSAN, "bad-sni", true)
},
},
{
desc: "TLS with SNI, env variable enabled but AutoSniSanValidation disabled",
handshakeFunc: testServerTLSHandshake,
handshakeInfoCtx: func(ctx context.Context) context.Context {
return newTestContextWithHandshakeInfo(ctx, makeRootProvider(t, "x509/server_ca_cert.pem"), nil, defaultTestCertSAN, "bad-sni", false)
},
enableSNIFlag: true,
},
{
desc: "TLS with empty SNI, env variable enabled, AutoSniSanValidation enabled",
handshakeFunc: testServerTLSHandshake,
handshakeInfoCtx: func(ctx context.Context) context.Context {
return newTestContextWithHandshakeInfo(ctx, makeRootProvider(t, "x509/server_ca_cert.pem"), nil, defaultTestCertSAN, "", true)
},
enableSNIFlag: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
testutils.SetEnvConfig(t, &envconfig.XDSSNIEnabled, test.enableSNIFlag)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ts := newTestServerWithHandshakeFunc(ctx, test.handshakeFunc)
defer ts.stop()
opts := ClientOptions{FallbackCreds: makeFallbackClientCreds(t)}
creds, err := NewClientCredentials(opts)
if err != nil {
t.Fatalf("NewClientCredentials(%v) failed: %v", opts, err)
}
conn, err := net.Dial("tcp", ts.address)
if err != nil {
t.Fatalf("net.Dial(%s) failed: %v", ts.address, err)
}
defer conn.Close()
_, ai, err := creds.ClientHandshake(test.handshakeInfoCtx(ctx), authority, conn)
if err != nil {
t.Fatalf("ClientHandshake() returned failed: %q", err)
}
if err := compareAuthInfo(ctx, ts, ai); err != nil {
t.Fatal(err)
}
})
}
}
func (s) TestClientCredsHandshakeTimeout(t *testing.T) {
clientDone := make(chan struct{})
ctx, sCancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer sCancel()
// A handshake function which simulates a handshake timeout from the
// server-side by simply blocking on the client-side handshake to timeout
// and not writing any handshake data.
hErr := errors.New("server handshake error")
ts := newTestServerWithHandshakeFunc(ctx, func(net.Conn) handshakeResult {
<-clientDone
return handshakeResult{err: hErr}
})
defer ts.stop()
opts := ClientOptions{FallbackCreds: makeFallbackClientCreds(t)}
creds, err := NewClientCredentials(opts)
if err != nil {
t.Fatalf("NewClientCredentials(%v) failed: %v", opts, err)
}
conn, err := net.Dial("tcp", ts.address)
if err != nil {
t.Fatalf("net.Dial(%s) failed: %v", ts.address, err)
}
defer conn.Close()
sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout)
defer sCancel()
ctx = newTestContextWithHandshakeInfo(sCtx, makeRootProvider(t, "x509/server_ca_cert.pem"), nil, defaultTestCertSAN, "", false)
if _, _, err := creds.ClientHandshake(ctx, authority, conn); err == nil {
t.Fatal("ClientHandshake() succeeded when expected to timeout")
}
close(clientDone)
// Read the handshake result from the testServer and make sure the expected
// error is returned.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
val, err := ts.hsResult.Receive(ctx)
if err != nil {
t.Fatalf("testServer failed to return handshake result: %v", err)
}
hsr := val.(handshakeResult)
if hsr.err != hErr {
t.Fatalf("testServer handshake returned error: %v, want: %v", hsr.err, hErr)
}
}
// TestClientCredsHandshakeFailure verifies different handshake failure cases.
func (s) TestClientCredsHandshakeFailure(t *testing.T) {
tests := []struct {
desc string
handshakeFunc testHandshakeFunc
rootProvider certprovider.Provider
san string
sni string
validateSANUsingSNI bool
enableSNIFlag bool
wantErr string
}{
{
desc: "cert validation failure",
handshakeFunc: testServerTLSHandshake,
rootProvider: makeRootProvider(t, "x509/client_ca_cert.pem"),
san: defaultTestCertSAN,
wantErr: "x509: certificate signed by unknown authority",
},
{
desc: "SAN mismatch",
handshakeFunc: testServerTLSHandshake,
rootProvider: makeRootProvider(t, "x509/server_ca_cert.pem"),
san: "bad-san",
wantErr: "do not match any of the accepted SANs",
},
{
desc: "SNI SAN mismatch",
handshakeFunc: testServerTLSHandshake,
rootProvider: makeRootProvider(t, "x509/server_ca_cert.pem"),
sni: "bad-sni",
validateSANUsingSNI: true,
wantErr: "do not match the SNI",
enableSNIFlag: true,
},
{
desc: "SNI set, AutoSniSanValidation disabled with SAN mismatch",
handshakeFunc: testServerTLSHandshake,
rootProvider: makeRootProvider(t, "x509/server_ca_cert.pem"),
sni: defaultTestCertSAN,
san: "bad-san",
validateSANUsingSNI: false,
wantErr: "do not match any of the accepted SANs",
enableSNIFlag: true,
},
{
desc: "SNI set with SAN mismatch and AutoSniSanValidation enabled, environment variable disabled",
handshakeFunc: testServerTLSHandshake,
rootProvider: makeRootProvider(t, "x509/server_ca_cert.pem"),
sni: defaultTestCertSAN,
san: "bad-san",
validateSANUsingSNI: true,
wantErr: "do not match any of the accepted SANs",
},
{
desc: "SNI empty, AutoSniSanValidation enabled with SAN mismatch",
handshakeFunc: testServerTLSHandshake,
rootProvider: makeRootProvider(t, "x509/server_ca_cert.pem"),
sni: "",
san: "bad-san",
validateSANUsingSNI: true,
wantErr: "do not match any of the accepted SANs",
enableSNIFlag: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
testutils.SetEnvConfig(t, &envconfig.XDSSNIEnabled, test.enableSNIFlag)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ts := newTestServerWithHandshakeFunc(ctx, test.handshakeFunc)
defer ts.stop()
opts := ClientOptions{FallbackCreds: makeFallbackClientCreds(t)}
creds, err := NewClientCredentials(opts)
if err != nil {
t.Fatalf("NewClientCredentials(%v) failed: %v", opts, err)
}
conn, err := net.Dial("tcp", ts.address)
if err != nil {
t.Fatalf("net.Dial(%s) failed: %v", ts.address, err)
}
defer conn.Close()
ctx = newTestContextWithHandshakeInfo(ctx, test.rootProvider, nil, test.san, test.sni, test.validateSANUsingSNI)
if _, _, err := creds.ClientHandshake(ctx, authority, conn); err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("ClientHandshake() returned %q, wantErr %q", err, test.wantErr)
}
})
}
}
// TestClientCredsProviderSwitch verifies the case where the first attempt of
// ClientHandshake fails because of a handshake failure. Then we update the
// certificate provider and the second attempt succeeds. This is an
// approximation of the flow of events when the control plane specifies new
// security config which results in new certificate providers being used.
func (s) TestClientCredsProviderSwitch(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ts := newTestServerWithHandshakeFunc(ctx, testServerTLSHandshake)
defer ts.stop()
opts := ClientOptions{FallbackCreds: makeFallbackClientCreds(t)}
creds, err := NewClientCredentials(opts)
if err != nil {
t.Fatalf("NewClientCredentials(%v) failed: %v", opts, err)
}
conn, err := net.Dial("tcp", ts.address)
if err != nil {
t.Fatalf("net.Dial(%s) failed: %v", ts.address, err)
}
defer conn.Close()
// Create a root provider which will fail the handshake because it does not
// use the correct trust roots.
root1 := makeRootProvider(t, "x509/client_ca_cert.pem")
handshakeInfo := xdsinternal.NewHandshakeInfo(root1, nil, []matcher.StringMatcher{matcher.NewExactStringMatcher(defaultTestCertSAN, false)}, "", false, false, false)
// We need to repeat most of what newTestContextWithHandshakeInfo() does
// here because we need access to the underlying HandshakeInfo so that we
// can update it before the next call to ClientHandshake().
var hiPtr atomic.Pointer[grpcsync.RefCounted[*xdsinternal.HandshakeInfo]]
hiPtr.Store(handshakeInfo)
addr := xdsinternal.SetHandshakeInfo(resolver.Address{}, &hiPtr)
ctx = icredentials.NewClientHandshakeInfoContext(ctx, credentials.ClientHandshakeInfo{Attributes: addr.Attributes})
if _, _, err := creds.ClientHandshake(ctx, authority, conn); err == nil {
t.Fatal("ClientHandshake() succeeded when expected to fail")
}
// Drain the result channel on the test server so that we can inspect the
// result for the next handshake.
_, err = ts.hsResult.Receive(ctx)
if err != nil {
t.Errorf("testServer failed to return handshake result: %v", err)
}
conn, err = net.Dial("tcp", ts.address)
if err != nil {
t.Fatalf("net.Dial(%s) failed: %v", ts.address, err)
}
defer conn.Close()
// Create a new root provider which uses the correct trust roots. And update
// the HandshakeInfo with the new provider.
root2 := makeRootProvider(t, "x509/server_ca_cert.pem")
handshakeInfo = xdsinternal.NewHandshakeInfo(root2, nil, []matcher.StringMatcher{matcher.NewExactStringMatcher(defaultTestCertSAN, false)}, "", false, false, false)
// Update the existing pointer, which address attribute will continue to
// point to.
hiPtr.Store(handshakeInfo)
_, ai, err := creds.ClientHandshake(ctx, authority, conn)
if err != nil {
t.Fatalf("ClientHandshake() returned failed: %q", err)
}
if err := compareAuthInfo(ctx, ts, ai); err != nil {
t.Fatal(err)
}
}
// TestClientClone verifies the Clone() method on client credentials.
func (s) TestClientClone(t *testing.T) {
opts := ClientOptions{FallbackCreds: makeFallbackClientCreds(t)}
orig, err := NewClientCredentials(opts)
if err != nil {
t.Fatalf("NewClientCredentials(%v) failed: %v", opts, err)
}
// The credsImpl does not have any exported fields, and it does not make
// sense to use any cmp options to look deep into. So, all we make sure here
// is that the cloned object points to a different location in memory.
if clone := orig.Clone(); clone == orig {
t.Fatal("return value from Clone() doesn't point to new credentials instance")
}
}

View File

@@ -0,0 +1,543 @@
/*
*
* 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
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"os"
"strings"
"testing"
"time"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/tls/certprovider"
xdsinternal "google.golang.org/grpc/internal/credentials/xds"
"google.golang.org/grpc/internal/grpcsync"
"google.golang.org/grpc/testdata"
)
func makeClientTLSConfig(t *testing.T, mTLS bool) *tls.Config {
t.Helper()
pemData, err := os.ReadFile(testdata.Path("x509/server_ca_cert.pem"))
if err != nil {
t.Fatal(err)
}
roots := x509.NewCertPool()
roots.AppendCertsFromPEM(pemData)
var certs []tls.Certificate
if mTLS {
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/client1_cert.pem"), testdata.Path("x509/client1_key.pem"))
if err != nil {
t.Fatal(err)
}
certs = append(certs, cert)
}
return &tls.Config{
Certificates: certs,
RootCAs: roots,
ServerName: "*.test.example.com",
// Setting this to true completely turns off the certificate validation
// on the client side. So, the client side handshake always seems to
// succeed. But if we want to turn this ON, we will need to generate
// certificates which work with localhost, or supply a custom
// verification function. So, the server credentials tests will rely
// solely on the success/failure of the server-side handshake.
InsecureSkipVerify: true,
NextProtos: []string{"h2"},
}
}
// Helper function to create a real TLS server credentials which is used as
// fallback credentials from multiple tests.
func makeFallbackServerCreds(t *testing.T) credentials.TransportCredentials {
t.Helper()
creds, err := credentials.NewServerTLSFromFile(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
if err != nil {
t.Fatal(err)
}
return creds
}
type errorCreds struct {
credentials.TransportCredentials
}
// TestServerCredsWithoutFallback verifies that the call to
// NewServerCredentials() fails when no fallback is specified.
func (s) TestServerCredsWithoutFallback(t *testing.T) {
if _, err := NewServerCredentials(ServerOptions{}); err == nil {
t.Fatal("NewServerCredentials() succeeded without specifying fallback")
}
}
type wrapperConn struct {
net.Conn
xdsHI *grpcsync.RefCounted[*xdsinternal.HandshakeInfo]
deadline time.Time
handshakeInfoErr error
}
func (wc *wrapperConn) XDSHandshakeInfo() (*grpcsync.RefCounted[*xdsinternal.HandshakeInfo], error) {
return wc.xdsHI, wc.handshakeInfoErr
}
func (wc *wrapperConn) GetDeadline() time.Time {
return wc.deadline
}
func newWrappedConn(conn net.Conn, xdsHI *grpcsync.RefCounted[*xdsinternal.HandshakeInfo], deadline time.Time) *wrapperConn {
return &wrapperConn{Conn: conn, xdsHI: xdsHI, deadline: deadline}
}
// TestServerCredsInvalidHandshakeInfo verifies scenarios where the passed in
// HandshakeInfo is invalid because it does not contain the expected certificate
// providers.
func (s) TestServerCredsInvalidHandshakeInfo(t *testing.T) {
opts := ServerOptions{FallbackCreds: &errorCreds{}}
creds, err := NewServerCredentials(opts)
if err != nil {
t.Fatalf("NewServerCredentials(%v) failed: %v", opts, err)
}
info := xdsinternal.NewHandshakeInfo(&fakeProvider{}, nil, nil, "", false, false, false)
conn := newWrappedConn(nil, info, time.Time{})
if _, _, err := creds.ServerHandshake(conn); err == nil {
t.Fatal("ServerHandshake succeeded without identity certificate provider in HandshakeInfo")
}
}
// TestServerCredsProviderFailure verifies the cases where an expected
// certificate provider is missing in the HandshakeInfo value in the context.
func (s) TestServerCredsProviderFailure(t *testing.T) {
opts := ServerOptions{FallbackCreds: &errorCreds{}}
creds, err := NewServerCredentials(opts)
if err != nil {
t.Fatalf("NewServerCredentials(%v) failed: %v", opts, err)
}
tests := []struct {
desc string
rootProvider certprovider.Provider
identityProvider certprovider.Provider
wantErr string
}{
{
desc: "erroring identity provider",
identityProvider: &fakeProvider{err: errors.New("identity provider error")},
wantErr: "identity provider error",
},
{
desc: "erroring root provider",
identityProvider: &fakeProvider{km: &certprovider.KeyMaterial{}},
rootProvider: &fakeProvider{err: errors.New("root provider error")},
wantErr: "root provider error",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
info := xdsinternal.NewHandshakeInfo(test.rootProvider, test.identityProvider, nil, "", false, false, false)
conn := newWrappedConn(nil, info, time.Time{})
if _, _, err := creds.ServerHandshake(conn); err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("ServerHandshake() returned error: %q, wantErr: %q", err, test.wantErr)
}
})
}
}
// TestServerCredsHandshake_XDSHandshakeInfoError verifies the case where the
// call to XDSHandshakeInfo() from the ServerHandshake() method returns an
// error, and the test verifies that the ServerHandshake() fails with the
// expected error.
func (s) TestServerCredsHandshake_XDSHandshakeInfoError(t *testing.T) {
opts := ServerOptions{FallbackCreds: &errorCreds{}}
creds, err := NewServerCredentials(opts)
if err != nil {
t.Fatalf("NewServerCredentials(%v) failed: %v", opts, err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
// Create a test server which uses the xDS server credentials created above
// to perform TLS handshake on incoming connections.
ts := newTestServerWithHandshakeFunc(ctx, func(rawConn net.Conn) handshakeResult {
// Create a wrapped conn which returns a nil HandshakeInfo and a non-nil error.
conn := newWrappedConn(rawConn, nil, time.Now().Add(defaultTestTimeout))
hiErr := errors.New("xdsHandshakeInfo error")
conn.handshakeInfoErr = hiErr
// Invoke the ServerHandshake() method on the xDS credentials and verify
// that the error returned by the XDSHandshakeInfo() method on the
// wrapped conn is returned here.
_, _, err := creds.ServerHandshake(conn)
if !errors.Is(err, hiErr) {
return handshakeResult{err: fmt.Errorf("ServerHandshake() returned err: %v, wantErr: %v", err, hiErr)}
}
return handshakeResult{}
})
defer ts.stop()
// Dial the test server, but don't trigger the TLS handshake. This will
// cause ServerHandshake() to fail.
rawConn, err := net.Dial("tcp", ts.address)
if err != nil {
t.Fatalf("net.Dial(%s) failed: %v", ts.address, err)
}
defer rawConn.Close()
// Read handshake result from the testServer which will return an error if
// the handshake succeeded.
val, err := ts.hsResult.Receive(ctx)
if err != nil {
t.Fatalf("testServer failed to return handshake result: %v", err)
}
hsr := val.(handshakeResult)
if hsr.err != nil {
t.Fatalf("testServer handshake failure: %v", hsr.err)
}
}
// TestServerCredsHandshakeTimeout verifies the case where the client does not
// send required handshake data before the deadline set on the net.Conn passed
// to ServerHandshake().
func (s) TestServerCredsHandshakeTimeout(t *testing.T) {
opts := ServerOptions{FallbackCreds: &errorCreds{}}
creds, err := NewServerCredentials(opts)
if err != nil {
t.Fatalf("NewServerCredentials(%v) failed: %v", opts, err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
// Create a test server which uses the xDS server credentials created above
// to perform TLS handshake on incoming connections.
ts := newTestServerWithHandshakeFunc(ctx, func(rawConn net.Conn) handshakeResult {
hi := xdsinternal.NewHandshakeInfo(makeRootProvider(t, "x509/client_ca_cert.pem"), makeIdentityProvider(t, "x509/server2_cert.pem", "x509/server2_key.pem"), nil, "", true, false, false)
// Create a wrapped conn which can return the HandshakeInfo created
// above with a very small deadline.
d := time.Now().Add(defaultTestShortTimeout)
rawConn.SetDeadline(d)
conn := newWrappedConn(rawConn, hi, d)
// ServerHandshake() on the xDS credentials is expected to fail.
if _, _, err := creds.ServerHandshake(conn); err == nil {
return handshakeResult{err: errors.New("ServerHandshake() succeeded when expected to timeout")}
}
return handshakeResult{}
})
defer ts.stop()
// Dial the test server, but don't trigger the TLS handshake. This will
// cause ServerHandshake() to fail.
rawConn, err := net.Dial("tcp", ts.address)
if err != nil {
t.Fatalf("net.Dial(%s) failed: %v", ts.address, err)
}
defer rawConn.Close()
// Read handshake result from the testServer and expect a failure result.
val, err := ts.hsResult.Receive(ctx)
if err != nil {
t.Fatalf("testServer failed to return handshake result: %v", err)
}
hsr := val.(handshakeResult)
if hsr.err != nil {
t.Fatalf("testServer handshake failure: %v", hsr.err)
}
}
// TestServerCredsHandshakeFailure verifies the case where the server-side
// credentials uses a root certificate which does not match the certificate
// presented by the client, and hence the handshake must fail.
func (s) TestServerCredsHandshakeFailure(t *testing.T) {
opts := ServerOptions{FallbackCreds: &errorCreds{}}
creds, err := NewServerCredentials(opts)
if err != nil {
t.Fatalf("NewServerCredentials(%v) failed: %v", opts, err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
// Create a test server which uses the xDS server credentials created above
// to perform TLS handshake on incoming connections.
ts := newTestServerWithHandshakeFunc(ctx, func(rawConn net.Conn) handshakeResult {
// Create a HandshakeInfo which has a root provider which does not match
// the certificate sent by the client.
hi := xdsinternal.NewHandshakeInfo(makeRootProvider(t, "x509/server_ca_cert.pem"), makeIdentityProvider(t, "x509/client2_cert.pem", "x509/client2_key.pem"), nil, "", true, false, false)
// Create a wrapped conn which can return the HandshakeInfo and
// configured deadline to the xDS credentials' ServerHandshake()
// method.
conn := newWrappedConn(rawConn, hi, time.Now().Add(defaultTestTimeout))
// ServerHandshake() on the xDS credentials is expected to fail.
if _, _, err := creds.ServerHandshake(conn); err == nil {
return handshakeResult{err: errors.New("ServerHandshake() succeeded when expected to fail")}
}
return handshakeResult{}
})
defer ts.stop()
// Dial the test server, and trigger the TLS handshake.
rawConn, err := net.Dial("tcp", ts.address)
if err != nil {
t.Fatalf("net.Dial(%s) failed: %v", ts.address, err)
}
defer rawConn.Close()
tlsConn := tls.Client(rawConn, makeClientTLSConfig(t, true))
tlsConn.SetDeadline(time.Now().Add(defaultTestTimeout))
if err := tlsConn.Handshake(); err != nil {
t.Fatal(err)
}
// Read handshake result from the testServer which will return an error if
// the handshake succeeded.
val, err := ts.hsResult.Receive(ctx)
if err != nil {
t.Fatalf("testServer failed to return handshake result: %v", err)
}
hsr := val.(handshakeResult)
if hsr.err != nil {
t.Fatalf("testServer handshake failure: %v", hsr.err)
}
}
// TestServerCredsHandshakeSuccess verifies success handshake cases.
func (s) TestServerCredsHandshakeSuccess(t *testing.T) {
tests := []struct {
desc string
fallbackCreds credentials.TransportCredentials
rootProvider certprovider.Provider
identityProvider certprovider.Provider
requireClientCert bool
}{
{
desc: "fallback",
fallbackCreds: makeFallbackServerCreds(t),
},
{
desc: "TLS",
fallbackCreds: &errorCreds{},
identityProvider: makeIdentityProvider(t, "x509/server2_cert.pem", "x509/server2_key.pem"),
},
{
desc: "mTLS",
fallbackCreds: &errorCreds{},
identityProvider: makeIdentityProvider(t, "x509/server2_cert.pem", "x509/server2_key.pem"),
rootProvider: makeRootProvider(t, "x509/client_ca_cert.pem"),
requireClientCert: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
// Create an xDS server credentials.
opts := ServerOptions{FallbackCreds: test.fallbackCreds}
creds, err := NewServerCredentials(opts)
if err != nil {
t.Fatalf("NewServerCredentials(%v) failed: %v", opts, err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
// Create a test server which uses the xDS server credentials
// created above to perform TLS handshake on incoming connections.
ts := newTestServerWithHandshakeFunc(ctx, func(rawConn net.Conn) handshakeResult {
// Create a HandshakeInfo with information from the test table.
hi := xdsinternal.NewHandshakeInfo(test.rootProvider, test.identityProvider, nil, "", test.requireClientCert, false, false)
// Create a wrapped conn which can return the HandshakeInfo and
// configured deadline to the xDS credentials' ServerHandshake()
// method.
conn := newWrappedConn(rawConn, hi, time.Now().Add(defaultTestTimeout))
// Invoke the ServerHandshake() method on the xDS credentials
// and make some sanity checks before pushing the result for
// inspection by the main test body.
_, ai, err := creds.ServerHandshake(conn)
if err != nil {
return handshakeResult{err: fmt.Errorf("ServerHandshake() failed: %v", err)}
}
if ai.AuthType() != "tls" {
return handshakeResult{err: fmt.Errorf("ServerHandshake returned authType %q, want %q", ai.AuthType(), "tls")}
}
info, ok := ai.(credentials.TLSInfo)
if !ok {
return handshakeResult{err: fmt.Errorf("ServerHandshake returned authInfo of type %T, want %T", ai, credentials.TLSInfo{})}
}
return handshakeResult{connState: info.State}
})
defer ts.stop()
// Dial the test server, and trigger the TLS handshake.
rawConn, err := net.Dial("tcp", ts.address)
if err != nil {
t.Fatalf("net.Dial(%s) failed: %v", ts.address, err)
}
defer rawConn.Close()
tlsConn := tls.Client(rawConn, makeClientTLSConfig(t, test.requireClientCert))
tlsConn.SetDeadline(time.Now().Add(defaultTestTimeout))
if err := tlsConn.Handshake(); err != nil {
t.Fatal(err)
}
// Read the handshake result from the testServer which contains the
// TLS connection state on the server-side and compare it with the
// one received on the client-side.
val, err := ts.hsResult.Receive(ctx)
if err != nil {
t.Fatalf("testServer failed to return handshake result: %v", err)
}
hsr := val.(handshakeResult)
if hsr.err != nil {
t.Fatalf("testServer handshake failure: %v", hsr.err)
}
// AuthInfo contains a variety of information. We only verify a
// subset here. This is the same subset which is verified in TLS
// credentials tests.
if err := compareConnState(tlsConn.ConnectionState(), hsr.connState); err != nil {
t.Fatal(err)
}
})
}
}
func (s) TestServerCredsProviderSwitch(t *testing.T) {
opts := ServerOptions{FallbackCreds: &errorCreds{}}
creds, err := NewServerCredentials(opts)
if err != nil {
t.Fatalf("NewServerCredentials(%v) failed: %v", opts, err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
// The first time the handshake function is invoked, it returns a
// HandshakeInfo which is expected to fail. Further invocations return a
// HandshakeInfo which is expected to succeed.
cnt := 0
// Create a test server which uses the xDS server credentials created above
// to perform TLS handshake on incoming connections.
ts := newTestServerWithHandshakeFunc(ctx, func(rawConn net.Conn) handshakeResult {
cnt++
var hi *grpcsync.RefCounted[*xdsinternal.HandshakeInfo]
if cnt == 1 {
// Create a HandshakeInfo which has a root provider which does not match
// the certificate sent by the client.
hi = xdsinternal.NewHandshakeInfo(makeRootProvider(t, "x509/server_ca_cert.pem"), makeIdentityProvider(t, "x509/client2_cert.pem", "x509/client2_key.pem"), nil, "", true, false, false)
// Create a wrapped conn which can return the HandshakeInfo and
// configured deadline to the xDS credentials' ServerHandshake()
// method.
conn := newWrappedConn(rawConn, hi, time.Now().Add(defaultTestTimeout))
// ServerHandshake() on the xDS credentials is expected to fail.
if _, _, err := creds.ServerHandshake(conn); err == nil {
return handshakeResult{err: errors.New("ServerHandshake() succeeded when expected to fail")}
}
return handshakeResult{}
}
hi = xdsinternal.NewHandshakeInfo(makeRootProvider(t, "x509/client_ca_cert.pem"), makeIdentityProvider(t, "x509/server1_cert.pem", "x509/server1_key.pem"), nil, "", true, false, false)
// Create a wrapped conn which can return the HandshakeInfo and
// configured deadline to the xDS credentials' ServerHandshake()
// method.
conn := newWrappedConn(rawConn, hi, time.Now().Add(defaultTestTimeout))
// Invoke the ServerHandshake() method on the xDS credentials
// and make some sanity checks before pushing the result for
// inspection by the main test body.
_, ai, err := creds.ServerHandshake(conn)
if err != nil {
return handshakeResult{err: fmt.Errorf("ServerHandshake() failed: %v", err)}
}
if ai.AuthType() != "tls" {
return handshakeResult{err: fmt.Errorf("ServerHandshake returned authType %q, want %q", ai.AuthType(), "tls")}
}
info, ok := ai.(credentials.TLSInfo)
if !ok {
return handshakeResult{err: fmt.Errorf("ServerHandshake returned authInfo of type %T, want %T", ai, credentials.TLSInfo{})}
}
return handshakeResult{connState: info.State}
})
defer ts.stop()
for i := 0; i < 5; i++ {
// Dial the test server, and trigger the TLS handshake.
rawConn, err := net.Dial("tcp", ts.address)
if err != nil {
t.Fatalf("net.Dial(%s) failed: %v", ts.address, err)
}
defer rawConn.Close()
tlsConn := tls.Client(rawConn, makeClientTLSConfig(t, true))
tlsConn.SetDeadline(time.Now().Add(defaultTestTimeout))
if err := tlsConn.Handshake(); err != nil {
t.Fatal(err)
}
// Read the handshake result from the testServer which contains the
// TLS connection state on the server-side and compare it with the
// one received on the client-side.
val, err := ts.hsResult.Receive(ctx)
if err != nil {
t.Fatalf("testServer failed to return handshake result: %v", err)
}
hsr := val.(handshakeResult)
if hsr.err != nil {
t.Fatalf("testServer handshake failure: %v", hsr.err)
}
if i == 0 {
// We expect the first handshake to fail. So, we skip checks which
// compare connection state.
continue
}
// AuthInfo contains a variety of information. We only verify a
// subset here. This is the same subset which is verified in TLS
// credentials tests.
if err := compareConnState(tlsConn.ConnectionState(), hsr.connState); err != nil {
t.Fatal(err)
}
}
}
// TestServerClone verifies the Clone() method on client credentials.
func (s) TestServerClone(t *testing.T) {
opts := ServerOptions{FallbackCreds: makeFallbackServerCreds(t)}
orig, err := NewServerCredentials(opts)
if err != nil {
t.Fatalf("NewServerCredentials(%v) failed: %v", opts, err)
}
// The credsImpl does not have any exported fields, and it does not make
// sense to use any cmp options to look deep into. So, all we make sure here
// is that the cloned object points to a different location in memory.
if clone := orig.Clone(); clone == orig {
t.Fatal("return value from Clone() doesn't point to new credentials instance")
}
}