1
// Copyright Istio Authors
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
7
// http://www.apache.org/licenses/LICENSE-2.0
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
29
"google.golang.org/grpc"
30
"google.golang.org/grpc/codes"
31
"google.golang.org/grpc/credentials"
32
"google.golang.org/grpc/metadata"
33
"google.golang.org/grpc/status"
35
pb "istio.io/api/security/v1alpha1"
36
testutil "istio.io/istio/pilot/test/util"
37
"istio.io/istio/pkg/config/constants"
38
"istio.io/istio/pkg/file"
39
"istio.io/istio/pkg/monitoring/monitortest"
40
"istio.io/istio/pkg/security"
41
"istio.io/istio/pkg/spiffe"
42
"istio.io/istio/pkg/test/env"
43
"istio.io/istio/pkg/test/util/retry"
44
"istio.io/istio/security/pkg/credentialfetcher/plugin"
45
"istio.io/istio/security/pkg/monitoring"
49
mockServerAddress = "localhost:0"
53
fakeCert = []string{"foo", "bar"}
54
fakeToken = "Bearer fakeToken"
55
validToken = "Bearer validToken"
56
authorizationMeta = "authorization"
59
type mockCAServer struct {
60
pb.UnimplementedIstioCertificateServiceServer
62
Authenticator *security.FakeAuthenticator
66
func (ca *mockCAServer) CreateCertificate(ctx context.Context, in *pb.IstioCertificateRequest) (*pb.IstioCertificateResponse, error) {
67
if ca.Authenticator != nil {
68
caller, err := security.Authenticate(ctx, []security.Authenticator{ca.Authenticator})
70
return nil, status.Error(codes.Unauthenticated, err.Error())
74
return &pb.IstioCertificateResponse{CertChain: ca.Certs}, nil
79
func tlsOptions(t *testing.T) grpc.ServerOption {
81
cert, err := tls.LoadX509KeyPair(
82
filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/cert-chain.pem"),
83
filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/key.pem"))
87
peerCertVerifier := spiffe.NewPeerCertVerifier()
88
if err := peerCertVerifier.AddMappingFromPEM("cluster.local",
89
testutil.ReadFile(t, filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/root-cert.pem"))); err != nil {
92
return grpc.Creds(credentials.NewTLS(&tls.Config{
93
Certificates: []tls.Certificate{cert},
94
ClientAuth: tls.VerifyClientCertIfGiven,
95
ClientCAs: peerCertVerifier.GetGeneralCertPool(),
96
MinVersion: tls.VersionTLS12,
100
func serve(t *testing.T, ca mockCAServer, opts ...grpc.ServerOption) string {
101
// create a local grpc server
102
s := grpc.NewServer(opts...)
104
lis, err := net.Listen("tcp", mockServerAddress)
106
t.Fatalf("failed to listen: %v", err)
110
pb.RegisterIstioCertificateServiceServer(s, &ca)
111
if err := s.Serve(lis); err != nil {
112
t.Logf("failed to serve: %v", err)
115
_, port, _ := net.SplitHostPort(lis.Addr().String())
116
return fmt.Sprintf("localhost:%s", port)
119
func TestCitadelClientRotation(t *testing.T) {
120
checkSign := func(t *testing.T, cli security.Client, expectError bool) {
122
resp, err := cli.CSRSign([]byte{0o1}, 1)
123
if expectError != (err != nil) {
124
t.Fatalf("expected error:%v, got error:%v", expectError, err)
126
if !expectError && !reflect.DeepEqual(resp, fakeCert) {
127
t.Fatalf("expected cert: %v", resp)
130
certDir := filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot")
131
t.Run("cert always present", func(t *testing.T) {
132
server := mockCAServer{Certs: fakeCert, Err: nil, Authenticator: security.NewFakeAuthenticator("ca")}
133
addr := serve(t, server, tlsOptions(t))
134
opts := &security.Options{
136
CredFetcher: plugin.CreateTokenPlugin("testdata/token"),
139
rootCert := path.Join(certDir, constants.RootCertFilename)
140
key := path.Join(certDir, constants.KeyFilename)
141
cert := path.Join(certDir, constants.CertChainFilename)
142
tlsOpts := &TLSOptions{
147
cli, err := NewCitadelClient(opts, tlsOpts)
149
t.Errorf("failed to create ca client: %v", err)
152
server.Authenticator.Set("fake", "")
153
checkSign(t, cli, false)
154
// Expiring the token is harder, so just switch to only allow certs
155
server.Authenticator.Set("", "istiod.istio-system.svc")
156
checkSign(t, cli, false)
157
checkSign(t, cli, false)
159
t.Run("cert never present", func(t *testing.T) {
160
server := mockCAServer{Certs: fakeCert, Err: nil, Authenticator: security.NewFakeAuthenticator("ca")}
161
addr := serve(t, server, tlsOptions(t))
162
opts := &security.Options{
164
CredFetcher: plugin.CreateTokenPlugin("testdata/token"),
167
rootCert := path.Join(certDir, constants.RootCertFilename)
168
key := path.Join(opts.ProvCert, constants.KeyFilename)
169
cert := path.Join(opts.ProvCert, constants.CertChainFilename)
170
tlsOpts := &TLSOptions{
175
cli, err := NewCitadelClient(opts, tlsOpts)
177
t.Errorf("failed to create ca client: %v", err)
180
server.Authenticator.Set("fake", "")
181
checkSign(t, cli, false)
182
server.Authenticator.Set("", "istiod.istio-system.svc")
183
checkSign(t, cli, true)
185
t.Run("cert present later", func(t *testing.T) {
187
server := mockCAServer{Certs: fakeCert, Err: nil, Authenticator: security.NewFakeAuthenticator("ca")}
188
addr := serve(t, server, tlsOptions(t))
189
opts := &security.Options{
191
CredFetcher: plugin.CreateTokenPlugin("testdata/token"),
194
rootCert := path.Join(certDir, constants.RootCertFilename)
195
key := path.Join(opts.ProvCert, constants.KeyFilename)
196
cert := path.Join(opts.ProvCert, constants.CertChainFilename)
197
tlsOpts := &TLSOptions{
202
cli, err := NewCitadelClient(opts, tlsOpts)
204
t.Errorf("failed to create ca client: %v", err)
207
server.Authenticator.Set("fake", "")
208
checkSign(t, cli, false)
209
checkSign(t, cli, false)
210
server.Authenticator.Set("", "istiod.istio-system.svc")
211
checkSign(t, cli, true)
212
if err := file.Copy(filepath.Join(certDir, "cert-chain.pem"), dir, "cert-chain.pem"); err != nil {
215
if err := file.Copy(filepath.Join(certDir, "key.pem"), dir, "key.pem"); err != nil {
218
checkSign(t, cli, false)
222
func TestCitadelClient(t *testing.T) {
223
testCases := map[string]struct {
225
expectedCert []string
230
server: mockCAServer{Certs: fakeCert, Err: nil},
231
expectedCert: fakeCert,
234
"Error in response": {
235
server: mockCAServer{Certs: nil, Err: fmt.Errorf("test failure")},
237
expectedErr: "rpc error: code = Unknown desc = test failure",
240
server: mockCAServer{Certs: []string{}, Err: nil},
242
expectedErr: "invalid empty CertChain",
245
server: mockCAServer{Certs: nil, Err: status.Error(codes.Unavailable, "test failure")},
247
expectedErr: "rpc error: code = Unavailable desc = test failure",
252
for id, tc := range testCases {
253
t.Run(id, func(t *testing.T) {
254
mt := monitortest.New(t)
255
addr := serve(t, tc.server)
256
cli, err := NewCitadelClient(&security.Options{CAEndpoint: addr}, nil)
258
t.Errorf("failed to create ca client: %v", err)
262
resp, err := cli.CSRSign([]byte{0o1}, 1)
264
if !strings.Contains(err.Error(), tc.expectedErr) {
265
t.Errorf("error (%s) does not match expected error (%s)", err.Error(), tc.expectedErr)
268
if tc.expectedErr != "" {
269
t.Errorf("expect error: %s but got no error", tc.expectedErr)
270
} else if !reflect.DeepEqual(resp, tc.expectedCert) {
271
t.Errorf("resp: got %+v, expected %v", resp, tc.expectedCert)
276
mt.Assert("num_outgoing_retries", map[string]string{"request_type": monitoring.CSR}, monitortest.AtLeast(1))
282
type mockTokenCAServer struct {
283
pb.UnimplementedIstioCertificateServiceServer
287
func (ca *mockTokenCAServer) CreateCertificate(ctx context.Context, in *pb.IstioCertificateRequest) (*pb.IstioCertificateResponse, error) {
288
targetJWT, err := extractBearerToken(ctx)
292
if targetJWT != validToken {
293
return nil, fmt.Errorf("token is not valid, wanted %q got %q", validToken, targetJWT)
295
return &pb.IstioCertificateResponse{CertChain: ca.Certs}, nil
298
func extractBearerToken(ctx context.Context) (string, error) {
299
md, ok := metadata.FromIncomingContext(ctx)
301
return "", fmt.Errorf("no metadata is attached")
304
authHeader, exists := md[authorizationMeta]
306
return "", fmt.Errorf("no HTTP authorization header exists")
309
for _, value := range authHeader {
310
if strings.HasPrefix(value, bearerTokenPrefix) {
311
return strings.TrimPrefix(value, bearerTokenPrefix), nil
315
return "", fmt.Errorf("no bearer token exists in HTTP authorization header")
318
// this test is to test whether the server side receive the correct token when
319
// we build the CSR sign request
320
func TestCitadelClientWithDifferentTypeToken(t *testing.T) {
321
testCases := map[string]struct {
322
server mockTokenCAServer
323
expectedCert []string
328
server: mockTokenCAServer{Certs: fakeCert},
329
expectedCert: fakeCert,
334
server: mockTokenCAServer{Certs: nil},
336
expectedErr: "rpc error: code = Unknown desc = no HTTP authorization header exists",
340
server: mockTokenCAServer{Certs: []string{}},
342
expectedErr: "rpc error: code = Unknown desc = token is not valid",
347
for id, tc := range testCases {
348
t.Run(id, func(t *testing.T) {
349
s := grpc.NewServer()
351
lis, err := net.Listen("tcp", mockServerAddress)
353
t.Fatalf("failed to listen: %v", err)
356
pb.RegisterIstioCertificateServiceServer(s, &tc.server)
357
if err := s.Serve(lis); err != nil {
358
t.Logf("failed to serve: %v", err)
362
opts := &security.Options{CAEndpoint: lis.Addr().String(), ClusterID: constants.DefaultClusterName, CredFetcher: plugin.CreateMockPlugin(tc.token)}
363
err = retry.UntilSuccess(func() error {
364
cli, err := NewCitadelClient(opts, nil)
366
return fmt.Errorf("failed to create ca client: %v", err)
369
resp, err := cli.CSRSign([]byte{0o1}, 1)
371
if !strings.Contains(err.Error(), tc.expectedErr) {
372
return fmt.Errorf("error (%s) does not match expected error (%s)", err.Error(), tc.expectedErr)
375
if tc.expectedErr != "" {
376
return fmt.Errorf("expect error: %s but got no error", tc.expectedErr)
377
} else if !reflect.DeepEqual(resp, tc.expectedCert) {
378
return fmt.Errorf("resp: got %+v, expected %v", resp, tc.expectedCert)
382
}, retry.Timeout(2*time.Second), retry.Delay(time.Millisecond))
384
t.Fatalf("test failed error is: %+v", err)