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