| // Copyright Istio 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 ca |
| |
| import ( |
| "bytes" |
| "context" |
| "crypto/rsa" |
| "testing" |
| "time" |
| ) |
| |
| import ( |
| v1 "k8s.io/api/core/v1" |
| "k8s.io/apimachinery/pkg/api/errors" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/runtime" |
| "k8s.io/client-go/kubernetes/fake" |
| ktesting "k8s.io/client-go/testing" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/security/pkg/cmd" |
| "github.com/apache/dubbo-go-pixiu/security/pkg/pki/util" |
| certutil "github.com/apache/dubbo-go-pixiu/security/pkg/util" |
| ) |
| |
| const caNamespace = "default" |
| |
| // TestJitterConfiguration tests the setup of jitter |
| func TestJitterConfiguration(t *testing.T) { |
| enableJitterOpts := getDefaultSelfSignedIstioCAOptions(nil) |
| enableJitterOpts.RotatorConfig.enableJitter = true |
| rotator0 := getRootCertRotator(enableJitterOpts) |
| if rotator0.backOffTime < time.Duration(0) { |
| t.Errorf("back off time should be zero or positive but got %v", rotator0.backOffTime) |
| } |
| if rotator0.backOffTime >= rotator0.config.CheckInterval { |
| t.Errorf("back off time should be shorter than rotation interval but got %v", |
| rotator0.backOffTime) |
| } |
| |
| disableJitterOpts := getDefaultSelfSignedIstioCAOptions(nil) |
| disableJitterOpts.RotatorConfig.enableJitter = false |
| rotator1 := getRootCertRotator(disableJitterOpts) |
| if rotator1.backOffTime > time.Duration(0) { |
| t.Errorf("back off time should be negative but got %v", rotator1.backOffTime) |
| } |
| } |
| |
| // TestRootCertRotatorWithoutRootCertSecret verifies that if root cert secret |
| // does not exist, the rotator does not add new root cert. |
| func TestRootCertRotatorWithoutRootCertSecret(t *testing.T) { |
| // Verifies that in self-signed CA mode, root cert rotator does not create CA secret. |
| rotator0 := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(nil)) |
| client0 := rotator0.config.client |
| client0.Secrets(rotator0.config.caStorageNamespace).Delete(context.TODO(), CASecret, metav1.DeleteOptions{}) |
| |
| rotator0.checkAndRotateRootCert() |
| caSecret, err := client0.Secrets(rotator0.config.caStorageNamespace).Get(context.TODO(), CASecret, metav1.GetOptions{}) |
| if !errors.IsNotFound(err) || caSecret != nil { |
| t.Errorf("CA secret should not exist, but get %v: %v", caSecret, err) |
| } |
| } |
| |
| type rootCertItem struct { |
| caSecret *v1.Secret |
| rootCertInKeyCertBundle []byte |
| } |
| |
| func verifyRootCertAndPrivateKey(t *testing.T, shouldMatch bool, itemA, itemB rootCertItem) { |
| isMatched := bytes.Equal(itemA.caSecret.Data[CACertFile], itemB.caSecret.Data[CACertFile]) |
| if isMatched != shouldMatch { |
| t.Errorf("Verification of root cert in CA secret failed. Want %v got %v", shouldMatch, isMatched) |
| } |
| isMatched = bytes.Equal(itemA.rootCertInKeyCertBundle, itemB.rootCertInKeyCertBundle) |
| if isMatched != shouldMatch { |
| t.Errorf("Verification of root cert in key cert bundle failed. Want %v got %v", shouldMatch, isMatched) |
| } |
| |
| // Root cert rotation does not change root private key. Root private key should |
| // remain the same. |
| isMatched = bytes.Equal(itemA.caSecret.Data[CAPrivateKeyFile], itemB.caSecret.Data[CAPrivateKeyFile]) |
| if !isMatched { |
| t.Errorf("Root private key should not change. Want %v got %v", shouldMatch, isMatched) |
| } |
| } |
| |
| func loadCert(rotator *SelfSignedCARootCertRotator) rootCertItem { |
| client := rotator.config.client |
| caSecret, _ := client.Secrets(rotator.config.caStorageNamespace).Get(context.TODO(), CASecret, metav1.GetOptions{}) |
| rootCert := rotator.ca.keyCertBundle.GetRootCertPem() |
| return rootCertItem{caSecret: caSecret, rootCertInKeyCertBundle: rootCert} |
| } |
| |
| // TestRootCertRotatorForSigningCitadel verifies that rotator rotates root cert, |
| // updates key cert bundle and config map. |
| func TestRootCertRotatorForSigningCitadel(t *testing.T) { |
| rotator := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(nil)) |
| |
| // Make a copy of CA secret, a copy of root cert form key cert bundle, and |
| // a copy of root cert from config map for verification. |
| certItem0 := loadCert(rotator) |
| |
| // Change grace period percentage to 0, so that root cert is not going to expire soon. |
| rotator.config.certInspector = certutil.NewCertUtil(0) |
| rotator.checkAndRotateRootCert() |
| // Verifies that when root cert remaining life is not in grace period time, |
| // root cert is not rotated. |
| certItem1 := loadCert(rotator) |
| verifyRootCertAndPrivateKey(t, true, certItem0, certItem1) |
| |
| // Change grace period percentage to 100, so that root cert is guarantee to rotate. |
| rotator.config.certInspector = certutil.NewCertUtil(100) |
| rotator.checkAndRotateRootCert() |
| certItem2 := loadCert(rotator) |
| verifyRootCertAndPrivateKey(t, false, certItem1, certItem2) |
| } |
| |
| // TestRootCertRotatorKeepCertFieldsUnchanged verifies that rotator |
| // extracts information from existing certificate and passes then into new root |
| // certificate. |
| func TestRootCertRotatorKeepCertFieldsUnchanged(t *testing.T) { |
| rotator := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(nil)) |
| // Update CASecret with a new root cert generated from custom cert options. The |
| // cert options differ from default cert options used by rotator. |
| oldCertOrg := "old cert org" |
| oldCertRSAKeySize := 2048 |
| customCertOptions := util.CertOptions{ |
| TTL: rotator.config.caCertTTL, |
| Org: oldCertOrg, |
| IsCA: true, |
| IsSelfSigned: true, |
| RSAKeySize: oldCertRSAKeySize, |
| } |
| updateRootCertWithCustomCertOptions(t, rotator, customCertOptions) |
| |
| // Make a copy of CA secret, a copy of root cert form key cert bundle, and |
| // a copy of root cert from config map for verification. |
| certItem0 := loadCert(rotator) |
| |
| // Change grace period percentage to 100, so that root cert is guarantee to rotate. |
| rotator.config.certInspector = certutil.NewCertUtil(100) |
| // Rotate the root certificate now. |
| rotator.checkAndRotateRootCert() |
| certItem1 := loadCert(rotator) |
| |
| if !bytes.Equal(certItem0.caSecret.Data[CAPrivateKeyFile], certItem1.caSecret.Data[CAPrivateKeyFile]) { |
| t.Errorf("private key should not change") |
| } |
| // verifyRootCertFields verifies that new root cert and private key matches the |
| // old root cert and private key. |
| verifyRootCertFields(t, certItem0, certItem1) |
| } |
| |
| // updateRootCertWithCustomCertOptions generate root cert and private key with |
| // custom cert options, and replaces root cert and key in CA secret. |
| func updateRootCertWithCustomCertOptions(t *testing.T, |
| rotator *SelfSignedCARootCertRotator, options util.CertOptions) { |
| certItem := loadCert(rotator) |
| |
| pemCert, pemKey, err := util.GenCertKeyFromOptions(options) |
| if err != nil { |
| t.Fatalf("failed to rotate secret: %v", err) |
| } |
| newSecret := certItem.caSecret |
| newSecret.Data[CACertFile] = pemCert |
| newSecret.Data[CAPrivateKeyFile] = pemKey |
| rotator.config.client.Secrets(rotator.config.caStorageNamespace).Update(context.TODO(), newSecret, metav1.UpdateOptions{}) |
| } |
| |
| // verifyRootCertFields verifies that certain fields in both new and old root |
| // cert and key should not change. |
| func verifyRootCertFields(t *testing.T, oldCertItem, newCertItem rootCertItem) { |
| if !bytes.Equal(oldCertItem.caSecret.Data[CAPrivateKeyFile], |
| newCertItem.caSecret.Data[CAPrivateKeyFile]) { |
| t.Errorf("private key should not change") |
| } |
| oldKeyLen := getPublicKeySizeInBits(oldCertItem.caSecret.Data[CAPrivateKeyFile]) |
| newKeyLen := getPublicKeySizeInBits(newCertItem.caSecret.Data[CAPrivateKeyFile]) |
| |
| if oldKeyLen != newKeyLen { |
| t.Errorf("Public key size should not change, (got %d) vs (expected %d)", |
| newKeyLen, oldKeyLen) |
| } |
| |
| oldRootCert, _ := util.ParsePemEncodedCertificate(oldCertItem.caSecret.Data[CACertFile]) |
| newRootCert, _ := util.ParsePemEncodedCertificate(newCertItem.caSecret.Data[CACertFile]) |
| if oldRootCert.Subject.String() != newRootCert.Subject.String() { |
| t.Errorf("certificate Subject does not match (old: %s) vs (new: %s)", |
| oldRootCert.Subject.String(), newRootCert.Subject.String()) |
| } |
| if oldRootCert.Issuer.String() != newRootCert.Issuer.String() { |
| t.Errorf("certificate Issuer does not match (old: %s) vs (new: %s)", |
| oldRootCert.Issuer.String(), newRootCert.Issuer.String()) |
| } |
| if oldRootCert.IsCA != newRootCert.IsCA { |
| t.Errorf("certificate IsCA does not match (old: %t) vs (new: %t)", |
| oldRootCert.IsCA, newRootCert.IsCA) |
| } |
| if oldRootCert.Version != newRootCert.Version { |
| t.Errorf("certificate Version does not match (old: %d) vs (new: %d)", |
| oldRootCert.Version, newRootCert.Version) |
| } |
| if oldRootCert.PublicKeyAlgorithm != newRootCert.PublicKeyAlgorithm { |
| t.Errorf("public key algorithm does not match (old: %s) vs (new: %s)", |
| oldRootCert.PublicKeyAlgorithm.String(), newRootCert.PublicKeyAlgorithm.String()) |
| } |
| } |
| |
| func getPublicKeySizeInBits(keyPem []byte) int { |
| privateKey, _ := util.ParsePemEncodedKey(keyPem) |
| k := privateKey.(*rsa.PrivateKey) |
| return k.PublicKey.Size() * 8 |
| } |
| |
| // TestKeyCertBundleReloadInRootCertRotatorForSigningCitadel verifies that |
| // rotator reloads root cert into KeyCertBundle if the root cert in key cert bundle is |
| // different from istio-ca-secret. |
| func TestKeyCertBundleReloadInRootCertRotatorForSigningCitadel(t *testing.T) { |
| rotator := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(nil)) |
| |
| // Mutate the root cert and private key as if they are rotated by other Citadel. |
| certItem0 := loadCert(rotator) |
| oldRootCert := certItem0.rootCertInKeyCertBundle |
| options := util.CertOptions{ |
| TTL: rotator.config.caCertTTL, |
| SignerPrivPem: certItem0.caSecret.Data[CAPrivateKeyFile], |
| Org: rotator.config.org, |
| IsCA: true, |
| IsSelfSigned: true, |
| RSAKeySize: rotator.ca.caRSAKeySize, |
| IsDualUse: rotator.config.dualUse, |
| } |
| pemCert, pemKey, ckErr := util.GenRootCertFromExistingKey(options) |
| if ckErr != nil { |
| t.Fatalf("failed to rotate secret: %s", ckErr.Error()) |
| } |
| newSecret := certItem0.caSecret |
| newSecret.Data[CACertFile] = pemCert |
| newSecret.Data[CAPrivateKeyFile] = pemKey |
| rotator.config.client.Secrets(rotator.config.caStorageNamespace).Update(context.TODO(), newSecret, metav1.UpdateOptions{}) |
| |
| // Change grace period percentage to 0, so that root cert is not going to expire soon. |
| rotator.config.certInspector = certutil.NewCertUtil(0) |
| rotator.checkAndRotateRootCert() |
| // Verifies that when root cert remaining life is not in grace period time, |
| // root cert is not rotated. |
| certItem1 := loadCert(rotator) |
| if !bytes.Equal(newSecret.Data[CACertFile], certItem1.caSecret.Data[CACertFile]) { |
| t.Error("root cert in istio-ca-secret should be the same.") |
| } |
| // Verifies that after rotation, the rotator should have reloaded root cert into |
| // key cert bundle. |
| if bytes.Equal(oldRootCert, rotator.ca.keyCertBundle.GetRootCertPem()) { |
| t.Error("root cert in key cert bundle should be different after rotation.") |
| } |
| if !bytes.Equal(certItem1.caSecret.Data[CACertFile], rotator.ca.keyCertBundle.GetRootCertPem()) { |
| t.Error("root cert in key cert bundle should be the same as root " + |
| "cert in istio-ca-secret after root cert rotation.") |
| } |
| } |
| |
| // TestRollbackAtRootCertRotatorForSigningCitadel verifies that rotator rollbacks |
| // new root cert if it fails to update new root cert into configmap. |
| func TestRollbackAtRootCertRotatorForSigningCitadel(t *testing.T) { |
| fakeClient := fake.NewSimpleClientset() |
| rotator := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(fakeClient)) |
| |
| // Make a copy of CA secret, a copy of root cert form key cert bundle, and |
| // a copy of root cert from config map for verification. |
| certItem0 := loadCert(rotator) |
| |
| // Change grace period percentage to 100, so that root cert is guarantee to rotate. |
| rotator.config.certInspector = certutil.NewCertUtil(100) |
| fakeClient.PrependReactor("update", "secrets", func(action ktesting.Action) (bool, runtime.Object, error) { |
| return true, &v1.Secret{}, errors.NewUnauthorized("no permission to update secret") |
| }) |
| rotator.checkAndRotateRootCert() |
| certItem1 := loadCert(rotator) |
| // Verify that root cert does not change. |
| verifyRootCertAndPrivateKey(t, true, certItem0, certItem1) |
| } |
| |
| // TestRootCertRotatorGoroutineForSigningCitadel verifies that rotator |
| // periodically rotates root cert, updates key cert bundle and config map. |
| func TestRootCertRotatorGoroutineForSigningCitadel(t *testing.T) { |
| t.Skip("https://github.com/istio/istio/issues/26570") |
| rotator := getRootCertRotator(getDefaultSelfSignedIstioCAOptions(nil)) |
| |
| // Make a copy of CA secret, a copy of root cert form key cert bundle, and |
| // a copy of root cert from config map for verification. |
| certItem0 := loadCert(rotator) |
| |
| // Configure rotator to periodically rotates root cert. |
| rotator.config.certInspector = certutil.NewCertUtil(100) |
| rotator.config.caCertTTL = 1 * time.Minute |
| rotator.config.CheckInterval = 500 * time.Millisecond |
| rootCertRotatorChan := make(chan struct{}) |
| go rotator.Run(rootCertRotatorChan) |
| defer close(rootCertRotatorChan) |
| |
| // Wait until root cert rotation is done. |
| time.Sleep(600 * time.Millisecond) |
| certItem1 := loadCert(rotator) |
| verifyRootCertAndPrivateKey(t, false, certItem0, certItem1) |
| |
| time.Sleep(600 * time.Millisecond) |
| certItem2 := loadCert(rotator) |
| verifyRootCertAndPrivateKey(t, false, certItem1, certItem2) |
| } |
| |
| func getDefaultSelfSignedIstioCAOptions(fclient *fake.Clientset) *IstioCAOptions { |
| caCertTTL := time.Hour |
| defaultCertTTL := 30 * time.Minute |
| maxCertTTL := time.Hour |
| org := "test.ca.Org" |
| client := fake.NewSimpleClientset().CoreV1() |
| if fclient != nil { |
| client = fclient.CoreV1() |
| } |
| rootCertFile := "" |
| rootCertCheckInverval := time.Hour |
| rsaKeySize := 2048 |
| |
| caopts, _ := NewSelfSignedIstioCAOptions(context.Background(), |
| cmd.DefaultRootCertGracePeriodPercentile, caCertTTL, |
| rootCertCheckInverval, defaultCertTTL, maxCertTTL, org, false, |
| caNamespace, -1, client, rootCertFile, false, rsaKeySize) |
| return caopts |
| } |
| |
| func getRootCertRotator(opts *IstioCAOptions) *SelfSignedCARootCertRotator { |
| ca, _ := NewIstioCA(opts) |
| ca.rootCertRotator.config.retryMax = time.Millisecond * 50 |
| ca.rootCertRotator.config.retryInterval = time.Millisecond * 5 |
| return ca.rootCertRotator |
| } |