From db92f20364b47cddf2c72b6f8a45495fea033ed0 Mon Sep 17 00:00:00 2001
From: Shuting Zhao <shutting06@gmail.com>
Date: Wed, 7 Oct 2020 14:17:37 -0700
Subject: [PATCH] - use self-signed certificate to build TLS webhook server; -
 remove CSR related code

---
 pkg/dclient/certificates.go | 157 +++++++++++++----------------------
 pkg/tls/tls.go              | 160 +++++++++++++++++++++++++++---------
 2 files changed, 178 insertions(+), 139 deletions(-)

diff --git a/pkg/dclient/certificates.go b/pkg/dclient/certificates.go
index 1047e8a7c3..6da2df4bbb 100644
--- a/pkg/dclient/certificates.go
+++ b/pkg/dclient/certificates.go
@@ -1,16 +1,15 @@
 package client
 
 import (
-	"errors"
+	"encoding/base64"
 	"fmt"
 	"net/url"
-	"time"
 
 	"github.com/kyverno/kyverno/pkg/config"
 	tls "github.com/kyverno/kyverno/pkg/tls"
-	certificates "k8s.io/api/certificates/v1beta1"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/client-go/rest"
 )
 
@@ -26,7 +25,8 @@ func (c *Client) InitTLSPemPair(configuration *rest.Config, fqdncn bool) (*tls.T
 	tlsPair := c.ReadTlsPair(certProps)
 	if tls.IsTLSPairShouldBeUpdated(tlsPair) {
 		logger.Info("Generating new key/certificate pair for TLS")
-		tlsPair, err = c.generateTLSPemPair(certProps, fqdncn)
+		// tlsPair, err = c.generateTLSPemPair(certProps, fqdncn)
+		tlsPair, err = c.buildTLSPemPair(certProps, fqdncn)
 		if err != nil {
 			return nil, err
 		}
@@ -39,107 +39,18 @@ func (c *Client) InitTLSPemPair(configuration *rest.Config, fqdncn bool) (*tls.T
 	return tlsPair, nil
 }
 
-//generateTlsPemPair Issues TLS certificate for webhook server using given PEM private key
+//buildTLSPemPair Issues TLS certificate for webhook server using self-signed CA cert
 // Returns signed and approved TLS certificate in PEM format
-func (c *Client) generateTLSPemPair(props tls.TlsCertificateProps, fqdncn bool) (*tls.TlsPemPair, error) {
-	privateKey, err := tls.TLSGeneratePrivateKey()
+func (c *Client) buildTLSPemPair(props tls.TlsCertificateProps, fqdncn bool) (*tls.TlsPemPair, error) {
+	caCert, caPEM, err := tls.GenerateCACert()
 	if err != nil {
 		return nil, err
 	}
 
-	certRequest, err := tls.CertificateGenerateRequest(privateKey, props, fqdncn)
-	if err != nil {
-		return nil, fmt.Errorf("Unable to create certificate request: %v", err)
-	}
-
-	certRequest, err = c.submitAndApproveCertificateRequest(certRequest)
-	if err != nil {
-		return nil, fmt.Errorf("Unable to submit and approve certificate request: %v", err)
-	}
-
-	tlsCert, err := c.fetchCertificateFromRequest(certRequest, 10)
-	if err != nil {
-		return nil, fmt.Errorf("Failed to configure a certificate for the Kyverno controller. A CA certificate is required to allow the Kubernetes API Server to communicate with Kyverno. You can either provide a certificate or configure your cluster to allow certificate signing. Please refer to https://github.com/kyverno/kyverno/installation.md.: %v", err)
-	}
-
-	return &tls.TlsPemPair{
-		Certificate: tlsCert,
-		PrivateKey:  tls.TLSPrivateKeyToPem(privateKey),
-	}, nil
-}
-
-// Submits and approves certificate request, returns request which need to be fetched
-func (c *Client) submitAndApproveCertificateRequest(req *certificates.CertificateSigningRequest) (*certificates.CertificateSigningRequest, error) {
-	logger := c.log.WithName("submitAndApproveCertificateRequest")
-	certClient, err := c.GetCSRInterface()
-	if err != nil {
+	if err := c.WriteCACert(caPEM, props); err != nil {
 		return nil, err
 	}
-	csrList, err := c.ListResource("", CSRs, "", nil)
-	if err != nil {
-		return nil, fmt.Errorf("Unable to list existing certificate requests: %v", err)
-	}
-
-	for _, csr := range csrList.Items {
-		if csr.GetName() == req.ObjectMeta.Name {
-			err := c.DeleteResource("", CSRs, "", csr.GetName(), false)
-			if err != nil {
-				return nil, fmt.Errorf("Unable to delete existing certificate request: %v", err)
-			}
-			logger.Info("Old certificate request is deleted")
-			break
-		}
-	}
-
-	unstrRes, err := c.CreateResource("", CSRs, "", req, false)
-	if err != nil {
-		return nil, err
-	}
-	logger.Info("Certificate request created", "name", unstrRes.GetName())
-
-	res, err := convertToCSR(unstrRes)
-	if err != nil {
-		return nil, err
-	}
-	res.Status.Conditions = append(res.Status.Conditions, certificates.CertificateSigningRequestCondition{
-		Type:    certificates.CertificateApproved,
-		Reason:  "NKP-Approve",
-		Message: "This CSR was approved by Nirmata kyverno controller",
-	})
-	res, err = certClient.UpdateApproval(res)
-	if err != nil {
-		return nil, fmt.Errorf("Unable to approve certificate request: %v", err)
-	}
-	logger.Info("Certificate request is approved", "name", res.ObjectMeta.Name)
-
-	return res, nil
-}
-
-// Fetches certificate from given request. Tries to obtain certificate for maxWaitSeconds
-func (c *Client) fetchCertificateFromRequest(req *certificates.CertificateSigningRequest, maxWaitSeconds uint8) ([]byte, error) {
-	// TODO: react of SIGINT and SIGTERM
-	timeStart := time.Now()
-	for time.Since(timeStart) < time.Duration(maxWaitSeconds)*time.Second {
-		unstrR, err := c.GetResource("", CSRs, "", req.ObjectMeta.Name)
-		if err != nil {
-			return nil, err
-		}
-		r, err := convertToCSR(unstrR)
-		if err != nil {
-			return nil, err
-		}
-
-		if r.Status.Certificate != nil {
-			return r.Status.Certificate, nil
-		}
-
-		for _, condition := range r.Status.Conditions {
-			if condition.Type == certificates.CertificateDenied {
-				return nil, errors.New(condition.String())
-			}
-		}
-	}
-	return nil, fmt.Errorf("Cerificate fetch timeout is reached: %d seconds", maxWaitSeconds)
+	return tls.GenerateCertPEM(caCert, props, fqdncn)
 }
 
 //ReadRootCASecret returns the RootCA from the pre-defined secret
@@ -213,6 +124,56 @@ func (c *Client) ReadTlsPair(props tls.TlsCertificateProps) *tls.TlsPemPair {
 	return &pemPair
 }
 
+func (c *Client) WriteCACert(caPEM *tls.TlsPemPair, props tls.TlsCertificateProps) error {
+	logger := c.log.WithName("CAcert")
+	name := generateRootCASecretName(props)
+
+	secretUnstr, err := c.GetResource("", Secrets, props.Namespace, name)
+	if err != nil {
+		secret := &v1.Secret{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "Secret",
+				APIVersion: "v1",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      name,
+				Namespace: props.Namespace,
+				Annotations: map[string]string{
+					selfSignedAnnotation: "true",
+				},
+			},
+			Data: map[string][]byte{
+				rootCAKey: caPEM.Certificate,
+			},
+			Type: v1.SecretTypeOpaque,
+		}
+
+		_, err := c.CreateResource("", Secrets, props.Namespace, secret, false)
+		if err == nil {
+			logger.Info("secret created", "name", name, "namespace", props.Namespace)
+		}
+		return err
+	}
+	// secret := v1.Secret{}
+	if _, ok := secretUnstr.GetAnnotations()[selfSignedAnnotation]; !ok {
+		secretUnstr.SetAnnotations(map[string]string{selfSignedAnnotation: "true"})
+	}
+
+	dataMap := map[string]interface{}{
+		rootCAKey: base64.StdEncoding.EncodeToString(caPEM.Certificate)}
+
+	if err := unstructured.SetNestedMap(secretUnstr.Object, dataMap, "data"); err != nil {
+		return err
+	}
+
+	_, err = c.UpdateResource("", Secrets, props.Namespace, secretUnstr, false)
+	if err != nil {
+		return err
+	}
+	logger.Info("secret updated", "name", name, "namespace", props.Namespace)
+	return nil
+}
+
 //WriteTlsPair Writes the pair of TLS certificate and key to the specified secret.
 // Updates existing secret or creates new one.
 func (c *Client) WriteTlsPair(props tls.TlsCertificateProps, pemPair *tls.TlsPemPair) error {
diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go
index e3cd9a94df..5dce7d6188 100644
--- a/pkg/tls/tls.go
+++ b/pkg/tls/tls.go
@@ -1,19 +1,21 @@
 package tls
 
 import (
+	"bytes"
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"encoding/pem"
 	"errors"
+	"fmt"
+	"math/big"
 	"net"
 	"time"
-
-	certificates "k8s.io/api/certificates/v1beta1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
+const certValidityDuration = 10 * 365 * 24 * time.Hour
+
 //TlsCertificateProps Properties of TLS certificate which should be issued for webhook server
 type TlsCertificateProps struct {
 	Service       string
@@ -27,6 +29,11 @@ type TlsPemPair struct {
 	PrivateKey  []byte
 }
 
+type KeyPair struct {
+	Cert *x509.Certificate
+	Key  *rsa.PrivateKey
+}
+
 //TLSGeneratePrivateKey Generates RSA private key
 func TLSGeneratePrivateKey() (*rsa.PrivateKey, error) {
 	return rsa.GenerateKey(rand.Reader, 2048)
@@ -42,29 +49,92 @@ func TLSPrivateKeyToPem(rsaKey *rsa.PrivateKey) []byte {
 	return pem.EncodeToMemory(privateKey)
 }
 
-//TlsCertificateRequestToPem Creates PEM block from raw certificate request
-func certificateRequestToPem(csrRaw []byte) []byte {
-	csrBlock := &pem.Block{
-		Type:  "CERTIFICATE REQUEST",
-		Bytes: csrRaw,
+func pemEncode(certificateDER []byte, key *rsa.PrivateKey) ([]byte, []byte, error) {
+	certBuf := &bytes.Buffer{}
+	if err := pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: certificateDER}); err != nil {
+		return nil, nil, fmt.Errorf("encoding cert: %v", err)
 	}
-
-	return pem.EncodeToMemory(csrBlock)
+	keyBuf := &bytes.Buffer{}
+	if err := pem.Encode(keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}); err != nil {
+		return nil, nil, fmt.Errorf("encoding key: %v", err)
+	}
+	return certBuf.Bytes(), keyBuf.Bytes(), nil
 }
 
-//CertificateGenerateRequest Generates raw certificate signing request
-func CertificateGenerateRequest(privateKey *rsa.PrivateKey, props TlsCertificateProps, fqdncn bool) (*certificates.CertificateSigningRequest, error) {
+func TLSCertificateToPem(certificateDER []byte) []byte {
+	certificate := &pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: certificateDER,
+	}
+
+	return pem.EncodeToMemory(certificate)
+}
+
+// GenerateCACert creates the self-signed CA cert and private key
+// it will be used to sign the webhook server certificate
+func GenerateCACert() (*KeyPair, *TlsPemPair, error) {
+	now := time.Now()
+	begin := now.Add(-1 * time.Hour)
+	end := now.Add(certValidityDuration)
+	templ := &x509.Certificate{
+		SerialNumber: big.NewInt(0),
+		Subject: pkix.Name{
+			CommonName: "*.kyverno.svc",
+		},
+		NotBefore:             begin,
+		NotAfter:              end,
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
+		BasicConstraintsValid: true,
+		IsCA:                  true,
+	}
+	key, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		return nil, nil, fmt.Errorf("error generating key: %v", err)
+	}
+	der, err := x509.CreateCertificate(rand.Reader, templ, templ, key.Public(), key)
+	if err != nil {
+		return nil, nil, fmt.Errorf("error creating certificate: %v", err)
+	}
+
+	pemPair := &TlsPemPair{
+		Certificate: TLSCertificateToPem(der),
+		PrivateKey:  TLSPrivateKeyToPem(key),
+	}
+
+	cert, err := x509.ParseCertificate(der)
+	if err != nil {
+		return nil, nil, fmt.Errorf("error parsing certificate %v", err)
+	}
+
+	caCert := &KeyPair{
+		Cert: cert,
+		Key:  key,
+	}
+
+	return caCert, pemPair, nil
+}
+
+// GenerateCertPEM takes the results of GenerateCACert and uses it to create the
+// PEM-encoded public certificate and private key, respectively
+func GenerateCertPEM(caCert *KeyPair, props TlsCertificateProps, fqdncn bool) (*TlsPemPair, error) {
+	now := time.Now()
+	begin := now.Add(-1 * time.Hour)
+	end := now.Add(certValidityDuration)
+
 	dnsNames := make([]string, 3)
-	dnsNames[0] = props.Service
-	dnsNames[1] = props.Service + "." + props.Namespace
+	dnsNames[0] = fmt.Sprintf("%s", props.Service)
+	csCommonName := dnsNames[0]
+
+	dnsNames[1] = fmt.Sprintf("%s.%s", props.Service, props.Namespace)
 	// The full service name is the CommonName for the certificate
 	commonName := GenerateInClusterServiceName(props)
-	dnsNames[2] = commonName
-	csCommonName := props.Service
+	dnsNames[2] = fmt.Sprintf("%s", commonName)
+
 	if fqdncn {
 		// use FQDN as CommonName as a workaournd for https://github.com/kyverno/kyverno/issues/542
 		csCommonName = commonName
 	}
+
 	var ips []net.IP
 	apiServerIP := net.ParseIP(props.ApiServerHost)
 	if apiServerIP != nil {
@@ -73,39 +143,47 @@ func CertificateGenerateRequest(privateKey *rsa.PrivateKey, props TlsCertificate
 		dnsNames = append(dnsNames, props.ApiServerHost)
 	}
 
-	csrTemplate := x509.CertificateRequest{
+	templ := &x509.Certificate{
+		SerialNumber: big.NewInt(1),
 		Subject: pkix.Name{
 			CommonName: csCommonName,
 		},
-		SignatureAlgorithm: x509.SHA256WithRSA,
-		DNSNames:           dnsNames,
-		IPAddresses:        ips,
+		DNSNames: dnsNames,
+		// IPAddresses:           ips,
+		NotBefore:             begin,
+		NotAfter:              end,
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
 	}
 
-	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, privateKey)
+	key, err := rsa.GenerateKey(rand.Reader, 2048)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("error generating key for webhook %v", err)
+	}
+	der, err := x509.CreateCertificate(rand.Reader, templ, caCert.Cert, key.Public(), caCert.Key)
+	if err != nil {
+		return nil, fmt.Errorf("error creating certificate for webhook %v", err)
 	}
 
-	return &certificates.CertificateSigningRequest{
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: "certificates.k8s.io/v1beta1",
-			Kind:       "CertificateSigningRequest",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Name: props.Service + "." + props.Namespace + ".cert-request",
-		},
-		Spec: certificates.CertificateSigningRequestSpec{
-			Request: certificateRequestToPem(csrBytes),
-			Groups:  []string{"system:masters", "system:authenticated"},
-			Usages: []certificates.KeyUsage{
-				certificates.UsageDigitalSignature,
-				certificates.UsageKeyEncipherment,
-				certificates.UsageServerAuth,
-				certificates.UsageClientAuth,
-			},
-		},
-	}, nil
+	pemPair := &TlsPemPair{
+		Certificate: TLSCertificateToPem(der),
+		PrivateKey:  TLSPrivateKeyToPem(key),
+	}
+
+	// certPEM := TLSCertificateToPem(der)
+	// keyPEM := TLSPrivateKeyToPem(key)
+	return pemPair, nil
+}
+
+//TlsCertificateRequestToPem Creates PEM block from raw certificate request
+func certificateRequestToPem(csrRaw []byte) []byte {
+	csrBlock := &pem.Block{
+		Type:  "CERTIFICATE REQUEST",
+		Bytes: csrRaw,
+	}
+
+	return pem.EncodeToMemory(csrBlock)
 }
 
 //GenerateInClusterServiceName The generated service name should be the common name for TLS certificate