| /* |
| Copyright 2017 The Kubernetes Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package certificate |
| |
| import ( |
| "bytes" |
| "crypto/tls" |
| "crypto/x509" |
| "crypto/x509/pkix" |
| "fmt" |
| "net" |
| "strings" |
| "testing" |
| "time" |
| |
| certificates "k8s.io/api/certificates/v1beta1" |
| "k8s.io/apimachinery/pkg/api/errors" |
| v1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/runtime/schema" |
| "k8s.io/apimachinery/pkg/util/wait" |
| watch "k8s.io/apimachinery/pkg/watch" |
| certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" |
| ) |
| |
| var storeCertData = newCertificateData(`-----BEGIN CERTIFICATE----- |
| MIICRzCCAfGgAwIBAgIJALMb7ecMIk3MMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV |
| BAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRvbjEYMBYGA1UE |
| CgwPR2xvYmFsIFNlY3VyaXR5MRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50MRswGQYD |
| VQQDDBJ0ZXN0LWNlcnRpZmljYXRlLTAwIBcNMTcwNDI2MjMyNjUyWhgPMjExNzA0 |
| MDIyMzI2NTJaMH4xCzAJBgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNV |
| BAcMBkxvbmRvbjEYMBYGA1UECgwPR2xvYmFsIFNlY3VyaXR5MRYwFAYDVQQLDA1J |
| VCBEZXBhcnRtZW50MRswGQYDVQQDDBJ0ZXN0LWNlcnRpZmljYXRlLTAwXDANBgkq |
| hkiG9w0BAQEFAANLADBIAkEAtBMa7NWpv3BVlKTCPGO/LEsguKqWHBtKzweMY2CV |
| tAL1rQm913huhxF9w+ai76KQ3MHK5IVnLJjYYA5MzP2H5QIDAQABo1AwTjAdBgNV |
| HQ4EFgQU22iy8aWkNSxv0nBxFxerfsvnZVMwHwYDVR0jBBgwFoAU22iy8aWkNSxv |
| 0nBxFxerfsvnZVMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAANBAEOefGbV |
| NcHxklaW06w6OBYJPwpIhCVozC1qdxGX1dg8VkEKzjOzjgqVD30m59OFmSlBmHsl |
| nkVA6wyOSDYBf3o= |
| -----END CERTIFICATE-----`, `-----BEGIN RSA PRIVATE KEY----- |
| MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEAtBMa7NWpv3BVlKTC |
| PGO/LEsguKqWHBtKzweMY2CVtAL1rQm913huhxF9w+ai76KQ3MHK5IVnLJjYYA5M |
| zP2H5QIDAQABAkAS9BfXab3OKpK3bIgNNyp+DQJKrZnTJ4Q+OjsqkpXvNltPJosf |
| G8GsiKu/vAt4HGqI3eU77NvRI+mL4MnHRmXBAiEA3qM4FAtKSRBbcJzPxxLEUSwg |
| XSCcosCktbkXvpYrS30CIQDPDxgqlwDEJQ0uKuHkZI38/SPWWqfUmkecwlbpXABK |
| iQIgZX08DA8VfvcA5/Xj1Zjdey9FVY6POLXen6RPiabE97UCICp6eUW7ht+2jjar |
| e35EltCRCjoejRHTuN9TC0uCoVipAiAXaJIx/Q47vGwiw6Y8KXsNU6y54gTbOSxX |
| 54LzHNk/+Q== |
| -----END RSA PRIVATE KEY-----`) |
| var bootstrapCertData = newCertificateData( |
| `-----BEGIN CERTIFICATE----- |
| MIICRzCCAfGgAwIBAgIJANXr+UzRFq4TMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV |
| BAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRvbjEYMBYGA1UE |
| CgwPR2xvYmFsIFNlY3VyaXR5MRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50MRswGQYD |
| VQQDDBJ0ZXN0LWNlcnRpZmljYXRlLTEwIBcNMTcwNDI2MjMyNzMyWhgPMjExNzA0 |
| MDIyMzI3MzJaMH4xCzAJBgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNV |
| BAcMBkxvbmRvbjEYMBYGA1UECgwPR2xvYmFsIFNlY3VyaXR5MRYwFAYDVQQLDA1J |
| VCBEZXBhcnRtZW50MRswGQYDVQQDDBJ0ZXN0LWNlcnRpZmljYXRlLTEwXDANBgkq |
| hkiG9w0BAQEFAANLADBIAkEAqvbkN4RShH1rL37JFp4fZPnn0JUhVWWsrP8NOomJ |
| pXdBDUMGWuEQIsZ1Gf9JrCQLu6ooRyHSKRFpAVbMQ3ABJwIDAQABo1AwTjAdBgNV |
| HQ4EFgQUEGBc6YYheEZ/5MhwqSUYYPYRj2MwHwYDVR0jBBgwFoAUEGBc6YYheEZ/ |
| 5MhwqSUYYPYRj2MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAANBAIyNmznk |
| 5dgJY52FppEEcfQRdS5k4XFPc22SHPcz77AHf5oWZ1WG9VezOZZPp8NCiFDDlDL8 |
| yma33a5eMyTjLD8= |
| -----END CERTIFICATE-----`, `-----BEGIN RSA PRIVATE KEY----- |
| MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqvbkN4RShH1rL37J |
| Fp4fZPnn0JUhVWWsrP8NOomJpXdBDUMGWuEQIsZ1Gf9JrCQLu6ooRyHSKRFpAVbM |
| Q3ABJwIDAQABAkBC2OBpGLMPHN8BJijIUDFkURakBvuOoX+/8MYiYk7QxEmfLCk6 |
| L6r+GLNFMfXwXcBmXtMKfZKAIKutKf098JaBAiEA10azfqt3G/5owrNA00plSyT6 |
| ZmHPzY9Uq1p/QTR/uOcCIQDLTkfBkLHm0UKeobbO/fSm6ZflhyBRDINy4FvwmZMt |
| wQIgYV/tmQJeIh91q3wBepFQOClFykG8CTMoDUol/YyNqUkCIHfp6Rr7fGL3JIMq |
| QQgf9DCK8SPZqq8DYXjdan0kKBJBAiEAyDb+07o2gpggo8BYUKSaiRCiyXfaq87f |
| eVqgpBq/QN4= |
| -----END RSA PRIVATE KEY-----`) |
| var apiServerCertData = newCertificateData( |
| `-----BEGIN CERTIFICATE----- |
| MIICRzCCAfGgAwIBAgIJAIydTIADd+yqMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV |
| BAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRvbjEYMBYGA1UE |
| CgwPR2xvYmFsIFNlY3VyaXR5MRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50MRswGQYD |
| VQQDDBJ0ZXN0LWNlcnRpZmljYXRlLTIwIBcNMTcwNDI2MjMyNDU4WhgPMjExNzA0 |
| MDIyMzI0NThaMH4xCzAJBgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNV |
| BAcMBkxvbmRvbjEYMBYGA1UECgwPR2xvYmFsIFNlY3VyaXR5MRYwFAYDVQQLDA1J |
| VCBEZXBhcnRtZW50MRswGQYDVQQDDBJ0ZXN0LWNlcnRpZmljYXRlLTIwXDANBgkq |
| hkiG9w0BAQEFAANLADBIAkEAuiRet28DV68Dk4A8eqCaqgXmymamUEjW/DxvIQqH |
| 3lbhtm8BwSnS9wUAajSLSWiq3fci2RbRgaSPjUrnbOHCLQIDAQABo1AwTjAdBgNV |
| HQ4EFgQU0vhI4OPGEOqT+VAWwxdhVvcmgdIwHwYDVR0jBBgwFoAU0vhI4OPGEOqT |
| +VAWwxdhVvcmgdIwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAANBALNeJGDe |
| nV5cXbp9W1bC12Tc8nnNXn4ypLE2JTQAvyp51zoZ8hQoSnRVx/VCY55Yu+br8gQZ |
| +tW+O/PoE7B3tuY= |
| -----END CERTIFICATE-----`, `-----BEGIN RSA PRIVATE KEY----- |
| MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEAuiRet28DV68Dk4A8 |
| eqCaqgXmymamUEjW/DxvIQqH3lbhtm8BwSnS9wUAajSLSWiq3fci2RbRgaSPjUrn |
| bOHCLQIDAQABAkEArDR1g9IqD3aUImNikDgAngbzqpAokOGyMoxeavzpEaFOgCzi |
| gi7HF7yHRmZkUt8CzdEvnHSqRjFuaaB0gGA+AQIhAOc8Z1h8ElLRSqaZGgI3jCTp |
| Izx9HNY//U5NGrXD2+ttAiEAzhOqkqI4+nDab7FpiD7MXI6fO549mEXeVBPvPtsS |
| OcECIQCIfkpOm+ZBBpO3JXaJynoqK4gGI6ALA/ik6LSUiIlfPQIhAISjd9hlfZME |
| bDQT1r8Q3Gx+h9LRqQeHgPBQ3F5ylqqBAiBaJ0hkYvrIdWxNlcLqD3065bJpHQ4S |
| WQkuZUQN1M/Xvg== |
| -----END RSA PRIVATE KEY-----`) |
| |
| type certificateData struct { |
| keyPEM []byte |
| certificatePEM []byte |
| certificate *tls.Certificate |
| } |
| |
| func newCertificateData(certificatePEM string, keyPEM string) *certificateData { |
| certificate, err := tls.X509KeyPair([]byte(certificatePEM), []byte(keyPEM)) |
| if err != nil { |
| panic(fmt.Sprintf("Unable to initialize certificate: %v", err)) |
| } |
| certs, err := x509.ParseCertificates(certificate.Certificate[0]) |
| if err != nil { |
| panic(fmt.Sprintf("Unable to initialize certificate leaf: %v", err)) |
| } |
| certificate.Leaf = certs[0] |
| return &certificateData{ |
| keyPEM: []byte(keyPEM), |
| certificatePEM: []byte(certificatePEM), |
| certificate: &certificate, |
| } |
| } |
| |
| func TestNewManagerNoRotation(t *testing.T) { |
| store := &fakeStore{ |
| cert: storeCertData.certificate, |
| } |
| if _, err := NewManager(&Config{ |
| Template: &x509.CertificateRequest{}, |
| Usages: []certificates.KeyUsage{}, |
| CertificateStore: store, |
| }); err != nil { |
| t.Fatalf("Failed to initialize the certificate manager: %v", err) |
| } |
| } |
| |
| type gaugeMock struct { |
| calls int |
| lastValue float64 |
| } |
| |
| func (g *gaugeMock) Set(v float64) { |
| g.calls++ |
| g.lastValue = v |
| } |
| |
| func TestSetRotationDeadline(t *testing.T) { |
| defer func(original func(float64) time.Duration) { jitteryDuration = original }(jitteryDuration) |
| |
| now := time.Now() |
| testCases := []struct { |
| name string |
| notBefore time.Time |
| notAfter time.Time |
| shouldRotate bool |
| }{ |
| {"just issued, still good", now.Add(-1 * time.Hour), now.Add(99 * time.Hour), false}, |
| {"half way expired, still good", now.Add(-24 * time.Hour), now.Add(24 * time.Hour), false}, |
| {"mostly expired, still good", now.Add(-69 * time.Hour), now.Add(31 * time.Hour), false}, |
| {"just about expired, should rotate", now.Add(-91 * time.Hour), now.Add(9 * time.Hour), true}, |
| {"nearly expired, should rotate", now.Add(-99 * time.Hour), now.Add(1 * time.Hour), true}, |
| {"already expired, should rotate", now.Add(-10 * time.Hour), now.Add(-1 * time.Hour), true}, |
| {"long duration", now.Add(-6 * 30 * 24 * time.Hour), now.Add(6 * 30 * 24 * time.Hour), true}, |
| {"short duration", now.Add(-30 * time.Second), now.Add(30 * time.Second), true}, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| g := gaugeMock{} |
| m := manager{ |
| cert: &tls.Certificate{ |
| Leaf: &x509.Certificate{ |
| NotBefore: tc.notBefore, |
| NotAfter: tc.notAfter, |
| }, |
| }, |
| getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, |
| usages: []certificates.KeyUsage{}, |
| certificateExpiration: &g, |
| } |
| jitteryDuration = func(float64) time.Duration { return time.Duration(float64(tc.notAfter.Sub(tc.notBefore)) * 0.7) } |
| lowerBound := tc.notBefore.Add(time.Duration(float64(tc.notAfter.Sub(tc.notBefore)) * 0.7)) |
| |
| deadline := m.nextRotationDeadline() |
| |
| if !deadline.Equal(lowerBound) { |
| t.Errorf("For notBefore %v, notAfter %v, the rotationDeadline %v should be %v.", |
| tc.notBefore, |
| tc.notAfter, |
| deadline, |
| lowerBound) |
| } |
| if g.calls != 1 { |
| t.Errorf("%d metrics were recorded, wanted %d", g.calls, 1) |
| } |
| if g.lastValue != float64(tc.notAfter.Unix()) { |
| t.Errorf("%f value for metric was recorded, wanted %d", g.lastValue, tc.notAfter.Unix()) |
| } |
| }) |
| } |
| } |
| |
| func TestCertSatisfiesTemplate(t *testing.T) { |
| testCases := []struct { |
| name string |
| cert *x509.Certificate |
| template *x509.CertificateRequest |
| shouldSatisfy bool |
| }{ |
| { |
| name: "No certificate, no template", |
| cert: nil, |
| template: nil, |
| shouldSatisfy: false, |
| }, |
| { |
| name: "No certificate", |
| cert: nil, |
| template: &x509.CertificateRequest{}, |
| shouldSatisfy: false, |
| }, |
| { |
| name: "No template", |
| cert: &x509.Certificate{ |
| Subject: pkix.Name{ |
| CommonName: "system:node:fake-node-name", |
| }, |
| }, |
| template: nil, |
| shouldSatisfy: true, |
| }, |
| { |
| name: "Mismatched common name", |
| cert: &x509.Certificate{ |
| Subject: pkix.Name{ |
| CommonName: "system:node:fake-node-name-2", |
| }, |
| }, |
| template: &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| CommonName: "system:node:fake-node-name", |
| }, |
| }, |
| shouldSatisfy: false, |
| }, |
| { |
| name: "Missing orgs in certificate", |
| cert: &x509.Certificate{ |
| Subject: pkix.Name{ |
| Organization: []string{"system:nodes"}, |
| }, |
| }, |
| template: &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| Organization: []string{"system:nodes", "foobar"}, |
| }, |
| }, |
| shouldSatisfy: false, |
| }, |
| { |
| name: "Extra orgs in certificate", |
| cert: &x509.Certificate{ |
| Subject: pkix.Name{ |
| Organization: []string{"system:nodes", "foobar"}, |
| }, |
| }, |
| template: &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| Organization: []string{"system:nodes"}, |
| }, |
| }, |
| shouldSatisfy: true, |
| }, |
| { |
| name: "Missing DNS names in certificate", |
| cert: &x509.Certificate{ |
| Subject: pkix.Name{}, |
| DNSNames: []string{"foo.example.com"}, |
| }, |
| template: &x509.CertificateRequest{ |
| Subject: pkix.Name{}, |
| DNSNames: []string{"foo.example.com", "bar.example.com"}, |
| }, |
| shouldSatisfy: false, |
| }, |
| { |
| name: "Extra DNS names in certificate", |
| cert: &x509.Certificate{ |
| Subject: pkix.Name{}, |
| DNSNames: []string{"foo.example.com", "bar.example.com"}, |
| }, |
| template: &x509.CertificateRequest{ |
| Subject: pkix.Name{}, |
| DNSNames: []string{"foo.example.com"}, |
| }, |
| shouldSatisfy: true, |
| }, |
| { |
| name: "Missing IP addresses in certificate", |
| cert: &x509.Certificate{ |
| Subject: pkix.Name{}, |
| IPAddresses: []net.IP{net.ParseIP("192.168.1.1")}, |
| }, |
| template: &x509.CertificateRequest{ |
| Subject: pkix.Name{}, |
| IPAddresses: []net.IP{net.ParseIP("192.168.1.1"), net.ParseIP("192.168.1.2")}, |
| }, |
| shouldSatisfy: false, |
| }, |
| { |
| name: "Extra IP addresses in certificate", |
| cert: &x509.Certificate{ |
| Subject: pkix.Name{}, |
| IPAddresses: []net.IP{net.ParseIP("192.168.1.1"), net.ParseIP("192.168.1.2")}, |
| }, |
| template: &x509.CertificateRequest{ |
| Subject: pkix.Name{}, |
| IPAddresses: []net.IP{net.ParseIP("192.168.1.1")}, |
| }, |
| shouldSatisfy: true, |
| }, |
| { |
| name: "Matching certificate", |
| cert: &x509.Certificate{ |
| Subject: pkix.Name{ |
| CommonName: "system:node:fake-node-name", |
| Organization: []string{"system:nodes"}, |
| }, |
| DNSNames: []string{"foo.example.com"}, |
| IPAddresses: []net.IP{net.ParseIP("192.168.1.1")}, |
| }, |
| template: &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| CommonName: "system:node:fake-node-name", |
| Organization: []string{"system:nodes"}, |
| }, |
| DNSNames: []string{"foo.example.com"}, |
| IPAddresses: []net.IP{net.ParseIP("192.168.1.1")}, |
| }, |
| shouldSatisfy: true, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| var tlsCert *tls.Certificate |
| |
| if tc.cert != nil { |
| tlsCert = &tls.Certificate{ |
| Leaf: tc.cert, |
| } |
| } |
| |
| m := manager{ |
| cert: tlsCert, |
| getTemplate: func() *x509.CertificateRequest { return tc.template }, |
| } |
| |
| result := m.certSatisfiesTemplate() |
| if result != tc.shouldSatisfy { |
| t.Errorf("cert: %+v, template: %+v, certSatisfiesTemplate returned %v, want %v", m.cert, tc.template, result, tc.shouldSatisfy) |
| } |
| }) |
| } |
| } |
| |
| func TestRotateCertCreateCSRError(t *testing.T) { |
| now := time.Now() |
| m := manager{ |
| cert: &tls.Certificate{ |
| Leaf: &x509.Certificate{ |
| NotBefore: now.Add(-2 * time.Hour), |
| NotAfter: now.Add(-1 * time.Hour), |
| }, |
| }, |
| getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, |
| usages: []certificates.KeyUsage{}, |
| certSigningRequestClient: fakeClient{ |
| failureType: createError, |
| }, |
| } |
| |
| if success, err := m.rotateCerts(); success { |
| t.Errorf("Got success from 'rotateCerts', wanted failure") |
| } else if err != nil { |
| t.Errorf("Got error %v from 'rotateCerts', wanted no error.", err) |
| } |
| } |
| |
| func TestRotateCertWaitingForResultError(t *testing.T) { |
| now := time.Now() |
| m := manager{ |
| cert: &tls.Certificate{ |
| Leaf: &x509.Certificate{ |
| NotBefore: now.Add(-2 * time.Hour), |
| NotAfter: now.Add(-1 * time.Hour), |
| }, |
| }, |
| getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, |
| usages: []certificates.KeyUsage{}, |
| certSigningRequestClient: fakeClient{ |
| failureType: watchError, |
| }, |
| } |
| |
| certificateWaitBackoff = wait.Backoff{Steps: 1} |
| if success, err := m.rotateCerts(); success { |
| t.Errorf("Got success from 'rotateCerts', wanted failure.") |
| } else if err != nil { |
| t.Errorf("Got error %v from 'rotateCerts', wanted no error.", err) |
| } |
| } |
| |
| func TestNewManagerBootstrap(t *testing.T) { |
| store := &fakeStore{} |
| |
| var cm Manager |
| cm, err := NewManager(&Config{ |
| Template: &x509.CertificateRequest{}, |
| Usages: []certificates.KeyUsage{}, |
| CertificateStore: store, |
| BootstrapCertificatePEM: bootstrapCertData.certificatePEM, |
| BootstrapKeyPEM: bootstrapCertData.keyPEM, |
| }) |
| if err != nil { |
| t.Fatalf("Failed to initialize the certificate manager: %v", err) |
| } |
| |
| cert := cm.Current() |
| |
| if cert == nil { |
| t.Errorf("Certificate was nil, expected something.") |
| } |
| if m, ok := cm.(*manager); !ok { |
| t.Errorf("Expected a '*manager' from 'NewManager'") |
| } else if !m.forceRotation { |
| t.Errorf("Expected rotation should happen during bootstrap, but it won't.") |
| } |
| } |
| |
| func TestNewManagerNoBootstrap(t *testing.T) { |
| now := time.Now() |
| cert, err := tls.X509KeyPair(storeCertData.certificatePEM, storeCertData.keyPEM) |
| if err != nil { |
| t.Fatalf("Unable to initialize a certificate: %v", err) |
| } |
| cert.Leaf = &x509.Certificate{ |
| NotBefore: now.Add(-24 * time.Hour), |
| NotAfter: now.Add(24 * time.Hour), |
| } |
| store := &fakeStore{ |
| cert: &cert, |
| } |
| |
| cm, err := NewManager(&Config{ |
| Template: &x509.CertificateRequest{}, |
| Usages: []certificates.KeyUsage{}, |
| CertificateStore: store, |
| BootstrapCertificatePEM: bootstrapCertData.certificatePEM, |
| BootstrapKeyPEM: bootstrapCertData.keyPEM, |
| }) |
| |
| if err != nil { |
| t.Fatalf("Failed to initialize the certificate manager: %v", err) |
| } |
| |
| currentCert := cm.Current() |
| |
| if currentCert == nil { |
| t.Errorf("Certificate was nil, expected something.") |
| } |
| if m, ok := cm.(*manager); !ok { |
| t.Errorf("Expected a '*manager' from 'NewManager'") |
| } else { |
| if m.forceRotation { |
| t.Errorf("Expected rotation should not happen during bootstrap, but it won't.") |
| } |
| } |
| } |
| |
| func TestGetCurrentCertificateOrBootstrap(t *testing.T) { |
| testCases := []struct { |
| description string |
| storeCert *tls.Certificate |
| bootstrapCertData []byte |
| bootstrapKeyData []byte |
| expectedCert *tls.Certificate |
| expectedShouldRotate bool |
| expectedErrMsg string |
| }{ |
| { |
| "return cert from store", |
| storeCertData.certificate, |
| nil, |
| nil, |
| storeCertData.certificate, |
| false, |
| "", |
| }, |
| { |
| "no cert in store and no bootstrap cert", |
| nil, |
| nil, |
| nil, |
| nil, |
| true, |
| "", |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.description, func(t *testing.T) { |
| store := &fakeStore{ |
| cert: tc.storeCert, |
| } |
| |
| certResult, shouldRotate, err := getCurrentCertificateOrBootstrap( |
| store, |
| tc.bootstrapCertData, |
| tc.bootstrapKeyData) |
| if certResult == nil || certResult.Certificate == nil || tc.expectedCert == nil { |
| if certResult != nil && tc.expectedCert != nil { |
| t.Errorf("Got certificate %v, wanted %v", certResult, tc.expectedCert) |
| } |
| } else { |
| if !certificatesEqual(certResult, tc.expectedCert) { |
| t.Errorf("Got certificate %v, wanted %v", certResult, tc.expectedCert) |
| } |
| } |
| if shouldRotate != tc.expectedShouldRotate { |
| t.Errorf("Got shouldRotate %t, wanted %t", shouldRotate, tc.expectedShouldRotate) |
| } |
| if err == nil { |
| if tc.expectedErrMsg != "" { |
| t.Errorf("Got err %v, wanted %q", err, tc.expectedErrMsg) |
| } |
| } else { |
| if tc.expectedErrMsg == "" || !strings.Contains(err.Error(), tc.expectedErrMsg) { |
| t.Errorf("Got err %v, wanted %q", err, tc.expectedErrMsg) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestInitializeCertificateSigningRequestClient(t *testing.T) { |
| var nilCertificate = &certificateData{} |
| testCases := []struct { |
| description string |
| storeCert *certificateData |
| bootstrapCert *certificateData |
| apiCert *certificateData |
| expectedCertBeforeStart *certificateData |
| expectedCertAfterStart *certificateData |
| }{ |
| { |
| description: "No current certificate, no bootstrap certificate", |
| storeCert: nilCertificate, |
| bootstrapCert: nilCertificate, |
| apiCert: apiServerCertData, |
| expectedCertBeforeStart: nilCertificate, |
| expectedCertAfterStart: apiServerCertData, |
| }, |
| { |
| description: "No current certificate, bootstrap certificate", |
| storeCert: nilCertificate, |
| bootstrapCert: bootstrapCertData, |
| apiCert: apiServerCertData, |
| expectedCertBeforeStart: bootstrapCertData, |
| expectedCertAfterStart: apiServerCertData, |
| }, |
| { |
| description: "Current certificate, no bootstrap certificate", |
| storeCert: storeCertData, |
| bootstrapCert: nilCertificate, |
| apiCert: apiServerCertData, |
| expectedCertBeforeStart: storeCertData, |
| expectedCertAfterStart: storeCertData, |
| }, |
| { |
| description: "Current certificate, bootstrap certificate", |
| storeCert: storeCertData, |
| bootstrapCert: bootstrapCertData, |
| apiCert: apiServerCertData, |
| expectedCertBeforeStart: storeCertData, |
| expectedCertAfterStart: storeCertData, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.description, func(t *testing.T) { |
| certificateStore := &fakeStore{ |
| cert: tc.storeCert.certificate, |
| } |
| |
| certificateManager, err := NewManager(&Config{ |
| Template: &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| Organization: []string{"system:nodes"}, |
| CommonName: "system:node:fake-node-name", |
| }, |
| }, |
| Usages: []certificates.KeyUsage{ |
| certificates.UsageDigitalSignature, |
| certificates.UsageKeyEncipherment, |
| certificates.UsageClientAuth, |
| }, |
| CertificateStore: certificateStore, |
| BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM, |
| BootstrapKeyPEM: tc.bootstrapCert.keyPEM, |
| }) |
| if err != nil { |
| t.Errorf("Got %v, wanted no error.", err) |
| } |
| |
| certificate := certificateManager.Current() |
| if !certificatesEqual(certificate, tc.expectedCertBeforeStart.certificate) { |
| t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertBeforeStart.certificate)) |
| } |
| if err := certificateManager.SetCertificateSigningRequestClient(&fakeClient{ |
| certificatePEM: tc.apiCert.certificatePEM, |
| }); err != nil { |
| t.Errorf("Got error %v, expected none.", err) |
| } |
| |
| if m, ok := certificateManager.(*manager); !ok { |
| t.Errorf("Expected a '*manager' from 'NewManager'") |
| } else { |
| if m.forceRotation { |
| if success, err := m.rotateCerts(); !success { |
| t.Errorf("Got failure from 'rotateCerts', wanted success.") |
| } else if err != nil { |
| t.Errorf("Got error %v, expected none.", err) |
| } |
| } |
| } |
| |
| certificate = certificateManager.Current() |
| if !certificatesEqual(certificate, tc.expectedCertAfterStart.certificate) { |
| t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertAfterStart.certificate)) |
| } |
| }) |
| } |
| } |
| |
| func TestInitializeOtherRESTClients(t *testing.T) { |
| var nilCertificate = &certificateData{} |
| testCases := []struct { |
| description string |
| storeCert *certificateData |
| bootstrapCert *certificateData |
| apiCert *certificateData |
| expectedCertBeforeStart *certificateData |
| expectedCertAfterStart *certificateData |
| }{ |
| { |
| description: "No current certificate, no bootstrap certificate", |
| storeCert: nilCertificate, |
| bootstrapCert: nilCertificate, |
| apiCert: apiServerCertData, |
| expectedCertBeforeStart: nilCertificate, |
| expectedCertAfterStart: apiServerCertData, |
| }, |
| { |
| description: "No current certificate, bootstrap certificate", |
| storeCert: nilCertificate, |
| bootstrapCert: bootstrapCertData, |
| apiCert: apiServerCertData, |
| expectedCertBeforeStart: bootstrapCertData, |
| expectedCertAfterStart: apiServerCertData, |
| }, |
| { |
| description: "Current certificate, no bootstrap certificate", |
| storeCert: storeCertData, |
| bootstrapCert: nilCertificate, |
| apiCert: apiServerCertData, |
| expectedCertBeforeStart: storeCertData, |
| expectedCertAfterStart: storeCertData, |
| }, |
| { |
| description: "Current certificate, bootstrap certificate", |
| storeCert: storeCertData, |
| bootstrapCert: bootstrapCertData, |
| apiCert: apiServerCertData, |
| expectedCertBeforeStart: storeCertData, |
| expectedCertAfterStart: storeCertData, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.description, func(t *testing.T) { |
| certificateStore := &fakeStore{ |
| cert: tc.storeCert.certificate, |
| } |
| |
| certificateManager, err := NewManager(&Config{ |
| Template: &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| Organization: []string{"system:nodes"}, |
| CommonName: "system:node:fake-node-name", |
| }, |
| }, |
| Usages: []certificates.KeyUsage{ |
| certificates.UsageDigitalSignature, |
| certificates.UsageKeyEncipherment, |
| certificates.UsageClientAuth, |
| }, |
| CertificateStore: certificateStore, |
| BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM, |
| BootstrapKeyPEM: tc.bootstrapCert.keyPEM, |
| CertificateSigningRequestClient: &fakeClient{ |
| certificatePEM: tc.apiCert.certificatePEM, |
| }, |
| }) |
| if err != nil { |
| t.Errorf("Got %v, wanted no error.", err) |
| } |
| |
| certificate := certificateManager.Current() |
| if !certificatesEqual(certificate, tc.expectedCertBeforeStart.certificate) { |
| t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertBeforeStart.certificate)) |
| } |
| |
| if m, ok := certificateManager.(*manager); !ok { |
| t.Errorf("Expected a '*manager' from 'NewManager'") |
| } else { |
| if m.forceRotation { |
| success, err := certificateManager.(*manager).rotateCerts() |
| if err != nil { |
| t.Errorf("Got error %v, expected none.", err) |
| return |
| } |
| if !success { |
| t.Errorf("Unexpected response 'rotateCerts': %t", success) |
| return |
| } |
| } |
| } |
| |
| certificate = certificateManager.Current() |
| if !certificatesEqual(certificate, tc.expectedCertAfterStart.certificate) { |
| t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertAfterStart.certificate)) |
| } |
| }) |
| } |
| } |
| |
| func TestServerHealth(t *testing.T) { |
| type certs struct { |
| storeCert *certificateData |
| bootstrapCert *certificateData |
| apiCert *certificateData |
| expectedCertBeforeStart *certificateData |
| expectedCertAfterStart *certificateData |
| } |
| |
| updatedCerts := certs{ |
| storeCert: storeCertData, |
| bootstrapCert: bootstrapCertData, |
| apiCert: apiServerCertData, |
| expectedCertBeforeStart: storeCertData, |
| expectedCertAfterStart: apiServerCertData, |
| } |
| |
| currentCerts := certs{ |
| storeCert: storeCertData, |
| bootstrapCert: bootstrapCertData, |
| apiCert: apiServerCertData, |
| expectedCertBeforeStart: storeCertData, |
| expectedCertAfterStart: storeCertData, |
| } |
| |
| testCases := []struct { |
| description string |
| certs |
| |
| failureType fakeClientFailureType |
| clientErr error |
| |
| expectRotateFail bool |
| expectHealthy bool |
| }{ |
| { |
| description: "Current certificate, bootstrap certificate", |
| certs: updatedCerts, |
| expectHealthy: true, |
| }, |
| { |
| description: "Generic error on create", |
| certs: currentCerts, |
| |
| failureType: createError, |
| expectRotateFail: true, |
| }, |
| { |
| description: "Unauthorized error on create", |
| certs: currentCerts, |
| |
| failureType: createError, |
| clientErr: errors.NewUnauthorized("unauthorized"), |
| expectRotateFail: true, |
| expectHealthy: true, |
| }, |
| { |
| description: "Generic unauthorized error on create", |
| certs: currentCerts, |
| |
| failureType: createError, |
| clientErr: errors.NewGenericServerResponse(401, "POST", schema.GroupResource{}, "", "", 0, true), |
| expectRotateFail: true, |
| expectHealthy: true, |
| }, |
| { |
| description: "Generic not found error on create", |
| certs: currentCerts, |
| |
| failureType: createError, |
| clientErr: errors.NewGenericServerResponse(404, "POST", schema.GroupResource{}, "", "", 0, true), |
| expectRotateFail: true, |
| expectHealthy: false, |
| }, |
| { |
| description: "Not found error on create", |
| certs: currentCerts, |
| |
| failureType: createError, |
| clientErr: errors.NewGenericServerResponse(404, "POST", schema.GroupResource{}, "", "", 0, false), |
| expectRotateFail: true, |
| expectHealthy: true, |
| }, |
| { |
| description: "Conflict error on watch", |
| certs: currentCerts, |
| |
| failureType: watchError, |
| clientErr: errors.NewGenericServerResponse(409, "POST", schema.GroupResource{}, "", "", 0, false), |
| expectRotateFail: true, |
| expectHealthy: false, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.description, func(t *testing.T) { |
| certificateStore := &fakeStore{ |
| cert: tc.storeCert.certificate, |
| } |
| |
| certificateManager, err := NewManager(&Config{ |
| Template: &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| Organization: []string{"system:nodes"}, |
| CommonName: "system:node:fake-node-name", |
| }, |
| }, |
| Usages: []certificates.KeyUsage{ |
| certificates.UsageDigitalSignature, |
| certificates.UsageKeyEncipherment, |
| certificates.UsageClientAuth, |
| }, |
| CertificateStore: certificateStore, |
| BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM, |
| BootstrapKeyPEM: tc.bootstrapCert.keyPEM, |
| CertificateSigningRequestClient: &fakeClient{ |
| certificatePEM: tc.apiCert.certificatePEM, |
| failureType: tc.failureType, |
| err: tc.clientErr, |
| }, |
| }) |
| if err != nil { |
| t.Errorf("Got %v, wanted no error.", err) |
| } |
| |
| certificate := certificateManager.Current() |
| if !certificatesEqual(certificate, tc.expectedCertBeforeStart.certificate) { |
| t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertBeforeStart.certificate)) |
| } |
| |
| if _, ok := certificateManager.(*manager); !ok { |
| t.Errorf("Expected a '*manager' from 'NewManager'") |
| } else { |
| success, err := certificateManager.(*manager).rotateCerts() |
| if err != nil { |
| t.Errorf("Got error %v, expected none.", err) |
| return |
| } |
| if !success != tc.expectRotateFail { |
| t.Errorf("Unexpected response 'rotateCerts': %t", success) |
| return |
| } |
| if actual := certificateManager.(*manager).ServerHealthy(); actual != tc.expectHealthy { |
| t.Errorf("Unexpected manager server health: %t", actual) |
| } |
| } |
| |
| certificate = certificateManager.Current() |
| if !certificatesEqual(certificate, tc.expectedCertAfterStart.certificate) { |
| t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertAfterStart.certificate)) |
| } |
| }) |
| } |
| } |
| |
| type fakeClientFailureType int |
| |
| const ( |
| none fakeClientFailureType = iota |
| createError |
| watchError |
| certificateSigningRequestDenied |
| ) |
| |
| type fakeClient struct { |
| certificatesclient.CertificateSigningRequestInterface |
| failureType fakeClientFailureType |
| certificatePEM []byte |
| err error |
| } |
| |
| func (c fakeClient) List(opts v1.ListOptions) (*certificates.CertificateSigningRequestList, error) { |
| if c.failureType == watchError { |
| if c.err != nil { |
| return nil, c.err |
| } |
| return nil, fmt.Errorf("Watch error") |
| } |
| csrReply := certificates.CertificateSigningRequestList{ |
| Items: []certificates.CertificateSigningRequest{ |
| {ObjectMeta: v1.ObjectMeta{UID: "fake-uid"}}, |
| }, |
| } |
| return &csrReply, nil |
| } |
| |
| func (c fakeClient) Create(*certificates.CertificateSigningRequest) (*certificates.CertificateSigningRequest, error) { |
| if c.failureType == createError { |
| if c.err != nil { |
| return nil, c.err |
| } |
| return nil, fmt.Errorf("Create error") |
| } |
| csrReply := certificates.CertificateSigningRequest{} |
| csrReply.UID = "fake-uid" |
| return &csrReply, nil |
| } |
| |
| func (c fakeClient) Watch(opts v1.ListOptions) (watch.Interface, error) { |
| if c.failureType == watchError { |
| if c.err != nil { |
| return nil, c.err |
| } |
| return nil, fmt.Errorf("Watch error") |
| } |
| return &fakeWatch{ |
| failureType: c.failureType, |
| certificatePEM: c.certificatePEM, |
| }, nil |
| } |
| |
| type fakeWatch struct { |
| failureType fakeClientFailureType |
| certificatePEM []byte |
| } |
| |
| func (w *fakeWatch) Stop() { |
| } |
| |
| func (w *fakeWatch) ResultChan() <-chan watch.Event { |
| var condition certificates.CertificateSigningRequestCondition |
| if w.failureType == certificateSigningRequestDenied { |
| condition = certificates.CertificateSigningRequestCondition{ |
| Type: certificates.CertificateDenied, |
| } |
| } else { |
| condition = certificates.CertificateSigningRequestCondition{ |
| Type: certificates.CertificateApproved, |
| } |
| } |
| |
| csr := certificates.CertificateSigningRequest{ |
| Status: certificates.CertificateSigningRequestStatus{ |
| Conditions: []certificates.CertificateSigningRequestCondition{ |
| condition, |
| }, |
| Certificate: []byte(w.certificatePEM), |
| }, |
| } |
| csr.UID = "fake-uid" |
| |
| c := make(chan watch.Event, 1) |
| c <- watch.Event{ |
| Type: watch.Added, |
| Object: &csr, |
| } |
| return c |
| } |
| |
| type fakeStore struct { |
| cert *tls.Certificate |
| } |
| |
| func (s *fakeStore) Current() (*tls.Certificate, error) { |
| if s.cert == nil { |
| noKeyErr := NoCertKeyError("") |
| return nil, &noKeyErr |
| } |
| return s.cert, nil |
| } |
| |
| // Accepts the PEM data for the cert/key pair and makes the new cert/key |
| // pair the 'current' pair, that will be returned by future calls to |
| // Current(). |
| func (s *fakeStore) Update(certPEM, keyPEM []byte) (*tls.Certificate, error) { |
| // In order to make the mocking work, whenever a cert/key pair is passed in |
| // to be updated in the mock store, assume that the certificate manager |
| // generated the key, and then asked the mock CertificateSigningRequest API |
| // to sign it, then the faked API returned a canned response. The canned |
| // signing response will not match the generated key. In order to make |
| // things work out, search here for the correct matching key and use that |
| // instead of the passed in key. That way this file of test code doesn't |
| // have to implement an actual certificate signing process. |
| for _, tc := range []*certificateData{storeCertData, bootstrapCertData, apiServerCertData} { |
| if bytes.Equal(tc.certificatePEM, certPEM) { |
| keyPEM = tc.keyPEM |
| } |
| } |
| cert, err := tls.X509KeyPair(certPEM, keyPEM) |
| if err != nil { |
| return nil, err |
| } |
| now := time.Now() |
| s.cert = &cert |
| s.cert.Leaf = &x509.Certificate{ |
| NotBefore: now.Add(-24 * time.Hour), |
| NotAfter: now.Add(24 * time.Hour), |
| } |
| return s.cert, nil |
| } |
| |
| func certificatesEqual(c1 *tls.Certificate, c2 *tls.Certificate) bool { |
| if c1 == nil || c2 == nil { |
| return c1 == c2 |
| } |
| if len(c1.Certificate) != len(c2.Certificate) { |
| return false |
| } |
| for i := 0; i < len(c1.Certificate); i++ { |
| if !bytes.Equal(c1.Certificate[i], c2.Certificate[i]) { |
| return false |
| } |
| } |
| return true |
| } |
| |
| func certificateString(c *tls.Certificate) string { |
| if c == nil { |
| return "certificate == nil" |
| } |
| if c.Leaf == nil { |
| return "certificate.Leaf == nil" |
| } |
| return c.Leaf.Subject.CommonName |
| } |