Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

credentials: check and expose SPIFFE ID #3626

Merged
merged 6 commits into from Jul 16, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
123 changes: 123 additions & 0 deletions credentials/credentials_test.go
Expand Up @@ -21,7 +21,9 @@ package credentials
import (
"context"
"crypto/tls"
"crypto/x509"
"net"
"net/url"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -362,3 +364,124 @@ func (s) TestAppendH2ToNextProtos(t *testing.T) {
})
}
}

func (s) TestParseSpiffeID(t *testing.T) {
tests := []struct {
name string
urls []*url.URL
// If we expect ParseSpiffeID to return an error.
expectError bool
// If we expect TLSInfo.SpiffeID to be plumbed.
expectID bool
}{
{
name: "empty URIs",
urls: []*url.URL{},
expectError: false,
expectID: false,
},
{
name: "good 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",
},
},
expectError: false,
expectID: true,
},
{
name: "invalid host",
urls: []*url.URL{
{
Scheme: "spiffe",
Host: "",
Path: "workload/wl1",
RawPath: "workload/wl1",
},
},
expectError: true,
expectID: false,
},
{
name: "invalid path",
urls: []*url.URL{
{
Scheme: "spiffe",
Host: "foo.bar.com",
Path: "",
RawPath: "",
},
},
expectError: true,
expectID: false,
},
{
name: "large path",
urls: []*url.URL{
{
Scheme: "spiffe",
Host: "foo.bar.com",
Path: string(make([]byte, 2050)),
RawPath: string(make([]byte, 2050)),
},
},
expectError: true,
expectID: false,
},
{
name: "large host",
urls: []*url.URL{
{
Scheme: "spiffe",
Host: string(make([]byte, 256)),
Path: "workload/wl1",
RawPath: "workload/wl1",
},
},
expectError: true,
expectID: false,
},
{
name: "multiple SPIFFE IDs",
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",
},
},
expectError: false,
expectID: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
info := TLSInfo{
State: tls.ConnectionState{PeerCertificates: []*x509.Certificate{{URIs: tt.urls}}}}
err := info.ParseSpiffeID()
if got, want := err != nil, tt.expectError; got != want {
t.Errorf("want expectError = %v, but got expectError = %v, with error %v", want, got, err)
}
if got, want := info.SpiffeID != nil, tt.expectID; got != want {
t.Errorf("want expectID = %v, but spiffe ID is %v", want, info.SpiffeID)
}
})
}
}
62 changes: 59 additions & 3 deletions credentials/tls.go
Expand Up @@ -25,14 +25,16 @@ import (
"fmt"
"io/ioutil"
"net"
"net/url"

"google.golang.org/grpc/credentials/internal"
)

// TLSInfo contains the auth information for a TLS authenticated connection.
// It implements the AuthInfo interface.
type TLSInfo struct {
State tls.ConnectionState
State tls.ConnectionState
SpiffeID *url.URL
ZhenLian marked this conversation as resolved.
Show resolved Hide resolved
ZhenLian marked this conversation as resolved.
Show resolved Hide resolved
CommonAuthInfo
}

Expand All @@ -53,6 +55,42 @@ func (t TLSInfo) GetSecurityValue() ChannelzSecurityValue {
return v
}

// ParseSpiffeID parses the Spiffe ID from State and fill it into SpiffeID.
// An error is returned only when we are sure Spiffe ID is used but the format
// is wrong.
func (t *TLSInfo) ParseSpiffeID() error {
if len(t.State.PeerCertificates) == 0 || len(t.State.PeerCertificates[0].URIs) == 0 {
return nil
}
spiffeIDCnt := 0
var spiffeID url.URL
for _, uri := range t.State.PeerCertificates[0].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.Host)+len(uri.Scheme)+len(uri.RawPath)+4 > 2048 ||
len(uri.Host)+len(uri.Scheme)+len(uri.Path)+4 > 2048 {
return fmt.Errorf("invalid SPIFFE ID: total ID length larger than 2048 bytes")
}
if len(uri.Host) == 0 || len(uri.RawPath) == 0 || len(uri.Path) == 0 {
return fmt.Errorf("invalid SPIFFE ID: domain or workload ID is empty")
}
if len(uri.Host) > 255 {
return fmt.Errorf("invalid SPIFFE ID: domain length larger than 255 characters")
}
// We use a default deep copy since we know the User field of a SPIFFE ID is empty.
spiffeID = *uri
spiffeIDCnt++
}
// A standard SPIFFE ID should be unique. If there are more, we don't raise
// any errors but simply not plumbing any of them.
if spiffeIDCnt == 1 {
ZhenLian marked this conversation as resolved.
Show resolved Hide resolved
t.SpiffeID = &spiffeID
}
return nil
}

// tlsCreds is the credentials required for authenticating a connection using TLS.
type tlsCreds struct {
// TLS configuration
Expand Down Expand Up @@ -94,7 +132,16 @@ func (c *tlsCreds) ClientHandshake(ctx context.Context, authority string, rawCon
conn.Close()
return nil, nil, ctx.Err()
}
return internal.WrapSyscallConn(rawConn, conn), TLSInfo{conn.ConnectionState(), CommonAuthInfo{PrivacyAndIntegrity}}, nil
tlsInfo := TLSInfo{
State: conn.ConnectionState(),
CommonAuthInfo: CommonAuthInfo{
SecurityLevel: PrivacyAndIntegrity,
},
}
if err := tlsInfo.ParseSpiffeID(); err != nil {
return nil, nil, err
}
return internal.WrapSyscallConn(rawConn, conn), tlsInfo, nil
}

func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, AuthInfo, error) {
Expand All @@ -103,7 +150,16 @@ func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, AuthInfo, error)
conn.Close()
return nil, nil, err
}
return internal.WrapSyscallConn(rawConn, conn), TLSInfo{conn.ConnectionState(), CommonAuthInfo{PrivacyAndIntegrity}}, nil
tlsInfo := TLSInfo{
State: conn.ConnectionState(),
CommonAuthInfo: CommonAuthInfo{
SecurityLevel: PrivacyAndIntegrity,
},
}
if err := tlsInfo.ParseSpiffeID(); err != nil {
return nil, nil, err
}
return internal.WrapSyscallConn(rawConn, conn), tlsInfo, nil
}

func (c *tlsCreds) Clone() TransportCredentials {
Expand Down