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
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:
322
credentials/alts/alts.go
Normal file
322
credentials/alts/alts.go
Normal 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
|
||||
}
|
||||
505
credentials/alts/alts_test.go
Normal file
505
credentials/alts/alts_test.go
Normal 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()
|
||||
}
|
||||
95
credentials/alts/internal/authinfo/authinfo.go
Normal file
95
credentials/alts/internal/authinfo/authinfo.go
Normal 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()
|
||||
}
|
||||
156
credentials/alts/internal/authinfo/authinfo_test.go
Normal file
156
credentials/alts/internal/authinfo/authinfo_test.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
67
credentials/alts/internal/common.go
Normal file
67
credentials/alts/internal/common.go
Normal 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()
|
||||
}
|
||||
131
credentials/alts/internal/conn/aeadrekey.go
Normal file
131
credentials/alts/internal/conn/aeadrekey.go
Normal 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]
|
||||
}
|
||||
263
credentials/alts/internal/conn/aeadrekey_test.go
Normal file
263
credentials/alts/internal/conn/aeadrekey_test.go
Normal 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
|
||||
}
|
||||
105
credentials/alts/internal/conn/aes128gcm.go
Normal file
105
credentials/alts/internal/conn/aes128gcm.go
Normal 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
|
||||
}
|
||||
223
credentials/alts/internal/conn/aes128gcm_test.go
Normal file
223
credentials/alts/internal/conn/aes128gcm_test.go
Normal 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)
|
||||
}
|
||||
116
credentials/alts/internal/conn/aes128gcmrekey.go
Normal file
116
credentials/alts/internal/conn/aes128gcmrekey.go
Normal 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
|
||||
}
|
||||
112
credentials/alts/internal/conn/aes128gcmrekey_test.go
Normal file
112
credentials/alts/internal/conn/aes128gcmrekey_test.go
Normal 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)
|
||||
}
|
||||
80
credentials/alts/internal/conn/common.go
Normal file
80
credentials/alts/internal/conn/common.go
Normal 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
|
||||
}
|
||||
62
credentials/alts/internal/conn/counter.go
Normal file
62
credentials/alts/internal/conn/counter.go
Normal 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
|
||||
}
|
||||
}
|
||||
141
credentials/alts/internal/conn/counter_test.go
Normal file
141
credentials/alts/internal/conn/counter_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
380
credentials/alts/internal/conn/record.go
Normal file
380
credentials/alts/internal/conn/record.go
Normal 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
|
||||
}
|
||||
563
credentials/alts/internal/conn/record_test.go
Normal file
563
credentials/alts/internal/conn/record_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
63
credentials/alts/internal/conn/utils.go
Normal file
63
credentials/alts/internal/conn/utils.go
Normal 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
|
||||
}
|
||||
381
credentials/alts/internal/handshaker/handshaker.go
Normal file
381
credentials/alts/internal/handshaker/handshaker.go
Normal 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)
|
||||
}
|
||||
437
credentials/alts/internal/handshaker/handshaker_test.go
Normal file
437
credentials/alts/internal/handshaker/handshaker_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
91
credentials/alts/internal/handshaker/service/service.go
Normal file
91
credentials/alts/internal/handshaker/service/service.go
Normal 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
|
||||
}
|
||||
80
credentials/alts/internal/handshaker/service/service_test.go
Normal file
80
credentials/alts/internal/handshaker/service/service_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
211
credentials/alts/internal/proto/grpc_gcp/altscontext.pb.go
Normal file
211
credentials/alts/internal/proto/grpc_gcp/altscontext.pb.go
Normal 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
|
||||
}
|
||||
1245
credentials/alts/internal/proto/grpc_gcp/handshaker.pb.go
Normal file
1245
credentials/alts/internal/proto/grpc_gcp/handshaker.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
144
credentials/alts/internal/proto/grpc_gcp/handshaker_grpc.pb.go
Normal file
144
credentials/alts/internal/proto/grpc_gcp/handshaker_grpc.pb.go
Normal 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",
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
323
credentials/alts/internal/testutil/testutil.go
Normal file
323
credentials/alts/internal/testutil/testutil.go
Normal 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
70
credentials/alts/utils.go
Normal 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)
|
||||
}
|
||||
167
credentials/alts/utils_test.go
Normal file
167
credentials/alts/utils_test.go
Normal 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
337
credentials/credentials.go
Normal 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
|
||||
}
|
||||
538
credentials/credentials_ext_test.go
Normal file
538
credentials/credentials_ext_test.go
Normal 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
|
||||
}
|
||||
322
credentials/credentials_test.go
Normal file
322
credentials/credentials_test.go
Normal 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
|
||||
}
|
||||
278
credentials/google/gcp_service_account_identity_credentials.go
Normal file
278
credentials/google/gcp_service_account_identity_credentials.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
178
credentials/google/google.go
Normal file
178
credentials/google/google.go
Normal 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()
|
||||
}
|
||||
268
credentials/google/google_test.go
Normal file
268
credentials/google/google_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
35
credentials/google/internal/internal.go
Normal file
35
credentials/google/internal/internal.go
Normal 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
128
credentials/google/xds.go
Normal 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)
|
||||
}
|
||||
60
credentials/google/xds_test.go
Normal file
60
credentials/google/xds_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
104
credentials/insecure/insecure.go
Normal file
104
credentials/insecure/insecure.go
Normal 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
50
credentials/jwt/doc.go
Normal 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
|
||||
118
credentials/jwt/file_reader.go
Normal file
118
credentials/jwt/file_reader.go
Normal 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
|
||||
}
|
||||
172
credentials/jwt/file_reader_test.go
Normal file
172
credentials/jwt/file_reader_test.go
Normal 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)
|
||||
}
|
||||
180
credentials/jwt/token_file_call_creds.go
Normal file
180
credentials/jwt/token_file_call_creds.go
Normal 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)
|
||||
}
|
||||
265
credentials/jwt/token_file_call_creds_ext_test.go
Normal file
265
credentials/jwt/token_file_call_creds_ext_test.go
Normal 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
|
||||
}
|
||||
555
credentials/jwt/token_file_call_creds_test.go
Normal file
555
credentials/jwt/token_file_call_creds_test.go
Normal 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
121
credentials/local/local.go
Normal 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
|
||||
}
|
||||
225
credentials/local/local_test.go
Normal file
225
credentials/local/local_test.go
Normal 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
244
credentials/oauth/oauth.go
Normal 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)
|
||||
}
|
||||
60
credentials/oauth/oauth_test.go
Normal file
60
credentials/oauth/oauth_test.go
Normal 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
407
credentials/sts/sts.go
Normal 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
803
credentials/sts/sts_test.go
Normal 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
322
credentials/tls.go
Normal 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
|
||||
}
|
||||
114
credentials/tls/certprovider/distributor.go
Normal file
114
credentials/tls/certprovider/distributor.go
Normal 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()
|
||||
}
|
||||
110
credentials/tls/certprovider/distributor_test.go
Normal file
110
credentials/tls/certprovider/distributor_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
103
credentials/tls/certprovider/pemfile/builder.go
Normal file
103
credentials/tls/certprovider/pemfile/builder.go
Normal 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
|
||||
}
|
||||
162
credentials/tls/certprovider/pemfile/builder_test.go
Normal file
162
credentials/tls/certprovider/pemfile/builder_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
297
credentials/tls/certprovider/pemfile/watcher.go
Normal file
297
credentials/tls/certprovider/pemfile/watcher.go
Normal 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()
|
||||
}
|
||||
629
credentials/tls/certprovider/pemfile/watcher_test.go
Normal file
629
credentials/tls/certprovider/pemfile/watcher_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
115
credentials/tls/certprovider/provider.go
Normal file
115
credentials/tls/certprovider/provider.go
Normal 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
|
||||
}
|
||||
196
credentials/tls/certprovider/store.go
Normal file
196
credentials/tls/certprovider/store.go
Normal 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)
|
||||
}
|
||||
400
credentials/tls/certprovider/store_test.go
Normal file
400
credentials/tls/certprovider/store_test.go
Normal 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
608
credentials/tls_ext_test.go
Normal 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
236
credentials/xds/xds.go
Normal 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
|
||||
}
|
||||
676
credentials/xds/xds_client_test.go
Normal file
676
credentials/xds/xds_client_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
543
credentials/xds/xds_server_test.go
Normal file
543
credentials/xds/xds_server_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user