1
16
17 package signer
18
19 import (
20 "context"
21 "crypto/ecdsa"
22 "crypto/elliptic"
23 "crypto/x509"
24 "crypto/x509/pkix"
25 "encoding/pem"
26 "io/ioutil"
27 "math/rand"
28 "testing"
29 "time"
30
31 "github.com/google/go-cmp/cmp"
32
33 capi "k8s.io/api/certificates/v1"
34 "k8s.io/apimachinery/pkg/util/diff"
35 "k8s.io/client-go/kubernetes/fake"
36 testclient "k8s.io/client-go/testing"
37 "k8s.io/client-go/util/cert"
38 "k8s.io/client-go/util/certificate/csr"
39 capihelper "k8s.io/kubernetes/pkg/apis/certificates/v1"
40 "k8s.io/kubernetes/pkg/controller/certificates"
41 testingclock "k8s.io/utils/clock/testing"
42 )
43
44 func TestSigner(t *testing.T) {
45 fakeClock := testingclock.FakeClock{}
46
47 s, err := newSigner("kubernetes.io/legacy-unknown", "./testdata/ca.crt", "./testdata/ca.key", nil, 1*time.Hour)
48 if err != nil {
49 t.Fatalf("failed to create signer: %v", err)
50 }
51
52 csrb, err := ioutil.ReadFile("./testdata/kubelet.csr")
53 if err != nil {
54 t.Fatalf("failed to read CSR: %v", err)
55 }
56 x509cr, err := capihelper.ParseCSR(csrb)
57 if err != nil {
58 t.Fatalf("failed to parse CSR: %v", err)
59 }
60
61 certData, err := s.sign(x509cr, []capi.KeyUsage{
62 capi.UsageSigning,
63 capi.UsageKeyEncipherment,
64 capi.UsageServerAuth,
65 capi.UsageClientAuth,
66 },
67
68 csr.DurationToExpirationSeconds(3*time.Hour),
69 fakeClock.Now,
70 )
71 if err != nil {
72 t.Fatalf("failed to sign CSR: %v", err)
73 }
74 if len(certData) == 0 {
75 t.Fatalf("expected a certificate after signing")
76 }
77
78 certs, err := cert.ParseCertsPEM(certData)
79 if err != nil {
80 t.Fatalf("failed to parse certificate: %v", err)
81 }
82 if len(certs) != 1 {
83 t.Fatalf("expected one certificate")
84 }
85
86 want := x509.Certificate{
87 Version: 3,
88 Subject: pkix.Name{
89 CommonName: "system:node:k-a-node-s36b",
90 Organization: []string{"system:nodes"},
91 },
92 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
93 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
94 BasicConstraintsValid: true,
95 NotBefore: fakeClock.Now().Add(-5 * time.Minute),
96 NotAfter: fakeClock.Now().Add(1 * time.Hour),
97 PublicKeyAlgorithm: x509.ECDSA,
98 SignatureAlgorithm: x509.SHA256WithRSA,
99 MaxPathLen: -1,
100 }
101
102 if !cmp.Equal(*certs[0], want, diff.IgnoreUnset()) {
103 t.Errorf("unexpected diff: %v", cmp.Diff(certs[0], want, diff.IgnoreUnset()))
104 }
105 }
106
107 func TestHandle(t *testing.T) {
108 cases := []struct {
109 name string
110
111 commonName string
112 dnsNames []string
113 org []string
114 usages []capi.KeyUsage
115
116 approved bool
117
118 failed bool
119
120 signerName string
121
122 err bool
123
124 constructionErr bool
125
126 verify func(*testing.T, []testclient.Action)
127 }{
128 {
129 name: "should sign if signerName is kubernetes.io/kube-apiserver-client",
130 signerName: "kubernetes.io/kube-apiserver-client",
131 commonName: "hello-world",
132 org: []string{"some-org"},
133 usages: []capi.KeyUsage{capi.UsageClientAuth, capi.UsageDigitalSignature, capi.UsageKeyEncipherment},
134 approved: true,
135 verify: func(t *testing.T, as []testclient.Action) {
136 if len(as) != 1 {
137 t.Errorf("expected one Update action but got %d", len(as))
138 return
139 }
140 csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
141 if len(csr.Status.Certificate) == 0 {
142 t.Errorf("expected certificate to be issued but it was not")
143 }
144 },
145 },
146 {
147 name: "should sign without key encipherment if signerName is kubernetes.io/kube-apiserver-client",
148 signerName: "kubernetes.io/kube-apiserver-client",
149 commonName: "hello-world",
150 org: []string{"some-org"},
151 usages: []capi.KeyUsage{capi.UsageClientAuth, capi.UsageDigitalSignature},
152 approved: true,
153 verify: func(t *testing.T, as []testclient.Action) {
154 if len(as) != 1 {
155 t.Errorf("expected one Update action but got %d", len(as))
156 return
157 }
158 csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
159 if len(csr.Status.Certificate) == 0 {
160 t.Errorf("expected certificate to be issued but it was not")
161 }
162 },
163 },
164 {
165 name: "should refuse to sign if signerName is kubernetes.io/kube-apiserver-client and contains an unexpected usage",
166 signerName: "kubernetes.io/kube-apiserver-client",
167 commonName: "hello-world",
168 org: []string{"some-org"},
169 usages: []capi.KeyUsage{capi.UsageServerAuth, capi.UsageClientAuth, capi.UsageDigitalSignature, capi.UsageKeyEncipherment},
170 approved: true,
171 verify: func(t *testing.T, as []testclient.Action) {
172 if len(as) != 1 {
173 t.Errorf("expected one Update action but got %d", len(as))
174 return
175 }
176 csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
177 if len(csr.Status.Certificate) != 0 {
178 t.Errorf("expected no certificate to be issued")
179 }
180 if !certificates.HasTrueCondition(csr, capi.CertificateFailed) {
181 t.Errorf("expected Failed condition")
182 }
183 },
184 },
185 {
186 name: "should sign if signerName is kubernetes.io/kube-apiserver-client-kubelet",
187 signerName: "kubernetes.io/kube-apiserver-client-kubelet",
188 commonName: "system:node:hello-world",
189 org: []string{"system:nodes"},
190 usages: []capi.KeyUsage{capi.UsageClientAuth, capi.UsageDigitalSignature, capi.UsageKeyEncipherment},
191 approved: true,
192 verify: func(t *testing.T, as []testclient.Action) {
193 if len(as) != 1 {
194 t.Errorf("expected one Update action but got %d", len(as))
195 return
196 }
197 csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
198 if len(csr.Status.Certificate) == 0 {
199 t.Errorf("expected certificate to be issued but it was not")
200 }
201 },
202 },
203 {
204 name: "should sign without usage key encipherment if signerName is kubernetes.io/kube-apiserver-client-kubelet",
205 signerName: "kubernetes.io/kube-apiserver-client-kubelet",
206 commonName: "system:node:hello-world",
207 org: []string{"system:nodes"},
208 usages: []capi.KeyUsage{capi.UsageClientAuth, capi.UsageDigitalSignature},
209 approved: true,
210 verify: func(t *testing.T, as []testclient.Action) {
211 if len(as) != 1 {
212 t.Errorf("expected one Update action but got %d", len(as))
213 return
214 }
215 csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
216 if len(csr.Status.Certificate) == 0 {
217 t.Errorf("expected certificate to be issued but it was not")
218 }
219 },
220 },
221 {
222 name: "should sign if signerName is kubernetes.io/legacy-unknown",
223 signerName: "kubernetes.io/legacy-unknown",
224 approved: true,
225 verify: func(t *testing.T, as []testclient.Action) {
226 if len(as) != 1 {
227 t.Errorf("expected one Update action but got %d", len(as))
228 return
229 }
230 csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
231 if len(csr.Status.Certificate) == 0 {
232 t.Errorf("expected certificate to be issued but it was not")
233 }
234 },
235 },
236 {
237 name: "should sign if signerName is kubernetes.io/kubelet-serving",
238 signerName: "kubernetes.io/kubelet-serving",
239 commonName: "system:node:testnode",
240 org: []string{"system:nodes"},
241 usages: []capi.KeyUsage{capi.UsageServerAuth, capi.UsageDigitalSignature, capi.UsageKeyEncipherment},
242 dnsNames: []string{"example.com"},
243 approved: true,
244 verify: func(t *testing.T, as []testclient.Action) {
245 if len(as) != 1 {
246 t.Errorf("expected one Update action but got %d", len(as))
247 return
248 }
249 csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
250 if len(csr.Status.Certificate) == 0 {
251 t.Errorf("expected certificate to be issued but it was not")
252 }
253 },
254 },
255 {
256 name: "should sign without usage key encipherment if signerName is kubernetes.io/kubelet-serving",
257 signerName: "kubernetes.io/kubelet-serving",
258 commonName: "system:node:testnode",
259 org: []string{"system:nodes"},
260 usages: []capi.KeyUsage{capi.UsageServerAuth, capi.UsageDigitalSignature},
261 dnsNames: []string{"example.com"},
262 approved: true,
263 verify: func(t *testing.T, as []testclient.Action) {
264 if len(as) != 1 {
265 t.Errorf("expected one Update action but got %d", len(as))
266 return
267 }
268 csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest)
269 if len(csr.Status.Certificate) == 0 {
270 t.Errorf("expected certificate to be issued but it was not")
271 }
272 },
273 },
274 {
275 name: "should do nothing if failed",
276 signerName: "kubernetes.io/kubelet-serving",
277 commonName: "system:node:testnode",
278 org: []string{"system:nodes"},
279 usages: []capi.KeyUsage{capi.UsageServerAuth, capi.UsageDigitalSignature, capi.UsageKeyEncipherment},
280 dnsNames: []string{"example.com"},
281 approved: true,
282 failed: true,
283 verify: func(t *testing.T, as []testclient.Action) {
284 if len(as) != 0 {
285 t.Errorf("expected no action to be taken")
286 }
287 },
288 },
289 {
290 name: "should do nothing if an unrecognised signerName is used",
291 signerName: "kubernetes.io/not-recognised",
292 constructionErr: true,
293 approved: true,
294 verify: func(t *testing.T, as []testclient.Action) {
295 if len(as) != 0 {
296 t.Errorf("expected no action to be taken")
297 }
298 },
299 },
300 {
301 name: "should do nothing if not approved",
302 signerName: "kubernetes.io/kubelet-serving",
303 verify: func(t *testing.T, as []testclient.Action) {
304 if len(as) != 0 {
305 t.Errorf("expected no action to be taken")
306 }
307 },
308 },
309 {
310 name: "should do nothing if signerName does not start with kubernetes.io",
311 signerName: "example.com/sample-name",
312 constructionErr: true,
313 approved: true,
314 verify: func(t *testing.T, as []testclient.Action) {
315 if len(as) != 0 {
316 t.Errorf("expected no action to be taken")
317 }
318 },
319 },
320 {
321 name: "should do nothing if signerName starts with kubernetes.io but is unrecognised",
322 signerName: "kubernetes.io/not-a-real-signer",
323 constructionErr: true,
324 approved: true,
325 verify: func(t *testing.T, as []testclient.Action) {
326 if len(as) != 0 {
327 t.Errorf("expected no action to be taken")
328 }
329 },
330 },
331 }
332
333 for _, c := range cases {
334 t.Run(c.name, func(t *testing.T) {
335 client := &fake.Clientset{}
336 s, err := newSigner(c.signerName, "./testdata/ca.crt", "./testdata/ca.key", client, 1*time.Hour)
337 switch {
338 case c.constructionErr && err != nil:
339 return
340 case c.constructionErr && err == nil:
341 t.Fatalf("expected failure during construction of controller")
342 case !c.constructionErr && err != nil:
343 t.Fatalf("failed to create signer: %v", err)
344
345 case !c.constructionErr && err == nil:
346
347 }
348
349 csr := makeTestCSR(csrBuilder{cn: c.commonName, signerName: c.signerName, approved: c.approved, failed: c.failed, usages: c.usages, org: c.org, dnsNames: c.dnsNames})
350 ctx := context.TODO()
351 if err := s.handle(ctx, csr); err != nil && !c.err {
352 t.Errorf("unexpected err: %v", err)
353 }
354 c.verify(t, client.Actions())
355 })
356 }
357 }
358
359
360
361 var insecureRand = rand.New(rand.NewSource(0))
362
363 type csrBuilder struct {
364 cn string
365 dnsNames []string
366 org []string
367 signerName string
368 approved bool
369 failed bool
370 usages []capi.KeyUsage
371 }
372
373 func makeTestCSR(b csrBuilder) *capi.CertificateSigningRequest {
374 pk, err := ecdsa.GenerateKey(elliptic.P256(), insecureRand)
375 if err != nil {
376 panic(err)
377 }
378 csrb, err := x509.CreateCertificateRequest(insecureRand, &x509.CertificateRequest{
379 Subject: pkix.Name{
380 CommonName: b.cn,
381 Organization: b.org,
382 },
383 DNSNames: b.dnsNames,
384 }, pk)
385 if err != nil {
386 panic(err)
387 }
388 csr := &capi.CertificateSigningRequest{
389 Spec: capi.CertificateSigningRequestSpec{
390 Request: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrb}),
391 Usages: b.usages,
392 },
393 }
394 if b.signerName != "" {
395 csr.Spec.SignerName = b.signerName
396 }
397 if b.approved {
398 csr.Status.Conditions = append(csr.Status.Conditions, capi.CertificateSigningRequestCondition{
399 Type: capi.CertificateApproved,
400 })
401 }
402 if b.failed {
403 csr.Status.Conditions = append(csr.Status.Conditions, capi.CertificateSigningRequestCondition{
404 Type: capi.CertificateFailed,
405 })
406 }
407 return csr
408 }
409
410 func Test_signer_duration(t *testing.T) {
411 t.Parallel()
412
413 tests := []struct {
414 name string
415 certTTL time.Duration
416 expirationSeconds *int32
417 want time.Duration
418 }{
419 {
420 name: "can request shorter duration than TTL",
421 certTTL: time.Hour,
422 expirationSeconds: csr.DurationToExpirationSeconds(30 * time.Minute),
423 want: 30 * time.Minute,
424 },
425 {
426 name: "cannot request longer duration than TTL",
427 certTTL: time.Hour,
428 expirationSeconds: csr.DurationToExpirationSeconds(3 * time.Hour),
429 want: time.Hour,
430 },
431 {
432 name: "cannot request negative duration",
433 certTTL: time.Hour,
434 expirationSeconds: csr.DurationToExpirationSeconds(-time.Minute),
435 want: 10 * time.Minute,
436 },
437 {
438 name: "cannot request duration less than 10 mins",
439 certTTL: time.Hour,
440 expirationSeconds: csr.DurationToExpirationSeconds(10*time.Minute - time.Second),
441 want: 10 * time.Minute,
442 },
443 {
444 name: "can request duration of exactly 10 mins",
445 certTTL: time.Hour,
446 expirationSeconds: csr.DurationToExpirationSeconds(10 * time.Minute),
447 want: 10 * time.Minute,
448 },
449 {
450 name: "can request duration equal to the default",
451 certTTL: time.Hour,
452 expirationSeconds: csr.DurationToExpirationSeconds(time.Hour),
453 want: time.Hour,
454 },
455 {
456 name: "can choose not to request a duration to get the default",
457 certTTL: time.Hour,
458 expirationSeconds: nil,
459 want: time.Hour,
460 },
461 }
462 for _, tt := range tests {
463 tt := tt
464
465 t.Run(tt.name, func(t *testing.T) {
466 t.Parallel()
467
468 s := &signer{
469 certTTL: tt.certTTL,
470 }
471 if got := s.duration(tt.expirationSeconds); got != tt.want {
472 t.Errorf("duration() = %v, want %v", got, tt.want)
473 }
474 })
475 }
476 }
477
View as plain text