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
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 / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
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
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 / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
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
This commit is contained in:
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user