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:
71
internal/xds/credentials/access_token.go
Normal file
71
internal/xds/credentials/access_token.go
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
*
|
||||
* 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 credentials
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
accesstokenpb "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/call_credentials/access_token/v3"
|
||||
)
|
||||
|
||||
const accessTokenCredsTypeURL = "type.googleapis.com/envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials"
|
||||
|
||||
func init() {
|
||||
RegisterCallCredsBuilder(accessTokenCredsTypeURL, func(config *anypb.Any) (credentials.PerRPCCredentials, func(), error) {
|
||||
var accessToken accesstokenpb.AccessTokenCredentials
|
||||
if err := anypb.UnmarshalTo(config, &accessToken, proto.UnmarshalOptions{}); err != nil {
|
||||
return nil, nil, fmt.Errorf("credentials: failed to unmarshal AccessTokenCredentials: %v", err)
|
||||
}
|
||||
if accessToken.GetToken() == "" {
|
||||
return nil, nil, fmt.Errorf("credentials: access token must be non-empty")
|
||||
}
|
||||
// These credentials hold no resources; the no-op cleanup satisfies
|
||||
// the registry contract.
|
||||
return &accessTokenCallCreds{token: accessToken.GetToken()}, func() {}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// accessTokenCallCreds implements credentials.PerRPCCredentials by attaching
|
||||
// a static bearer token to each RPC (gRFC A102). The credentials require
|
||||
// transport security: the token is only ever sent on connections that provide
|
||||
// privacy and integrity, and RPCs on weaker connections fail.
|
||||
type accessTokenCallCreds struct {
|
||||
token string
|
||||
}
|
||||
|
||||
// GetRequestMetadata returns the token as an authorization header. It fails
|
||||
// if the connection does not provide privacy and integrity.
|
||||
func (c *accessTokenCallCreds) 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: unable to transfer access token PerRPCCredentials: %v", err)
|
||||
}
|
||||
return map[string]string{"authorization": "Bearer " + c.token}, nil
|
||||
}
|
||||
|
||||
// RequireTransportSecurity indicates whether the credentials require
|
||||
// transport security.
|
||||
func (c *accessTokenCallCreds) RequireTransportSecurity() bool {
|
||||
return true
|
||||
}
|
||||
126
internal/xds/credentials/access_token_test.go
Normal file
126
internal/xds/credentials/access_token_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
*
|
||||
* 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 credentials_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
xdscreds "google.golang.org/grpc/internal/xds/credentials"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
accesstokenpb "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/call_credentials/access_token/v3"
|
||||
)
|
||||
|
||||
const accessTokenCredsTypeURL = "type.googleapis.com/envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials"
|
||||
|
||||
func accessTokenConfig(t *testing.T, token string) *anypb.Any {
|
||||
t.Helper()
|
||||
a, err := anypb.New(&accesstokenpb.AccessTokenCredentials{Token: token})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal AccessTokenCredentials: %v", err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Tests that building access token call credentials fails on malformed
|
||||
// configs and on an empty token.
|
||||
func (s) TestAccessTokenCredsBuild_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *anypb.Any
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "unmarshal_failure",
|
||||
config: &anypb.Any{TypeUrl: accessTokenCredsTypeURL, Value: []byte{0xff}},
|
||||
wantErr: "failed to unmarshal AccessTokenCredentials",
|
||||
},
|
||||
{
|
||||
name: "empty_token",
|
||||
config: accessTokenConfig(t, ""),
|
||||
wantErr: "access token must be non-empty",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
callCreds, _, err := xdscreds.GetCallCredsBuilder(accessTokenCredsTypeURL)(tt.config)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("Build returned error %v, want error containing %q", err, tt.wantErr)
|
||||
}
|
||||
if callCreds != nil {
|
||||
t.Errorf("Build returned non-nil call credentials alongside error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that the token is attached as a bearer authorization header on
|
||||
// connections providing privacy and integrity, and that an error is returned
|
||||
// on weaker connections.
|
||||
func (s) TestAccessTokenCredsGetRequestMetadata(t *testing.T) {
|
||||
callCreds, cleanup, err := xdscreds.GetCallCredsBuilder(accessTokenCredsTypeURL)(accessTokenConfig(t, "test-token"))
|
||||
if err != nil {
|
||||
t.Fatalf("Build failed: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if !callCreds.RequireTransportSecurity() {
|
||||
t.Error("RequireTransportSecurity() = false, want true")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// The token must be attached on a connection with privacy and integrity.
|
||||
secureCtx := credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
|
||||
AuthInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
|
||||
})
|
||||
md, err := callCreds.GetRequestMetadata(secureCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRequestMetadata() on a secure connection failed: %v", err)
|
||||
}
|
||||
if got, want := md["authorization"], "Bearer test-token"; got != want {
|
||||
t.Fatalf("GetRequestMetadata() on a secure connection returned authorization header %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// RPCs on a connection that does not provide privacy and integrity must
|
||||
// fail.
|
||||
insecureCtx := credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
|
||||
AuthInfo: &testAuthInfo{secLevel: credentials.NoSecurity},
|
||||
})
|
||||
if _, err := callCreds.GetRequestMetadata(insecureCtx); err == nil {
|
||||
t.Fatal("GetRequestMetadata() on an insecure connection succeeded, want error")
|
||||
}
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
217
internal/xds/credentials/credentials.go
Normal file
217
internal/xds/credentials/credentials.go
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
*
|
||||
* 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 credentials provides the credentials used for xDS-configured side
|
||||
// channels (gRFC A102): built channel and call credentials paired with the
|
||||
// identity of the configuration they were built from, and registries of
|
||||
// credential builders keyed by proto type URL.
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/google"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/credentials/tls/certprovider"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
xdscredspb "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/xds/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
insecureCredsTypeURL = "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials"
|
||||
googleDefaultCredsTypeURL = "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.google_default.v3.GoogleDefaultCredentials"
|
||||
xdsCredsTypeURL = "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.xds.v3.XdsCredentials"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterChannelCredsBuilder(insecureCredsTypeURL, func(*anypb.Any, CertProviderConfigResolver) (credentials.Bundle, func(), error) {
|
||||
return insecure.NewBundle(), func() {}, nil
|
||||
})
|
||||
RegisterChannelCredsBuilder(googleDefaultCredsTypeURL, func(*anypb.Any, CertProviderConfigResolver) (credentials.Bundle, func(), error) {
|
||||
return google.NewDefaultCredentials(), func() {}, nil
|
||||
})
|
||||
// The xds credential resolves to its required fallback credential, whose
|
||||
// builder is looked up in the registry: a side-channel target is not an
|
||||
// xDS cluster, so there is no xDS security configuration for it.
|
||||
RegisterChannelCredsBuilder(xdsCredsTypeURL, func(config *anypb.Any, resolver CertProviderConfigResolver) (credentials.Bundle, func(), error) {
|
||||
var xdsCfg xdscredspb.XdsCredentials
|
||||
if err := anypb.UnmarshalTo(config, &xdsCfg, proto.UnmarshalOptions{}); err != nil {
|
||||
return nil, nil, fmt.Errorf("credentials: failed to unmarshal XdsCredentials: %v", err)
|
||||
}
|
||||
fallback := xdsCfg.GetFallbackCredentials()
|
||||
if fallback == nil {
|
||||
return nil, nil, fmt.Errorf("credentials: xds credentials missing required fallback credentials")
|
||||
}
|
||||
if b := GetChannelCredsBuilder(fallback.GetTypeUrl()); b != nil {
|
||||
return b(fallback, resolver)
|
||||
}
|
||||
return nil, nil, fmt.Errorf("credentials: unsupported fallback credentials type %q in xds credentials", fallback.GetTypeUrl())
|
||||
})
|
||||
}
|
||||
|
||||
// CertProviderConfigResolver resolves certificate provider instance names to
|
||||
// their configuration. It is implemented by the bootstrap config, and
|
||||
// declared here so that credential builders do not depend on the bootstrap
|
||||
// package.
|
||||
type CertProviderConfigResolver interface {
|
||||
CertProviderConfigs() map[string]*certprovider.BuildableConfig
|
||||
}
|
||||
|
||||
// ChannelCredsBuilder builds a channel credentials bundle from a GrpcService
|
||||
// channel credentials plugin config. The resolver gives access to the
|
||||
// certificate provider instances configured in the bootstrap config; builders
|
||||
// that do not reference them ignore it. The returned function releases the
|
||||
// resources held by the bundle when it is no longer needed.
|
||||
type ChannelCredsBuilder func(config *anypb.Any, resolver CertProviderConfigResolver) (credentials.Bundle, func(), error)
|
||||
|
||||
// CallCredsBuilder builds per-RPC credentials from a GrpcService call
|
||||
// credentials plugin config. The returned function releases the resources
|
||||
// held by the credentials when they are no longer needed.
|
||||
type CallCredsBuilder func(config *anypb.Any) (credentials.PerRPCCredentials, func(), error)
|
||||
|
||||
var (
|
||||
// channelCredsBuilders is a map from proto type URL to
|
||||
// ChannelCredsBuilder.
|
||||
channelCredsBuilders = make(map[string]ChannelCredsBuilder)
|
||||
// callCredsBuilders is a map from proto type URL to CallCredsBuilder.
|
||||
callCredsBuilders = make(map[string]CallCredsBuilder)
|
||||
)
|
||||
|
||||
// RegisterChannelCredsBuilder registers the builder for the given proto type
|
||||
// URL. Must be called at init time. Not thread safe.
|
||||
func RegisterChannelCredsBuilder(typeURL string, b ChannelCredsBuilder) {
|
||||
channelCredsBuilders[typeURL] = b
|
||||
}
|
||||
|
||||
// GetChannelCredsBuilder returns the builder registered for the given proto
|
||||
// type URL, or nil if there is none.
|
||||
func GetChannelCredsBuilder(typeURL string) ChannelCredsBuilder {
|
||||
return channelCredsBuilders[typeURL]
|
||||
}
|
||||
|
||||
// RegisterCallCredsBuilder registers the builder for the given proto type
|
||||
// URL. Must be called at init time. Not thread safe.
|
||||
func RegisterCallCredsBuilder(typeURL string, b CallCredsBuilder) {
|
||||
callCredsBuilders[typeURL] = b
|
||||
}
|
||||
|
||||
// GetCallCredsBuilder returns the builder registered for the given proto type
|
||||
// URL, or nil if there is none.
|
||||
func GetCallCredsBuilder(typeURL string) CallCredsBuilder {
|
||||
return callCredsBuilders[typeURL]
|
||||
}
|
||||
|
||||
// Identity identifies the configuration a credential was built from.
|
||||
type Identity struct {
|
||||
// Type is the bootstrap credential type name for JSON-sourced
|
||||
// credentials, or the proto type URL for proto-sourced ones. The two
|
||||
// namespaces cannot collide: proto type URLs contain dots and slashes.
|
||||
Type string
|
||||
// Data is the raw JSON configuration, or the proto config value bytes.
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// Equal reports whether i and other describe the same configuration.
|
||||
func (i Identity) Equal(other Identity) bool {
|
||||
return i.Type == other.Type && bytes.Equal(i.Data, other.Data)
|
||||
}
|
||||
|
||||
// ChannelCreds pairs a built credentials bundle with the identity of the
|
||||
// configuration it was built from.
|
||||
type ChannelCreds struct {
|
||||
bundle credentials.Bundle
|
||||
identity Identity
|
||||
cleanup func()
|
||||
}
|
||||
|
||||
// NewChannelCreds pairs the given bundle with its identity. cleanup releases
|
||||
// the resources held by the bundle and is run by Close; it must be nil when
|
||||
// the bundle is owned by another component (e.g. the bootstrap config), in
|
||||
// which case Close is a no-op.
|
||||
func NewChannelCreds(bundle credentials.Bundle, identity Identity, cleanup func()) *ChannelCreds {
|
||||
if cleanup != nil {
|
||||
cleanup = sync.OnceFunc(cleanup)
|
||||
}
|
||||
return &ChannelCreds{bundle: bundle, identity: identity, cleanup: cleanup}
|
||||
}
|
||||
|
||||
// Bundle returns the built credentials bundle.
|
||||
func (c *ChannelCreds) Bundle() credentials.Bundle {
|
||||
return c.bundle
|
||||
}
|
||||
|
||||
// Equal reports whether c and other were built from the same configuration.
|
||||
func (c *ChannelCreds) Equal(other *ChannelCreds) bool {
|
||||
if c == nil || other == nil {
|
||||
return c == other
|
||||
}
|
||||
return c.identity.Equal(other.identity)
|
||||
}
|
||||
|
||||
// Close releases the resources held by the bundle, if owned. It is
|
||||
// idempotent.
|
||||
func (c *ChannelCreds) Close() {
|
||||
if c.cleanup != nil {
|
||||
c.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// CallCreds pairs built per-RPC credentials with the identity of the
|
||||
// configuration they were built from.
|
||||
type CallCreds struct {
|
||||
creds credentials.PerRPCCredentials
|
||||
identity Identity
|
||||
cleanup func()
|
||||
}
|
||||
|
||||
// NewCallCreds pairs the given per-RPC credentials with their identity.
|
||||
// cleanup releases the resources held by the credentials and is run by Close;
|
||||
// it must be nil when the credentials are owned by another component (e.g.
|
||||
// the bootstrap config), in which case Close is a no-op.
|
||||
func NewCallCreds(creds credentials.PerRPCCredentials, identity Identity, cleanup func()) *CallCreds {
|
||||
if cleanup != nil {
|
||||
cleanup = sync.OnceFunc(cleanup)
|
||||
}
|
||||
return &CallCreds{creds: creds, identity: identity, cleanup: cleanup}
|
||||
}
|
||||
|
||||
// Credentials returns the built per-RPC credentials.
|
||||
func (c *CallCreds) Credentials() credentials.PerRPCCredentials {
|
||||
return c.creds
|
||||
}
|
||||
|
||||
// Equal reports whether c and other were built from the same configuration.
|
||||
func (c *CallCreds) Equal(other *CallCreds) bool {
|
||||
if c == nil || other == nil {
|
||||
return c == other
|
||||
}
|
||||
return c.identity.Equal(other.identity)
|
||||
}
|
||||
|
||||
// Close releases the resources held by the credentials, if owned. It is
|
||||
// idempotent.
|
||||
func (c *CallCreds) Close() {
|
||||
if c.cleanup != nil {
|
||||
c.cleanup()
|
||||
}
|
||||
}
|
||||
174
internal/xds/credentials/tls.go
Normal file
174
internal/xds/credentials/tls.go
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
*
|
||||
* 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 credentials
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/tls/certprovider"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
tlscredspb "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/tls/v3"
|
||||
)
|
||||
|
||||
const tlsCredsTypeURL = "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.tls.v3.TlsCredentials"
|
||||
|
||||
func init() {
|
||||
RegisterChannelCredsBuilder(tlsCredsTypeURL, buildTLSCredentials)
|
||||
}
|
||||
|
||||
// buildTLSCredentials builds TLS channel credentials from a TlsCredentials
|
||||
// plugin config, whose root and identity certificates are sourced from
|
||||
// certificate provider instances configured in the bootstrap config. Unknown
|
||||
// instance names and provider build failures are errors, resulting in the
|
||||
// resource being NACKed, the same way CommonTlsContext instances are handled
|
||||
// (gRFC A29). The returned cleanup closes the certificate providers.
|
||||
func buildTLSCredentials(config *anypb.Any, resolver CertProviderConfigResolver) (credentials.Bundle, func(), error) {
|
||||
var tlsCfg tlscredspb.TlsCredentials
|
||||
if err := anypb.UnmarshalTo(config, &tlsCfg, proto.UnmarshalOptions{}); err != nil {
|
||||
return nil, nil, fmt.Errorf("credentials: failed to unmarshal TlsCredentials: %v", err)
|
||||
}
|
||||
if resolver == nil {
|
||||
return nil, nil, fmt.Errorf("credentials: no bootstrap configuration available to resolve certificate provider instances")
|
||||
}
|
||||
|
||||
rootInstanceName := tlsCfg.GetRootCertificateProvider().GetInstanceName()
|
||||
if rootInstanceName == "" {
|
||||
return nil, nil, fmt.Errorf("credentials: tls credentials must specify root_certificate_provider with an instance_name")
|
||||
}
|
||||
rootCfg, err := certProviderConfig(resolver, rootInstanceName, "root")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
rootProvider, err := rootCfg.Build(certprovider.BuildOptions{
|
||||
CertName: tlsCfg.GetRootCertificateProvider().GetCertificateName(),
|
||||
WantRoot: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("credentials: failed to build root certificate provider: %v", err)
|
||||
}
|
||||
|
||||
b := &tlsBundle{rootProvider: rootProvider}
|
||||
// The identity certificate provider is optional, and is configured only
|
||||
// for mTLS. When the field is set, it must name a provider instance.
|
||||
if identity := tlsCfg.GetIdentityCertificateProvider(); identity != nil {
|
||||
identityInstanceName := identity.GetInstanceName()
|
||||
if identityInstanceName == "" {
|
||||
rootProvider.Close()
|
||||
return nil, nil, fmt.Errorf("credentials: tls credentials identity_certificate_provider must specify an instance_name")
|
||||
}
|
||||
identityCfg, err := certProviderConfig(resolver, identityInstanceName, "identity")
|
||||
if err != nil {
|
||||
rootProvider.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
identityProvider, err := identityCfg.Build(certprovider.BuildOptions{
|
||||
CertName: identity.GetCertificateName(),
|
||||
WantIdentity: true,
|
||||
})
|
||||
if err != nil {
|
||||
rootProvider.Close()
|
||||
return nil, nil, fmt.Errorf("credentials: failed to build identity certificate provider: %v", err)
|
||||
}
|
||||
b.identityProvider = identityProvider
|
||||
}
|
||||
return b, b.close, nil
|
||||
}
|
||||
|
||||
// certProviderConfig looks up the certificate provider instance with the
|
||||
// given name via the resolver. kind names the certificate the provider is
|
||||
// used for (root or identity), for error messages.
|
||||
func certProviderConfig(resolver CertProviderConfigResolver, instanceName, kind string) (*certprovider.BuildableConfig, error) {
|
||||
cfg, ok := resolver.CertProviderConfigs()[instanceName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("credentials: tls credentials %s certificate provider: instance name %q missing in bootstrap configuration", kind, instanceName)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// tlsBundle is a credentials.Bundle providing client-side TLS transport
|
||||
// credentials whose server root CA certificates, and optionally client
|
||||
// identity certificates, come from certificate provider instances. The key
|
||||
// material is fetched from the providers on every handshake, so certificate
|
||||
// reloads are picked up. It carries no per-RPC credentials.
|
||||
type tlsBundle struct {
|
||||
rootProvider certprovider.Provider
|
||||
identityProvider certprovider.Provider // nil when no identity certificate is configured
|
||||
}
|
||||
|
||||
func (b *tlsBundle) TransportCredentials() credentials.TransportCredentials {
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *tlsBundle) PerRPCCredentials() credentials.PerRPCCredentials {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *tlsBundle) NewWithMode(string) (credentials.Bundle, error) {
|
||||
return nil, fmt.Errorf("credentials: xDS TLS channel credentials only support one mode")
|
||||
}
|
||||
|
||||
// close closes the certificate providers. Subsequent handshakes fail.
|
||||
func (b *tlsBundle) close() {
|
||||
b.rootProvider.Close()
|
||||
if b.identityProvider != nil {
|
||||
b.identityProvider.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *tlsBundle) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
||||
rootKM, err := b.rootProvider.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("credentials: failed to get root certificates: %v", err)
|
||||
}
|
||||
if rootKM.Roots == nil {
|
||||
return nil, nil, errors.New("credentials: root certificate provider returned no root certificates")
|
||||
}
|
||||
cfg := &tls.Config{RootCAs: rootKM.Roots}
|
||||
if b.identityProvider != nil {
|
||||
identityKM, err := b.identityProvider.KeyMaterial(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("credentials: failed to get identity certificates: %v", err)
|
||||
}
|
||||
cfg.Certificates = identityKM.Certs
|
||||
}
|
||||
return credentials.NewTLS(cfg).ClientHandshake(ctx, authority, rawConn)
|
||||
}
|
||||
|
||||
func (b *tlsBundle) ServerHandshake(net.Conn) (net.Conn, credentials.AuthInfo, error) {
|
||||
return nil, nil, errors.New("credentials: server handshake is not supported by xDS TLS channel credentials")
|
||||
}
|
||||
|
||||
func (b *tlsBundle) Info() credentials.ProtocolInfo {
|
||||
return credentials.ProtocolInfo{SecurityProtocol: "tls"}
|
||||
}
|
||||
|
||||
func (b *tlsBundle) Clone() credentials.TransportCredentials {
|
||||
return &tlsBundle{rootProvider: b.rootProvider, identityProvider: b.identityProvider}
|
||||
}
|
||||
|
||||
func (b *tlsBundle) OverrideServerName(string) error {
|
||||
return errors.New("credentials: overriding server name is not supported by xDS TLS channel credentials")
|
||||
}
|
||||
287
internal/xds/credentials/tls_test.go
Normal file
287
internal/xds/credentials/tls_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
*
|
||||
* 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 credentials_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
xdscreds "google.golang.org/grpc/internal/xds/credentials"
|
||||
"google.golang.org/grpc/testdata"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
tlscredspb "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/tls/v3"
|
||||
v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTestTimeout = 10 * time.Second
|
||||
|
||||
tlsCredsTypeURL = "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.tls.v3.TlsCredentials"
|
||||
)
|
||||
|
||||
// testBootstrapConfig returns a bootstrap config with two certificate
|
||||
// provider instances: "root-instance" watching the CA certificate that signed
|
||||
// the test server's certificate, and "identity-instance" watching a client
|
||||
// certificate and key.
|
||||
func testBootstrapConfig(t *testing.T) *bootstrap.Config {
|
||||
t.Helper()
|
||||
|
||||
rootCert := testdata.Path("x509/server_ca_cert.pem")
|
||||
clientCert := testdata.Path("x509/client1_cert.pem")
|
||||
clientKey := testdata.Path("x509/client1_key.pem")
|
||||
|
||||
contents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
|
||||
Servers: json.RawMessage(`[{"server_uri": "passthrough:///unused", "channel_creds": [{"type": "insecure"}]}]`),
|
||||
Node: json.RawMessage(`{"id": "test-node"}`),
|
||||
CertificateProviders: map[string]json.RawMessage{
|
||||
"root-instance": json.RawMessage(fmt.Sprintf(`{
|
||||
"plugin_name": "file_watcher",
|
||||
"config": {"ca_certificate_file": %q}
|
||||
}`, rootCert)),
|
||||
"identity-instance": json.RawMessage(fmt.Sprintf(`{
|
||||
"plugin_name": "file_watcher",
|
||||
"config": {"certificate_file": %q, "private_key_file": %q}
|
||||
}`, clientCert, clientKey)),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewContentsForTesting() failed: %v", err)
|
||||
}
|
||||
cfg, err := bootstrap.NewConfigFromContents(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConfigFromContents() failed: %v", err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// tlsCredsConfig returns a marshaled TlsCredentials plugin config referencing
|
||||
// the given provider instance names. An empty identity omits the identity
|
||||
// certificate provider.
|
||||
func tlsCredsConfig(t *testing.T, root, identity string) *anypb.Any {
|
||||
t.Helper()
|
||||
cfg := &tlscredspb.TlsCredentials{}
|
||||
if root != "" {
|
||||
cfg.RootCertificateProvider = &v3tlspb.CommonTlsContext_CertificateProviderInstance{InstanceName: root}
|
||||
}
|
||||
if identity != "" {
|
||||
cfg.IdentityCertificateProvider = &v3tlspb.CommonTlsContext_CertificateProviderInstance{InstanceName: identity}
|
||||
}
|
||||
a, err := anypb.New(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal TlsCredentials: %v", err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Tests that building TLS channel credentials validates the certificate
|
||||
// provider instance names against the bootstrap config.
|
||||
func (s) TestTLSCredsBuild_Errors(t *testing.T) {
|
||||
bc := testBootstrapConfig(t)
|
||||
|
||||
// The tlsCredsConfig helper cannot express an identity certificate
|
||||
// provider with an empty instance name, so build that config directly.
|
||||
emptyIdentityInstance, err := anypb.New(&tlscredspb.TlsCredentials{
|
||||
RootCertificateProvider: &v3tlspb.CommonTlsContext_CertificateProviderInstance{InstanceName: "root-instance"},
|
||||
IdentityCertificateProvider: &v3tlspb.CommonTlsContext_CertificateProviderInstance{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal TlsCredentials: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config *anypb.Any
|
||||
resolver xdscreds.CertProviderConfigResolver
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "unmarshal_failure",
|
||||
config: &anypb.Any{TypeUrl: tlsCredsTypeURL, Value: []byte{0xff}},
|
||||
resolver: bc,
|
||||
wantErr: "failed to unmarshal TlsCredentials",
|
||||
},
|
||||
{
|
||||
name: "missing_root_certificate_provider",
|
||||
config: tlsCredsConfig(t, "", "identity-instance"),
|
||||
resolver: bc,
|
||||
wantErr: "must specify root_certificate_provider",
|
||||
},
|
||||
{
|
||||
name: "empty_identity_instance_name",
|
||||
config: emptyIdentityInstance,
|
||||
resolver: bc,
|
||||
wantErr: "identity_certificate_provider must specify an instance_name",
|
||||
},
|
||||
{
|
||||
name: "unknown_root_instance",
|
||||
config: tlsCredsConfig(t, "unknown-instance", ""),
|
||||
resolver: bc,
|
||||
wantErr: `"unknown-instance" missing in bootstrap`,
|
||||
},
|
||||
{
|
||||
name: "unknown_identity_instance",
|
||||
config: tlsCredsConfig(t, "root-instance", "unknown-instance"),
|
||||
resolver: bc,
|
||||
wantErr: `"unknown-instance" missing in bootstrap`,
|
||||
},
|
||||
{
|
||||
name: "nil_resolver",
|
||||
config: tlsCredsConfig(t, "root-instance", ""),
|
||||
resolver: nil,
|
||||
wantErr: "no bootstrap configuration",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, err := xdscreds.GetChannelCredsBuilder(tlsCredsTypeURL)(tt.config, tt.resolver)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("Build returned error %v, want error containing %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// startTestTLSServer starts a TLS server that performs one handshake per
|
||||
// accepted connection. If mTLS is true, the server requires and verifies a
|
||||
// client certificate.
|
||||
func startTestTLSServer(t *testing.T, mTLS bool) net.Listener {
|
||||
t.Helper()
|
||||
|
||||
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 certificate: %v", err)
|
||||
}
|
||||
// gRPC's TLS credentials enforce ALPN, so the server must advertise h2.
|
||||
cfg := &tls.Config{Certificates: []tls.Certificate{serverCert}, NextProtos: []string{"h2"}}
|
||||
if mTLS {
|
||||
pem, err := os.ReadFile(testdata.Path("x509/client_ca_cert.pem"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read client CA certificate: %v", err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(pem) {
|
||||
t.Fatal("Failed to parse client CA certificate")
|
||||
}
|
||||
cfg.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
cfg.ClientCAs = pool
|
||||
}
|
||||
|
||||
lis, err := tls.Listen("tcp", "localhost:0", cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start test TLS server: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { lis.Close() })
|
||||
go func() {
|
||||
for {
|
||||
conn, err := lis.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
conn.(*tls.Conn).Handshake()
|
||||
conn.Close()
|
||||
}()
|
||||
}
|
||||
}()
|
||||
return lis
|
||||
}
|
||||
|
||||
// clientHandshake dials the test server and performs a client-side TLS
|
||||
// handshake with credentials built from the given plugin config.
|
||||
func clientHandshake(t *testing.T, config *anypb.Any, bc *bootstrap.Config) error {
|
||||
t.Helper()
|
||||
|
||||
bundle, cleanup, err := xdscreds.GetChannelCredsBuilder(tlsCredsTypeURL)(config, bc)
|
||||
if err != nil {
|
||||
t.Fatalf("Build failed: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
mTLS := false
|
||||
var cfg tlscredspb.TlsCredentials
|
||||
if err := config.UnmarshalTo(&cfg); err == nil && cfg.GetIdentityCertificateProvider() != nil {
|
||||
mTLS = true
|
||||
}
|
||||
lis := startTestTLSServer(t, mTLS)
|
||||
|
||||
rawConn, err := net.Dial("tcp", lis.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to dial test server: %v", err)
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
// The test server certificate is issued for *.test.example.com.
|
||||
_, _, err = bundle.TransportCredentials().ClientHandshake(ctx, "x.test.example.com", rawConn)
|
||||
return err
|
||||
}
|
||||
|
||||
// Tests that TLS channel credentials backed by certificate provider instances
|
||||
// complete a TLS handshake, with and without an identity certificate.
|
||||
func (s) TestTLSCredsHandshake(t *testing.T) {
|
||||
bc := testBootstrapConfig(t)
|
||||
|
||||
if err := clientHandshake(t, tlsCredsConfig(t, "root-instance", ""), bc); err != nil {
|
||||
t.Fatalf("ClientHandshake() with root-only TLS credentials failed: %v", err)
|
||||
}
|
||||
if err := clientHandshake(t, tlsCredsConfig(t, "root-instance", "identity-instance"), bc); err != nil {
|
||||
t.Fatalf("ClientHandshake() with mTLS credentials failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that closed TLS channel credentials fail handshakes.
|
||||
func (s) TestTLSCredsClose(t *testing.T) {
|
||||
bc := testBootstrapConfig(t)
|
||||
|
||||
bundle, cleanup, err := xdscreds.GetChannelCredsBuilder(tlsCredsTypeURL)(tlsCredsConfig(t, "root-instance", ""), bc)
|
||||
if err != nil {
|
||||
t.Fatalf("Build failed: %v", err)
|
||||
}
|
||||
cleanup()
|
||||
|
||||
// A handshake after close must fail.
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if _, _, err := bundle.TransportCredentials().ClientHandshake(ctx, "x.test.example.com", client); err == nil || !strings.Contains(err.Error(), "closed") {
|
||||
t.Fatalf("ClientHandshake() after close returned error %v, want error containing %q", err, "closed")
|
||||
}
|
||||
}
|
||||
76
internal/xds/credentials/xds_test.go
Normal file
76
internal/xds/credentials/xds_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
*
|
||||
* 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 credentials_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
xdscreds "google.golang.org/grpc/internal/xds/credentials"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
xdscredspb "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/xds/v3"
|
||||
)
|
||||
|
||||
const xdsCredsTypeURL = "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.xds.v3.XdsCredentials"
|
||||
|
||||
// Tests that building xds channel credentials fails on malformed configs and
|
||||
// on missing or unsupported fallback credentials.
|
||||
func (s) TestXDSCredsBuild_Errors(t *testing.T) {
|
||||
missingFallback, err := anypb.New(&xdscredspb.XdsCredentials{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal XdsCredentials: %v", err)
|
||||
}
|
||||
unsupportedFallback, err := anypb.New(&xdscredspb.XdsCredentials{
|
||||
FallbackCredentials: &anypb.Any{TypeUrl: "type.googleapis.com/unknown.Credentials"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal XdsCredentials: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config *anypb.Any
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "unmarshal_failure",
|
||||
config: &anypb.Any{TypeUrl: xdsCredsTypeURL, Value: []byte{0xff}},
|
||||
wantErr: "failed to unmarshal XdsCredentials",
|
||||
},
|
||||
{
|
||||
name: "missing_fallback_credentials",
|
||||
config: missingFallback,
|
||||
wantErr: "missing required fallback credentials",
|
||||
},
|
||||
{
|
||||
name: "unsupported_fallback_credentials_type",
|
||||
config: unsupportedFallback,
|
||||
wantErr: "unsupported fallback credentials type",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, err := xdscreds.GetChannelCredsBuilder(xdsCredsTypeURL)(tt.config, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("Build returned error %v, want error containing %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user