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

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

View 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
}

View 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}
}

View 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()
}
}

View 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")
}

View 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")
}
}

View 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)
}
})
}
}