istio

Форк
0
388 строк · 11.8 Кб
1
// Copyright Istio Authors
2
//
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
6
//
7
//     http://www.apache.org/licenses/LICENSE-2.0
8
//
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.
14

15
package caclient
16

17
import (
18
	"context"
19
	"crypto/tls"
20
	"fmt"
21
	"net"
22
	"path"
23
	"path/filepath"
24
	"reflect"
25
	"strings"
26
	"testing"
27
	"time"
28

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"
34

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"
46
)
47

48
const (
49
	mockServerAddress = "localhost:0"
50
)
51

52
var (
53
	fakeCert          = []string{"foo", "bar"}
54
	fakeToken         = "Bearer fakeToken"
55
	validToken        = "Bearer validToken"
56
	authorizationMeta = "authorization"
57
)
58

59
type mockCAServer struct {
60
	pb.UnimplementedIstioCertificateServiceServer
61
	Certs         []string
62
	Authenticator *security.FakeAuthenticator
63
	Err           error
64
}
65

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})
69
		if caller == nil {
70
			return nil, status.Error(codes.Unauthenticated, err.Error())
71
		}
72
	}
73
	if ca.Err == nil {
74
		return &pb.IstioCertificateResponse{CertChain: ca.Certs}, nil
75
	}
76
	return nil, ca.Err
77
}
78

79
func tlsOptions(t *testing.T) grpc.ServerOption {
80
	t.Helper()
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"))
84
	if err != nil {
85
		t.Fatal(err)
86
	}
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 {
90
		t.Fatal(err)
91
	}
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,
97
	}))
98
}
99

100
func serve(t *testing.T, ca mockCAServer, opts ...grpc.ServerOption) string {
101
	// create a local grpc server
102
	s := grpc.NewServer(opts...)
103
	t.Cleanup(s.Stop)
104
	lis, err := net.Listen("tcp", mockServerAddress)
105
	if err != nil {
106
		t.Fatalf("failed to listen: %v", err)
107
	}
108

109
	go func() {
110
		pb.RegisterIstioCertificateServiceServer(s, &ca)
111
		if err := s.Serve(lis); err != nil {
112
			t.Logf("failed to serve: %v", err)
113
		}
114
	}()
115
	_, port, _ := net.SplitHostPort(lis.Addr().String())
116
	return fmt.Sprintf("localhost:%s", port)
117
}
118

119
func TestCitadelClientRotation(t *testing.T) {
120
	checkSign := func(t *testing.T, cli security.Client, expectError bool) {
121
		t.Helper()
122
		resp, err := cli.CSRSign([]byte{0o1}, 1)
123
		if expectError != (err != nil) {
124
			t.Fatalf("expected error:%v, got error:%v", expectError, err)
125
		}
126
		if !expectError && !reflect.DeepEqual(resp, fakeCert) {
127
			t.Fatalf("expected cert: %v", resp)
128
		}
129
	}
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{
135
			CAEndpoint:  addr,
136
			CredFetcher: plugin.CreateTokenPlugin("testdata/token"),
137
			ProvCert:    certDir,
138
		}
139
		rootCert := path.Join(certDir, constants.RootCertFilename)
140
		key := path.Join(certDir, constants.KeyFilename)
141
		cert := path.Join(certDir, constants.CertChainFilename)
142
		tlsOpts := &TLSOptions{
143
			RootCert: rootCert,
144
			Key:      key,
145
			Cert:     cert,
146
		}
147
		cli, err := NewCitadelClient(opts, tlsOpts)
148
		if err != nil {
149
			t.Errorf("failed to create ca client: %v", err)
150
		}
151
		t.Cleanup(cli.Close)
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)
158
	})
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{
163
			CAEndpoint:  addr,
164
			CredFetcher: plugin.CreateTokenPlugin("testdata/token"),
165
			ProvCert:    ".",
166
		}
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{
171
			RootCert: rootCert,
172
			Key:      key,
173
			Cert:     cert,
174
		}
175
		cli, err := NewCitadelClient(opts, tlsOpts)
176
		if err != nil {
177
			t.Errorf("failed to create ca client: %v", err)
178
		}
179
		t.Cleanup(cli.Close)
180
		server.Authenticator.Set("fake", "")
181
		checkSign(t, cli, false)
182
		server.Authenticator.Set("", "istiod.istio-system.svc")
183
		checkSign(t, cli, true)
184
	})
185
	t.Run("cert present later", func(t *testing.T) {
186
		dir := t.TempDir()
187
		server := mockCAServer{Certs: fakeCert, Err: nil, Authenticator: security.NewFakeAuthenticator("ca")}
188
		addr := serve(t, server, tlsOptions(t))
189
		opts := &security.Options{
190
			CAEndpoint:  addr,
191
			CredFetcher: plugin.CreateTokenPlugin("testdata/token"),
192
			ProvCert:    dir,
193
		}
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{
198
			RootCert: rootCert,
199
			Key:      key,
200
			Cert:     cert,
201
		}
202
		cli, err := NewCitadelClient(opts, tlsOpts)
203
		if err != nil {
204
			t.Errorf("failed to create ca client: %v", err)
205
		}
206
		t.Cleanup(cli.Close)
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 {
213
			t.Fatal(err)
214
		}
215
		if err := file.Copy(filepath.Join(certDir, "key.pem"), dir, "key.pem"); err != nil {
216
			t.Fatal(err)
217
		}
218
		checkSign(t, cli, false)
219
	})
220
}
221

222
func TestCitadelClient(t *testing.T) {
223
	testCases := map[string]struct {
224
		server       mockCAServer
225
		expectedCert []string
226
		expectedErr  string
227
		expectRetry  bool
228
	}{
229
		"Valid certs": {
230
			server:       mockCAServer{Certs: fakeCert, Err: nil},
231
			expectedCert: fakeCert,
232
			expectedErr:  "",
233
		},
234
		"Error in response": {
235
			server:       mockCAServer{Certs: nil, Err: fmt.Errorf("test failure")},
236
			expectedCert: nil,
237
			expectedErr:  "rpc error: code = Unknown desc = test failure",
238
		},
239
		"Empty response": {
240
			server:       mockCAServer{Certs: []string{}, Err: nil},
241
			expectedCert: nil,
242
			expectedErr:  "invalid empty CertChain",
243
		},
244
		"retry": {
245
			server:       mockCAServer{Certs: nil, Err: status.Error(codes.Unavailable, "test failure")},
246
			expectedCert: nil,
247
			expectedErr:  "rpc error: code = Unavailable desc = test failure",
248
			expectRetry:  true,
249
		},
250
	}
251

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)
257
			if err != nil {
258
				t.Errorf("failed to create ca client: %v", err)
259
			}
260
			t.Cleanup(cli.Close)
261

262
			resp, err := cli.CSRSign([]byte{0o1}, 1)
263
			if err != nil {
264
				if !strings.Contains(err.Error(), tc.expectedErr) {
265
					t.Errorf("error (%s) does not match expected error (%s)", err.Error(), tc.expectedErr)
266
				}
267
			} else {
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)
272
				}
273
			}
274

275
			if tc.expectRetry {
276
				mt.Assert("num_outgoing_retries", map[string]string{"request_type": monitoring.CSR}, monitortest.AtLeast(1))
277
			}
278
		})
279
	}
280
}
281

282
type mockTokenCAServer struct {
283
	pb.UnimplementedIstioCertificateServiceServer
284
	Certs []string
285
}
286

287
func (ca *mockTokenCAServer) CreateCertificate(ctx context.Context, in *pb.IstioCertificateRequest) (*pb.IstioCertificateResponse, error) {
288
	targetJWT, err := extractBearerToken(ctx)
289
	if err != nil {
290
		return nil, err
291
	}
292
	if targetJWT != validToken {
293
		return nil, fmt.Errorf("token is not valid, wanted %q got %q", validToken, targetJWT)
294
	}
295
	return &pb.IstioCertificateResponse{CertChain: ca.Certs}, nil
296
}
297

298
func extractBearerToken(ctx context.Context) (string, error) {
299
	md, ok := metadata.FromIncomingContext(ctx)
300
	if !ok {
301
		return "", fmt.Errorf("no metadata is attached")
302
	}
303

304
	authHeader, exists := md[authorizationMeta]
305
	if !exists {
306
		return "", fmt.Errorf("no HTTP authorization header exists")
307
	}
308

309
	for _, value := range authHeader {
310
		if strings.HasPrefix(value, bearerTokenPrefix) {
311
			return strings.TrimPrefix(value, bearerTokenPrefix), nil
312
		}
313
	}
314

315
	return "", fmt.Errorf("no bearer token exists in HTTP authorization header")
316
}
317

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
324
		expectedErr  string
325
		token        string
326
	}{
327
		"Valid Token": {
328
			server:       mockTokenCAServer{Certs: fakeCert},
329
			expectedCert: fakeCert,
330
			expectedErr:  "",
331
			token:        validToken,
332
		},
333
		"Empty Token": {
334
			server:       mockTokenCAServer{Certs: nil},
335
			expectedCert: nil,
336
			expectedErr:  "rpc error: code = Unknown desc = no HTTP authorization header exists",
337
			token:        "",
338
		},
339
		"InValid Token": {
340
			server:       mockTokenCAServer{Certs: []string{}},
341
			expectedCert: nil,
342
			expectedErr:  "rpc error: code = Unknown desc = token is not valid",
343
			token:        fakeToken,
344
		},
345
	}
346

347
	for id, tc := range testCases {
348
		t.Run(id, func(t *testing.T) {
349
			s := grpc.NewServer()
350
			defer s.Stop()
351
			lis, err := net.Listen("tcp", mockServerAddress)
352
			if err != nil {
353
				t.Fatalf("failed to listen: %v", err)
354
			}
355
			go func() {
356
				pb.RegisterIstioCertificateServiceServer(s, &tc.server)
357
				if err := s.Serve(lis); err != nil {
358
					t.Logf("failed to serve: %v", err)
359
				}
360
			}()
361

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)
365
				if err != nil {
366
					return fmt.Errorf("failed to create ca client: %v", err)
367
				}
368
				t.Cleanup(cli.Close)
369
				resp, err := cli.CSRSign([]byte{0o1}, 1)
370
				if err != nil {
371
					if !strings.Contains(err.Error(), tc.expectedErr) {
372
						return fmt.Errorf("error (%s) does not match expected error (%s)", err.Error(), tc.expectedErr)
373
					}
374
				} else {
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)
379
					}
380
				}
381
				return nil
382
			}, retry.Timeout(2*time.Second), retry.Delay(time.Millisecond))
383
			if err != nil {
384
				t.Fatalf("test failed error is: %+v", err)
385
			}
386
		})
387
	}
388
}
389

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.