blob: 545097ea453aa9080caeeb25c51cc210f0ef9279 [file] [log] [blame]
/*
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
}