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 }