From a53f478d21928f572d0ee08b8cdbe7531d35f459 Mon Sep 17 00:00:00 2001 From: liz Date: Mon, 27 Aug 2018 16:27:14 -0400 Subject: [PATCH] Two implmentations of cert renewal --- .../app/phases/certs/renewal/certsapi.go | 151 ++++++++++++++++++ .../app/phases/certs/renewal/filerenewal.go | 51 ++++++ .../phases/certs/renewal/filerenewal_test.go | 61 +++++++ .../app/phases/certs/renewal/interface.go | 29 ++++ .../app/phases/certs/renewal/renewal_test.go | 133 +++++++++++++++ 5 files changed, 425 insertions(+) create mode 100644 cmd/kubeadm/app/phases/certs/renewal/certsapi.go create mode 100644 cmd/kubeadm/app/phases/certs/renewal/filerenewal.go create mode 100644 cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go create mode 100644 cmd/kubeadm/app/phases/certs/renewal/interface.go create mode 100644 cmd/kubeadm/app/phases/certs/renewal/renewal_test.go diff --git a/cmd/kubeadm/app/phases/certs/renewal/certsapi.go b/cmd/kubeadm/app/phases/certs/renewal/certsapi.go new file mode 100644 index 00000000000..d7efc1936e5 --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/certsapi.go @@ -0,0 +1,151 @@ +/* +Copyright 2018 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 renewal + +import ( + "crypto" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "errors" + "fmt" + "time" + + certsapi "k8s.io/api/certificates/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + certstype "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" + certutil "k8s.io/client-go/util/cert" +) + +const ( + certAPIPrefixName = "kubeadm-cert" +) + +var ( + watchTimeout = 5 * time.Minute +) + +// CertsAPIRenewal creates new certificates using the certs API +type CertsAPIRenewal struct { + client certstype.CertificatesV1beta1Interface +} + +// NewCertsAPIRenawal takes a certificate pair to construct the Interface. +func NewCertsAPIRenawal(client kubernetes.Interface) Interface { + return &CertsAPIRenewal{ + client: client.CertificatesV1beta1(), + } +} + +// Renew takes a certificate using the cert and key. +func (r *CertsAPIRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.PrivateKey, error) { + reqTmp := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: cfg.CommonName, + Organization: cfg.Organization, + }, + DNSNames: cfg.AltNames.DNSNames, + IPAddresses: cfg.AltNames.IPs, + } + + key, err := certutil.NewPrivateKey() + if err != nil { + return nil, nil, fmt.Errorf("Couldn't create new private key: %v", err) + } + + csr, err := x509.CreateCertificateRequest(rand.Reader, reqTmp, key) + if err != nil { + return nil, nil, fmt.Errorf("Couldn't create csr: %v", err) + } + + usages := make([]certsapi.KeyUsage, len(cfg.Usages)) + for i, usage := range cfg.Usages { + certsAPIUsage, ok := usageMap[usage] + if !ok { + return nil, nil, fmt.Errorf("unknown key usage %v", usage) + } + usages[i] = certsAPIUsage + } + + k8sCSR := &certsapi.CertificateSigningRequest{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: certAPIPrefixName, + }, + Spec: certsapi.CertificateSigningRequestSpec{ + Request: csr, + Usages: usages, + }, + } + + req, err := r.client.CertificateSigningRequests().Create(k8sCSR) + if err != nil { + return nil, nil, fmt.Errorf("couldn't create certificate signing request: %v", err) + } + + watcher, err := r.client.CertificateSigningRequests().Watch(metav1.ListOptions{ + Watch: true, + FieldSelector: fields.Set{"metadata.name": req.Name}.String(), + }) + if err != nil { + return nil, nil, fmt.Errorf("couldn't watch for certificate creation: %v", err) + } + defer watcher.Stop() + + select { + case ev := <-watcher.ResultChan(): + if ev.Type != watch.Modified { + return nil, nil, fmt.Errorf("unexpected event receieved: %q", ev.Type) + } + case <-time.After(watchTimeout): + return nil, nil, errors.New("timeout trying to sign certificate") + } + + req, err = r.client.CertificateSigningRequests().Get(req.Name, metav1.GetOptions{}) + if len(req.Status.Conditions) < 1 { + return nil, nil, errors.New("certificate signing request has no statuses") + } + + // TODO: under what circumstances are there more than one? + if status := req.Status.Conditions[0].Type; status != certsapi.CertificateApproved { + return nil, nil, fmt.Errorf("Unexpected certificate status %v", status) + } + + cert, err := x509.ParseCertificate(req.Status.Certificate) + if err != nil { + return nil, nil, fmt.Errorf("couldn't parse issued certificate: %v", err) + } + + return cert, key, nil +} + +var usageMap = map[x509.ExtKeyUsage]certsapi.KeyUsage{ + x509.ExtKeyUsageAny: certsapi.UsageAny, + x509.ExtKeyUsageServerAuth: certsapi.UsageServerAuth, + x509.ExtKeyUsageClientAuth: certsapi.UsageClientAuth, + x509.ExtKeyUsageCodeSigning: certsapi.UsageCodeSigning, + x509.ExtKeyUsageEmailProtection: certsapi.UsageEmailProtection, + x509.ExtKeyUsageIPSECEndSystem: certsapi.UsageIPsecEndSystem, + x509.ExtKeyUsageIPSECTunnel: certsapi.UsageIPsecTunnel, + x509.ExtKeyUsageIPSECUser: certsapi.UsageIPsecUser, + x509.ExtKeyUsageTimeStamping: certsapi.UsageTimestamping, + x509.ExtKeyUsageOCSPSigning: certsapi.UsageOCSPSigning, + x509.ExtKeyUsageMicrosoftServerGatedCrypto: certsapi.UsageMicrosoftSGC, + x509.ExtKeyUsageNetscapeServerGatedCrypto: certsapi.UsageNetscapSGC, +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go b/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go new file mode 100644 index 00000000000..bdaa95a8d8b --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/filerenewal.go @@ -0,0 +1,51 @@ +/* +Copyright 2018 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 renewal + +import ( + "crypto" + "crypto/rsa" + "crypto/x509" + "fmt" + + certutil "k8s.io/client-go/util/cert" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil" +) + +// FileRenewal renews a certificate using local certs +type FileRenewal struct { + caCert *x509.Certificate + caKey crypto.PrivateKey +} + +// NewFileRenewal takes a certificate pair to construct the Interface. +func NewFileRenewal(caCert *x509.Certificate, caKey crypto.PrivateKey) Interface { + return &FileRenewal{ + caCert: caCert, + caKey: caKey, + } +} + +// Renew takes a certificate using the cert and key +func (r *FileRenewal) Renew(cfg *certutil.Config) (*x509.Certificate, crypto.PrivateKey, error) { + caKey, ok := r.caKey.(*rsa.PrivateKey) + if !ok { + return nil, nil, fmt.Errorf("unsupported private key type %t", r.caKey) + } + + return pkiutil.NewCertAndKey(r.caCert, caKey, cfg) +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go b/cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go new file mode 100644 index 00000000000..3c8a9e58f35 --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/filerenewal_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2018 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 renewal + +import ( + "crypto/x509" + "testing" + + certutil "k8s.io/client-go/util/cert" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" +) + +func TestFileRenew(t *testing.T) { + caCertCfg := &certutil.Config{CommonName: "kubernetes"} + caCert, caKey, err := certs.NewCACertAndKey(caCertCfg) + if err != nil { + t.Fatalf("couldn't create CA: %v", err) + } + + fr := NewFileRenewal(caCert, caKey) + + certCfg := &certutil.Config{ + CommonName: "test-certs", + AltNames: certutil.AltNames{ + DNSNames: []string{"test-domain.space"}, + }, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + cert, _, err := fr.Renew(certCfg) + if err != nil { + t.Fatalf("unexpected error renewing cert: %v", err) + } + + pool := x509.NewCertPool() + pool.AddCert(caCert) + + _, err = cert.Verify(x509.VerifyOptions{ + DNSName: "test-domain.space", + Roots: pool, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + if err != nil { + t.Errorf("couldn't verify new cert: %v", err) + } + +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/interface.go b/cmd/kubeadm/app/phases/certs/renewal/interface.go new file mode 100644 index 00000000000..3ab3d2770df --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/interface.go @@ -0,0 +1,29 @@ +/* +Copyright 2018 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 renewal + +import ( + "crypto" + "crypto/x509" + + certutil "k8s.io/client-go/util/cert" +) + +// Interface represents a standard way to renew a certificate. +type Interface interface { + Renew(*certutil.Config) (*x509.Certificate, crypto.PrivateKey, error) +} diff --git a/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go b/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go new file mode 100644 index 00000000000..631e6edb178 --- /dev/null +++ b/cmd/kubeadm/app/phases/certs/renewal/renewal_test.go @@ -0,0 +1,133 @@ +/* +Copyright 2018 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 renewal + +import ( + "crypto/rsa" + "crypto/x509" + "testing" + "time" + + certsapi "k8s.io/api/certificates/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + fakecerts "k8s.io/client-go/kubernetes/typed/certificates/v1beta1/fake" + k8stesting "k8s.io/client-go/testing" + certutil "k8s.io/client-go/util/cert" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil" +) + +func TestRenewImplementations(t *testing.T) { + caCertCfg := &certutil.Config{CommonName: "kubernetes"} + caCert, caKey, err := certs.NewCACertAndKey(caCertCfg) + if err != nil { + t.Fatalf("couldn't create CA: %v", err) + } + + client := &fakecerts.FakeCertificatesV1beta1{ + Fake: &k8stesting.Fake{}, + } + certReq := getCertReq(t, caCert, caKey) + client.AddReactor("get", "certificatesigningrequests", defaultReactionFunc(certReq)) + watcher := watch.NewFakeWithChanSize(1, false) + watcher.Modify(certReq) + client.AddWatchReactor("certificatesigningrequests", k8stesting.DefaultWatchReactor(watcher, nil)) + + // override the timeout so tests are faster + watchTimeout = time.Second + + tests := []struct { + name string + impl Interface + }{ + { + name: "filerenewal", + impl: NewFileRenewal(caCert, caKey), + }, + { + name: "certs api", + impl: &CertsAPIRenewal{ + client: client, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + certCfg := &certutil.Config{ + CommonName: "test-certs", + AltNames: certutil.AltNames{ + DNSNames: []string{"test-domain.space"}, + }, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + cert, _, err := test.impl.Renew(certCfg) + if err != nil { + t.Fatalf("unexpected error renewing cert: %v", err) + } + + pool := x509.NewCertPool() + pool.AddCert(caCert) + + _, err = cert.Verify(x509.VerifyOptions{ + DNSName: "test-domain.space", + Roots: pool, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + if err != nil { + t.Errorf("couldn't verify new cert: %v", err) + } + }) + } +} + +func defaultReactionFunc(obj runtime.Object) k8stesting.ReactionFunc { + return func(act k8stesting.Action) (bool, runtime.Object, error) { + return true, obj, nil + } +} + +func getCertReq(t *testing.T, caCert *x509.Certificate, caKey *rsa.PrivateKey) *certsapi.CertificateSigningRequest { + cert, _, err := pkiutil.NewCertAndKey(caCert, caKey, &certutil.Config{ + CommonName: "testcert", + AltNames: certutil.AltNames{ + DNSNames: []string{"test-domain.space"}, + }, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + if err != nil { + t.Fatalf("couldn't generate cert: %v", err) + } + + return &certsapi.CertificateSigningRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testcert", + }, + Status: certsapi.CertificateSigningRequestStatus{ + Conditions: []certsapi.CertificateSigningRequestCondition{ + certsapi.CertificateSigningRequestCondition{ + Type: certsapi.CertificateApproved, + }, + }, + Certificate: cert.Raw, + }, + } +}