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
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:
75
experimental/credentials/internal/spiffe.go
Normal file
75
experimental/credentials/internal/spiffe.go
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
*
|
||||
* 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 internal defines APIs for parsing SPIFFE ID.
|
||||
//
|
||||
// All APIs in this package are experimental.
|
||||
package internal
|
||||
|
||||
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
|
||||
}
|
||||
233
experimental/credentials/internal/spiffe_test.go
Normal file
233
experimental/credentials/internal/spiffe_test.go
Normal file
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
*
|
||||
* 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 internal
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
58
experimental/credentials/internal/syscallconn.go
Normal file
58
experimental/credentials/internal/syscallconn.go
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
*
|
||||
* 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 internal
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
56
experimental/credentials/internal/syscallconn_test.go
Normal file
56
experimental/credentials/internal/syscallconn_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
*
|
||||
* 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 internal
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user