Files
upstream-mirror e241b33f26
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (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
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
Stale bot / stale (push) Failing after 1s
snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 20:40:15 +05:00

288 lines
9.2 KiB
Go

/*
*
* 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")
}
}