istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/k8s/chiron/utils.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package chiron
    16  
    17  import (
    18  	"context"
    19  	"crypto/x509"
    20  	"encoding/pem"
    21  	"fmt"
    22  	"net"
    23  	"os"
    24  	"time"
    25  
    26  	cert "k8s.io/api/certificates/v1"
    27  	corev1 "k8s.io/api/core/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/fields"
    30  	clientset "k8s.io/client-go/kubernetes"
    31  
    32  	"istio.io/istio/pkg/log"
    33  	"istio.io/istio/pkg/ptr"
    34  	"istio.io/istio/security/pkg/pki/util"
    35  )
    36  
    37  const (
    38  	// The size of a private key for a leaf certificate.
    39  	keySize = 2048
    40  )
    41  
    42  var certWatchTimeout = 60 * time.Second
    43  
    44  // GenKeyCertK8sCA : Generates a key pair and gets public certificate signed by K8s_CA
    45  // Options are meant to sign DNS certs
    46  // 1. Generate a CSR
    47  // 2. Call SignCSRK8s to finish rest of the flow
    48  func GenKeyCertK8sCA(client clientset.Interface, dnsName,
    49  	caFilePath string, signerName string, approveCsr bool, requestedLifetime time.Duration,
    50  ) ([]byte, []byte, []byte, error) {
    51  	// 1. Generate a CSR
    52  	options := util.CertOptions{
    53  		Host:       dnsName,
    54  		RSAKeySize: keySize,
    55  		IsDualUse:  false,
    56  		PKCS8Key:   false,
    57  	}
    58  	csrPEM, keyPEM, err := util.GenCSR(options)
    59  	if err != nil {
    60  		log.Errorf("CSR generation error (%v)", err)
    61  		return nil, nil, nil, err
    62  	}
    63  	usages := []cert.KeyUsage{
    64  		cert.UsageDigitalSignature,
    65  		cert.UsageKeyEncipherment,
    66  		cert.UsageServerAuth,
    67  	}
    68  	if signerName == "" {
    69  		return nil, nil, nil, fmt.Errorf("signerName is required for Kubernetes CA")
    70  	}
    71  	certChain, caCert, err := SignCSRK8s(client, csrPEM, signerName, usages, dnsName, caFilePath, approveCsr, true, requestedLifetime)
    72  
    73  	return certChain, keyPEM, caCert, err
    74  }
    75  
    76  // SignCSRK8s generates a certificate from CSR using the K8s CA
    77  // 1. Submit a CSR
    78  // 2. Approve a CSR
    79  // 3. Read the signed certificate
    80  // 4. Clean up the artifacts (e.g., delete CSR)
    81  func SignCSRK8s(client clientset.Interface, csrData []byte, signerName string, usages []cert.KeyUsage,
    82  	dnsName, caFilePath string, approveCsr, appendCaCert bool, requestedLifetime time.Duration,
    83  ) ([]byte, []byte, error) {
    84  	// 1. Submit the CSR
    85  	csr, err := submitCSR(client, csrData, signerName, usages, requestedLifetime)
    86  	if err != nil {
    87  		return nil, nil, err
    88  	}
    89  	log.Debugf("CSR (%v) has been created", csr.Name)
    90  
    91  	// clean up certificate request after deletion
    92  	defer func() {
    93  		_ = cleanupCSR(client, csr)
    94  	}()
    95  
    96  	// 2. Approve the CSR
    97  	if approveCsr {
    98  		approvalMessage := fmt.Sprintf("CSR (%s) for the certificate (%s) is approved", csr.Name, dnsName)
    99  		err = approveCSR(client, csr, approvalMessage)
   100  		if err != nil {
   101  			return nil, nil, fmt.Errorf("failed to approve CSR request: %v", err)
   102  		}
   103  		log.Debugf("CSR (%v) is approved", csr.Name)
   104  	}
   105  
   106  	// 3. Read the signed certificate
   107  	certChain, caCert, err := readSignedCertificate(client, csr, certWatchTimeout, caFilePath, appendCaCert)
   108  	if err != nil {
   109  		return nil, nil, err
   110  	}
   111  
   112  	// If there is a failure of cleaning up CSR, the error is returned.
   113  	return certChain, caCert, err
   114  }
   115  
   116  // Read CA certificate and check whether it is a valid certificate.
   117  func readCACert(caCertPath string) ([]byte, error) {
   118  	caCert, err := os.ReadFile(caCertPath)
   119  	if err != nil {
   120  		log.Errorf("failed to read CA cert, cert. path: %v, error: %v", caCertPath, err)
   121  		return nil, fmt.Errorf("failed to read CA cert, cert. path: %v, error: %v", caCertPath, err)
   122  	}
   123  
   124  	b, _ := pem.Decode(caCert)
   125  	if b == nil {
   126  		return nil, fmt.Errorf("could not decode pem")
   127  	}
   128  	if b.Type != "CERTIFICATE" {
   129  		return nil, fmt.Errorf("ca certificate contains wrong type: %v", b.Type)
   130  	}
   131  	if _, err := x509.ParseCertificate(b.Bytes); err != nil {
   132  		return nil, fmt.Errorf("ca certificate parsing returns an error: %v", err)
   133  	}
   134  
   135  	return caCert, nil
   136  }
   137  
   138  func isTCPReachable(host string, port int) bool {
   139  	addr := fmt.Sprintf("%s:%d", host, port)
   140  	conn, err := net.DialTimeout("tcp", addr, 1*time.Second)
   141  	if err != nil {
   142  		log.Debugf("DialTimeout() returns err: %v", err)
   143  		// No connection yet, so no need to conn.Close()
   144  		return false
   145  	}
   146  	err = conn.Close()
   147  	if err != nil {
   148  		log.Infof("tcp connection is not closed: %v", err)
   149  	}
   150  	return true
   151  }
   152  
   153  func submitCSR(
   154  	client clientset.Interface,
   155  	csrData []byte,
   156  	signerName string,
   157  	usages []cert.KeyUsage,
   158  	requestedLifetime time.Duration,
   159  ) (*cert.CertificateSigningRequest, error) {
   160  	log.Debugf("create CSR for signer %v", signerName)
   161  	csr := &cert.CertificateSigningRequest{
   162  		// Username, UID, Groups will be injected by API server.
   163  		TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"},
   164  		ObjectMeta: metav1.ObjectMeta{
   165  			GenerateName: "csr-workload-",
   166  		},
   167  		Spec: cert.CertificateSigningRequestSpec{
   168  			Request:    csrData,
   169  			Usages:     usages,
   170  			SignerName: signerName,
   171  		},
   172  	}
   173  	if requestedLifetime != time.Duration(0) {
   174  		csr.Spec.ExpirationSeconds = ptr.Of(int32(requestedLifetime.Seconds()))
   175  	}
   176  	resp, err := client.CertificatesV1().CertificateSigningRequests().Create(context.Background(), csr, metav1.CreateOptions{})
   177  	if err != nil {
   178  		return nil, fmt.Errorf("failed to create CSR: %v", err)
   179  	}
   180  	return resp, nil
   181  }
   182  
   183  func approveCSR(client clientset.Interface, csr *cert.CertificateSigningRequest, approvalMessage string) error {
   184  	csr.Status.Conditions = append(csr.Status.Conditions, cert.CertificateSigningRequestCondition{
   185  		Type:    cert.CertificateApproved,
   186  		Reason:  approvalMessage,
   187  		Message: approvalMessage,
   188  		Status:  corev1.ConditionTrue,
   189  	})
   190  	_, err := client.CertificatesV1().CertificateSigningRequests().UpdateApproval(context.TODO(), csr.Name, csr, metav1.UpdateOptions{})
   191  	if err != nil {
   192  		log.Errorf("failed to approve CSR (%v): %v", csr.Name, err)
   193  		return err
   194  	}
   195  	return nil
   196  }
   197  
   198  // Read the signed certificate
   199  // verify and append CA certificate to certChain if appendCaCert is true
   200  func readSignedCertificate(client clientset.Interface, csr *cert.CertificateSigningRequest,
   201  	watchTimeout time.Duration, caCertPath string, appendCaCert bool,
   202  ) ([]byte, []byte, error) {
   203  	// First try to read the signed CSR through a watching mechanism
   204  	certPEM, err := readSignedCsr(client, csr.Name, watchTimeout)
   205  	if err != nil {
   206  		return nil, nil, err
   207  	}
   208  	if len(certPEM) == 0 {
   209  		return nil, nil, fmt.Errorf("no certificate returned for the CSR: %q", csr.Name)
   210  	}
   211  	certsParsed, _, err := util.ParsePemEncodedCertificateChain(certPEM)
   212  	if err != nil {
   213  		return nil, nil, fmt.Errorf("decoding certificate failed")
   214  	}
   215  	if !appendCaCert || caCertPath == "" {
   216  		return certPEM, nil, nil
   217  	}
   218  	caCert, err := readCACert(caCertPath)
   219  	if err != nil {
   220  		return nil, nil, fmt.Errorf("error when retrieving CA cert: (%v)", err)
   221  	}
   222  	// Verify the certificate chain before returning the certificate
   223  	roots := x509.NewCertPool()
   224  	if roots == nil {
   225  		return nil, nil, fmt.Errorf("failed to create cert pool")
   226  	}
   227  	if ok := roots.AppendCertsFromPEM(caCert); !ok {
   228  		return nil, nil, fmt.Errorf("failed to append CA certificate")
   229  	}
   230  	intermediates := x509.NewCertPool()
   231  	if len(certsParsed) > 1 {
   232  		for _, cert := range certsParsed[1:] {
   233  			intermediates.AddCert(cert)
   234  		}
   235  	}
   236  	_, err = certsParsed[0].Verify(x509.VerifyOptions{
   237  		Roots:         roots,
   238  		Intermediates: intermediates,
   239  	})
   240  	if err != nil {
   241  		return nil, nil, fmt.Errorf("failed to verify the certificate chain: %v", err)
   242  	}
   243  
   244  	return append(certPEM, caCert...), caCert, nil
   245  }
   246  
   247  // Return signed CSR through a watcher. If no CSR is read, return nil.
   248  func readSignedCsr(client clientset.Interface, csr string, watchTimeout time.Duration) ([]byte, error) {
   249  	selector := fields.OneTermEqualSelector("metadata.name", csr).String()
   250  	// Setup a List+Watch, like informers do
   251  	// A simple Watch will fail if the cert is signed too quickly
   252  	l, _ := client.CertificatesV1().CertificateSigningRequests().List(context.Background(), metav1.ListOptions{
   253  		FieldSelector: selector,
   254  	})
   255  	if l != nil && len(l.Items) > 0 {
   256  		reqSigned := l.Items[0]
   257  		if reqSigned.Status.Certificate != nil {
   258  			return reqSigned.Status.Certificate, nil
   259  		}
   260  	}
   261  	var rv string
   262  	if l != nil {
   263  		rv = l.ResourceVersion
   264  	}
   265  	watcher, err := client.CertificatesV1().CertificateSigningRequests().Watch(context.Background(), metav1.ListOptions{
   266  		ResourceVersion: rv,
   267  		FieldSelector:   selector,
   268  	})
   269  	if err != nil {
   270  		return nil, fmt.Errorf("failed to watch CSR %v", csr)
   271  	}
   272  
   273  	// Set a timeout
   274  	timer := time.After(watchTimeout)
   275  	for {
   276  		select {
   277  		case r := <-watcher.ResultChan():
   278  			reqSigned := r.Object.(*cert.CertificateSigningRequest)
   279  			if reqSigned.Status.Certificate != nil {
   280  				return reqSigned.Status.Certificate, nil
   281  			}
   282  		case <-timer:
   283  			return nil, fmt.Errorf("timeout when watching CSR %v", csr)
   284  		}
   285  	}
   286  }
   287  
   288  // Clean up the CSR
   289  func cleanupCSR(client clientset.Interface, csr *cert.CertificateSigningRequest) error {
   290  	err := client.CertificatesV1().CertificateSigningRequests().Delete(context.TODO(), csr.Name, metav1.DeleteOptions{})
   291  	if err != nil {
   292  		log.Errorf("failed to delete CSR (%v): %v", csr.Name, err)
   293  	} else {
   294  		log.Debugf("deleted CSR: %v", csr.Name)
   295  	}
   296  	return err
   297  }