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
288 lines
9.2 KiB
Go
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")
|
|
}
|
|
}
|