istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/pki/ca/ca.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 ca
    16  
    17  import (
    18  	"context"
    19  	"crypto/elliptic"
    20  	"crypto/x509"
    21  	"encoding/pem"
    22  	"fmt"
    23  	"os"
    24  	"time"
    25  
    26  	v1 "k8s.io/api/core/v1"
    27  	apierror "k8s.io/apimachinery/pkg/api/errors"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    30  
    31  	"istio.io/istio/pkg/backoff"
    32  	"istio.io/istio/pkg/log"
    33  	"istio.io/istio/security/pkg/cmd"
    34  	caerror "istio.io/istio/security/pkg/pki/error"
    35  	"istio.io/istio/security/pkg/pki/util"
    36  	certutil "istio.io/istio/security/pkg/util"
    37  )
    38  
    39  const (
    40  	// istioCASecretType is the Istio secret annotation type.
    41  	istioCASecretType = "istio.io/ca-root"
    42  
    43  	// CACertFile is the CA certificate chain file.
    44  	CACertFile = "ca-cert.pem"
    45  	// CAPrivateKeyFile is the private key file of CA.
    46  	CAPrivateKeyFile = "ca-key.pem"
    47  	// CASecret stores the key/cert of self-signed CA for persistency purpose.
    48  	CASecret = "istio-ca-secret"
    49  	// CertChainFile is the ID/name for the certificate chain file.
    50  	CertChainFile = "cert-chain.pem"
    51  	// PrivateKeyFile is the ID/name for the private key file.
    52  	PrivateKeyFile = "key.pem"
    53  	// RootCertFile is the ID/name for the CA root certificate file.
    54  	RootCertFile = "root-cert.pem"
    55  	// TLSSecretCACertFile is the CA certificate file name as it exists in tls type k8s secret.
    56  	TLSSecretCACertFile = "tls.crt"
    57  	// TLSSecretCAPrivateKeyFile is the CA certificate key file name as it exists in tls type k8s secret.
    58  	TLSSecretCAPrivateKeyFile = "tls.key"
    59  	// TLSSecretRootCertFile is the root cert file name as it exists in tls type k8s secret.
    60  	TLSSecretRootCertFile = "ca.crt"
    61  	// The standard key size to use when generating an RSA private key
    62  	rsaKeySize = 2048
    63  	// CACertsSecret stores the plugin CA certificates, in external istiod scenario, the secret can be in the config cluster.
    64  	CACertsSecret = "cacerts"
    65  	// IstioGenerated is the key indicating the secret is generated by Istio.
    66  	IstioGenerated = "istio-generated"
    67  )
    68  
    69  // SigningCAFileBundle locations of the files used for the signing CA
    70  type SigningCAFileBundle struct {
    71  	RootCertFile    string
    72  	CertChainFiles  []string
    73  	SigningCertFile string
    74  	SigningKeyFile  string
    75  }
    76  
    77  var pkiCaLog = log.RegisterScope("pkica", "Citadel CA log")
    78  
    79  // caTypes is the enum for the CA type.
    80  type caTypes int
    81  
    82  type CertOpts struct {
    83  	// SubjectIDs are used for building the SAN extension for the certificate.
    84  	SubjectIDs []string
    85  
    86  	// TTL is the requested lifetime (Time to live) to be applied in the certificate.
    87  	TTL time.Duration
    88  
    89  	// ForCA indicates whether the signed certificate if for CA.
    90  	// If true, the signed certificate is a CA certificate, otherwise, it is a workload certificate.
    91  	ForCA bool
    92  
    93  	// Cert Signer info
    94  	CertSigner string
    95  }
    96  
    97  const (
    98  	// selfSignedCA means the Istio CA uses a self signed certificate.
    99  	selfSignedCA caTypes = iota
   100  	// pluggedCertCA means the Istio CA uses a operator-specified key/cert.
   101  	pluggedCertCA
   102  )
   103  
   104  // IstioCAOptions holds the configurations for creating an Istio CA.
   105  type IstioCAOptions struct {
   106  	CAType caTypes
   107  
   108  	DefaultCertTTL time.Duration
   109  	MaxCertTTL     time.Duration
   110  	CARSAKeySize   int
   111  
   112  	KeyCertBundle *util.KeyCertBundle
   113  
   114  	// Config for creating self-signed root cert rotator.
   115  	RotatorConfig *SelfSignedCARootCertRotatorConfig
   116  
   117  	// OnRootCertUpdate is the cb which can only be called by self-signed root cert rotator
   118  	OnRootCertUpdate func() error
   119  }
   120  
   121  type RootCertUpdateFunc func() error
   122  
   123  // NewSelfSignedIstioCAOptions returns a new IstioCAOptions instance using self-signed certificate.
   124  func NewSelfSignedIstioCAOptions(ctx context.Context,
   125  	rootCertGracePeriodPercentile int, caCertTTL, rootCertCheckInverval, defaultCertTTL,
   126  	maxCertTTL time.Duration, org string, useCacertsSecretName, dualUse bool, namespace string, client corev1.CoreV1Interface,
   127  	rootCertFile string, enableJitter bool, caRSAKeySize int,
   128  ) (caOpts *IstioCAOptions, err error) {
   129  	caOpts = &IstioCAOptions{
   130  		CAType:         selfSignedCA,
   131  		DefaultCertTTL: defaultCertTTL,
   132  		MaxCertTTL:     maxCertTTL,
   133  		RotatorConfig: &SelfSignedCARootCertRotatorConfig{
   134  			CheckInterval:      rootCertCheckInverval,
   135  			caCertTTL:          caCertTTL,
   136  			retryInterval:      cmd.ReadSigningCertRetryInterval,
   137  			retryMax:           cmd.ReadSigningCertRetryMax,
   138  			certInspector:      certutil.NewCertUtil(rootCertGracePeriodPercentile),
   139  			caStorageNamespace: namespace,
   140  			dualUse:            dualUse,
   141  			org:                org,
   142  			rootCertFile:       rootCertFile,
   143  			enableJitter:       enableJitter,
   144  			client:             client,
   145  		},
   146  	}
   147  
   148  	// always use ``istio-ca-secret` in priority, otherwise fall back to `cacerts`
   149  	var caCertName string
   150  	b := backoff.NewExponentialBackOff(backoff.DefaultOption())
   151  	err = b.RetryWithContext(ctx, func() error {
   152  		caCertName = CASecret
   153  		// 1. fetch `istio-ca-secret` in priority
   154  		err := loadSelfSignedCaSecret(client, namespace, caCertName, rootCertFile, caOpts)
   155  		if err == nil {
   156  			return nil
   157  		} else if apierror.IsNotFound(err) {
   158  			// 2. if `istio-ca-secret` not exist and use cacerts enabled, fallback to fetch `cacerts`
   159  			if useCacertsSecretName {
   160  				caCertName = CACertsSecret
   161  				err := loadSelfSignedCaSecret(client, namespace, caCertName, rootCertFile, caOpts)
   162  				if err == nil {
   163  					return nil
   164  				} else if apierror.IsNotFound(err) { // if neither `istio-ca-secret` nor `cacerts` exists, we create a `cacerts`
   165  					// continue to create `cacerts`
   166  				} else {
   167  					return err
   168  				}
   169  			}
   170  
   171  			// 3. if use cacerts disabled, create `istio-ca-secret`, otherwise create `cacerts`.
   172  			pkiCaLog.Infof("CASecret %s not found, will create one", caCertName)
   173  			options := util.CertOptions{
   174  				TTL:          caCertTTL,
   175  				Org:          org,
   176  				IsCA:         true,
   177  				IsSelfSigned: true,
   178  				RSAKeySize:   caRSAKeySize,
   179  				IsDualUse:    dualUse,
   180  			}
   181  			pemCert, pemKey, ckErr := util.GenCertKeyFromOptions(options)
   182  			if ckErr != nil {
   183  				pkiCaLog.Warnf("unable to generate CA cert and key for self-signed CA (%v)", ckErr)
   184  				return fmt.Errorf("unable to generate CA cert and key for self-signed CA (%v)", ckErr)
   185  			}
   186  
   187  			rootCerts, err := util.AppendRootCerts(pemCert, rootCertFile)
   188  			if err != nil {
   189  				pkiCaLog.Warnf("failed to append root certificates (%v)", err)
   190  				return fmt.Errorf("failed to append root certificates (%v)", err)
   191  			}
   192  			if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromPem(pemCert, pemKey, nil, rootCerts); err != nil {
   193  				pkiCaLog.Warnf("failed to create CA KeyCertBundle (%v)", err)
   194  				return fmt.Errorf("failed to create CA KeyCertBundle (%v)", err)
   195  			}
   196  			// Write the key/cert back to secret, so they will be persistent when CA restarts.
   197  			secret := BuildSecret(caCertName, namespace, nil, nil, pemCert, pemCert, pemKey, istioCASecretType)
   198  			_, err = client.Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
   199  			if err != nil {
   200  				pkiCaLog.Warnf("Failed to create secret %s (%v)", caCertName, err)
   201  				return err
   202  			}
   203  			pkiCaLog.Infof("Using self-generated public key: %v", string(rootCerts))
   204  			return nil
   205  		}
   206  		return err
   207  	})
   208  	pkiCaLog.Infof("Set secret name for self-signed CA cert rotator to %s", caCertName)
   209  	caOpts.RotatorConfig.secretName = caCertName
   210  	return caOpts, err
   211  }
   212  
   213  func loadSelfSignedCaSecret(client corev1.CoreV1Interface, namespace string, caCertName string, rootCertFile string, caOpts *IstioCAOptions) error {
   214  	caSecret, err := client.Secrets(namespace).Get(context.TODO(), caCertName, metav1.GetOptions{})
   215  	if err == nil {
   216  		pkiCaLog.Infof("Load signing key and cert from existing secret %s/%s", caSecret.Namespace, caSecret.Name)
   217  		rootCerts, err := util.AppendRootCerts(caSecret.Data[CACertFile], rootCertFile)
   218  		if err != nil {
   219  			return fmt.Errorf("failed to append root certificates (%v)", err)
   220  		}
   221  		if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromPem(caSecret.Data[CACertFile],
   222  			caSecret.Data[CAPrivateKeyFile], nil, rootCerts); err != nil {
   223  			return fmt.Errorf("failed to create CA KeyCertBundle (%v)", err)
   224  		}
   225  		pkiCaLog.Infof("Using existing public key: %v", string(rootCerts))
   226  	}
   227  	return err
   228  }
   229  
   230  // NewSelfSignedDebugIstioCAOptions returns a new IstioCAOptions instance using self-signed certificate produced by in-memory CA,
   231  // which runs without K8s, and no local ca key file presented.
   232  func NewSelfSignedDebugIstioCAOptions(rootCertFile string, caCertTTL, defaultCertTTL, maxCertTTL time.Duration,
   233  	org string, caRSAKeySize int,
   234  ) (caOpts *IstioCAOptions, err error) {
   235  	caOpts = &IstioCAOptions{
   236  		CAType:         selfSignedCA,
   237  		DefaultCertTTL: defaultCertTTL,
   238  		MaxCertTTL:     maxCertTTL,
   239  		CARSAKeySize:   caRSAKeySize,
   240  	}
   241  
   242  	options := util.CertOptions{
   243  		TTL:          caCertTTL,
   244  		Org:          org,
   245  		IsCA:         true,
   246  		IsSelfSigned: true,
   247  		RSAKeySize:   caRSAKeySize,
   248  		IsDualUse:    true, // hardcoded to true for K8S as well
   249  	}
   250  	pemCert, pemKey, ckErr := util.GenCertKeyFromOptions(options)
   251  	if ckErr != nil {
   252  		return nil, fmt.Errorf("unable to generate CA cert and key for self-signed CA (%v)", ckErr)
   253  	}
   254  
   255  	rootCerts, err := util.AppendRootCerts(pemCert, rootCertFile)
   256  	if err != nil {
   257  		return nil, fmt.Errorf("failed to append root certificates (%v)", err)
   258  	}
   259  
   260  	if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromPem(pemCert, pemKey, nil, rootCerts); err != nil {
   261  		return nil, fmt.Errorf("failed to create CA KeyCertBundle (%v)", err)
   262  	}
   263  
   264  	return caOpts, nil
   265  }
   266  
   267  // NewPluggedCertIstioCAOptions returns a new IstioCAOptions instance using given certificate.
   268  func NewPluggedCertIstioCAOptions(fileBundle SigningCAFileBundle,
   269  	defaultCertTTL, maxCertTTL time.Duration, caRSAKeySize int,
   270  ) (caOpts *IstioCAOptions, err error) {
   271  	caOpts = &IstioCAOptions{
   272  		CAType:         pluggedCertCA,
   273  		DefaultCertTTL: defaultCertTTL,
   274  		MaxCertTTL:     maxCertTTL,
   275  		CARSAKeySize:   caRSAKeySize,
   276  	}
   277  
   278  	if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromFile(
   279  		fileBundle.SigningCertFile, fileBundle.SigningKeyFile, fileBundle.CertChainFiles, fileBundle.RootCertFile); err != nil {
   280  		return nil, fmt.Errorf("failed to create CA KeyCertBundle (%v)", err)
   281  	}
   282  
   283  	// Validate that the passed in signing cert can be used as CA.
   284  	// The check can't be done inside `KeyCertBundle`, since bundle could also be used to
   285  	// validate workload certificates (i.e., where the leaf certificate is not a CA).
   286  	b, err := os.ReadFile(fileBundle.SigningCertFile)
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  	block, _ := pem.Decode(b)
   291  	if block == nil {
   292  		return nil, fmt.Errorf("invalid PEM encoded certificate")
   293  	}
   294  	cert, err := x509.ParseCertificate(block.Bytes)
   295  	if err != nil {
   296  		return nil, fmt.Errorf("failed to parse X.509 certificate")
   297  	}
   298  	if !cert.IsCA {
   299  		return nil, fmt.Errorf("certificate is not authorized to sign other certificates")
   300  	}
   301  
   302  	return caOpts, nil
   303  }
   304  
   305  // BuildSecret returns a secret struct, contents of which are filled with parameters passed in.
   306  // Adds the "istio-generated" key if the secret name is `cacerts`.
   307  func BuildSecret(scrtName, namespace string, certChain, privateKey, rootCert, caCert, caPrivateKey []byte, secretType v1.SecretType) *v1.Secret {
   308  	secret := &v1.Secret{
   309  		Data: map[string][]byte{
   310  			CertChainFile:    certChain,
   311  			PrivateKeyFile:   privateKey,
   312  			RootCertFile:     rootCert,
   313  			CACertFile:       caCert,
   314  			CAPrivateKeyFile: caPrivateKey,
   315  		},
   316  		ObjectMeta: metav1.ObjectMeta{
   317  			Name:      scrtName,
   318  			Namespace: namespace,
   319  		},
   320  		Type: secretType,
   321  	}
   322  
   323  	if scrtName == CACertsSecret {
   324  		secret.Data[IstioGenerated] = []byte("")
   325  	}
   326  
   327  	return secret
   328  }
   329  
   330  // IstioCA generates keys and certificates for Istio identities.
   331  type IstioCA struct {
   332  	defaultCertTTL time.Duration
   333  	maxCertTTL     time.Duration
   334  	caRSAKeySize   int
   335  
   336  	keyCertBundle *util.KeyCertBundle
   337  
   338  	// rootCertRotator periodically rotates self-signed root cert for CA. It is nil
   339  	// if CA is not self-signed CA.
   340  	rootCertRotator *SelfSignedCARootCertRotator
   341  }
   342  
   343  // NewIstioCA returns a new IstioCA instance.
   344  func NewIstioCA(opts *IstioCAOptions) (*IstioCA, error) {
   345  	ca := &IstioCA{
   346  		maxCertTTL:    opts.MaxCertTTL,
   347  		keyCertBundle: opts.KeyCertBundle,
   348  		caRSAKeySize:  opts.CARSAKeySize,
   349  	}
   350  
   351  	if opts.CAType == selfSignedCA && opts.RotatorConfig != nil && opts.RotatorConfig.CheckInterval > time.Duration(0) {
   352  		ca.rootCertRotator = NewSelfSignedCARootCertRotator(opts.RotatorConfig, ca, opts.OnRootCertUpdate)
   353  	}
   354  
   355  	// if CA cert becomes invalid before workload cert it's going to cause workload cert to be invalid too,
   356  	// however citatel won't rotate if that happens, this function will prevent that using cert chain TTL as
   357  	// the workload TTL
   358  	defaultCertTTL, err := ca.minTTL(opts.DefaultCertTTL)
   359  	if err != nil {
   360  		return ca, fmt.Errorf("failed to get default cert TTL %s", err.Error())
   361  	}
   362  	ca.defaultCertTTL = defaultCertTTL
   363  
   364  	return ca, nil
   365  }
   366  
   367  func (ca *IstioCA) Run(stopChan chan struct{}) {
   368  	if ca.rootCertRotator != nil {
   369  		// Start root cert rotator in a separate goroutine.
   370  		go ca.rootCertRotator.Run(stopChan)
   371  	}
   372  }
   373  
   374  // Sign takes a PEM-encoded CSR and cert opts, and returns a signed certificate.
   375  func (ca *IstioCA) Sign(csrPEM []byte, certOpts CertOpts) (
   376  	[]byte, error,
   377  ) {
   378  	return ca.sign(csrPEM, certOpts.SubjectIDs, certOpts.TTL, true, certOpts.ForCA)
   379  }
   380  
   381  // SignWithCertChain is similar to Sign but returns the leaf cert and the entire cert chain.
   382  func (ca *IstioCA) SignWithCertChain(csrPEM []byte, certOpts CertOpts) (
   383  	[]string, error,
   384  ) {
   385  	cert, err := ca.signWithCertChain(csrPEM, certOpts.SubjectIDs, certOpts.TTL, true, certOpts.ForCA)
   386  	if err != nil {
   387  		return nil, err
   388  	}
   389  	return []string{string(cert)}, nil
   390  }
   391  
   392  // GetCAKeyCertBundle returns the KeyCertBundle for the CA.
   393  func (ca *IstioCA) GetCAKeyCertBundle() *util.KeyCertBundle {
   394  	return ca.keyCertBundle
   395  }
   396  
   397  // GenKeyCert generates a certificate signed by the CA,
   398  // returns the certificate chain and the private key.
   399  func (ca *IstioCA) GenKeyCert(hostnames []string, certTTL time.Duration, checkLifetime bool) ([]byte, []byte, error) {
   400  	opts := util.CertOptions{
   401  		RSAKeySize: rsaKeySize,
   402  	}
   403  
   404  	// use the type of private key the CA uses to generate an intermediate CA of that type (e.g. CA cert using RSA will
   405  	// cause intermediate CAs using RSA to be generated)
   406  	_, signingKey, _, _ := ca.keyCertBundle.GetAll()
   407  	curve, err := util.GetEllipticCurve(signingKey)
   408  	if err == nil {
   409  		opts.ECSigAlg = util.EcdsaSigAlg
   410  		switch curve {
   411  		case elliptic.P384():
   412  			opts.ECCCurve = util.P384Curve
   413  		default:
   414  			opts.ECCCurve = util.P256Curve
   415  		}
   416  	}
   417  
   418  	csrPEM, privPEM, err := util.GenCSR(opts)
   419  	if err != nil {
   420  		return nil, nil, err
   421  	}
   422  
   423  	certPEM, err := ca.signWithCertChain(csrPEM, hostnames, certTTL, checkLifetime, false)
   424  	if err != nil {
   425  		return nil, nil, err
   426  	}
   427  
   428  	return certPEM, privPEM, nil
   429  }
   430  
   431  func (ca *IstioCA) minTTL(defaultCertTTL time.Duration) (time.Duration, error) {
   432  	certChainPem := ca.keyCertBundle.GetCertChainPem()
   433  	if len(certChainPem) == 0 {
   434  		return defaultCertTTL, nil
   435  	}
   436  
   437  	certChainExpiration, err := util.TimeBeforeCertExpires(certChainPem, time.Now())
   438  	if err != nil {
   439  		return 0, fmt.Errorf("failed to get cert chain TTL %s", err.Error())
   440  	}
   441  
   442  	if certChainExpiration.Seconds() <= 0 {
   443  		return 0, fmt.Errorf("cert chain has expired")
   444  	}
   445  
   446  	if defaultCertTTL.Seconds() > certChainExpiration.Seconds() {
   447  		return certChainExpiration, nil
   448  	}
   449  
   450  	return defaultCertTTL, nil
   451  }
   452  
   453  func (ca *IstioCA) sign(csrPEM []byte, subjectIDs []string, requestedLifetime time.Duration, checkLifetime, forCA bool) ([]byte, error) {
   454  	signingCert, signingKey, _, _ := ca.keyCertBundle.GetAll()
   455  	if signingCert == nil {
   456  		return nil, caerror.NewError(caerror.CANotReady, fmt.Errorf("Istio CA is not ready")) // nolint
   457  	}
   458  
   459  	csr, err := util.ParsePemEncodedCSR(csrPEM)
   460  	if err != nil {
   461  		return nil, caerror.NewError(caerror.CSRError, err)
   462  	}
   463  
   464  	if err := csr.CheckSignature(); err != nil {
   465  		return nil, caerror.NewError(caerror.CSRError, err)
   466  	}
   467  
   468  	lifetime := requestedLifetime
   469  	// If the requested requestedLifetime is non-positive, apply the default TTL.
   470  	if requestedLifetime.Seconds() <= 0 {
   471  		lifetime = ca.defaultCertTTL
   472  	}
   473  	// If checkLifetime is set and the requested TTL is greater than maxCertTTL, return an error
   474  	if checkLifetime && requestedLifetime.Seconds() > ca.maxCertTTL.Seconds() {
   475  		return nil, caerror.NewError(caerror.TTLError, fmt.Errorf(
   476  			"requested TTL %s is greater than the max allowed TTL %s", requestedLifetime, ca.maxCertTTL))
   477  	}
   478  
   479  	certBytes, err := util.GenCertFromCSR(csr, signingCert, csr.PublicKey, *signingKey, subjectIDs, lifetime, forCA)
   480  	if err != nil {
   481  		return nil, caerror.NewError(caerror.CertGenError, err)
   482  	}
   483  
   484  	block := &pem.Block{
   485  		Type:  "CERTIFICATE",
   486  		Bytes: certBytes,
   487  	}
   488  	cert := pem.EncodeToMemory(block)
   489  
   490  	return cert, nil
   491  }
   492  
   493  func (ca *IstioCA) signWithCertChain(csrPEM []byte, subjectIDs []string, requestedLifetime time.Duration, lifetimeCheck,
   494  	forCA bool,
   495  ) ([]byte, error) {
   496  	cert, err := ca.sign(csrPEM, subjectIDs, requestedLifetime, lifetimeCheck, forCA)
   497  	if err != nil {
   498  		return nil, err
   499  	}
   500  
   501  	chainPem := ca.GetCAKeyCertBundle().GetCertChainPem()
   502  	if len(chainPem) > 0 {
   503  		cert = append(cert, chainPem...)
   504  	}
   505  	return cert, nil
   506  }