1/* 2Copyright 2018 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package exec 18 19import ( 20 "bytes" 21 "crypto/ecdsa" 22 "crypto/elliptic" 23 "crypto/rand" 24 "crypto/tls" 25 "crypto/x509" 26 "crypto/x509/pkix" 27 "encoding/json" 28 "encoding/pem" 29 "fmt" 30 "io/ioutil" 31 "math/big" 32 "net/http" 33 "net/http/httptest" 34 "reflect" 35 "strings" 36 "testing" 37 "time" 38 39 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 40 "k8s.io/apimachinery/pkg/runtime" 41 "k8s.io/client-go/pkg/apis/clientauthentication" 42 "k8s.io/client-go/tools/clientcmd/api" 43 "k8s.io/client-go/transport" 44) 45 46var ( 47 certData = []byte(`-----BEGIN CERTIFICATE----- 48MIIC6jCCAdSgAwIBAgIBCzALBgkqhkiG9w0BAQswIzEhMB8GA1UEAwwYMTAuMTMu 49MTI5LjEwNkAxNDIxMzU5MDU4MB4XDTE1MDExNTIyMDEzMVoXDTE2MDExNTIyMDEz 50MlowGzEZMBcGA1UEAxMQb3BlbnNoaWZ0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEB 51BQADggEPADCCAQoCggEBAKtdhz0+uCLXw5cSYns9rU/XifFSpb/x24WDdrm72S/v 52b9BPYsAStiP148buylr1SOuNi8sTAZmlVDDIpIVwMLff+o2rKYDicn9fjbrTxTOj 53lI4pHJBH+JU3AJ0tbajupioh70jwFS0oYpwtneg2zcnE2Z4l6mhrj2okrc5Q1/X2 54I2HChtIU4JYTisObtin10QKJX01CLfYXJLa8upWzKZ4/GOcHG+eAV3jXWoXidtjb 551Usw70amoTZ6mIVCkiu1QwCoa8+ycojGfZhvqMsAp1536ZcCul+Na+AbCv4zKS7F 56kQQaImVrXdUiFansIoofGlw/JNuoKK6ssVpS5Ic3pgcCAwEAAaM1MDMwDgYDVR0P 57AQH/BAQDAgCgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCwYJ 58KoZIhvcNAQELA4IBAQCKLREH7bXtXtZ+8vI6cjD7W3QikiArGqbl36bAhhWsJLp/ 59p/ndKz39iFNaiZ3GlwIURWOOKx3y3GA0x9m8FR+Llthf0EQ8sUjnwaknWs0Y6DQ3 60jjPFZOpV3KPCFrdMJ3++E3MgwFC/Ih/N2ebFX9EcV9Vcc6oVWMdwT0fsrhu683rq 616GSR/3iVX1G/pmOiuaR0fNUaCyCfYrnI4zHBDgSfnlm3vIvN2lrsR/DQBakNL8DJ 62HBgKxMGeUPoneBv+c8DMXIL0EhaFXRlBv9QW45/GiAIOuyFJ0i6hCtGZpJjq4OpQ 63BRjCI+izPzFTjsxD4aORE+WOkyWFCGPWKfNejfw0 64-----END CERTIFICATE-----`) 65 keyData = []byte(`-----BEGIN RSA PRIVATE KEY----- 66MIIEowIBAAKCAQEAq12HPT64ItfDlxJiez2tT9eJ8VKlv/HbhYN2ubvZL+9v0E9i 67wBK2I/Xjxu7KWvVI642LyxMBmaVUMMikhXAwt9/6jaspgOJyf1+NutPFM6OUjikc 68kEf4lTcAnS1tqO6mKiHvSPAVLShinC2d6DbNycTZniXqaGuPaiStzlDX9fYjYcKG 690hTglhOKw5u2KfXRAolfTUIt9hcktry6lbMpnj8Y5wcb54BXeNdaheJ22NvVSzDv 70RqahNnqYhUKSK7VDAKhrz7JyiMZ9mG+oywCnXnfplwK6X41r4BsK/jMpLsWRBBoi 71ZWtd1SIVqewiih8aXD8k26gorqyxWlLkhzemBwIDAQABAoIBAD2XYRs3JrGHQUpU 72FkdbVKZkvrSY0vAZOqBTLuH0zUv4UATb8487anGkWBjRDLQCgxH+jucPTrztekQK 73aW94clo0S3aNtV4YhbSYIHWs1a0It0UdK6ID7CmdWkAj6s0T8W8lQT7C46mWYVLm 745mFnCTHi6aB42jZrqmEpC7sivWwuU0xqj3Ml8kkxQCGmyc9JjmCB4OrFFC8NNt6M 75ObvQkUI6Z3nO4phTbpxkE1/9dT0MmPIF7GhHVzJMS+EyyRYUDllZ0wvVSOM3qZT0 76JMUaBerkNwm9foKJ1+dv2nMKZZbJajv7suUDCfU44mVeaEO+4kmTKSGCGjjTBGkr 777L1ySDECgYEA5ElIMhpdBzIivCuBIH8LlUeuzd93pqssO1G2Xg0jHtfM4tz7fyeI 78cr90dc8gpli24dkSxzLeg3Tn3wIj/Bu64m2TpZPZEIlukYvgdgArmRIPQVxerYey 79OkrfTNkxU1HXsYjLCdGcGXs5lmb+K/kuTcFxaMOs7jZi7La+jEONwf8CgYEAwCs/ 80rUOOA0klDsWWisbivOiNPII79c9McZCNBqncCBfMUoiGe8uWDEO4TFHN60vFuVk9 818PkwpCfvaBUX+ajvbafIfHxsnfk1M04WLGCeqQ/ym5Q4sQoQOcC1b1y9qc/xEWfg 82nIUuia0ukYRpl7qQa3tNg+BNFyjypW8zukUAC/kCgYB1/Kojuxx5q5/oQVPrx73k 832bevD+B3c+DYh9MJqSCNwFtUpYIWpggPxoQan4LwdsmO0PKzocb/ilyNFj4i/vII 84NToqSc/WjDFpaDIKyuu9oWfhECye45NqLWhb/6VOuu4QA/Nsj7luMhIBehnEAHW+ 85GkzTKM8oD1PxpEG3nPKXYQKBgQC6AuMPRt3XBl1NkCrpSBy/uObFlFaP2Enpf39S 863OZ0Gv0XQrnSaL1kP8TMcz68rMrGX8DaWYsgytstR4W+jyy7WvZwsUu+GjTJ5aMG 8777uEcEBpIi9CBzivfn7hPccE8ZgqPf+n4i6q66yxBJflW5xhvafJqDtW2LcPNbW/ 88bvzdmQKBgExALRUXpq+5dbmkdXBHtvXdRDZ6rVmrnjy4nI5bPw+1GqQqk6uAR6B/ 89F6NmLCQOO4PDG/cuatNHIr2FrwTmGdEL6ObLUGWn9Oer9gJhHVqqsY5I4sEPo4XX 90stR0Yiw0buV6DL/moUO0HIM9Bjh96HJp+LxiIS6UCdIhMPp5HoQa 91-----END RSA PRIVATE KEY-----`) 92 validCert *tls.Certificate 93) 94 95func init() { 96 cert, err := tls.X509KeyPair(certData, keyData) 97 if err != nil { 98 panic(err) 99 } 100 cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 101 if err != nil { 102 panic(err) 103 } 104 validCert = &cert 105} 106 107func TestCacheKey(t *testing.T) { 108 c1 := &api.ExecConfig{ 109 Command: "foo-bar", 110 Args: []string{"1", "2"}, 111 Env: []api.ExecEnvVar{ 112 {Name: "3", Value: "4"}, 113 {Name: "5", Value: "6"}, 114 {Name: "7", Value: "8"}, 115 }, 116 APIVersion: "client.authentication.k8s.io/v1alpha1", 117 } 118 c2 := &api.ExecConfig{ 119 Command: "foo-bar", 120 Args: []string{"1", "2"}, 121 Env: []api.ExecEnvVar{ 122 {Name: "3", Value: "4"}, 123 {Name: "5", Value: "6"}, 124 {Name: "7", Value: "8"}, 125 }, 126 APIVersion: "client.authentication.k8s.io/v1alpha1", 127 } 128 c3 := &api.ExecConfig{ 129 Command: "foo-bar", 130 Args: []string{"1", "2"}, 131 Env: []api.ExecEnvVar{ 132 {Name: "3", Value: "4"}, 133 {Name: "5", Value: "6"}, 134 }, 135 APIVersion: "client.authentication.k8s.io/v1alpha1", 136 } 137 key1 := cacheKey(c1) 138 key2 := cacheKey(c2) 139 key3 := cacheKey(c3) 140 if key1 != key2 { 141 t.Error("key1 and key2 didn't match") 142 } 143 if key1 == key3 { 144 t.Error("key1 and key3 matched") 145 } 146 if key2 == key3 { 147 t.Error("key2 and key3 matched") 148 } 149} 150 151func compJSON(t *testing.T, got, want []byte) { 152 t.Helper() 153 gotJSON := &bytes.Buffer{} 154 wantJSON := &bytes.Buffer{} 155 156 if err := json.Indent(gotJSON, got, "", " "); err != nil { 157 t.Errorf("got invalid JSON: %v", err) 158 } 159 if err := json.Indent(wantJSON, want, "", " "); err != nil { 160 t.Errorf("want invalid JSON: %v", err) 161 } 162 g := strings.TrimSpace(gotJSON.String()) 163 w := strings.TrimSpace(wantJSON.String()) 164 if g != w { 165 t.Errorf("wanted %q, got %q", w, g) 166 } 167} 168 169func TestRefreshCreds(t *testing.T) { 170 tests := []struct { 171 name string 172 config api.ExecConfig 173 output string 174 interactive bool 175 response *clientauthentication.Response 176 wantInput string 177 wantCreds credentials 178 wantExpiry time.Time 179 wantErr bool 180 }{ 181 { 182 name: "basic-request", 183 config: api.ExecConfig{ 184 APIVersion: "client.authentication.k8s.io/v1alpha1", 185 }, 186 wantInput: `{ 187 "kind":"ExecCredential", 188 "apiVersion":"client.authentication.k8s.io/v1alpha1", 189 "spec": {} 190 }`, 191 output: `{ 192 "kind": "ExecCredential", 193 "apiVersion": "client.authentication.k8s.io/v1alpha1", 194 "status": { 195 "token": "foo-bar" 196 } 197 }`, 198 wantCreds: credentials{token: "foo-bar"}, 199 }, 200 { 201 name: "interactive", 202 config: api.ExecConfig{ 203 APIVersion: "client.authentication.k8s.io/v1alpha1", 204 }, 205 interactive: true, 206 wantInput: `{ 207 "kind":"ExecCredential", 208 "apiVersion":"client.authentication.k8s.io/v1alpha1", 209 "spec": { 210 "interactive": true 211 } 212 }`, 213 output: `{ 214 "kind": "ExecCredential", 215 "apiVersion": "client.authentication.k8s.io/v1alpha1", 216 "status": { 217 "token": "foo-bar" 218 } 219 }`, 220 wantCreds: credentials{token: "foo-bar"}, 221 }, 222 { 223 name: "response", 224 config: api.ExecConfig{ 225 APIVersion: "client.authentication.k8s.io/v1alpha1", 226 }, 227 response: &clientauthentication.Response{ 228 Header: map[string][]string{ 229 "WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`}, 230 }, 231 Code: 401, 232 }, 233 wantInput: `{ 234 "kind":"ExecCredential", 235 "apiVersion":"client.authentication.k8s.io/v1alpha1", 236 "spec": { 237 "response": { 238 "header": { 239 "WWW-Authenticate": [ 240 "Basic realm=\"Access to the staging site\", charset=\"UTF-8\"" 241 ] 242 }, 243 "code": 401 244 } 245 } 246 }`, 247 output: `{ 248 "kind": "ExecCredential", 249 "apiVersion": "client.authentication.k8s.io/v1alpha1", 250 "status": { 251 "token": "foo-bar" 252 } 253 }`, 254 wantCreds: credentials{token: "foo-bar"}, 255 }, 256 { 257 name: "expiry", 258 config: api.ExecConfig{ 259 APIVersion: "client.authentication.k8s.io/v1alpha1", 260 }, 261 wantInput: `{ 262 "kind":"ExecCredential", 263 "apiVersion":"client.authentication.k8s.io/v1alpha1", 264 "spec": {} 265 }`, 266 output: `{ 267 "kind": "ExecCredential", 268 "apiVersion": "client.authentication.k8s.io/v1alpha1", 269 "status": { 270 "token": "foo-bar", 271 "expirationTimestamp": "2006-01-02T15:04:05Z" 272 } 273 }`, 274 wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC), 275 wantCreds: credentials{token: "foo-bar"}, 276 }, 277 { 278 name: "no-group-version", 279 config: api.ExecConfig{ 280 APIVersion: "client.authentication.k8s.io/v1alpha1", 281 }, 282 wantInput: `{ 283 "kind":"ExecCredential", 284 "apiVersion":"client.authentication.k8s.io/v1alpha1", 285 "spec": {} 286 }`, 287 output: `{ 288 "kind": "ExecCredential", 289 "status": { 290 "token": "foo-bar" 291 } 292 }`, 293 wantErr: true, 294 }, 295 { 296 name: "no-status", 297 config: api.ExecConfig{ 298 APIVersion: "client.authentication.k8s.io/v1alpha1", 299 }, 300 wantInput: `{ 301 "kind":"ExecCredential", 302 "apiVersion":"client.authentication.k8s.io/v1alpha1", 303 "spec": {} 304 }`, 305 output: `{ 306 "kind": "ExecCredential", 307 "apiVersion":"client.authentication.k8s.io/v1alpha1" 308 }`, 309 wantErr: true, 310 }, 311 { 312 name: "no-creds", 313 config: api.ExecConfig{ 314 APIVersion: "client.authentication.k8s.io/v1alpha1", 315 }, 316 wantInput: `{ 317 "kind":"ExecCredential", 318 "apiVersion":"client.authentication.k8s.io/v1alpha1", 319 "spec": {} 320 }`, 321 output: `{ 322 "kind": "ExecCredential", 323 "apiVersion":"client.authentication.k8s.io/v1alpha1", 324 "status": {} 325 }`, 326 wantErr: true, 327 }, 328 { 329 name: "TLS credentials", 330 config: api.ExecConfig{ 331 APIVersion: "client.authentication.k8s.io/v1alpha1", 332 }, 333 wantInput: `{ 334 "kind":"ExecCredential", 335 "apiVersion":"client.authentication.k8s.io/v1alpha1", 336 "spec": {} 337 }`, 338 output: fmt.Sprintf(`{ 339 "kind": "ExecCredential", 340 "apiVersion": "client.authentication.k8s.io/v1alpha1", 341 "status": { 342 "clientKeyData": %q, 343 "clientCertificateData": %q 344 } 345 }`, keyData, certData), 346 wantCreds: credentials{cert: validCert}, 347 }, 348 { 349 name: "bad TLS credentials", 350 config: api.ExecConfig{ 351 APIVersion: "client.authentication.k8s.io/v1alpha1", 352 }, 353 wantInput: `{ 354 "kind":"ExecCredential", 355 "apiVersion":"client.authentication.k8s.io/v1alpha1", 356 "spec": {} 357 }`, 358 output: `{ 359 "kind": "ExecCredential", 360 "apiVersion": "client.authentication.k8s.io/v1alpha1", 361 "status": { 362 "clientKeyData": "foo", 363 "clientCertificateData": "bar" 364 } 365 }`, 366 wantErr: true, 367 }, 368 { 369 name: "cert but no key", 370 config: api.ExecConfig{ 371 APIVersion: "client.authentication.k8s.io/v1alpha1", 372 }, 373 wantInput: `{ 374 "kind":"ExecCredential", 375 "apiVersion":"client.authentication.k8s.io/v1alpha1", 376 "spec": {} 377 }`, 378 output: fmt.Sprintf(`{ 379 "kind": "ExecCredential", 380 "apiVersion": "client.authentication.k8s.io/v1alpha1", 381 "status": { 382 "clientCertificateData": %q 383 } 384 }`, certData), 385 wantErr: true, 386 }, 387 { 388 name: "beta-basic-request", 389 config: api.ExecConfig{ 390 APIVersion: "client.authentication.k8s.io/v1beta1", 391 }, 392 output: `{ 393 "kind": "ExecCredential", 394 "apiVersion": "client.authentication.k8s.io/v1beta1", 395 "status": { 396 "token": "foo-bar" 397 } 398 }`, 399 wantCreds: credentials{token: "foo-bar"}, 400 }, 401 { 402 name: "beta-expiry", 403 config: api.ExecConfig{ 404 APIVersion: "client.authentication.k8s.io/v1beta1", 405 }, 406 output: `{ 407 "kind": "ExecCredential", 408 "apiVersion": "client.authentication.k8s.io/v1beta1", 409 "status": { 410 "token": "foo-bar", 411 "expirationTimestamp": "2006-01-02T15:04:05Z" 412 } 413 }`, 414 wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC), 415 wantCreds: credentials{token: "foo-bar"}, 416 }, 417 { 418 name: "beta-no-group-version", 419 config: api.ExecConfig{ 420 APIVersion: "client.authentication.k8s.io/v1beta1", 421 }, 422 output: `{ 423 "kind": "ExecCredential", 424 "status": { 425 "token": "foo-bar" 426 } 427 }`, 428 wantErr: true, 429 }, 430 { 431 name: "beta-no-status", 432 config: api.ExecConfig{ 433 APIVersion: "client.authentication.k8s.io/v1beta1", 434 }, 435 output: `{ 436 "kind": "ExecCredential", 437 "apiVersion":"client.authentication.k8s.io/v1beta1" 438 }`, 439 wantErr: true, 440 }, 441 { 442 name: "beta-no-token", 443 config: api.ExecConfig{ 444 APIVersion: "client.authentication.k8s.io/v1beta1", 445 }, 446 output: `{ 447 "kind": "ExecCredential", 448 "apiVersion":"client.authentication.k8s.io/v1beta1", 449 "status": {} 450 }`, 451 wantErr: true, 452 }, 453 } 454 455 for _, test := range tests { 456 t.Run(test.name, func(t *testing.T) { 457 c := test.config 458 459 c.Command = "./testdata/test-plugin.sh" 460 c.Env = append(c.Env, api.ExecEnvVar{ 461 Name: "TEST_OUTPUT", 462 Value: test.output, 463 }) 464 465 a, err := newAuthenticator(newCache(), &c) 466 if err != nil { 467 t.Fatal(err) 468 } 469 470 stderr := &bytes.Buffer{} 471 a.stderr = stderr 472 a.interactive = test.interactive 473 a.environ = func() []string { return nil } 474 475 if err := a.refreshCredsLocked(test.response); err != nil { 476 if !test.wantErr { 477 t.Errorf("get token %v", err) 478 } 479 return 480 } 481 if test.wantErr { 482 t.Fatal("expected error getting token") 483 } 484 485 if !reflect.DeepEqual(a.cachedCreds, &test.wantCreds) { 486 t.Errorf("expected credentials %+v got %+v", &test.wantCreds, a.cachedCreds) 487 } 488 489 if !a.exp.Equal(test.wantExpiry) { 490 t.Errorf("expected expiry %v got %v", test.wantExpiry, a.exp) 491 } 492 493 if test.wantInput == "" { 494 if got := strings.TrimSpace(stderr.String()); got != "" { 495 t.Errorf("expected no input parameters, got %q", got) 496 } 497 return 498 } 499 500 compJSON(t, stderr.Bytes(), []byte(test.wantInput)) 501 }) 502 } 503} 504 505func TestRoundTripper(t *testing.T) { 506 wantToken := "" 507 508 n := time.Now() 509 now := func() time.Time { return n } 510 511 env := []string{""} 512 environ := func() []string { 513 s := make([]string, len(env)) 514 copy(s, env) 515 return s 516 } 517 518 setOutput := func(s string) { 519 env[0] = "TEST_OUTPUT=" + s 520 } 521 522 handler := func(w http.ResponseWriter, r *http.Request) { 523 gotToken := "" 524 parts := strings.Split(r.Header.Get("Authorization"), " ") 525 if len(parts) > 1 && strings.EqualFold(parts[0], "bearer") { 526 gotToken = parts[1] 527 } 528 529 if wantToken != gotToken { 530 http.Error(w, "Unauthorized", http.StatusUnauthorized) 531 return 532 } 533 fmt.Fprintln(w, "ok") 534 } 535 server := httptest.NewServer(http.HandlerFunc(handler)) 536 537 c := api.ExecConfig{ 538 Command: "./testdata/test-plugin.sh", 539 APIVersion: "client.authentication.k8s.io/v1alpha1", 540 } 541 a, err := newAuthenticator(newCache(), &c) 542 if err != nil { 543 t.Fatal(err) 544 } 545 a.environ = environ 546 a.now = now 547 a.stderr = ioutil.Discard 548 549 tc := &transport.Config{} 550 if err := a.UpdateTransportConfig(tc); err != nil { 551 t.Fatal(err) 552 } 553 client := http.Client{ 554 Transport: tc.WrapTransport(http.DefaultTransport), 555 } 556 557 get := func(t *testing.T, statusCode int) { 558 t.Helper() 559 resp, err := client.Get(server.URL) 560 if err != nil { 561 t.Fatal(err) 562 } 563 defer resp.Body.Close() 564 if resp.StatusCode != statusCode { 565 t.Errorf("wanted status %d got %d", statusCode, resp.StatusCode) 566 } 567 } 568 569 setOutput(`{ 570 "kind": "ExecCredential", 571 "apiVersion": "client.authentication.k8s.io/v1alpha1", 572 "status": { 573 "token": "token1" 574 } 575 }`) 576 wantToken = "token1" 577 get(t, http.StatusOK) 578 579 setOutput(`{ 580 "kind": "ExecCredential", 581 "apiVersion": "client.authentication.k8s.io/v1alpha1", 582 "status": { 583 "token": "token2" 584 } 585 }`) 586 // Previous token should be cached 587 get(t, http.StatusOK) 588 589 wantToken = "token2" 590 // Token is still cached, hits unauthorized but causes token to rotate. 591 get(t, http.StatusUnauthorized) 592 // Follow up request uses the rotated token. 593 get(t, http.StatusOK) 594 595 setOutput(`{ 596 "kind": "ExecCredential", 597 "apiVersion": "client.authentication.k8s.io/v1alpha1", 598 "status": { 599 "token": "token3", 600 "expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `" 601 } 602 }`) 603 wantToken = "token3" 604 // Token is still cached, hit's unauthorized but causes rotation to token with an expiry. 605 get(t, http.StatusUnauthorized) 606 get(t, http.StatusOK) 607 608 // Move time forward 2 hours, "token3" is now expired. 609 n = n.Add(time.Hour * 2) 610 setOutput(`{ 611 "kind": "ExecCredential", 612 "apiVersion": "client.authentication.k8s.io/v1alpha1", 613 "status": { 614 "token": "token4", 615 "expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `" 616 } 617 }`) 618 wantToken = "token4" 619 // Old token is expired, should refresh automatically without hitting a 401. 620 get(t, http.StatusOK) 621} 622 623func TestTLSCredentials(t *testing.T) { 624 now := time.Now() 625 626 certPool := x509.NewCertPool() 627 cert, key := genClientCert(t) 628 if !certPool.AppendCertsFromPEM(cert) { 629 t.Fatal("failed to add client cert to CertPool") 630 } 631 632 server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 633 fmt.Fprintln(w, "ok") 634 })) 635 server.TLS = &tls.Config{ 636 ClientAuth: tls.RequireAndVerifyClientCert, 637 ClientCAs: certPool, 638 } 639 server.StartTLS() 640 defer server.Close() 641 642 a, err := newAuthenticator(newCache(), &api.ExecConfig{ 643 Command: "./testdata/test-plugin.sh", 644 APIVersion: "client.authentication.k8s.io/v1alpha1", 645 }) 646 if err != nil { 647 t.Fatal(err) 648 } 649 var output *clientauthentication.ExecCredential 650 a.environ = func() []string { 651 data, err := runtime.Encode(codecs.LegacyCodec(a.group), output) 652 if err != nil { 653 t.Fatal(err) 654 } 655 return []string{"TEST_OUTPUT=" + string(data)} 656 } 657 a.now = func() time.Time { return now } 658 a.stderr = ioutil.Discard 659 660 // We're not interested in server's cert, this test is about client cert. 661 tc := &transport.Config{TLS: transport.TLSConfig{Insecure: true}} 662 if err := a.UpdateTransportConfig(tc); err != nil { 663 t.Fatal(err) 664 } 665 666 get := func(t *testing.T, desc string, wantErr bool) { 667 t.Run(desc, func(t *testing.T) { 668 tlsCfg, err := transport.TLSConfigFor(tc) 669 if err != nil { 670 t.Fatal("TLSConfigFor:", err) 671 } 672 client := http.Client{ 673 Transport: &http.Transport{TLSClientConfig: tlsCfg}, 674 } 675 resp, err := client.Get(server.URL) 676 switch { 677 case err != nil && !wantErr: 678 t.Errorf("got client.Get error: %q, want nil", err) 679 case err == nil && wantErr: 680 t.Error("got nil client.Get error, want non-nil") 681 } 682 if err == nil { 683 resp.Body.Close() 684 } 685 }) 686 } 687 688 output = &clientauthentication.ExecCredential{ 689 Status: &clientauthentication.ExecCredentialStatus{ 690 ClientCertificateData: string(cert), 691 ClientKeyData: string(key), 692 ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, 693 }, 694 } 695 get(t, "valid TLS cert", false) 696 697 // Advance time to force re-exec. 698 nCert, nKey := genClientCert(t) 699 now = now.Add(time.Hour * 2) 700 output = &clientauthentication.ExecCredential{ 701 Status: &clientauthentication.ExecCredentialStatus{ 702 ClientCertificateData: string(nCert), 703 ClientKeyData: string(nKey), 704 ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, 705 }, 706 } 707 get(t, "untrusted TLS cert", true) 708 709 now = now.Add(time.Hour * 2) 710 output = &clientauthentication.ExecCredential{ 711 Status: &clientauthentication.ExecCredentialStatus{ 712 ClientCertificateData: string(cert), 713 ClientKeyData: string(key), 714 ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, 715 }, 716 } 717 get(t, "valid TLS cert again", false) 718} 719 720func TestConcurrentUpdateTransportConfig(t *testing.T) { 721 n := time.Now() 722 now := func() time.Time { return n } 723 724 env := []string{""} 725 environ := func() []string { 726 s := make([]string, len(env)) 727 copy(s, env) 728 return s 729 } 730 731 c := api.ExecConfig{ 732 Command: "./testdata/test-plugin.sh", 733 APIVersion: "client.authentication.k8s.io/v1alpha1", 734 } 735 a, err := newAuthenticator(newCache(), &c) 736 if err != nil { 737 t.Fatal(err) 738 } 739 a.environ = environ 740 a.now = now 741 a.stderr = ioutil.Discard 742 743 stopCh := make(chan struct{}) 744 defer close(stopCh) 745 746 numConcurrent := 2 747 748 for i := 0; i < numConcurrent; i++ { 749 go func() { 750 for { 751 tc := &transport.Config{} 752 a.UpdateTransportConfig(tc) 753 754 select { 755 case <-stopCh: 756 return 757 default: 758 continue 759 } 760 } 761 }() 762 } 763 time.Sleep(2 * time.Second) 764} 765 766// genClientCert generates an x509 certificate for testing. Certificate and key 767// are returned in PEM encoding. The generated cert expires in 24 hours. 768func genClientCert(t *testing.T) ([]byte, []byte) { 769 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 770 if err != nil { 771 t.Fatal(err) 772 } 773 keyRaw, err := x509.MarshalECPrivateKey(key) 774 if err != nil { 775 t.Fatal(err) 776 } 777 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 778 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 779 if err != nil { 780 t.Fatal(err) 781 } 782 cert := &x509.Certificate{ 783 SerialNumber: serialNumber, 784 Subject: pkix.Name{Organization: []string{"Acme Co"}}, 785 NotBefore: time.Now(), 786 NotAfter: time.Now().Add(24 * time.Hour), 787 788 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 789 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 790 BasicConstraintsValid: true, 791 } 792 certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key) 793 if err != nil { 794 t.Fatal(err) 795 } 796 return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}), 797 pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw}) 798} 799