Skip to content

Commit 75e2768

Browse files
ZhenLianGarrettGutierrez1
authored andcommitted
internal/credentials: fix a bug and add one more helper function SPIFFEIDFromCert (#3929)
* internal/credentials: fix a bug and add one more helper function
1 parent 17493ac commit 75e2768

File tree

7 files changed

+270
-17
lines changed

7 files changed

+270
-17
lines changed

internal/credentials/spiffe.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ package credentials
2525

2626
import (
2727
"crypto/tls"
28+
"crypto/x509"
2829
"net/url"
2930

3031
"google.golang.org/grpc/grpclog"
@@ -38,8 +39,17 @@ func SPIFFEIDFromState(state tls.ConnectionState) *url.URL {
3839
if len(state.PeerCertificates) == 0 || len(state.PeerCertificates[0].URIs) == 0 {
3940
return nil
4041
}
42+
return SPIFFEIDFromCert(state.PeerCertificates[0])
43+
}
44+
45+
// SPIFFEIDFromCert parses the SPIFFE ID from x509.Certificate. If the SPIFFE
46+
// ID format is invalid, return nil with warning.
47+
func SPIFFEIDFromCert(cert *x509.Certificate) *url.URL {
48+
if cert == nil || cert.URIs == nil {
49+
return nil
50+
}
4151
var spiffeID *url.URL
42-
for _, uri := range state.PeerCertificates[0].URIs {
52+
for _, uri := range cert.URIs {
4353
if uri == nil || uri.Scheme != "spiffe" || uri.Opaque != "" || (uri.User != nil && uri.User.Username() != "") {
4454
continue
4555
}
@@ -48,7 +58,7 @@ func SPIFFEIDFromState(state tls.ConnectionState) *url.URL {
4858
logger.Warning("invalid SPIFFE ID: total ID length larger than 2048 bytes")
4959
return nil
5060
}
51-
if len(uri.Host) == 0 || len(uri.RawPath) == 0 || len(uri.Path) == 0 {
61+
if len(uri.Host) == 0 || len(uri.Path) == 0 {
5262
logger.Warning("invalid SPIFFE ID: domain or workload ID is empty")
5363
return nil
5464
}
@@ -57,7 +67,7 @@ func SPIFFEIDFromState(state tls.ConnectionState) *url.URL {
5767
return nil
5868
}
5969
// A valid SPIFFE certificate can only have exactly one URI SAN field.
60-
if len(state.PeerCertificates[0].URIs) > 1 {
70+
if len(cert.URIs) > 1 {
6171
logger.Warning("invalid SPIFFE ID: multiple URI SANs")
6272
return nil
6373
}

internal/credentials/spiffe_test.go

+67-14
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@ package credentials
2121
import (
2222
"crypto/tls"
2323
"crypto/x509"
24+
"encoding/pem"
25+
"io/ioutil"
2426
"net/url"
2527
"testing"
2628

2729
"google.golang.org/grpc/internal/grpctest"
30+
"google.golang.org/grpc/testdata"
2831
)
2932

33+
const wantURI = "spiffe://foo.bar.com/client/workload/1"
34+
3035
type s struct {
3136
grpctest.Tester
3237
}
@@ -40,12 +45,12 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
4045
name string
4146
urls []*url.URL
4247
// If we expect a SPIFFE ID to be returned.
43-
expectID bool
48+
wantID bool
4449
}{
4550
{
46-
name: "empty URIs",
47-
urls: []*url.URL{},
48-
expectID: false,
51+
name: "empty URIs",
52+
urls: []*url.URL{},
53+
wantID: false,
4954
},
5055
{
5156
name: "good SPIFFE ID",
@@ -57,7 +62,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
5762
RawPath: "workload/wl1",
5863
},
5964
},
60-
expectID: true,
65+
wantID: true,
6166
},
6267
{
6368
name: "invalid host",
@@ -69,7 +74,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
6974
RawPath: "workload/wl1",
7075
},
7176
},
72-
expectID: false,
77+
wantID: false,
7378
},
7479
{
7580
name: "invalid path",
@@ -81,7 +86,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
8186
RawPath: "",
8287
},
8388
},
84-
expectID: false,
89+
wantID: false,
8590
},
8691
{
8792
name: "large path",
@@ -93,7 +98,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
9398
RawPath: string(make([]byte, 2050)),
9499
},
95100
},
96-
expectID: false,
101+
wantID: false,
97102
},
98103
{
99104
name: "large host",
@@ -105,7 +110,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
105110
RawPath: "workload/wl1",
106111
},
107112
},
108-
expectID: false,
113+
wantID: false,
109114
},
110115
{
111116
name: "multiple URI SANs",
@@ -129,7 +134,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
129134
RawPath: "workload/wl1",
130135
},
131136
},
132-
expectID: false,
137+
wantID: false,
133138
},
134139
{
135140
name: "multiple URI SANs without SPIFFE ID",
@@ -147,7 +152,7 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
147152
RawPath: "workload/wl1",
148153
},
149154
},
150-
expectID: false,
155+
wantID: false,
151156
},
152157
{
153158
name: "multiple URI SANs with one SPIFFE ID",
@@ -165,15 +170,63 @@ func (s) TestSPIFFEIDFromState(t *testing.T) {
165170
RawPath: "workload/wl1",
166171
},
167172
},
168-
expectID: false,
173+
wantID: false,
169174
},
170175
}
171176
for _, tt := range tests {
172177
t.Run(tt.name, func(t *testing.T) {
173178
state := tls.ConnectionState{PeerCertificates: []*x509.Certificate{{URIs: tt.urls}}}
174179
id := SPIFFEIDFromState(state)
175-
if got, want := id != nil, tt.expectID; got != want {
176-
t.Errorf("want expectID = %v, but SPIFFE ID is %v", want, id)
180+
if got, want := id != nil, tt.wantID; got != want {
181+
t.Errorf("want wantID = %v, but SPIFFE ID is %v", want, id)
182+
}
183+
})
184+
}
185+
}
186+
187+
func (s) TestSPIFFEIDFromCert(t *testing.T) {
188+
tests := []struct {
189+
name string
190+
dataPath string
191+
// If we expect a SPIFFE ID to be returned.
192+
wantID bool
193+
}{
194+
{
195+
name: "good certificate with SPIFFE ID",
196+
dataPath: "x509/spiffe_cert.pem",
197+
wantID: true,
198+
},
199+
{
200+
name: "bad certificate with SPIFFE ID and another URI",
201+
dataPath: "x509/multiple_uri_cert.pem",
202+
wantID: false,
203+
},
204+
{
205+
name: "certificate without SPIFFE ID",
206+
dataPath: "x509/client1_cert.pem",
207+
wantID: false,
208+
},
209+
}
210+
for _, tt := range tests {
211+
t.Run(tt.name, func(t *testing.T) {
212+
data, err := ioutil.ReadFile(testdata.Path(tt.dataPath))
213+
if err != nil {
214+
t.Fatalf("ioutil.ReadFile(%s) failed: %v", testdata.Path(tt.dataPath), err)
215+
}
216+
block, _ := pem.Decode(data)
217+
if block == nil {
218+
t.Fatalf("Failed to parse the certificate: byte block is nil")
219+
}
220+
cert, err := x509.ParseCertificate(block.Bytes)
221+
if err != nil {
222+
t.Fatalf("x509.ParseCertificate(%b) failed: %v", block.Bytes, err)
223+
}
224+
uri := SPIFFEIDFromCert(cert)
225+
if (uri != nil) != tt.wantID {
226+
t.Fatalf("wantID got and want mismatch, got %t, want %t", uri != nil, tt.wantID)
227+
}
228+
if uri != nil && uri.String() != wantURI {
229+
t.Fatalf("SPIFFE ID not expected, got %s, want %s", uri.String(), wantURI)
177230
}
178231
})
179232
}

testdata/x509/create.sh

+19
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,24 @@ openssl x509 -req \
100100
-extensions test_client
101101
openssl verify -verbose -CAfile client_ca_cert.pem client2_cert.pem
102102

103+
# Generate a cert with SPIFFE ID.
104+
openssl req -x509 \
105+
-newkey rsa:4096 \
106+
-keyout spiffe_key.pem \
107+
-out spiffe_cert.pem \
108+
-nodes \
109+
-days 3650 \
110+
-subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-client1/ \
111+
-addext "subjectAltName = URI:spiffe://foo.bar.com/client/workload/1"
112+
113+
# Generate a cert with SPIFFE ID and another SAN URI field(which doesn't meet SPIFFE specs).
114+
openssl req -x509 \
115+
-newkey rsa:4096 \
116+
-keyout multiple_uri_key.pem \
117+
-out multiple_uri_cert.pem \
118+
-nodes \
119+
-days 3650 \
120+
-subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-client1/ \
121+
-addext "subjectAltName = URI:spiffe://foo.bar.com/client/workload/1, URI:https://bar.baz.com/client"
103122
# Cleanup the CSRs.
104123
rm *_csr.pem

testdata/x509/multiple_uri_cert.pem

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIFzjCCA7agAwIBAgIUI8r7b2hX9DRwEQGWuRdk32eU5kowDQYJKoZIhvcNAQEL
3+
BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL
4+
BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMDEwMDcwNjQx
5+
MTRaFw0zMDEwMDUwNjQxMTRaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM
6+
MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu
7+
dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCm/zjNYkfCTcq7tnVf
8+
qkPEde1+M6s2z05iWDfoBeZfC2NwUxIBqAC6XTXTxqYSjEVRCQUzjVxyWQNiwuz7
9+
pK/xGZhP/Ih2uSQKTw8vkXay4HCOt9DR0S/XGcQNImdbawKgnGven8Jrg8UZDXrt
10+
9R9Z0nRajB1eXvXOsEEoEfOnYthc6P+MxWJc0lnfaTlowyEgv84Ha13y1h46W6yC
11+
+WNBT/kWqp/mzDTv/Ima8xcqEft9VUZ82qJ1DVt1064x8KOzm2x7F7QSIcjxr39M
12+
fbASm8Vdnt10XfhdsDVkxTlBJs8WKGn0uw8MyPNjFG01OpYDHLAfJTL3XlvaUjfF
13+
yDMFsRDVjfkuYIkqAVWQ7eleFfOFBYzaVf2K+2OvCR+vGAPa5NQ59kwogJYLjV/O
14+
43axChBizcPM0p7gmRhhO7TQz7LLTea30rBJ/YtXdxFR11y9Jdq+i2KwWi8O50iO
15+
hzxUBkbcQD/W9Bcn7gOkD/pgEGynWvFSs+UHjLeyL0COk0NiuYIMlOgwtI5BGwzD
16+
bdLuTU/ZQm4BJBjEIGVHFqKyTqUXcw5t9fWxH8V0XNs8zqj9J7lvNKu9b88GnyaJ
17+
fKMdDO4rVTJHmvFDHP9MUJHC9SabW8+hK0nuU7n3+Pc07ToCAan+Ych5bQHsRMjI
18+
9EvxKVNfwIwNrmRr3mhbOU9xIQIDAQABo4GjMIGgMB0GA1UdDgQWBBS6jnt9IccJ
19+
SOuE1KwP68VCBPB4hTAfBgNVHSMEGDAWgBS6jnt9IccJSOuE1KwP68VCBPB4hTAP
20+
BgNVHRMBAf8EBTADAQH/ME0GA1UdEQRGMESGJnNwaWZmZTovL2Zvby5iYXIuY29t
21+
L2NsaWVudC93b3JrbG9hZC8xhhpodHRwczovL2Jhci5iYXouY29tL2NsaWVudDAN
22+
BgkqhkiG9w0BAQsFAAOCAgEAoR4LbmvtSXLiVg7BRilvSxIWgcG6AI75/afuaM20
23+
PUTpyDhnrPxEaytb5LP0w42BCMoIHXDLE0Jmbxqbi+ku/Qw1R6723J7gwRSUYIg1
24+
a2S5Gue4AFp7aSLDUZhl0jPphq7OMKozzH5TrDgjKljYjPURClc/ODSlGdzOqlif
25+
CbDHwrCorb+BFM3aFDE0pF06pnMDXcn/Ob9QCLIpvZEOWe/fJbPtTUiA5cY3knne
26+
regyhvfqfVZtU52qg+9o6q5QchVqOt19alAsISK9/H4iVE+S79AiYEAU4yM4S6p5
27+
VW44idy3KXmr5kyVwJhe3t9f5Ckuswmo6hL32ec6M52ElrS8Er0vFt4bjfNgq996
28+
lTm4/reL/Anko9chQiGBe7F8J82OfxjLoVH9CbZjIoS4LiZPkey3Ze9HUV1sHhM/
29+
umkL54jRsVjEwwSCIcF9onzmiD8D7FV3AQ9W/RbBF3wZvVBBs9ZKQCxek2pZX/eZ
30+
Q+BvXwG7NGArowpqbi+tSrW3O+XZzY7nXbbf23jCBwkBn3jvqn1Kwsr/T/HbXUaz
31+
dDUvkwgyrX7NfvvZ20svtKLlBZTO5D8P9fy0+cHsS0XkPhw6UbHk396hoOmVZ+OG
32+
E5uVb2sBy+vx+82IwVzWN0o7380AEmAA5nrA6fMaxTxmo07pOF7avAZ34LgHJIjr
33+
sTM=
34+
-----END CERTIFICATE-----

testdata/x509/multiple_uri_key.pem

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-----BEGIN PRIVATE KEY-----
2+
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
51+
zJOG+48NgohVKfBhGN7JyxV1dq/X
52+
-----END PRIVATE KEY-----

testdata/x509/spiffe_cert.pem

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
-----BEGIN CERTIFICATE-----
2+
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
32+
p+z97N7TvWfgE1HOmYDfsbaHjPFZKg==
33+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)