k8s.io/kubernetes@v1.29.3/pkg/controller/certificates/signer/signer_test.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 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 // requesting a duration that is greater than TTL is ignored 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 // parameters to be set on the generated CSR 111 commonName string 112 dnsNames []string 113 org []string 114 usages []capi.KeyUsage 115 // whether the generated CSR should be marked as approved 116 approved bool 117 // whether the generated CSR should be marked as failed 118 failed bool 119 // the signerName to be set on the generated CSR 120 signerName string 121 // if true, expect an error to be returned 122 err bool 123 // if true, expect an error to be returned during construction 124 constructionErr bool 125 // additional verification function 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 // continue with rest of test 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 // noncryptographic for faster testing 360 // DO NOT COPY THIS CODE 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 }