istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/nodeagent/caclient/providers/citadel/client_test.go (about) 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 citadel 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 }