istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/pki/ca/selfsignedcarootcertrotator_test.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  	"bytes"
    19  	"context"
    20  	"crypto/rsa"
    21  	"testing"
    22  	"time"
    23  
    24  	v1 "k8s.io/api/core/v1"
    25  	"k8s.io/apimachinery/pkg/api/errors"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/client-go/kubernetes/fake"
    29  	ktesting "k8s.io/client-go/testing"
    30  
    31  	"istio.io/istio/security/pkg/cmd"
    32  	"istio.io/istio/security/pkg/pki/util"
    33  	certutil "istio.io/istio/security/pkg/util"
    34  )
    35  
    36  const caNamespace = "default"
    37  
    38  // TestJitterConfiguration tests the setup of jitter
    39  func TestJitterConfiguration(t *testing.T) {
    40  	enableJitterOpts := getDefaultSelfSignedIstioCAOptions(nil)
    41  	enableJitterOpts.RotatorConfig.enableJitter = true
    42  	rotator0 := getRootCertRotator(enableJitterOpts)
    43  	if rotator0.backOffTime < time.Duration(0) {
    44  		t.Errorf("back off time should be zero or positive but got %v", rotator0.backOffTime)
    45  	}
    46  	if rotator0.backOffTime >= rotator0.config.CheckInterval {
    47  		t.Errorf("back off time should be shorter than rotation interval but got %v",
    48  			rotator0.backOffTime)
    49  	}
    50  
    51  	disableJitterOpts := getDefaultSelfSignedIstioCAOptions(nil)
    52  	disableJitterOpts.RotatorConfig.enableJitter = false
    53  	rotator1 := getRootCertRotator(disableJitterOpts)
    54  	if rotator1.backOffTime > time.Duration(0) {
    55  		t.Errorf("back off time should be negative but got %v", rotator1.backOffTime)
    56  	}
    57  }
    58  
    59  // TestRootCertRotatorWithoutRootCertSecret verifies that if root cert secret
    60  // does not exist, the rotator does not add new root cert.
    61  func TestRootCertRotatorWithoutRootCertSecret(t *testing.T) {
    62  	// Verifies that in self-signed CA mode, root cert rotator does not create CA secret.
    63  	rotator0 := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(nil))
    64  	client0 := rotator0.config.client
    65  	client0.Secrets(rotator0.config.caStorageNamespace).Delete(context.TODO(), rotator0.config.secretName, metav1.DeleteOptions{})
    66  
    67  	rotator0.checkAndRotateRootCert()
    68  	caSecret, err := client0.Secrets(rotator0.config.caStorageNamespace).Get(context.TODO(), rotator0.config.secretName, metav1.GetOptions{})
    69  	if !errors.IsNotFound(err) || caSecret != nil {
    70  		t.Errorf("CA secret should not exist, but get %v: %v", caSecret, err)
    71  	}
    72  }
    73  
    74  type rootCertItem struct {
    75  	caSecret                *v1.Secret
    76  	rootCertInKeyCertBundle []byte
    77  }
    78  
    79  func verifyRootCertAndPrivateKey(t *testing.T, shouldMatch bool, itemA, itemB rootCertItem) {
    80  	isMatched := bytes.Equal(itemA.caSecret.Data[CACertFile], itemB.caSecret.Data[CACertFile])
    81  	if isMatched != shouldMatch {
    82  		t.Errorf("Verification of root cert in CA secret failed. Want %v got %v", shouldMatch, isMatched)
    83  	}
    84  	isMatched = bytes.Equal(itemA.rootCertInKeyCertBundle, itemB.rootCertInKeyCertBundle)
    85  	if isMatched != shouldMatch {
    86  		t.Errorf("Verification of root cert in key cert bundle failed. Want %v got %v", shouldMatch, isMatched)
    87  	}
    88  
    89  	// Root cert rotation does not change root private key. Root private key should
    90  	// remain the same.
    91  	isMatched = bytes.Equal(itemA.caSecret.Data[CAPrivateKeyFile], itemB.caSecret.Data[CAPrivateKeyFile])
    92  	if !isMatched {
    93  		t.Errorf("Root private key should not change. Want %v got %v", shouldMatch, isMatched)
    94  	}
    95  }
    96  
    97  func loadCert(rotator *SelfSignedCARootCertRotator) rootCertItem {
    98  	client := rotator.config.client
    99  	caSecret, _ := client.Secrets(rotator.config.caStorageNamespace).Get(context.TODO(), rotator.config.secretName, metav1.GetOptions{})
   100  	rootCert := rotator.ca.keyCertBundle.GetRootCertPem()
   101  	return rootCertItem{caSecret: caSecret, rootCertInKeyCertBundle: rootCert}
   102  }
   103  
   104  // TestRootCertRotatorForSigningCitadel verifies that rotator rotates root cert,
   105  // updates key cert bundle and config map.
   106  func TestRootCertRotatorForSigningCitadel(t *testing.T) {
   107  	rotator := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(nil))
   108  
   109  	// Make a copy of CA secret, a copy of root cert form key cert bundle, and
   110  	// a copy of root cert from config map for verification.
   111  	certItem0 := loadCert(rotator)
   112  
   113  	// Change grace period percentage to 0, so that root cert is not going to expire soon.
   114  	rotator.config.certInspector = certutil.NewCertUtil(0)
   115  	rotator.checkAndRotateRootCert()
   116  	// Verifies that when root cert remaining life is not in grace period time,
   117  	// root cert is not rotated.
   118  	certItem1 := loadCert(rotator)
   119  	verifyRootCertAndPrivateKey(t, true, certItem0, certItem1)
   120  
   121  	// Change grace period percentage to 100, so that root cert is guarantee to rotate.
   122  	rotator.config.certInspector = certutil.NewCertUtil(100)
   123  	rotator.checkAndRotateRootCert()
   124  	certItem2 := loadCert(rotator)
   125  	verifyRootCertAndPrivateKey(t, false, certItem1, certItem2)
   126  }
   127  
   128  // TestRootCertRotatorKeepCertFieldsUnchanged verifies that rotator
   129  // extracts information from existing certificate and passes then into new root
   130  // certificate.
   131  func TestRootCertRotatorKeepCertFieldsUnchanged(t *testing.T) {
   132  	rotator := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(nil))
   133  	// Update CASecret with a new root cert generated from custom cert options. The
   134  	// cert options differ from default cert options used by rotator.
   135  	oldCertOrg := "old cert org"
   136  	oldCertRSAKeySize := 2048
   137  	customCertOptions := util.CertOptions{
   138  		TTL:          rotator.config.caCertTTL,
   139  		Org:          oldCertOrg,
   140  		IsCA:         true,
   141  		IsSelfSigned: true,
   142  		RSAKeySize:   oldCertRSAKeySize,
   143  	}
   144  	updateRootCertWithCustomCertOptions(t, rotator, customCertOptions)
   145  
   146  	// Make a copy of CA secret, a copy of root cert form key cert bundle, and
   147  	// a copy of root cert from config map for verification.
   148  	certItem0 := loadCert(rotator)
   149  
   150  	// Change grace period percentage to 100, so that root cert is guarantee to rotate.
   151  	rotator.config.certInspector = certutil.NewCertUtil(100)
   152  	// Rotate the root certificate now.
   153  	rotator.checkAndRotateRootCert()
   154  	certItem1 := loadCert(rotator)
   155  
   156  	if !bytes.Equal(certItem0.caSecret.Data[CAPrivateKeyFile], certItem1.caSecret.Data[CAPrivateKeyFile]) {
   157  		t.Errorf("private key should not change")
   158  	}
   159  	// verifyRootCertFields verifies that new root cert and private key matches the
   160  	// old root cert and private key.
   161  	verifyRootCertFields(t, certItem0, certItem1)
   162  }
   163  
   164  // updateRootCertWithCustomCertOptions generate root cert and private key with
   165  // custom cert options, and replaces root cert and key in CA secret.
   166  func updateRootCertWithCustomCertOptions(t *testing.T,
   167  	rotator *SelfSignedCARootCertRotator, options util.CertOptions,
   168  ) {
   169  	certItem := loadCert(rotator)
   170  
   171  	pemCert, pemKey, err := util.GenCertKeyFromOptions(options)
   172  	if err != nil {
   173  		t.Fatalf("failed to rotate secret: %v", err)
   174  	}
   175  	newSecret := certItem.caSecret
   176  	newSecret.Data[CACertFile] = pemCert
   177  	newSecret.Data[CAPrivateKeyFile] = pemKey
   178  	rotator.config.client.Secrets(rotator.config.caStorageNamespace).Update(context.TODO(), newSecret, metav1.UpdateOptions{})
   179  }
   180  
   181  // verifyRootCertFields verifies that certain fields in both new and old root
   182  // cert and key should not change.
   183  func verifyRootCertFields(t *testing.T, oldCertItem, newCertItem rootCertItem) {
   184  	if !bytes.Equal(oldCertItem.caSecret.Data[CAPrivateKeyFile],
   185  		newCertItem.caSecret.Data[CAPrivateKeyFile]) {
   186  		t.Errorf("private key should not change")
   187  	}
   188  	oldKeyLen := getPublicKeySizeInBits(oldCertItem.caSecret.Data[CAPrivateKeyFile])
   189  	newKeyLen := getPublicKeySizeInBits(newCertItem.caSecret.Data[CAPrivateKeyFile])
   190  
   191  	if oldKeyLen != newKeyLen {
   192  		t.Errorf("Public key size should not change, (got %d) vs (expected %d)",
   193  			newKeyLen, oldKeyLen)
   194  	}
   195  
   196  	oldRootCert, _ := util.ParsePemEncodedCertificate(oldCertItem.caSecret.Data[CACertFile])
   197  	newRootCert, _ := util.ParsePemEncodedCertificate(newCertItem.caSecret.Data[CACertFile])
   198  	if oldRootCert.Subject.String() != newRootCert.Subject.String() {
   199  		t.Errorf("certificate Subject does not match (old: %s) vs (new: %s)",
   200  			oldRootCert.Subject.String(), newRootCert.Subject.String())
   201  	}
   202  	if oldRootCert.Issuer.String() != newRootCert.Issuer.String() {
   203  		t.Errorf("certificate Issuer does not match (old: %s) vs (new: %s)",
   204  			oldRootCert.Issuer.String(), newRootCert.Issuer.String())
   205  	}
   206  	if oldRootCert.IsCA != newRootCert.IsCA {
   207  		t.Errorf("certificate IsCA does not match (old: %t) vs (new: %t)",
   208  			oldRootCert.IsCA, newRootCert.IsCA)
   209  	}
   210  	if oldRootCert.Version != newRootCert.Version {
   211  		t.Errorf("certificate Version does not match (old: %d) vs (new: %d)",
   212  			oldRootCert.Version, newRootCert.Version)
   213  	}
   214  	if oldRootCert.PublicKeyAlgorithm != newRootCert.PublicKeyAlgorithm {
   215  		t.Errorf("public key algorithm does not match (old: %s) vs (new: %s)",
   216  			oldRootCert.PublicKeyAlgorithm.String(), newRootCert.PublicKeyAlgorithm.String())
   217  	}
   218  }
   219  
   220  func getPublicKeySizeInBits(keyPem []byte) int {
   221  	privateKey, _ := util.ParsePemEncodedKey(keyPem)
   222  	k := privateKey.(*rsa.PrivateKey)
   223  	return k.PublicKey.Size() * 8
   224  }
   225  
   226  // TestKeyCertBundleReloadInRootCertRotatorForSigningCitadel verifies that
   227  // rotator reloads root cert into KeyCertBundle if the root cert in key cert bundle is
   228  // different from istio-ca-secret.
   229  func TestKeyCertBundleReloadInRootCertRotatorForSigningCitadel(t *testing.T) {
   230  	rotator := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(nil))
   231  
   232  	// Mutate the root cert and private key as if they are rotated by other Citadel.
   233  	certItem0 := loadCert(rotator)
   234  	oldRootCert := certItem0.rootCertInKeyCertBundle
   235  	options := util.CertOptions{
   236  		TTL:           rotator.config.caCertTTL,
   237  		SignerPrivPem: certItem0.caSecret.Data[CAPrivateKeyFile],
   238  		Org:           rotator.config.org,
   239  		IsCA:          true,
   240  		IsSelfSigned:  true,
   241  		RSAKeySize:    rotator.ca.caRSAKeySize,
   242  		IsDualUse:     rotator.config.dualUse,
   243  	}
   244  	pemCert, pemKey, ckErr := util.GenRootCertFromExistingKey(options)
   245  	if ckErr != nil {
   246  		t.Fatalf("failed to rotate secret: %s", ckErr.Error())
   247  	}
   248  	newSecret := certItem0.caSecret
   249  	newSecret.Data[CACertFile] = pemCert
   250  	newSecret.Data[CAPrivateKeyFile] = pemKey
   251  	rotator.config.client.Secrets(rotator.config.caStorageNamespace).Update(context.TODO(), newSecret, metav1.UpdateOptions{})
   252  
   253  	// Change grace period percentage to 0, so that root cert is not going to expire soon.
   254  	rotator.config.certInspector = certutil.NewCertUtil(0)
   255  	rotator.checkAndRotateRootCert()
   256  	// Verifies that when root cert remaining life is not in grace period time,
   257  	// root cert is not rotated.
   258  	certItem1 := loadCert(rotator)
   259  	if !bytes.Equal(newSecret.Data[CACertFile], certItem1.caSecret.Data[CACertFile]) {
   260  		t.Error("root cert in istio-ca-secret should be the same.")
   261  	}
   262  	// Verifies that after rotation, the rotator should have reloaded root cert into
   263  	// key cert bundle.
   264  	if bytes.Equal(oldRootCert, rotator.ca.keyCertBundle.GetRootCertPem()) {
   265  		t.Error("root cert in key cert bundle should be different after rotation.")
   266  	}
   267  	if !bytes.Equal(certItem1.caSecret.Data[CACertFile], rotator.ca.keyCertBundle.GetRootCertPem()) {
   268  		t.Error("root cert in key cert bundle should be the same as root " +
   269  			"cert in istio-ca-secret after root cert rotation.")
   270  	}
   271  }
   272  
   273  // TestRollbackAtRootCertRotatorForSigningCitadel verifies that rotator rollbacks
   274  // new root cert if it fails to update new root cert into configmap.
   275  func TestRollbackAtRootCertRotatorForSigningCitadel(t *testing.T) {
   276  	fakeClient := fake.NewSimpleClientset()
   277  	rotator := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(fakeClient))
   278  
   279  	// Make a copy of CA secret, a copy of root cert form key cert bundle, and
   280  	// a copy of root cert from config map for verification.
   281  	certItem0 := loadCert(rotator)
   282  
   283  	// Change grace period percentage to 100, so that root cert is guarantee to rotate.
   284  	rotator.config.certInspector = certutil.NewCertUtil(100)
   285  	fakeClient.PrependReactor("update", "secrets", func(action ktesting.Action) (bool, runtime.Object, error) {
   286  		return true, &v1.Secret{}, errors.NewUnauthorized("no permission to update secret")
   287  	})
   288  	rotator.checkAndRotateRootCert()
   289  	certItem1 := loadCert(rotator)
   290  	// Verify that root cert does not change.
   291  	verifyRootCertAndPrivateKey(t, true, certItem0, certItem1)
   292  }
   293  
   294  // TestRootCertRotatorGoroutineForSigningCitadel verifies that rotator
   295  // periodically rotates root cert, updates key cert bundle and config map.
   296  func TestRootCertRotatorGoroutineForSigningCitadel(t *testing.T) {
   297  	t.Skip("https://github.com/istio/istio/issues/26570")
   298  	rotator := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(nil))
   299  
   300  	// Make a copy of CA secret, a copy of root cert form key cert bundle, and
   301  	// a copy of root cert from config map for verification.
   302  	certItem0 := loadCert(rotator)
   303  
   304  	// Configure rotator to periodically rotates root cert.
   305  	rotator.config.certInspector = certutil.NewCertUtil(100)
   306  	rotator.config.caCertTTL = 1 * time.Minute
   307  	rotator.config.CheckInterval = 500 * time.Millisecond
   308  	rootCertRotatorChan := make(chan struct{})
   309  	go rotator.Run(rootCertRotatorChan)
   310  	defer close(rootCertRotatorChan)
   311  
   312  	// Wait until root cert rotation is done.
   313  	time.Sleep(600 * time.Millisecond)
   314  	certItem1 := loadCert(rotator)
   315  	verifyRootCertAndPrivateKey(t, false, certItem0, certItem1)
   316  
   317  	time.Sleep(600 * time.Millisecond)
   318  	certItem2 := loadCert(rotator)
   319  	verifyRootCertAndPrivateKey(t, false, certItem1, certItem2)
   320  }
   321  
   322  func getDefaultSelfSignedIstioCAOptions(fclient *fake.Clientset) *IstioCAOptions {
   323  	caCertTTL := time.Hour
   324  	defaultCertTTL := 30 * time.Minute
   325  	maxCertTTL := time.Hour
   326  	org := "test.ca.Org"
   327  	client := fake.NewSimpleClientset().CoreV1()
   328  	if fclient != nil {
   329  		client = fclient.CoreV1()
   330  	}
   331  	rootCertFile := ""
   332  	rootCertCheckInverval := time.Hour
   333  	rsaKeySize := 2048
   334  
   335  	caopts, _ := NewSelfSignedIstioCAOptions(context.Background(),
   336  		cmd.DefaultRootCertGracePeriodPercentile, caCertTTL,
   337  		rootCertCheckInverval, defaultCertTTL, maxCertTTL, org, false, false,
   338  		caNamespace, client, rootCertFile, false, rsaKeySize)
   339  	return caopts
   340  }
   341  
   342  func getRootCertRotator(opts *IstioCAOptions) *SelfSignedCARootCertRotator {
   343  	ca, _ := NewIstioCA(opts)
   344  	ca.rootCertRotator.config.retryMax = time.Millisecond * 50
   345  	ca.rootCertRotator.config.retryInterval = time.Millisecond * 5
   346  	return ca.rootCertRotator
   347  }