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,35 @@
/*
* Copyright 2021 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"
)
// clientHandshakeInfoKey is a struct used as the key to store
// ClientHandshakeInfo in a context.
type clientHandshakeInfoKey struct{}
// ClientHandshakeInfoFromContext extracts the ClientHandshakeInfo from ctx.
func ClientHandshakeInfoFromContext(ctx context.Context) any {
return ctx.Value(clientHandshakeInfoKey{})
}
// NewClientHandshakeInfoContext creates a context with chi.
func NewClientHandshakeInfoContext(ctx context.Context, chi any) context.Context {
return context.WithValue(ctx, clientHandshakeInfoKey{}, chi)
}

View File

@@ -0,0 +1,75 @@
/*
*
* Copyright 2020 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 defines APIs for parsing SPIFFE ID.
//
// All APIs in this package are experimental.
package credentials
import (
"crypto/tls"
"crypto/x509"
"net/url"
"google.golang.org/grpc/grpclog"
)
var logger = grpclog.Component("credentials")
// SPIFFEIDFromState parses the SPIFFE ID from State. If the SPIFFE ID format
// is invalid, return nil with warning.
func SPIFFEIDFromState(state tls.ConnectionState) *url.URL {
if len(state.PeerCertificates) == 0 || len(state.PeerCertificates[0].URIs) == 0 {
return nil
}
return SPIFFEIDFromCert(state.PeerCertificates[0])
}
// SPIFFEIDFromCert parses the SPIFFE ID from x509.Certificate. If the SPIFFE
// ID format is invalid, return nil with warning.
func SPIFFEIDFromCert(cert *x509.Certificate) *url.URL {
if cert == nil || cert.URIs == nil {
return nil
}
var spiffeID *url.URL
for _, uri := range cert.URIs {
if uri == nil || uri.Scheme != "spiffe" || uri.Opaque != "" || (uri.User != nil && uri.User.Username() != "") {
continue
}
// From this point, we assume the uri is intended for a SPIFFE ID.
if len(uri.String()) > 2048 {
logger.Warning("invalid SPIFFE ID: total ID length larger than 2048 bytes")
return nil
}
if len(uri.Host) == 0 || len(uri.Path) == 0 {
logger.Warning("invalid SPIFFE ID: domain or workload ID is empty")
return nil
}
if len(uri.Host) > 255 {
logger.Warning("invalid SPIFFE ID: domain length larger than 255 characters")
return nil
}
// A valid SPIFFE certificate can only have exactly one URI SAN field.
if len(cert.URIs) > 1 {
logger.Warning("invalid SPIFFE ID: multiple URI SANs")
return nil
}
spiffeID = uri
}
return spiffeID
}

View File

@@ -0,0 +1,107 @@
/*
*
* Copyright 2025 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 spiffe defines APIs for working with SPIFFE Bundle Maps.
//
// All APIs in this package are experimental.
package spiffe
import (
"crypto/x509"
"encoding/json"
"fmt"
"github.com/spiffe/go-spiffe/v2/bundle/spiffebundle"
"github.com/spiffe/go-spiffe/v2/spiffeid"
)
type partialParsedSPIFFEBundleMap struct {
Bundles map[string]json.RawMessage `json:"trust_domains"`
}
// BundleMapFromBytes parses bytes into a SPIFFE Bundle Map. See the
// SPIFFE Bundle Map spec for more detail -
// https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#4-spiffe-bundle-format
// If duplicate keys are encountered in the JSON parsing, Go's default unmarshal
// behavior occurs which causes the last processed entry to be the entry in the
// parsed map.
func BundleMapFromBytes(bundleMapBytes []byte) (map[string]*spiffebundle.Bundle, error) {
var result partialParsedSPIFFEBundleMap
if err := json.Unmarshal(bundleMapBytes, &result); err != nil {
return nil, err
}
if result.Bundles == nil {
return nil, fmt.Errorf("spiffe: BundleMapFromBytes() no bundles parsed from spiffe bundle map bytes")
}
bundleMap := map[string]*spiffebundle.Bundle{}
for td, jsonBundle := range result.Bundles {
trustDomain, err := spiffeid.TrustDomainFromString(td)
if err != nil {
return nil, fmt.Errorf("spiffe: BundleMapFromBytes() invalid trust domain %q found when parsing SPIFFE Bundle Map: %v", td, err)
}
bundle, err := spiffebundle.Parse(trustDomain, jsonBundle)
if err != nil {
return nil, fmt.Errorf("spiffe: BundleMapFromBytes() failed to parse bundle for trust domain %q: %v", td, err)
}
bundleMap[td] = bundle
}
return bundleMap, nil
}
// GetRootsFromSPIFFEBundleMap returns the root trust certificates from the
// SPIFFE bundle map for the given trust domain from the leaf certificate.
func GetRootsFromSPIFFEBundleMap(bundleMap map[string]*spiffebundle.Bundle, leafCert *x509.Certificate) (*x509.CertPool, error) {
// 1. Upon receiving a peer certificate, verify that it is a well-formed SPIFFE
// leaf certificate. In particular, it must have a single URI SAN containing
// a well-formed SPIFFE ID ([SPIFFE ID format]).
spiffeID, err := idFromCert(leafCert)
if err != nil {
return nil, fmt.Errorf("spiffe: could not get spiffe ID from peer leaf cert but verification with spiffe trust map was configured: %v", err)
}
// 2. Use the trust domain in the peer certificate's SPIFFE ID to lookup
// the SPIFFE trust bundle. If the trust domain is not contained in the
// configured trust map, reject the certificate.
spiffeBundle, ok := bundleMap[spiffeID.TrustDomain().Name()]
if !ok {
return nil, fmt.Errorf("spiffe: no bundle found for peer certificates trust domain %q but verification with a SPIFFE trust map was configured", spiffeID.TrustDomain().Name())
}
roots := spiffeBundle.X509Authorities()
rootPool := x509.NewCertPool()
for _, root := range roots {
rootPool.AddCert(root)
}
return rootPool, nil
}
// idFromCert parses the SPIFFE ID from the x509.Certificate. If the certificate
// does not have a valid SPIFFE ID, returns an error.
func idFromCert(cert *x509.Certificate) (*spiffeid.ID, error) {
if cert == nil {
return nil, fmt.Errorf("input cert is nil")
}
// A valid SPIFFE Certificate should have exactly one URI.
if len(cert.URIs) != 1 {
return nil, fmt.Errorf("input cert has %v URIs but should have 1", len(cert.URIs))
}
id, err := spiffeid.FromURI(cert.URIs[0])
if err != nil {
return nil, fmt.Errorf("invalid spiffeid: %v", err)
}
return &id, nil
}

View File

@@ -0,0 +1,246 @@
/*
*
* Copyright 2025 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 spiffe
import (
"crypto/x509"
"encoding/pem"
"io"
"net/url"
"os"
"strings"
"testing"
"google.golang.org/grpc/testdata"
)
const wantURI = "spiffe://foo.bar.com/client/workload/1"
func loadFileBytes(t *testing.T, filePath string) []byte {
bytes, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("Error reading file: %v", err)
}
return bytes
}
func TestKnownSPIFFEBundle(t *testing.T) {
spiffeBundleFile := testdata.Path("spiffe/spiffebundle.json")
spiffeBundleBytes := loadFileBytes(t, spiffeBundleFile)
bundles, err := BundleMapFromBytes(spiffeBundleBytes)
if err != nil {
t.Fatalf("BundleMapFromBytes(%v) error during parsing: %v", spiffeBundleFile, err)
}
const wantBundleSize = 2
if len(bundles) != wantBundleSize {
t.Fatalf("BundleMapFromBytes(%v) did not parse correct bundle length. got %v want %v", spiffeBundleFile, len(bundles), wantBundleSize)
}
if bundles["example.com"] == nil {
t.Fatalf("BundleMapFromBytes(%v) got no bundle for example.com", spiffeBundleFile)
}
if bundles["test.example.com"] == nil {
t.Fatalf("BundleMapFromBytes(%v) got no bundle for test.example.com", spiffeBundleFile)
}
wantExampleComCert := loadX509Cert(t, testdata.Path("spiffe/spiffe_cert.pem"))
wantTestExampleComCert := loadX509Cert(t, testdata.Path("spiffe/server1_spiffe.pem"))
if !bundles["example.com"].X509Authorities()[0].Equal(wantExampleComCert) {
t.Errorf("BundleMapFromBytes(%v) parsed wrong cert for example.com.", spiffeBundleFile)
}
if !bundles["test.example.com"].X509Authorities()[0].Equal(wantTestExampleComCert) {
t.Errorf("BundleMapFromBytes(%v) parsed wrong cert for test.example.com", spiffeBundleFile)
}
}
func loadX509Cert(t *testing.T, filePath string) *x509.Certificate {
t.Helper()
certFile, _ := os.Open(filePath)
certRaw, _ := io.ReadAll(certFile)
block, _ := pem.Decode([]byte(certRaw))
if block == nil {
t.Fatalf("pem.Decode(%v) = nil. Want a value.", certRaw)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("x509.ParseCertificate(%v) failed %v", block.Bytes, err.Error())
}
return cert
}
func TestSPIFFEBundleMapFailures(t *testing.T) {
filePaths := []string{
testdata.Path("spiffe/spiffebundle_corrupted_cert.json"),
testdata.Path("spiffe/spiffebundle_malformed.json"),
testdata.Path("spiffe/spiffebundle_wrong_kid.json"),
testdata.Path("spiffe/spiffebundle_wrong_kty.json"),
testdata.Path("spiffe/spiffebundle_wrong_multi_certs.json"),
testdata.Path("spiffe/spiffebundle_wrong_root.json"),
testdata.Path("spiffe/spiffebundle_wrong_seq_type.json"),
testdata.Path("spiffe/spiffebundle_invalid_trustdomain.json"),
testdata.Path("spiffe/spiffebundle_empty_string_key.json"),
testdata.Path("spiffe/spiffebundle_empty_keys.json"),
}
for _, path := range filePaths {
t.Run(path, func(t *testing.T) {
bundleBytes := loadFileBytes(t, path)
if _, err := BundleMapFromBytes(bundleBytes); err == nil {
t.Fatalf("BundleMapFromBytes(%v) did not fail but should have", path)
}
})
}
}
func TestSPIFFEBundleMapX509Failures(t *testing.T) {
// SPIFFE Bundles only support a use of x509-svid and jwt-svid. If a
// use other than this is specified, the parser does not fail, it
// just doesn't add an x509 authority or jwt authority to the bundle
filePath := testdata.Path("spiffe/spiffebundle_wrong_use.json")
bundleBytes := loadFileBytes(t, filePath)
bundle, err := BundleMapFromBytes(bundleBytes)
if err != nil {
t.Fatalf("BundleMapFromBytes(%v) failed with error: %v", filePath, err)
}
if len(bundle["example.com"].X509Authorities()) != 0 {
t.Fatalf("BundleMapFromBytes(%v) did not have empty bundle but should have", filePath)
}
}
func TestGetRootsFromSPIFFEBundleMapSuccess(t *testing.T) {
bundleMapFile := testdata.Path("spiffe/spiffebundle_match_client_spiffe.json")
bundleBytes := loadFileBytes(t, bundleMapFile)
bundle, err := BundleMapFromBytes(bundleBytes)
if err != nil {
t.Fatalf("BundleMapFromBytes(%v) failed with error: %v", bundleMapFile, err)
}
cert := loadX509Cert(t, testdata.Path("spiffe/client_spiffe.pem"))
gotRoots, err := GetRootsFromSPIFFEBundleMap(bundle, cert)
if err != nil {
t.Fatalf("GetRootsFromSPIFFEBundleMap() failed with err %v", err)
}
wantRoot := loadX509Cert(t, testdata.Path("spiffe/spiffe_cert.pem"))
wantRoots := x509.NewCertPool()
wantRoots.AddCert(wantRoot)
if !gotRoots.Equal(wantRoots) {
t.Fatalf("GetRootsFromSPIFFEBundleMap() got %v want %v", gotRoots, wantRoots)
}
}
func TestGetRootsFromSPIFFEBundleMapFailures(t *testing.T) {
bundleMapFile := testdata.Path("spiffe/spiffebundle.json")
bundleBytes := loadFileBytes(t, bundleMapFile)
bundle, err := BundleMapFromBytes(bundleBytes)
certWithTwoURIs := loadX509Cert(t, testdata.Path("spiffe/client_spiffe.pem"))
certWithTwoURIs.URIs = append(certWithTwoURIs.URIs, certWithTwoURIs.URIs[0])
certWithNoURIs := loadX509Cert(t, testdata.Path("spiffe/client_spiffe.pem"))
certWithNoURIs.URIs = nil
if err != nil {
t.Fatalf("BundleMapFromBytes(%v) failed with error: %v", bundleMapFile, err)
}
tests := []struct {
name string
bundleMapFile string
leafCert *x509.Certificate
wantErr string
}{
{
name: "no bundle for peer cert spiffeID",
leafCert: loadX509Cert(t, testdata.Path("spiffe/client_spiffe.pem")),
wantErr: "no bundle found for peer certificates",
},
{
name: "cert has invalid SPIFFE id",
leafCert: loadX509Cert(t, testdata.Path("ca.pem")),
wantErr: "could not get spiffe ID from peer leaf cert",
},
{
name: "nil cert",
leafCert: nil,
wantErr: "input cert is nil",
},
{
name: "cert has multiple URIs",
leafCert: certWithTwoURIs,
wantErr: "input cert has 2 URIs but should have 1",
},
{
name: "cert has no URIs",
leafCert: certWithNoURIs,
wantErr: "input cert has 0 URIs but should have 1",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err = GetRootsFromSPIFFEBundleMap(bundle, tc.leafCert)
if err == nil {
t.Fatalf("GetRootsFromSPIFFEBundleMap() got no error but want error containing %v.", tc.wantErr)
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("GetRootsFromSPIFFEBundleMap() got error: %v. want error to contain %v", err, tc.wantErr)
}
})
}
}
func TestIDFromCert(t *testing.T) {
cert := loadX509Cert(t, testdata.Path("x509/spiffe_cert.pem"))
uri, err := idFromCert(cert)
if err != nil {
t.Fatalf("idFromCert() failed with err: %v", err)
}
if uri != nil && uri.String() != wantURI {
t.Fatalf("ID not expected, got %s, want %s", uri.String(), wantURI)
}
}
func TestIDFromCertFileFailures(t *testing.T) {
certWithNoURIs := loadX509Cert(t, testdata.Path("spiffe/client_spiffe.pem"))
certWithNoURIs.URIs = nil
certWithInvalidSPIFFEID := loadX509Cert(t, testdata.Path("spiffe/client_spiffe.pem"))
certWithInvalidSPIFFEID.URIs = []*url.URL{{Path: "non-spiffe.bad"}}
tests := []struct {
name string
cert *x509.Certificate
}{
{
name: "certificate with multiple URIs",
cert: loadX509Cert(t, testdata.Path("x509/multiple_uri_cert.pem")),
},
{
name: "certificate with invalidSPIFFE ID",
cert: certWithInvalidSPIFFEID,
},
{
name: "nil cert",
cert: nil,
},
{
name: "cert with no URIs",
cert: certWithNoURIs,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := idFromCert(tt.cert); err == nil {
t.Fatalf("idFromCert() succeeded but want error")
}
})
}
}

View File

@@ -0,0 +1,233 @@
/*
*
* Copyright 2020 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 (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"net/url"
"os"
"testing"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/testdata"
)
const wantURI = "spiffe://foo.bar.com/client/workload/1"
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
func (s) TestSPIFFEIDFromState(t *testing.T) {
tests := []struct {
name string
urls []*url.URL
// If we expect a SPIFFE ID to be returned.
wantID bool
}{
{
name: "empty URIs",
urls: []*url.URL{},
wantID: false,
},
{
name: "good SPIFFE ID",
urls: []*url.URL{
{
Scheme: "spiffe",
Host: "foo.bar.com",
Path: "workload/wl1",
RawPath: "workload/wl1",
},
},
wantID: true,
},
{
name: "invalid host",
urls: []*url.URL{
{
Scheme: "spiffe",
Host: "",
Path: "workload/wl1",
RawPath: "workload/wl1",
},
},
wantID: false,
},
{
name: "invalid path",
urls: []*url.URL{
{
Scheme: "spiffe",
Host: "foo.bar.com",
Path: "",
RawPath: "",
},
},
wantID: false,
},
{
name: "large path",
urls: []*url.URL{
{
Scheme: "spiffe",
Host: "foo.bar.com",
Path: string(make([]byte, 2050)),
RawPath: string(make([]byte, 2050)),
},
},
wantID: false,
},
{
name: "large host",
urls: []*url.URL{
{
Scheme: "spiffe",
Host: string(make([]byte, 256)),
Path: "workload/wl1",
RawPath: "workload/wl1",
},
},
wantID: false,
},
{
name: "multiple URI SANs",
urls: []*url.URL{
{
Scheme: "spiffe",
Host: "foo.bar.com",
Path: "workload/wl1",
RawPath: "workload/wl1",
},
{
Scheme: "spiffe",
Host: "bar.baz.com",
Path: "workload/wl2",
RawPath: "workload/wl2",
},
{
Scheme: "https",
Host: "foo.bar.com",
Path: "workload/wl1",
RawPath: "workload/wl1",
},
},
wantID: false,
},
{
name: "multiple URI SANs without SPIFFE ID",
urls: []*url.URL{
{
Scheme: "https",
Host: "foo.bar.com",
Path: "workload/wl1",
RawPath: "workload/wl1",
},
{
Scheme: "ssh",
Host: "foo.bar.com",
Path: "workload/wl1",
RawPath: "workload/wl1",
},
},
wantID: false,
},
{
name: "multiple URI SANs with one SPIFFE ID",
urls: []*url.URL{
{
Scheme: "spiffe",
Host: "foo.bar.com",
Path: "workload/wl1",
RawPath: "workload/wl1",
},
{
Scheme: "https",
Host: "foo.bar.com",
Path: "workload/wl1",
RawPath: "workload/wl1",
},
},
wantID: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := tls.ConnectionState{PeerCertificates: []*x509.Certificate{{URIs: tt.urls}}}
id := SPIFFEIDFromState(state)
if got, want := id != nil, tt.wantID; got != want {
t.Errorf("want wantID = %v, but SPIFFE ID is %v", want, id)
}
})
}
}
func (s) TestSPIFFEIDFromCert(t *testing.T) {
tests := []struct {
name string
dataPath string
// If we expect a SPIFFE ID to be returned.
wantID bool
}{
{
name: "good certificate with SPIFFE ID",
dataPath: "x509/spiffe_cert.pem",
wantID: true,
},
{
name: "bad certificate with SPIFFE ID and another URI",
dataPath: "x509/multiple_uri_cert.pem",
wantID: false,
},
{
name: "certificate without SPIFFE ID",
dataPath: "x509/client1_cert.pem",
wantID: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := os.ReadFile(testdata.Path(tt.dataPath))
if err != nil {
t.Fatalf("os.ReadFile(%s) failed: %v", testdata.Path(tt.dataPath), err)
}
block, _ := pem.Decode(data)
if block == nil {
t.Fatalf("Failed to parse the certificate: byte block is nil")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("x509.ParseCertificate(%b) failed: %v", block.Bytes, err)
}
uri := SPIFFEIDFromCert(cert)
if (uri != nil) != tt.wantID {
t.Fatalf("wantID got and want mismatch, got %t, want %t", uri != nil, tt.wantID)
}
if uri != nil && uri.String() != wantURI {
t.Fatalf("SPIFFE ID not expected, got %s, want %s", uri.String(), wantURI)
}
})
}
}

View File

@@ -0,0 +1,58 @@
/*
*
* Copyright 2018 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 (
"net"
"syscall"
)
type sysConn = syscall.Conn
// syscallConn keeps reference of rawConn to support syscall.Conn for channelz.
// SyscallConn() (the method in interface syscall.Conn) is explicitly
// implemented on this type,
//
// Interface syscall.Conn is implemented by most net.Conn implementations (e.g.
// TCPConn, UnixConn), but is not part of net.Conn interface. So wrapper conns
// that embed net.Conn don't implement syscall.Conn. (Side note: tls.Conn
// doesn't embed net.Conn, so even if syscall.Conn is part of net.Conn, it won't
// help here).
type syscallConn struct {
net.Conn
// sysConn is a type alias of syscall.Conn. It's necessary because the name
// `Conn` collides with `net.Conn`.
sysConn
}
// WrapSyscallConn tries to wrap rawConn and newConn into a net.Conn that
// implements syscall.Conn. rawConn will be used to support syscall, and newConn
// will be used for read/write.
//
// This function returns newConn if rawConn doesn't implement syscall.Conn.
func WrapSyscallConn(rawConn, newConn net.Conn) net.Conn {
sysConn, ok := rawConn.(syscall.Conn)
if !ok {
return newConn
}
return &syscallConn{
Conn: newConn,
sysConn: sysConn,
}
}

View File

@@ -0,0 +1,56 @@
/*
*
* Copyright 2018 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 (
"net"
"syscall"
"testing"
)
func (*syscallConn) SyscallConn() (syscall.RawConn, error) {
return nil, nil
}
type nonSyscallConn struct {
net.Conn
}
func (s) TestWrapSyscallConn(t *testing.T) {
sc := &syscallConn{}
nsc := &nonSyscallConn{}
wrapConn := WrapSyscallConn(sc, nsc)
if _, ok := wrapConn.(syscall.Conn); !ok {
t.Errorf("returned conn (type %T) doesn't implement syscall.Conn, want implement", wrapConn)
}
}
func (s) TestWrapSyscallConnNoWrap(t *testing.T) {
nscRaw := &nonSyscallConn{}
nsc := &nonSyscallConn{}
wrapConn := WrapSyscallConn(nscRaw, nsc)
if _, ok := wrapConn.(syscall.Conn); ok {
t.Errorf("returned conn (type %T) implements syscall.Conn, want not implement", wrapConn)
}
if wrapConn != nsc {
t.Errorf("returned conn is %p, want %p (the passed-in newConn)", wrapConn, nsc)
}
}

View File

@@ -0,0 +1,52 @@
/*
*
* Copyright 2020 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 (
"crypto/tls"
)
const alpnProtoStrH2 = "h2"
// AppendH2ToNextProtos appends h2 to next protos.
func AppendH2ToNextProtos(ps []string) []string {
for _, p := range ps {
if p == alpnProtoStrH2 {
return ps
}
}
ret := make([]string, 0, len(ps)+1)
ret = append(ret, ps...)
return append(ret, alpnProtoStrH2)
}
// CloneTLSConfig returns a shallow clone of the exported
// fields of cfg, ignoring the unexported sync.Once, which
// contains a mutex and must not be copied.
//
// If cfg is nil, a new zero tls.Config is returned.
//
// TODO: inline this function if possible.
func CloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return cfg.Clone()
}

View File

@@ -0,0 +1,60 @@
/*
*
* Copyright 2020 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 (
"reflect"
"testing"
)
func (s) TestAppendH2ToNextProtos(t *testing.T) {
tests := []struct {
name string
ps []string
want []string
}{
{
name: "empty",
ps: nil,
want: []string{"h2"},
},
{
name: "only h2",
ps: []string{"h2"},
want: []string{"h2"},
},
{
name: "with h2",
ps: []string{"alpn", "h2"},
want: []string{"alpn", "h2"},
},
{
name: "no h2",
ps: []string{"alpn"},
want: []string{"alpn", "h2"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := AppendH2ToNextProtos(tt.ps); !reflect.DeepEqual(got, tt.want) {
t.Errorf("AppendH2ToNextProtos() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,469 @@
/*
*
* Copyright 2020 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 xds contains non-user facing functionality of the xds credentials.
package xds
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strings"
"sync/atomic"
"google.golang.org/grpc/attributes"
"google.golang.org/grpc/credentials/tls/certprovider"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/credentials/spiffe"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpcsync"
"google.golang.org/grpc/internal/xds/matcher"
"google.golang.org/grpc/resolver"
)
func init() {
internal.GetXDSHandshakeInfoForTesting = HandshakeInfoFromAttributes
}
// handshakeAttrKey is the type used as the key to store HandshakeInfo in
// the Attributes field of resolver.Address.
type handshakeAttrKey struct{}
// hostnameKey is the type used as the key to store the hostname in the
// Attributes field of resolver.Address.
type hostnameKey struct{}
// SetAddressHostname returns a copy of addr in which the Attributes field is
// updated with the provided hostname.
func SetAddressHostname(addr resolver.Address, hostname string) resolver.Address {
addr.Attributes = addr.Attributes.WithValue(hostnameKey{}, hostname)
return addr
}
// Hostname returns the endpoint hostname stored in attr.
func Hostname(attr *attributes.Attributes) string {
if attr == nil {
return ""
}
v := attr.Value(hostnameKey{})
hn, _ := v.(string)
return hn
}
// SetHandshakeInfo returns a copy of addr in which the Attributes field is
// updated with hiPtr.
func SetHandshakeInfo(addr resolver.Address, hiPtr *atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]]) resolver.Address {
addr.Attributes = addr.Attributes.WithValue(handshakeAttrKey{}, hiPtr)
return addr
}
// HandshakeInfoFromAttributes returns the atomic pointer to the
// reference-counted HandshakeInfo stored in attr.
func HandshakeInfoFromAttributes(attr *attributes.Attributes) *atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]] {
v := attr.Value(handshakeAttrKey{})
hi, _ := v.(*atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]])
return hi
}
// HandshakeInfo wraps all the security configuration required by client and
// server handshake methods in xds credentials. The xDS implementation will be
// responsible for populating these fields.
type HandshakeInfo struct {
// The fields below are initialized when the HandshakeInfo is created, and are
// not changed thereafter.
sanMatchers []matcher.StringMatcher // Only on the client side.
requireClientCert bool // Only on server side.
sni string // Only on client side, used for Server Name Indication in TLS handshake.
validateSANUsingSNI bool // Only on client side, indicates whether to perform validation of SANs based on SNI value.
useAutoHostSNI bool // Only on client side, indicates whether to use endpoint hostname as SNI.
rootProvider certprovider.Provider
identityProvider certprovider.Provider
}
// NewHandshakeInfo returns a new reference counted HandshakeInfo configured
// with the provided options.
func NewHandshakeInfo(rootProvider, identityProvider certprovider.Provider, sanMatchers []matcher.StringMatcher, sni string, requireClientCert, validateSANUsingSNI, useAutoHostSNI bool) *grpcsync.RefCounted[*HandshakeInfo] {
hi := &HandshakeInfo{
rootProvider: rootProvider,
identityProvider: identityProvider,
sanMatchers: sanMatchers,
sni: sni,
requireClientCert: requireClientCert,
validateSANUsingSNI: validateSANUsingSNI,
useAutoHostSNI: useAutoHostSNI,
}
return grpcsync.NewRefCounted(hi, hi.close)
}
func (hi *HandshakeInfo) close() {
if hi.rootProvider != nil {
hi.rootProvider.Close()
}
if hi.identityProvider != nil {
hi.identityProvider.Close()
}
}
// ClientSideTLSConfig loads the HandshakeInfo from hiPtr, marks it as in-use,
// and returns the tls.Config along with a done callback that MUST be invoked
// when the handshake completes. If no HandshakeInfo is stored in hiPtr or if
// fallback credentials should be used, useFallback returns true.
func ClientSideTLSConfig(ctx context.Context, hiPtr *atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]], hostname string) (cfg *tls.Config, useFallback bool, done func(), err error) {
if hiPtr == nil {
return nil, true, func() {}, nil
}
for {
hiRC := hiPtr.Load()
if hiRC == nil {
return nil, false, func() {}, errors.New("xds: connection closed or HandshakeInfo dead")
}
if !hiRC.TryIncrement() {
if hiPtr.Load() != hiRC {
continue
}
return nil, false, func() {}, errors.New("xds: connection closed or HandshakeInfo dead")
}
hi := hiRC.Value()
if hi == nil || hi.UseFallbackCreds() {
hiRC.Decrement()
return nil, true, func() {}, nil
}
cfg, err := hi.clientSideTLSConfigInternal(ctx, hostname)
if err != nil {
hiRC.Decrement()
return nil, false, func() {}, err
}
return cfg, false, hiRC.Decrement, nil
}
}
// ServerSideTLSConfig checks if hi is configured and marks it as in-use,
// returning the tls.Config along with a done callback that MUST be invoked when
// the handshake completes. If hi is nil or fallback credentials should be used,
// useFallback returns true.
func ServerSideTLSConfig(ctx context.Context, hiRC *grpcsync.RefCounted[*HandshakeInfo]) (cfg *tls.Config, useFallback bool, done func(), err error) {
if hiRC == nil {
return nil, true, func() {}, nil
}
hi := hiRC.Value()
if hi == nil || hi.UseFallbackCreds() {
return nil, true, func() {}, nil
}
if !hiRC.TryIncrement() {
return nil, false, func() {}, errors.New("xds: connection closed or HandshakeInfo dead")
}
cfg, err = hi.serverSideTLSConfigInternal(ctx)
if err != nil {
hiRC.Decrement()
return nil, false, func() {}, err
}
return cfg, false, hiRC.Decrement, nil
}
// UseFallbackCreds returns true when fallback credentials are to be used based
// on the contents of the HandshakeInfo.
func (hi *HandshakeInfo) UseFallbackCreds() bool {
if hi == nil {
return true
}
return hi.identityProvider == nil && hi.rootProvider == nil
}
// GetSANMatchersForTesting returns the SAN matchers stored in HandshakeInfo.
// To be used only for testing purposes.
func (hi *HandshakeInfo) GetSANMatchersForTesting() []matcher.StringMatcher {
return append([]matcher.StringMatcher{}, hi.sanMatchers...)
}
// clientSideTLSConfigInternal constructs a tls.Config to be used in a
// client-side handshake based on the contents of the HandshakeInfo.
//
// hostname is passed as a parameter here instead of being part of the
// HandshakeInfo because HandshakeInfo contains cluster-level security
// configuration that applies to all endpoints in the cluster, while hostname is
// specific to each endpoint. This allows sharing a single HandshakeInfo
// instance across multiple endpoints in the same cluster.
func (hi *HandshakeInfo) clientSideTLSConfigInternal(ctx context.Context, hostname string) (*tls.Config, error) {
// On the client side, rootProvider is mandatory. IdentityProvider is
// optional based on whether the client is doing TLS or mTLS.
if hi.rootProvider == nil {
return nil, errors.New("xds: CertificateProvider to fetch trusted roots is missing, cannot perform TLS handshake. Please check configuration on the management server")
}
// InsecureSkipVerify needs to be set to true because we need to perform
// custom verification to check the SAN on the received certificate.
// Currently the Go stdlib does complete verification of the cert (which
// includes hostname verification) or none. We are forced to go with the
// latter and perform the normal cert validation ourselves.
cfg := &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{"h2"},
}
km, err := hi.rootProvider.KeyMaterial(ctx)
if err != nil {
return nil, fmt.Errorf("xds: fetching trusted roots from CertificateProvider failed: %v", err)
}
cfg.RootCAs = km.Roots
// If AutoHostSNI is true, and the endpoint hostname is present, we use the
// endpoint hostname as the SNI value and also for SAN validation.
// Otherwise, we use the SNI value from HandshakeInfo (which is configured
// by the control plane) and validating SANs based on that.
sni := hi.sni
if hi.useAutoHostSNI && hostname != "" {
sni = hostname
}
cfg.VerifyPeerCertificate = hi.buildVerifyFunc(km, true, sni)
if hi.identityProvider != nil {
km, err := hi.identityProvider.KeyMaterial(ctx)
if err != nil {
return nil, fmt.Errorf("xds: fetching identity certificates from CertificateProvider failed: %v", err)
}
cfg.Certificates = km.Certs
}
if envconfig.XDSSNIEnabled && sni != "" {
cfg.ServerName = sni
}
return cfg, nil
}
func (hi *HandshakeInfo) buildVerifyFunc(km *certprovider.KeyMaterial, isClient bool, sni string) func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
return func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("xds: no peer certificates presented")
}
// Parse all raw certificates presented by the peer.
var certs []*x509.Certificate
for _, rc := range rawCerts {
cert, err := x509.ParseCertificate(rc)
if err != nil {
return err
}
certs = append(certs, cert)
}
// Build the intermediates list and verify that the leaf certificate is
// signed by one of the root certificates. If a SPIFFE Bundle Map is
// configured, it is used to get the root certs. Otherwise, the
// configured roots in the root provider are used.
intermediates := x509.NewCertPool()
for _, cert := range certs[1:] {
intermediates.AddCert(cert)
}
roots := km.Roots
// If a SPIFFE Bundle Map is configured, find the roots for the trust
// domain of the leaf certificate.
if km.SPIFFEBundleMap != nil {
var err error
roots, err = spiffe.GetRootsFromSPIFFEBundleMap(km.SPIFFEBundleMap, certs[0])
if err != nil {
return err
}
}
opts := x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
if isClient {
opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
} else {
opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
}
if _, err := certs[0].Verify(opts); err != nil {
return err
}
// If XDSSNIEnabled and AutoSNISANValidation are both true and the SNI is
// non-empty, validate only DNS SANs against the SNI. Otherwise, fallback to
// validating all received SANs against the control plane provided SAN
// matchers.
if envconfig.XDSSNIEnabled && hi.validateSANUsingSNI && sni != "" {
// Verify SAN of leaf certificate with SNI using exact DNS matcher.
for _, san := range certs[0].DNSNames {
if dnsMatch(sni, san) {
return nil
}
}
return fmt.Errorf("xds: received DNS SANs: %v do not match the SNI: %s", certs[0].DNSNames, sni)
}
// The SANs sent by the xDS control plane are encoded as SPIFFE IDs. We need to
// only look at the SANs on the leaf cert.
if cert := certs[0]; !hi.MatchingSANExists(cert) {
// TODO: Print the complete certificate once the x509 package
// supports a String() method on the Certificate type.
return fmt.Errorf("xds: received SANs {DNSNames: %v, EmailAddresses: %v, IPAddresses: %v, URIs: %v} do not match any of the accepted SANs", cert.DNSNames, cert.EmailAddresses, cert.IPAddresses, cert.URIs)
}
return nil
}
}
// serverSideTLSConfigInternal constructs a tls.Config to be used in a
// server-side handshake based on the contents of the HandshakeInfo.
func (hi *HandshakeInfo) serverSideTLSConfigInternal(ctx context.Context) (*tls.Config, error) {
cfg := &tls.Config{
ClientAuth: tls.NoClientCert,
NextProtos: []string{"h2"},
}
// On the server side, identityProvider is mandatory. RootProvider is
// optional based on whether the server is doing TLS or mTLS.
if hi.identityProvider == nil {
return nil, errors.New("xds: CertificateProvider to fetch identity certificate is missing, cannot perform TLS handshake. Please check configuration on the management server")
}
if hi.requireClientCert {
cfg.ClientAuth = tls.RequireAndVerifyClientCert
}
// identityProvider is mandatory on the server side.
km, err := hi.identityProvider.KeyMaterial(ctx)
if err != nil {
return nil, fmt.Errorf("xds: fetching identity certificates from CertificateProvider failed: %v", err)
}
cfg.Certificates = km.Certs
if hi.rootProvider != nil {
km, err := hi.rootProvider.KeyMaterial(ctx)
if err != nil {
return nil, fmt.Errorf("xds: fetching trusted roots from CertificateProvider failed: %v", err)
}
if km.SPIFFEBundleMap != nil && hi.requireClientCert {
// ClientAuth, if set greater than tls.RequireAnyClientCert, must be
// dropped to tls.RequireAnyClientCert so that custom verification
// to use SPIFFE Bundles is done.
cfg.ClientAuth = tls.RequireAnyClientCert
cfg.VerifyPeerCertificate = hi.buildVerifyFunc(km, false, "")
} else {
cfg.ClientCAs = km.Roots
}
}
return cfg, nil
}
// MatchingSANExists returns true if the SANs contained in cert match the
// criteria enforced by the list of SAN matchers in HandshakeInfo.
//
// If the list of SAN matchers in the HandshakeInfo is empty, this function
// returns true for all input certificates.
func (hi *HandshakeInfo) MatchingSANExists(cert *x509.Certificate) bool {
if len(hi.sanMatchers) == 0 {
return true
}
// SANs can be specified in any of these four fields on the parsed cert.
for _, san := range cert.DNSNames {
if hi.matchSAN(san, true) {
return true
}
}
for _, san := range cert.EmailAddresses {
if hi.matchSAN(san, false) {
return true
}
}
for _, san := range cert.IPAddresses {
if hi.matchSAN(san.String(), false) {
return true
}
}
for _, san := range cert.URIs {
if hi.matchSAN(san.String(), false) {
return true
}
}
return false
}
// Caller must hold mu.
func (hi *HandshakeInfo) matchSAN(san string, isDNS bool) bool {
for _, matcher := range hi.sanMatchers {
if em := matcher.ExactMatch(); em != "" && isDNS {
// This is a special case which is documented in the xDS protos.
// If the DNS SAN is a wildcard entry, and the match criteria is
// `exact`, then we need to perform DNS wildcard matching
// instead of regular string comparison.
if dnsMatch(em, san) {
return true
}
continue
}
if matcher.Match(san) {
return true
}
}
return false
}
// dnsMatch implements a DNS wildcard matching algorithm based on RFC2828 and
// grpc-java's implementation in `OkHostnameVerifier` class.
//
// NOTE: Here the `host` argument is the one from the set of string matchers in
// the xDS proto and the `san` argument is a DNS SAN from the certificate, and
// this is the one which can potentially contain a wildcard pattern.
func dnsMatch(host, san string) bool {
// Add trailing "." and turn them into absolute domain names.
if !strings.HasSuffix(host, ".") {
host += "."
}
if !strings.HasSuffix(san, ".") {
san += "."
}
// Domain names are case-insensitive.
host = strings.ToLower(host)
san = strings.ToLower(san)
// If san does not contain a wildcard, do exact match.
if !strings.Contains(san, "*") {
return host == san
}
// Wildcard dns matching rules
// - '*' is only permitted in the left-most label and must be the only
// character in that label. For example, *.example.com is permitted, while
// *a.example.com, a*.example.com, a*b.example.com, a.*.example.com are
// not permitted.
// - '*' matches a single domain name component. For example, *.example.com
// matches test.example.com but does not match sub.test.example.com.
// - Wildcard patterns for single-label domain names are not permitted.
if san == "*." || !strings.HasPrefix(san, "*.") || strings.Contains(san[1:], "*") {
return false
}
// Optimization: at this point, we know that the san contains a '*' and
// is the first domain component of san. So, the host name must be at
// least as long as the san to be able to match.
if len(host) < len(san) {
return false
}
// Hostname must end with the non-wildcard portion of san.
if !strings.HasSuffix(host, san[1:]) {
return false
}
// At this point we know that the hostName and san share the same suffix
// (the non-wildcard portion of san). Now, we just need to make sure
// that the '*' does not match across domain components.
hostPrefix := strings.TrimSuffix(host, san[1:])
return !strings.Contains(hostPrefix, ".")
}

View File

@@ -0,0 +1,668 @@
/*
*
* Copyright 2021 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 xds
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/netip"
"net/url"
"os"
"regexp"
"strings"
"sync/atomic"
"testing"
"time"
"google.golang.org/grpc/testdata"
"google.golang.org/grpc/credentials/tls/certprovider"
"google.golang.org/grpc/internal/credentials/spiffe"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpcsync"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/xds/matcher"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
func (s) TestDNSMatch(t *testing.T) {
tests := []struct {
desc string
host string
pattern string
wantMatch bool
}{
{
desc: "invalid wildcard 1",
host: "aa.example.com",
pattern: "*a.example.com",
wantMatch: false,
},
{
desc: "invalid wildcard 2",
host: "aa.example.com",
pattern: "a*.example.com",
wantMatch: false,
},
{
desc: "invalid wildcard 3",
host: "abc.example.com",
pattern: "a*c.example.com",
wantMatch: false,
},
{
desc: "wildcard in one of the middle components",
host: "abc.test.example.com",
pattern: "abc.*.example.com",
wantMatch: false,
},
{
desc: "single component wildcard",
host: "a.example.com",
pattern: "*",
wantMatch: false,
},
{
desc: "short host name",
host: "a.com",
pattern: "*.example.com",
wantMatch: false,
},
{
desc: "suffix mismatch",
host: "a.notexample.com",
pattern: "*.example.com",
wantMatch: false,
},
{
desc: "wildcard match across components",
host: "sub.test.example.com",
pattern: "*.example.com.",
wantMatch: false,
},
{
desc: "host doesn't end in period",
host: "test.example.com",
pattern: "test.example.com.",
wantMatch: true,
},
{
desc: "pattern doesn't end in period",
host: "test.example.com.",
pattern: "test.example.com",
wantMatch: true,
},
{
desc: "case insensitive",
host: "TEST.EXAMPLE.COM.",
pattern: "test.example.com.",
wantMatch: true,
},
{
desc: "simple match",
host: "test.example.com",
pattern: "test.example.com",
wantMatch: true,
},
{
desc: "good wildcard",
host: "a.example.com",
pattern: "*.example.com",
wantMatch: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
gotMatch := dnsMatch(test.host, test.pattern)
if gotMatch != test.wantMatch {
t.Fatalf("dnsMatch(%s, %s) = %v, want %v", test.host, test.pattern, gotMatch, test.wantMatch)
}
})
}
}
func (s) TestMatchingSANExists_FailureCases(t *testing.T) {
url1, err := url.Parse("http://golang.org")
if err != nil {
t.Fatalf("url.Parse() failed: %v", err)
}
url2, err := url.Parse("https://github.com/grpc/grpc-go")
if err != nil {
t.Fatalf("url.Parse() failed: %v", err)
}
inputCert := &x509.Certificate{
DNSNames: []string{"foo.bar.example.com", "bar.baz.test.com", "*.example.com"},
EmailAddresses: []string{"foobar@example.com", "barbaz@test.com"},
IPAddresses: []net.IP{
netip.MustParseAddr("192.0.0.1").AsSlice(),
netip.MustParseAddr("2001:db8::68").AsSlice(),
},
URIs: []*url.URL{url1, url2},
}
tests := []struct {
desc string
sanMatchers []matcher.StringMatcher
}{
{
desc: "exact match",
sanMatchers: []matcher.StringMatcher{
matcher.NewExactStringMatcher("abcd.test.com", false),
matcher.NewExactStringMatcher("http://golang", false),
matcher.NewExactStringMatcher("HTTP://GOLANG.ORG", false),
},
},
{
desc: "prefix match",
sanMatchers: []matcher.StringMatcher{
matcher.NewPrefixStringMatcher("i-aint-the-one", false),
matcher.NewPrefixStringMatcher("192.168.1.1", false),
matcher.NewPrefixStringMatcher("FOO.BAR", false),
},
},
{
desc: "suffix match",
sanMatchers: []matcher.StringMatcher{
matcher.NewSuffixStringMatcher("i-aint-the-one", false),
matcher.NewSuffixStringMatcher("1::68", false),
matcher.NewSuffixStringMatcher(".COM", false),
},
},
{
desc: "regex match",
sanMatchers: []matcher.StringMatcher{
matcher.NewRegexStringMatcher(regexp.MustCompile(`.*\.examples\.com`)),
matcher.NewRegexStringMatcher(regexp.MustCompile(`192\.[0-9]{1,3}\.1\.1`)),
},
},
{
desc: "contains match",
sanMatchers: []matcher.StringMatcher{
matcher.NewContainsStringMatcher("i-aint-the-one", false),
matcher.NewContainsStringMatcher("2001:db8:1:1::68", false),
matcher.NewContainsStringMatcher("GRPC", false),
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
hi := NewHandshakeInfo(nil, nil, test.sanMatchers, "", false, false, false)
defer hi.Decrement()
if hi.Value().MatchingSANExists(inputCert) {
t.Fatalf("hi.MatchingSANExists(%+v) with SAN matchers +%v succeeded when expected to fail", inputCert, test.sanMatchers)
}
})
}
}
func (s) TestMatchingSANExists_Success(t *testing.T) {
url1, err := url.Parse("http://golang.org")
if err != nil {
t.Fatalf("url.Parse() failed: %v", err)
}
url2, err := url.Parse("https://github.com/grpc/grpc-go")
if err != nil {
t.Fatalf("url.Parse() failed: %v", err)
}
inputCert := &x509.Certificate{
DNSNames: []string{"baz.test.com", "*.example.com"},
EmailAddresses: []string{"foobar@example.com", "barbaz@test.com"},
IPAddresses: []net.IP{
netip.MustParseAddr("192.0.0.1").AsSlice(),
netip.MustParseAddr("2001:db8::68").AsSlice(),
},
URIs: []*url.URL{url1, url2},
}
tests := []struct {
desc string
sanMatchers []matcher.StringMatcher
}{
{
desc: "no san matchers",
},
{
desc: "exact match dns wildcard",
sanMatchers: []matcher.StringMatcher{
matcher.NewPrefixStringMatcher("192.168.1.1", false),
matcher.NewExactStringMatcher("https://github.com/grpc/grpc-java", false),
matcher.NewExactStringMatcher("abc.example.com", false),
},
},
{
desc: "exact match ignore case",
sanMatchers: []matcher.StringMatcher{
matcher.NewExactStringMatcher("FOOBAR@EXAMPLE.COM", true),
},
},
{
desc: "prefix match",
sanMatchers: []matcher.StringMatcher{
matcher.NewSuffixStringMatcher(".co.in", false),
matcher.NewPrefixStringMatcher("192.168.1.1", false),
matcher.NewPrefixStringMatcher("baz.test", false),
},
},
{
desc: "prefix match ignore case",
sanMatchers: []matcher.StringMatcher{
matcher.NewPrefixStringMatcher("BAZ.test", true),
},
},
{
desc: "suffix match",
sanMatchers: []matcher.StringMatcher{
matcher.NewRegexStringMatcher(regexp.MustCompile(`192\.[0-9]{1,3}\.1\.1`)),
matcher.NewSuffixStringMatcher("192.168.1.1", false),
matcher.NewSuffixStringMatcher("@test.com", false),
},
},
{
desc: "suffix match ignore case",
sanMatchers: []matcher.StringMatcher{
matcher.NewSuffixStringMatcher("@test.COM", true),
},
},
{
desc: "regex match",
sanMatchers: []matcher.StringMatcher{
matcher.NewContainsStringMatcher("https://github.com/grpc/grpc-java", false),
matcher.NewRegexStringMatcher(regexp.MustCompile(`192\.[0-9]{1,3}\.1\.1`)),
matcher.NewRegexStringMatcher(regexp.MustCompile(`.*\.test\.com`)),
},
},
{
desc: "contains match",
sanMatchers: []matcher.StringMatcher{
matcher.NewExactStringMatcher("https://github.com/grpc/grpc-java", false),
matcher.NewContainsStringMatcher("2001:68::db8", false),
matcher.NewContainsStringMatcher("192.0.0", false),
},
},
{
desc: "contains match ignore case",
sanMatchers: []matcher.StringMatcher{
matcher.NewContainsStringMatcher("GRPC", true),
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
hi := NewHandshakeInfo(nil, nil, test.sanMatchers, "", false, false, false)
defer hi.Decrement()
if !hi.Value().MatchingSANExists(inputCert) {
t.Fatalf("hi.MatchingSANExists(%+v) with SAN matchers +%v failed when expected to succeed", inputCert, test.sanMatchers)
}
})
}
}
// Equal reports whether the handshake info structs are identical.
func (hi *HandshakeInfo) Equal(other *HandshakeInfo) bool {
if hi == nil && other == nil {
return true
}
if hi == nil || other == nil {
return false
}
r1, i1 := hi.rootProvider, hi.identityProvider
r2, i2 := other.rootProvider, other.identityProvider
switch {
case r1 != r2 || i1 != i2:
return false
case hi.requireClientCert != other.requireClientCert:
return false
case hi.sni != other.sni:
return false
case hi.validateSANUsingSNI != other.validateSANUsingSNI:
return false
case hi.useAutoHostSNI != other.useAutoHostSNI:
return false
case len(hi.sanMatchers) != len(other.sanMatchers):
return false
}
for i := range hi.sanMatchers {
if !hi.sanMatchers[i].Equal(other.sanMatchers[i]) {
return false
}
}
return true
}
type testCertProvider struct {
certprovider.Provider
}
func (s) TestEqual(t *testing.T) {
tests := []struct {
desc string
hi1 *grpcsync.RefCounted[*HandshakeInfo]
hi2 *grpcsync.RefCounted[*HandshakeInfo]
wantMatch bool
}{
{
desc: "both HandshakeInfo are nil",
hi1: func() *grpcsync.RefCounted[*HandshakeInfo] {
return grpcsync.NewRefCounted((*HandshakeInfo)(nil), func() {})
}(),
hi2: func() *grpcsync.RefCounted[*HandshakeInfo] {
return grpcsync.NewRefCounted((*HandshakeInfo)(nil), func() {})
}(),
wantMatch: true,
},
{
desc: "one HandshakeInfo is nil",
hi1: func() *grpcsync.RefCounted[*HandshakeInfo] {
return grpcsync.NewRefCounted((*HandshakeInfo)(nil), func() {})
}(),
hi2: NewHandshakeInfo(&testCertProvider{}, nil, nil, "", false, false, false),
wantMatch: false,
},
{
desc: "different root providers",
hi1: NewHandshakeInfo(&testCertProvider{}, nil, nil, "", false, false, false),
hi2: NewHandshakeInfo(&testCertProvider{}, nil, nil, "", false, false, false),
wantMatch: false,
},
{
desc: "same providers, same SAN matchers",
hi1: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{
matcher.NewExactStringMatcher("foo.com", false),
}, "", false, false, false),
hi2: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{
matcher.NewExactStringMatcher("foo.com", false),
}, "", false, false, false),
wantMatch: true,
},
{
desc: "same providers, different SAN matchers",
hi1: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{
matcher.NewExactStringMatcher("foo.com", false),
}, "", false, false, false),
hi2: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{
matcher.NewExactStringMatcher("bar.com", false),
}, "", false, false, false),
wantMatch: false,
},
{
desc: "same SAN matchers with different content",
hi1: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{
matcher.NewExactStringMatcher("foo.com", false),
}, "", false, false, false),
hi2: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, []matcher.StringMatcher{
matcher.NewExactStringMatcher("foo.com", false),
matcher.NewExactStringMatcher("bar.com", false),
}, "", false, false, false),
wantMatch: false,
},
{
desc: "different requireClientCert flags",
hi1: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, nil, "", true, false, false),
hi2: NewHandshakeInfo(testCertProvider{}, testCertProvider{}, nil, "", false, false, false),
wantMatch: false,
},
{
desc: "same identity provider, different root provider",
hi1: NewHandshakeInfo(&testCertProvider{}, testCertProvider{}, nil, "", false, false, false),
hi2: NewHandshakeInfo(&testCertProvider{}, testCertProvider{}, nil, "", false, false, false),
wantMatch: false,
},
{
desc: "different identity provider, same root provider",
hi1: NewHandshakeInfo(testCertProvider{}, &testCertProvider{}, nil, "", false, false, false),
hi2: NewHandshakeInfo(testCertProvider{}, &testCertProvider{}, nil, "", false, false, false),
wantMatch: false,
},
{
desc: "same sni and validateSANUsingSNI",
hi1: NewHandshakeInfo(nil, nil, nil, "sni", false, true, false),
hi2: NewHandshakeInfo(nil, nil, nil, "sni", false, true, false),
wantMatch: true,
},
{
desc: "different sni",
hi1: NewHandshakeInfo(nil, nil, nil, "sni1", false, false, false),
hi2: NewHandshakeInfo(nil, nil, nil, "sni2", false, false, false),
wantMatch: false,
},
{
desc: "same sni, different validateSANUsingSNI",
hi1: NewHandshakeInfo(nil, nil, nil, "sni", false, false, false),
hi2: NewHandshakeInfo(nil, nil, nil, "sni", false, true, false),
wantMatch: false,
},
{
desc: "different autoHostSNI",
hi1: NewHandshakeInfo(nil, nil, nil, "", false, false, true),
hi2: NewHandshakeInfo(nil, nil, nil, "", false, false, false),
wantMatch: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
if gotMatch := test.hi1.Value().Equal(test.hi2.Value()); gotMatch != test.wantMatch {
t.Errorf("hi1.Equal(hi2) = %v; wantMatch %v", gotMatch, test.wantMatch)
}
})
}
}
type testCertProviderWithKeyMaterial struct{}
func (p *testCertProviderWithKeyMaterial) KeyMaterial(_ context.Context) (*certprovider.KeyMaterial, error) {
km := &certprovider.KeyMaterial{}
spiffeBundleMapContents, err := os.ReadFile(testdata.Path("spiffe_end2end/client_spiffebundle.json"))
if err != nil {
return nil, err
}
bundleMap, err := spiffe.BundleMapFromBytes(spiffeBundleMapContents)
if err != nil {
return nil, err
}
km.SPIFFEBundleMap = bundleMap
rootFileContents, err := os.ReadFile(testdata.Path("spiffe_end2end/ca.pem"))
if err != nil {
return nil, err
}
trustPool := x509.NewCertPool()
if !trustPool.AppendCertsFromPEM(rootFileContents) {
return nil, fmt.Errorf("Failed to parse root certificate")
}
km.Roots = trustPool
certFileContents, err := os.ReadFile(testdata.Path("spiffe_end2end/client_spiffe.pem"))
if err != nil {
return nil, err
}
keyFileContents, err := os.ReadFile(testdata.Path("spiffe_end2end/client.key"))
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(certFileContents, keyFileContents)
if err != nil {
return nil, err
}
km.Certs = []tls.Certificate{cert}
return km, nil
}
func (p *testCertProviderWithKeyMaterial) Close() {}
func (s) TestBuildVerifyFuncFailures(t *testing.T) {
tests := []struct {
desc string
peerCertChain [][]byte
wantErr string
}{
{
desc: "invalid x509",
peerCertChain: [][]byte{[]byte("NOT_A_CERT")},
wantErr: "x509: malformed certificate",
},
{
desc: "invalid SPIFFE ID in peer cert",
// server1.pem doesn't have a valid SPIFFE ID, so attempted to get a
// root from the SPIFFE Bundle Map will fail
peerCertChain: loadCert(t, testdata.Path("server1.pem"), testdata.Path("server1.key")),
wantErr: "spiffe: could not get spiffe ID from peer leaf cert but verification with spiffe trust map was configure",
},
}
testProvider := testCertProviderWithKeyMaterial{}
hi := NewHandshakeInfo(&testProvider, &testProvider, nil, "", true, false, false)
defer hi.Decrement()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
hiPtr := new(atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]])
hiPtr.Store(hi)
cfg, _, done, err := ClientSideTLSConfig(ctx, hiPtr, "")
if err != nil {
t.Fatalf("GetClientSideTLSConfig() failed with err %v", err)
}
defer done()
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
err = cfg.VerifyPeerCertificate(tc.peerCertChain, nil)
if !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("VerifyPeerCertificate got err %v, want: %v", err, tc.wantErr)
}
})
}
}
func loadCert(t *testing.T, certPath, keyPath string) [][]byte {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
t.Fatalf("LoadX509KeyPair(%s, %s) failed: %v", certPath, keyPath, err)
}
return cert.Certificate
}
type testProviderWithRoots struct {
roots *x509.CertPool
}
func (m *testProviderWithRoots) KeyMaterial(context.Context) (*certprovider.KeyMaterial, error) {
return &certprovider.KeyMaterial{
Roots: m.roots,
}, nil
}
func (m *testProviderWithRoots) Close() {}
// Tests the scenario where the SNI provided by the xDS server is only present
// in the URI SAN of the certificate presented by the server, and not in the DNS
// SAN. It verifies that if the SNI-based SAN validation is turned on, the TLS
// handshake fails due to SNI mismatch.
func (s) TestAutoHostSNI_DNS_SANValidation_Failures(t *testing.T) {
testutils.SetEnvConfig(t, &envconfig.XDSSNIEnabled, true)
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("rsa.GenerateKey() failed: %v", err)
}
testURL, err := url.Parse("http://wrong.sni.domain")
if err != nil {
t.Fatalf("url.Parse() failed: %v", err)
}
template := &x509.Certificate{
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
DNSNames: []string{"unrelated.example.com"},
URIs: []*url.URL{testURL}, // URI matching SNI
}
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
if err != nil {
t.Fatalf("x509.CreateCertificate() failed: %v", err)
}
cert, err := x509.ParseCertificate(derBytes)
if err != nil {
t.Fatalf("x509.ParseCertificate() failed: %v", err)
}
roots := x509.NewCertPool()
roots.AddCert(cert)
provider := &testProviderWithRoots{roots: roots}
hi := NewHandshakeInfo(provider, nil, nil, "wrong.sni.domain", true, true, false)
defer hi.Decrement()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
hiPtr := new(atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]])
hiPtr.Store(hi)
cfg, _, done, err := ClientSideTLSConfig(ctx, hiPtr, "")
if err != nil {
t.Fatalf("GetClientSideTLSConfig() failed: %v", err)
}
defer done()
err = cfg.VerifyPeerCertificate([][]byte{derBytes}, nil)
if err == nil {
t.Fatalf("VerifyPeerCertificate() unexpectedly succeeded when SNI was only in URI SAN")
}
if !strings.Contains(err.Error(), "received DNS SANs: [unrelated.example.com] do not match the SNI: wrong.sni.domain") {
t.Fatalf("VerifyPeerCertificate() failed with %v, want error containing 'do not match the SNI'", err)
}
}
func (s) TestVerifyPeerCertificateZeroCerts(t *testing.T) {
provider := &testProviderWithRoots{roots: x509.NewCertPool()}
hi := NewHandshakeInfo(provider, nil, nil, "", true, false, false)
defer hi.Decrement()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
hiPtr := new(atomic.Pointer[grpcsync.RefCounted[*HandshakeInfo]])
hiPtr.Store(hi)
cfg, _, done, err := ClientSideTLSConfig(ctx, hiPtr, "")
if err != nil {
t.Fatalf("GetClientSideTLSConfig() failed: %v", err)
}
defer done()
const wantErr = "no peer certificates presented"
if err := cfg.VerifyPeerCertificate(nil, nil); err == nil || !strings.Contains(err.Error(), wantErr) {
t.Errorf("VerifyPeerCertificate(nil, nil) = %v, want error containing %q", err, wantErr)
}
}