Skip to content

Commit

Permalink
internal/credentials: fix a bug and add one more helper function SPIF…
Browse files Browse the repository at this point in the history
…FEIDFromCert (#3929)

* internal/credentials: fix a bug and add one more helper function
  • Loading branch information
ZhenLian committed Oct 9, 2020
1 parent 9abcdab commit 84e85f7
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 17 deletions.
16 changes: 13 additions & 3 deletions internal/credentials/spiffe.go
Expand Up @@ -25,6 +25,7 @@ package credentials

import (
"crypto/tls"
"crypto/x509"
"net/url"

"google.golang.org/grpc/grpclog"
Expand All @@ -38,8 +39,17 @@ 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 state.PeerCertificates[0].URIs {
for _, uri := range cert.URIs {
if uri == nil || uri.Scheme != "spiffe" || uri.Opaque != "" || (uri.User != nil && uri.User.Username() != "") {
continue
}
Expand All @@ -48,7 +58,7 @@ func SPIFFEIDFromState(state tls.ConnectionState) *url.URL {
logger.Warning("invalid SPIFFE ID: total ID length larger than 2048 bytes")
return nil
}
if len(uri.Host) == 0 || len(uri.RawPath) == 0 || len(uri.Path) == 0 {
if len(uri.Host) == 0 || len(uri.Path) == 0 {
logger.Warning("invalid SPIFFE ID: domain or workload ID is empty")
return nil
}
Expand All @@ -57,7 +67,7 @@ func SPIFFEIDFromState(state tls.ConnectionState) *url.URL {
return nil
}
// A valid SPIFFE certificate can only have exactly one URI SAN field.
if len(state.PeerCertificates[0].URIs) > 1 {
if len(cert.URIs) > 1 {
logger.Warning("invalid SPIFFE ID: multiple URI SANs")
return nil
}
Expand Down
81 changes: 67 additions & 14 deletions internal/credentials/spiffe_test.go
Expand Up @@ -21,12 +21,17 @@ package credentials
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"net/url"
"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
}
Expand All @@ -40,12 +45,12 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
name string
urls []*url.URL
// If we expect a SPIFFE ID to be returned.
expectID bool
wantID bool
}{
{
name: "empty URIs",
urls: []*url.URL{},
expectID: false,
name: "empty URIs",
urls: []*url.URL{},
wantID: false,
},
{
name: "good SPIFFE ID",
Expand All @@ -57,7 +62,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
RawPath: "workload/wl1",
},
},
expectID: true,
wantID: true,
},
{
name: "invalid host",
Expand All @@ -69,7 +74,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
RawPath: "workload/wl1",
},
},
expectID: false,
wantID: false,
},
{
name: "invalid path",
Expand All @@ -81,7 +86,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
RawPath: "",
},
},
expectID: false,
wantID: false,
},
{
name: "large path",
Expand All @@ -93,7 +98,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
RawPath: string(make([]byte, 2050)),
},
},
expectID: false,
wantID: false,
},
{
name: "large host",
Expand All @@ -105,7 +110,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
RawPath: "workload/wl1",
},
},
expectID: false,
wantID: false,
},
{
name: "multiple URI SANs",
Expand All @@ -129,7 +134,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
RawPath: "workload/wl1",
},
},
expectID: false,
wantID: false,
},
{
name: "multiple URI SANs without SPIFFE ID",
Expand All @@ -147,7 +152,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
RawPath: "workload/wl1",
},
},
expectID: false,
wantID: false,
},
{
name: "multiple URI SANs with one SPIFFE ID",
Expand All @@ -165,15 +170,63 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
RawPath: "workload/wl1",
},
},
expectID: false,
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.expectID; got != want {
t.Errorf("want expectID = %v, but SPIFFE ID is %v", want, id)
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 := ioutil.ReadFile(testdata.Path(tt.dataPath))
if err != nil {
t.Fatalf("ioutil.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)
}
})
}
Expand Down
19 changes: 19 additions & 0 deletions testdata/x509/create.sh
Expand Up @@ -100,5 +100,24 @@ openssl x509 -req \
-extensions test_client
openssl verify -verbose -CAfile client_ca_cert.pem client2_cert.pem

# Generate a cert with SPIFFE ID.
openssl req -x509 \
-newkey rsa:4096 \
-keyout spiffe_key.pem \
-out spiffe_cert.pem \
-nodes \
-days 3650 \
-subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-client1/ \
-addext "subjectAltName = URI:spiffe://foo.bar.com/client/workload/1"

# Generate a cert with SPIFFE ID and another SAN URI field(which doesn't meet SPIFFE specs).
openssl req -x509 \
-newkey rsa:4096 \
-keyout multiple_uri_key.pem \
-out multiple_uri_cert.pem \
-nodes \
-days 3650 \
-subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-client1/ \
-addext "subjectAltName = URI:spiffe://foo.bar.com/client/workload/1, URI:https://bar.baz.com/client"
# Cleanup the CSRs.
rm *_csr.pem
34 changes: 34 additions & 0 deletions testdata/x509/multiple_uri_cert.pem
@@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIFzjCCA7agAwIBAgIUI8r7b2hX9DRwEQGWuRdk32eU5kowDQYJKoZIhvcNAQEL
BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL
BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMDEwMDcwNjQx
MTRaFw0zMDEwMDUwNjQxMTRaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM
MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu
dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCm/zjNYkfCTcq7tnVf
qkPEde1+M6s2z05iWDfoBeZfC2NwUxIBqAC6XTXTxqYSjEVRCQUzjVxyWQNiwuz7
pK/xGZhP/Ih2uSQKTw8vkXay4HCOt9DR0S/XGcQNImdbawKgnGven8Jrg8UZDXrt
9R9Z0nRajB1eXvXOsEEoEfOnYthc6P+MxWJc0lnfaTlowyEgv84Ha13y1h46W6yC
+WNBT/kWqp/mzDTv/Ima8xcqEft9VUZ82qJ1DVt1064x8KOzm2x7F7QSIcjxr39M
fbASm8Vdnt10XfhdsDVkxTlBJs8WKGn0uw8MyPNjFG01OpYDHLAfJTL3XlvaUjfF
yDMFsRDVjfkuYIkqAVWQ7eleFfOFBYzaVf2K+2OvCR+vGAPa5NQ59kwogJYLjV/O
43axChBizcPM0p7gmRhhO7TQz7LLTea30rBJ/YtXdxFR11y9Jdq+i2KwWi8O50iO
hzxUBkbcQD/W9Bcn7gOkD/pgEGynWvFSs+UHjLeyL0COk0NiuYIMlOgwtI5BGwzD
bdLuTU/ZQm4BJBjEIGVHFqKyTqUXcw5t9fWxH8V0XNs8zqj9J7lvNKu9b88GnyaJ
fKMdDO4rVTJHmvFDHP9MUJHC9SabW8+hK0nuU7n3+Pc07ToCAan+Ych5bQHsRMjI
9EvxKVNfwIwNrmRr3mhbOU9xIQIDAQABo4GjMIGgMB0GA1UdDgQWBBS6jnt9IccJ
SOuE1KwP68VCBPB4hTAfBgNVHSMEGDAWgBS6jnt9IccJSOuE1KwP68VCBPB4hTAP
BgNVHRMBAf8EBTADAQH/ME0GA1UdEQRGMESGJnNwaWZmZTovL2Zvby5iYXIuY29t
L2NsaWVudC93b3JrbG9hZC8xhhpodHRwczovL2Jhci5iYXouY29tL2NsaWVudDAN
BgkqhkiG9w0BAQsFAAOCAgEAoR4LbmvtSXLiVg7BRilvSxIWgcG6AI75/afuaM20
PUTpyDhnrPxEaytb5LP0w42BCMoIHXDLE0Jmbxqbi+ku/Qw1R6723J7gwRSUYIg1
a2S5Gue4AFp7aSLDUZhl0jPphq7OMKozzH5TrDgjKljYjPURClc/ODSlGdzOqlif
CbDHwrCorb+BFM3aFDE0pF06pnMDXcn/Ob9QCLIpvZEOWe/fJbPtTUiA5cY3knne
regyhvfqfVZtU52qg+9o6q5QchVqOt19alAsISK9/H4iVE+S79AiYEAU4yM4S6p5
VW44idy3KXmr5kyVwJhe3t9f5Ckuswmo6hL32ec6M52ElrS8Er0vFt4bjfNgq996
lTm4/reL/Anko9chQiGBe7F8J82OfxjLoVH9CbZjIoS4LiZPkey3Ze9HUV1sHhM/
umkL54jRsVjEwwSCIcF9onzmiD8D7FV3AQ9W/RbBF3wZvVBBs9ZKQCxek2pZX/eZ
Q+BvXwG7NGArowpqbi+tSrW3O+XZzY7nXbbf23jCBwkBn3jvqn1Kwsr/T/HbXUaz
dDUvkwgyrX7NfvvZ20svtKLlBZTO5D8P9fy0+cHsS0XkPhw6UbHk396hoOmVZ+OG
E5uVb2sBy+vx+82IwVzWN0o7380AEmAA5nrA6fMaxTxmo07pOF7avAZ34LgHJIjr
sTM=
-----END CERTIFICATE-----
52 changes: 52 additions & 0 deletions testdata/x509/multiple_uri_key.pem
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
zJOG+48NgohVKfBhGN7JyxV1dq/X
-----END PRIVATE KEY-----
33 changes: 33 additions & 0 deletions testdata/x509/spiffe_cert.pem
@@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFsjCCA5qgAwIBAgIUPxiyjwxoDyMeRvl4g9TSdvLlCA0wDQYJKoZIhvcNAQEL
BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL
BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMDEwMDYxNzI2
MzFaFw0zMDEwMDQxNzI2MzFaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM
MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu
dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCV84YR/EV55qfFynHh
QvWEZW5hUI9q0DeD5kG5CarrkOj11rZuQIBZ7X23CJbeoVbrvbYLghsPYJzxS/n3
Qlwwzb5k+L0Qt+HrBD836HcSK5k1oh0jGGMaGownap+XCZH9g52s/8iiwfI02CmN
TbwsNp7wtSEFgNOd2OlzhT6wBLF2Q6uxfBmsDpiChxe2Fs1lyan9RH8fYEf7sxwP
E+SgBfEs7dSG5ZwFfdF+pd1T3IfrVjIxechKO1MO7HTSxbOTj6eHf1NeErDTGPA7
VrnDCupRgcDGyAhFd54r62R8TbTjn5MwzMxElO45Ck/Ej7Qw/GWeaBHj/dMa6mhE
R55PvnKuyj+k9t0Rf0HDZyONtY5/OLqI/xVr27Y1o9v5FysNgjWPkZMRpvuCzkeC
2RuE6k2TfBDRLiCyYu/Zzw+ZtUyTAKtWtefLdQBjrYpnhrDPpmrnTWomX/e9pylE
WfkyxCswiPnDw7ypI7uFSTkz0+bUaROmAtlPvR+3SjaQDWigwz3eJsdIaeg5AY9q
//rWaal6l2iR0Ou9L6A9lLxh5iN/ch+OGk4QPK6pFbOy3IqYfmQ+IpAXG0da9RT2
EN76cNa3bldEjRRON8oQ3HZmhOQJqVxhQciUz84sTjAqH8WvqqbdG9HKUoZ19T5Z
9vNldjlQn33Mi5gBxdugqdnmCQIDAQABo4GHMIGEMB0GA1UdDgQWBBT8rr0kPapk
bGLJ4EU1582sw7WlOTAfBgNVHSMEGDAWgBT8rr0kPapkbGLJ4EU1582sw7WlOTAP
BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t
L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA15Ne+Lz5cN1/B
fkys4QHDWJ0n5Zy9OtwSW6aTyqIIwls6OOSkJn3qJMoT2oFvoHoOxb0swyN+zUoD
pmPEd7FHkMm8BhRqoyH3UZGR7kOSIIcfvldVZbW9mD88A04qvLsWkkanMyGhkYV4
0TXyb8USdjeNm1H32iF4k24czSpvoOYo9HOQv+4aFcqTMnGwS7CvwU6O6vVU8gIy
HYP/oWnkhap6X7acjPxYoW5IDZdN9vdMz9wQlKlc799lWqOCuwl68NSuTNcNNFyn
TXfFWZaghb7iXsUezGYTY9glsPxY0Egmbcmxut0gz0U2BNVvNGKUUu55MlAS7yXO
Y7eTfSSf6DJesFQKwTg8qlyNLjzbLSmhvz6EPV55ToUxPPA9CIOrWQwXv4GdySuH
bwof3U5p/cq2NDtxv8KGisjK04l++s+Ea8AS6T6O8+08nBFGgfNW331eWtU91JoQ
e6Q4DWipiNzkIvISk48V8CT9eRB2KD7NsigQprePRN3gDZREh+01gwbVUX2gbtHx
1RGxEjO6H0kUuaoXF5E6+WGwgn8MA47qUy1WXC5QDFpc5LyaoVaMFv8bcoWSNXAS
Oes+ZDWDXWq6F+9Kt0zWmO651cVquLTjmgt48fgL6m8rU13ikjH7dFnimrwRxfOD
p+z97N7TvWfgE1HOmYDfsbaHjPFZKg==
-----END CERTIFICATE-----

0 comments on commit 84e85f7

Please sign in to comment.