Unverified Commit a53f478d authored by liz's avatar liz

Two implmentations of cert renewal

parent 816f2a48
/*
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,
}
/*
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)
}
/*
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)
}
}
/*
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)
}
/*
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,
},
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment