istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/pki/ca/ca_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/tls" 21 "crypto/x509" 22 "os" 23 "reflect" 24 "sync" 25 "testing" 26 "time" 27 28 v1 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/client-go/kubernetes/fake" 31 32 caerror "istio.io/istio/security/pkg/pki/error" 33 "istio.io/istio/security/pkg/pki/util" 34 ) 35 36 var ( 37 cert1Pem = ` 38 -----BEGIN CERTIFICATE----- 39 MIIC3jCCAcagAwIBAgIJAMwyWk0iqlOoMA0GCSqGSIb3DQEBCwUAMBwxGjAYBgNV 40 BAoMEWs4cy5jbHVzdGVyLmxvY2FsMB4XDTE4MDkyMTAyMjAzNFoXDTI4MDkxODAy 41 MjAzNFowHDEaMBgGA1UECgwRazhzLmNsdXN0ZXIubG9jYWwwggEiMA0GCSqGSIb3 42 DQEBAQUAA4IBDwAwggEKAoIBAQC8TDtfy23OKCRnkSYrKZwuHG5lOmTZgLwoFR1h 43 3NDTkjR9406CjnAy6Gl73CRG3zRYVgY/2dGNqTzAKRCeKZlOzBlK6Kilb0NIJ6it 44 s6ooMAxwXlr7jOKiSn6xbaexVMrP0VPUbCgJxQtGs3++hQ14D6WnyfdzPBZJLKbI 45 tVdDnAcl/FJXKVV9gIg+MM0gETWOYj5Yd8Ye0FTvoFcgs8NKkxhEZe/LeYa7XYsk 46 S0PymwbHwNZcfC4znp2bzu28LUmUe6kL97YU8ubvhR0muRy6h5MnQNMQrRG5Q5j4 47 A2+tkO0vto8gOb6/lacEUVYuQdSkMZJiqWEjWgWKeAYdkTJDAgMBAAGjIzAhMA4G 48 A1UdDwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IB 49 AQAxWP3MT0IelJcb+e7fNTfMS0r3UhpiNkRU368Z7gJ4tDNOGRPzntW6CLnaE+3g 50 IjOMAE8jlXeEmNuXtDQqQoZwWc1D5ma3jyc83E5H9LJzjfmn5rAHafr29YH85Ms2 51 VlKdpP+teYg8Cag9u4ar/AUR4zMUEpGK5U+T9IH44lVqVH23T+DxAT+btsyuGiB0 52 DsM76XVDj4g3OKCUalu7a8FHvgTkBpUJBl7vwh9kqo9HwCaj4iC2CwveOm0WtSgy 53 K9PpVDxTGNSxqsxKn7DJQ15NTOP+gr29ABqFKwRr+S8ggw6evzHbABQTUMebaRSr 54 iH7cSgrzZBiUvJmZRi7/BrYU 55 -----END CERTIFICATE-----` 56 57 key1Pem = ` 58 -----BEGIN PRIVATE KEY----- 59 MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQC8TDtfy23OKCRn 60 kSYrKZwuHG5lOmTZgLwoFR1h3NDTkjR9406CjnAy6Gl73CRG3zRYVgY/2dGNqTzA 61 KRCeKZlOzBlK6Kilb0NIJ6its6ooMAxwXlr7jOKiSn6xbaexVMrP0VPUbCgJxQtG 62 s3++hQ14D6WnyfdzPBZJLKbItVdDnAcl/FJXKVV9gIg+MM0gETWOYj5Yd8Ye0FTv 63 oFcgs8NKkxhEZe/LeYa7XYskS0PymwbHwNZcfC4znp2bzu28LUmUe6kL97YU8ubv 64 hR0muRy6h5MnQNMQrRG5Q5j4A2+tkO0vto8gOb6/lacEUVYuQdSkMZJiqWEjWgWK 65 eAYdkTJDAgMBAAECggEBAJTemFqmVQwWxKF1Kn4ZibcTF1zFDBLCKwBtoStMD3YW 66 M5YL7nhd8OruwOcCJ1Q5CAOHD63PolOjp7otPUwui1y3FJAa3areCo2zfTLHxxG6 67 2zrD/p6+xjeVOhFBJsGWzjn7v5FEaWs/9ChTpf2U6A8yH8BGd3MN4Hi96qboaDO0 68 fFz3zOu7sgjkDNZiapZpUuqs7a6MCCr2T3FPwdWUiILZF2t5yWd/l8KabP+3QvvR 69 tDU6sNv4j8e+dsF2l9ZT81JLkN+f6HvWcLVAADvcBqMcd8lmMSPgxSbytzKanx7o 70 wtzIiGkNZBCVKGO7IK2ByCluiyHDpGul60Th7HUluDECgYEA9/Q1gT8LTHz1n6vM 71 2n2umQN9R+xOaEYN304D5DQqptN3S0BCJ4dihD0uqEB5osstRTf4QpP/qb2hMDP4 72 qWbWyrc7Z5Lyt6HI1ly6VpVnYKb3HDeJ9M+5Se1ttdwyRCzuT4ZBhT5bbqBatsOU 73 V7+dyrJKbk8r9K4qy29UFozz/38CgYEAwmhzPVak99rVmqTpe0gPERW//n+PdW3P 74 Ta6ongU8zkkw9LAFwgjGtNpd4nlk0iQigiM4jdJDFl6edrRXv2cisEfJ9+s53AOb 75 hXui4HAn2rusPK+Dq2InkHYTGjEGDpx94zC/bjYR1GBIsthIh0w2G9ql8yvLatxG 76 x6oXEsb7Lz0CgYEA7Oj+/mDYUNrMbSVfdBvF6Rl2aHQWbncQ5h3Khg55+i/uuY3K 77 J66pqKQ0ojoIfk0XEh3qLOLv0qUHD+F4Y5OJAuOT9OBo3J/OH1M2D2hs/+JIFUPT 78 on+fEE21F6AuvwkXIhCrJb5w6gB47Etuv3CsOXGkwEURQJXw+bODapB+yc0CgYEA 79 t7zoTay6NdcJ0yLR2MZ+FvOrhekhuSaTqyPMEa15jq32KwzCJGUPCJbp7MY217V3 80 N+/533A+H8JFmoNP+4KKcnknFb2n7Z0rO7licyUNRdniK2jm1O/r3Mj7vOFgjCaz 81 hCnqg0tvBn4Jt55aziTlbuXzuiRGGTUfYE4NiJ2vgTECgYEA8di9yqGhETYQkoT3 82 E70JpEmkCWiHl/h2ClLcDkj0gXKFxmhzmvs8G5On4S8toNiJ6efmz0KlHN1F7Ldi 83 2iVd9LZnFVP1YwG0mvTJxxc5P5Uy5q/EhCLBAetqoTkWYlPcpkcathmCbCpJG4/x 84 iOmuuOfQWnMfcVk8I0YDL5+G9Pg= 85 -----END PRIVATE KEY-----` 86 ) 87 88 // TODO (myidpt): Test Istio CA can load plugin key/certs from secret. 89 90 func TestCreateSelfSignedIstioCAWithoutSecret(t *testing.T) { 91 caCertTTL := time.Hour 92 defaultCertTTL := 30 * time.Minute 93 maxCertTTL := time.Hour 94 org := "test.ca.Org" 95 const caNamespace = "default" 96 client := fake.NewSimpleClientset() 97 rootCertFile := "" 98 rootCertCheckInverval := time.Hour 99 rsaKeySize := 2048 100 101 caopts, err := NewSelfSignedIstioCAOptions(context.Background(), 102 0, caCertTTL, rootCertCheckInverval, defaultCertTTL, 103 maxCertTTL, org, false, false, caNamespace, client.CoreV1(), 104 rootCertFile, false, rsaKeySize) 105 if err != nil { 106 t.Fatalf("Failed to create a self-signed CA Options: %v", err) 107 } 108 109 ca, err := NewIstioCA(caopts) 110 if err != nil { 111 t.Errorf("Got error while creating self-signed CA: %v", err) 112 } 113 if ca == nil { 114 t.Fatalf("Failed to create a self-signed CA.") 115 } 116 117 signingCert, _, certChainBytes, rootCertBytes := ca.GetCAKeyCertBundle().GetAll() 118 rootCert, err := util.ParsePemEncodedCertificate(rootCertBytes) 119 if err != nil { 120 t.Error(err) 121 } 122 // Root cert and siging cert are the same for self-signed CA. 123 if !rootCert.Equal(signingCert) { 124 t.Error("CA root cert does not match signing cert") 125 } 126 127 if ttl := rootCert.NotAfter.Sub(rootCert.NotBefore); ttl != caCertTTL { 128 t.Errorf("Unexpected CA certificate TTL (expecting %v, actual %v)", caCertTTL, ttl) 129 } 130 131 if certOrg := rootCert.Issuer.Organization[0]; certOrg != org { 132 t.Errorf("Unexpected CA certificate organization (expecting %v, actual %v)", org, certOrg) 133 } 134 135 if len(certChainBytes) != 0 { 136 t.Errorf("Cert chain should be empty") 137 } 138 139 // Check the signing cert stored in K8s secret. 140 caSecret, err := client.CoreV1().Secrets("default").Get(context.TODO(), CASecret, metav1.GetOptions{}) 141 if err != nil { 142 t.Errorf("Failed to get secret (error: %s)", err) 143 } 144 145 signingCertFromSecret, err := util.ParsePemEncodedCertificate(caSecret.Data[CACertFile]) 146 if err != nil { 147 t.Errorf("Failed to parse cert (error: %s)", err) 148 } 149 150 if !signingCertFromSecret.Equal(signingCert) { 151 t.Error("CA signing cert does not match the K8s secret") 152 } 153 } 154 155 func TestCreateSelfSignedIstioCAWithoutSecretAndUseCacertsEnabled(t *testing.T) { 156 caCertTTL := time.Hour 157 defaultCertTTL := 30 * time.Minute 158 maxCertTTL := time.Hour 159 org := "test.ca.Org" 160 const caNamespace = "default" 161 client := fake.NewSimpleClientset() 162 rootCertFile := "" 163 rootCertCheckInverval := time.Hour 164 rsaKeySize := 2048 165 166 caopts, err := NewSelfSignedIstioCAOptions(context.Background(), 167 0, caCertTTL, rootCertCheckInverval, defaultCertTTL, 168 maxCertTTL, org, true, false, caNamespace, client.CoreV1(), 169 rootCertFile, false, rsaKeySize) 170 if err != nil { 171 t.Fatalf("Failed to create a self-signed CA Options: %v", err) 172 } 173 174 ca, err := NewIstioCA(caopts) 175 if err != nil { 176 t.Errorf("Got error while creating self-signed CA: %v", err) 177 } 178 if ca == nil { 179 t.Fatalf("Failed to create a self-signed CA.") 180 } 181 182 signingCert, _, certChainBytes, rootCertBytes := ca.GetCAKeyCertBundle().GetAll() 183 rootCert, err := util.ParsePemEncodedCertificate(rootCertBytes) 184 if err != nil { 185 t.Error(err) 186 } 187 // Root cert and siging cert are the same for self-signed CA. 188 if !rootCert.Equal(signingCert) { 189 t.Error("CA root cert does not match signing cert") 190 } 191 192 if ttl := rootCert.NotAfter.Sub(rootCert.NotBefore); ttl != caCertTTL { 193 t.Errorf("Unexpected CA certificate TTL (expecting %v, actual %v)", caCertTTL, ttl) 194 } 195 196 if certOrg := rootCert.Issuer.Organization[0]; certOrg != org { 197 t.Errorf("Unexpected CA certificate organization (expecting %v, actual %v)", org, certOrg) 198 } 199 200 if len(certChainBytes) != 0 { 201 t.Errorf("Cert chain should be empty") 202 } 203 204 // Check the signing cert stored in K8s secret. 205 caSecret, err := client.CoreV1().Secrets("default").Get(context.TODO(), CACertsSecret, metav1.GetOptions{}) 206 if err != nil { 207 t.Errorf("Failed to get secret (error: %s)", err) 208 } 209 210 signingCertFromSecret, err := util.ParsePemEncodedCertificate(caSecret.Data[CACertFile]) 211 if err != nil { 212 t.Errorf("Failed to parse cert (error: %s)", err) 213 } 214 215 if !signingCertFromSecret.Equal(signingCert) { 216 t.Error("CA signing cert does not match the K8s secret") 217 } 218 } 219 220 func TestCreateSelfSignedIstioCAWithSecret(t *testing.T) { 221 rootCertPem := cert1Pem 222 // Use the same signing cert and root cert for self-signed CA. 223 signingCertPem := []byte(cert1Pem) 224 signingKeyPem := []byte(key1Pem) 225 226 client := fake.NewSimpleClientset() 227 initSecret := BuildSecret(CASecret, "default", nil, nil, nil, signingCertPem, signingKeyPem, istioCASecretType) 228 _, err := client.CoreV1().Secrets("default").Create(context.TODO(), initSecret, metav1.CreateOptions{}) 229 if err != nil { 230 t.Errorf("Failed to create secret (error: %s)", err) 231 } 232 233 caCertTTL := time.Hour 234 defaultCertTTL := 30 * time.Minute 235 maxCertTTL := time.Hour 236 org := "test.ca.Org" 237 caNamespace := "default" 238 const rootCertFile = "" 239 rootCertCheckInverval := time.Hour 240 rsaKeySize := 2048 241 242 caopts, err := NewSelfSignedIstioCAOptions(context.Background(), 243 0, caCertTTL, rootCertCheckInverval, defaultCertTTL, maxCertTTL, 244 org, false, false, caNamespace, client.CoreV1(), 245 rootCertFile, false, rsaKeySize) 246 if err != nil { 247 t.Fatalf("Failed to create a self-signed CA Options: %v", err) 248 } 249 250 ca, err := NewIstioCA(caopts) 251 if err != nil { 252 t.Errorf("Got error while creating self-signed CA: %v", err) 253 } 254 if ca == nil { 255 t.Fatalf("Failed to create a self-signed CA.") 256 } 257 258 signingCert, err := util.ParsePemEncodedCertificate(signingCertPem) 259 if err != nil { 260 t.Errorf("Failed to parse cert (error: %s)", err) 261 } 262 263 signingCertFromCA, _, certChainBytesFromCA, rootCertBytesFromCA := ca.GetCAKeyCertBundle().GetAll() 264 265 if !signingCert.Equal(signingCertFromCA) { 266 t.Error("Signing cert does not match") 267 } 268 269 if !bytes.Equal(rootCertBytesFromCA, []byte(rootCertPem)) { 270 t.Error("Root cert does not match") 271 } 272 273 if len(certChainBytesFromCA) != 0 { 274 t.Errorf("Cert chain should be empty") 275 } 276 } 277 278 func TestCreateSelfSignedIstioCAReadSigningCertOnly(t *testing.T) { 279 caCertTTL := time.Hour 280 defaultCertTTL := 30 * time.Minute 281 maxCertTTL := time.Hour 282 org := "test.ca.Org" 283 caNamespace := "default" 284 const rootCertFile = "" 285 rootCertCheckInverval := time.Hour 286 rsaKeySize := 2048 287 288 client := fake.NewSimpleClientset() 289 290 // succeed creating a self-signed cert 291 ctx0, cancel0 := context.WithTimeout(context.Background(), time.Millisecond*50) 292 defer cancel0() 293 _, err := NewSelfSignedIstioCAOptions(ctx0, 0, 294 caCertTTL, defaultCertTTL, rootCertCheckInverval, maxCertTTL, org, false, false, 295 caNamespace, client.CoreV1(), rootCertFile, false, rsaKeySize) 296 if err != nil { 297 t.Errorf("Got unexpected error: %v", err) 298 } 299 300 // Using existing CASecret. 301 secret, err := client.CoreV1().Secrets("default").Get(context.TODO(), CASecret, metav1.GetOptions{}) 302 if err != nil { 303 t.Errorf("Got unexpected error %v", err) 304 } 305 signingCertPem := secret.Data[CACertFile] 306 307 ctx1, cancel1 := context.WithCancel(context.Background()) 308 defer cancel1() 309 caopts, err := NewSelfSignedIstioCAOptions(ctx1, 0, 310 caCertTTL, defaultCertTTL, rootCertCheckInverval, maxCertTTL, org, false, false, 311 caNamespace, client.CoreV1(), rootCertFile, false, rsaKeySize) 312 if err != nil { 313 t.Errorf("Unexpected error: %v", err) 314 } 315 316 ca, err := NewIstioCA(caopts) 317 if err != nil { 318 t.Errorf("Got error while creating self-signed CA: %v", err) 319 } 320 if ca == nil { 321 t.Fatalf("Failed to create a self-signed CA.") 322 } 323 324 signingCert, err := util.ParsePemEncodedCertificate(signingCertPem) 325 if err != nil { 326 t.Errorf("Failed to parse cert (error: %s)", err) 327 } 328 329 signingCertFromCA, _, _, rootCertBytesFromCA := ca.GetCAKeyCertBundle().GetAll() 330 if !signingCert.Equal(signingCertFromCA) { 331 t.Error("Signing cert does not match") 332 } 333 if !bytes.Equal(rootCertBytesFromCA, secret.Data[CACertFile]) { 334 t.Error("Root cert does not match") 335 } 336 } 337 338 func TestConcurrentCreateSelfSignedIstioCA(t *testing.T) { 339 caCertTTL := time.Hour 340 defaultCertTTL := 30 * time.Minute 341 maxCertTTL := time.Hour 342 org := "test.ca.Org" 343 caNamespace := "default" 344 const rootCertFile = "" 345 rootCertCheckInverval := time.Hour 346 rsaKeySize := 2048 347 348 client := fake.NewSimpleClientset() 349 350 parallel := 10 351 wg := sync.WaitGroup{} 352 wg.Add(parallel) 353 rootCertCh := make(chan []byte, parallel) 354 privateKeyCh := make(chan []byte, parallel) 355 356 for i := 0; i < parallel; i++ { 357 go func() { 358 defer wg.Done() 359 // succeed creating a self-signed cert 360 ctx0, cancel0 := context.WithTimeout(context.Background(), 20*time.Second) 361 defer cancel0() 362 caOpts, err := NewSelfSignedIstioCAOptions(ctx0, 0, 363 caCertTTL, defaultCertTTL, rootCertCheckInverval, maxCertTTL, org, false, false, 364 caNamespace, client.CoreV1(), rootCertFile, false, rsaKeySize) 365 if err != nil { 366 t.Errorf("NewSelfSignedIstioCAOptions got unexpected error: %v", err) 367 } 368 cert, privateKey, certChain, rootCert := caOpts.KeyCertBundle.GetAllPem() 369 if !bytes.Equal(cert, rootCert) { 370 t.Error("Root cert and cert do not match") 371 } 372 // self signed certs do not contain cert chain 373 if len(certChain) > 0 { 374 t.Error("Cert chain should not exist") 375 } 376 rootCertCh <- rootCert 377 privateKeyCh <- privateKey 378 }() 379 } 380 wg.Wait() 381 caSecret, err := client.CoreV1().Secrets(caNamespace).Get(context.TODO(), CASecret, metav1.GetOptions{}) 382 if err != nil || caSecret == nil { 383 t.Errorf("failed getting ca secret %v", err) 384 } 385 rootCert := caSecret.Data[CACertFile] 386 privateKey := caSecret.Data[CAPrivateKeyFile] 387 388 for { 389 select { 390 case current := <-rootCertCh: 391 if !bytes.Equal(rootCert, current) { 392 t.Error("Root cert does not match") 393 } 394 case current := <-privateKeyCh: 395 if !bytes.Equal(privateKey, current) { 396 t.Error("Private key does not match", string(privateKey), "\n", string(current)) 397 } 398 default: 399 return 400 } 401 } 402 } 403 404 func TestCreatePluggedCertCA(t *testing.T) { 405 rootCertFile := "../testdata/multilevelpki/root-cert.pem" 406 certChainFile := []string{"../testdata/multilevelpki/int2-cert-chain.pem"} 407 signingCertFile := "../testdata/multilevelpki/int2-cert.pem" 408 signingKeyFile := "../testdata/multilevelpki/int2-key.pem" 409 rsaKeySize := 2048 410 411 defaultWorkloadCertTTL := 99999 * time.Hour 412 maxWorkloadCertTTL := time.Hour 413 414 caopts, err := NewPluggedCertIstioCAOptions(SigningCAFileBundle{rootCertFile, certChainFile, signingCertFile, signingKeyFile}, 415 defaultWorkloadCertTTL, maxWorkloadCertTTL, rsaKeySize) 416 if err != nil { 417 t.Fatalf("Failed to create a plugged-cert CA Options: %v", err) 418 } 419 420 t0 := time.Now() 421 ca, err := NewIstioCA(caopts) 422 if err != nil { 423 t.Errorf("Got error while creating plugged-cert CA: %v", err) 424 } 425 if ca == nil { 426 t.Fatalf("Failed to create a plugged-cert CA.") 427 } 428 429 signingCertBytes, signingKeyBytes, certChainBytes, rootCertBytes := ca.GetCAKeyCertBundle().GetAllPem() 430 if !comparePem(signingCertBytes, signingCertFile) { 431 t.Errorf("Failed to verify loading of signing cert pem.") 432 } 433 if !comparePem(signingKeyBytes, signingKeyFile) { 434 t.Errorf("Failed to verify loading of signing key pem.") 435 } 436 if !comparePem(certChainBytes, certChainFile[0]) { 437 t.Errorf("Failed to verify loading of cert chain pem.") 438 } 439 if !comparePem(rootCertBytes, rootCertFile) { 440 t.Errorf("Failed to verify loading of root cert pem.") 441 } 442 443 certChain, err := util.ParsePemEncodedCertificate(certChainBytes) 444 if err != nil { 445 t.Fatalf("Failed to parse cert chain pem.") 446 } 447 // if CA cert becomes invalid before workload cert it's going to cause workload cert to be invalid too, 448 // however citatel won't rotate if that happens 449 delta := certChain.NotAfter.Sub(t0.Add(ca.defaultCertTTL)) 450 if delta >= time.Second*2 { 451 t.Errorf("Invalid default cert TTL, should be the same as cert chain: %v VS (expected) %v", 452 t0.Add(ca.defaultCertTTL), 453 certChain.NotAfter) 454 } 455 } 456 457 func TestSignCSR(t *testing.T) { 458 subjectID := "spiffe://example.com/ns/foo/sa/bar" 459 cases := map[string]struct { 460 forCA bool 461 certOpts util.CertOptions 462 maxTTL time.Duration 463 requestedTTL time.Duration 464 verifyFields util.VerifyFields 465 expectedError string 466 }{ 467 "Workload uses RSA": { 468 forCA: false, 469 certOpts: util.CertOptions{ 470 // This value is not used, instead, subjectID should be used in certificate. 471 Host: "spiffe://different.com/test", 472 RSAKeySize: 2048, 473 IsCA: false, 474 }, 475 maxTTL: time.Hour, 476 requestedTTL: 30 * time.Minute, 477 verifyFields: util.VerifyFields{ 478 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 479 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, 480 IsCA: false, 481 Host: subjectID, 482 }, 483 expectedError: "", 484 }, 485 "Workload uses EC": { 486 forCA: false, 487 certOpts: util.CertOptions{ 488 // This value is not used, instead, subjectID should be used in certificate. 489 Host: "spiffe://different.com/test", 490 ECSigAlg: util.EcdsaSigAlg, 491 IsCA: false, 492 }, 493 maxTTL: time.Hour, 494 requestedTTL: 30 * time.Minute, 495 verifyFields: util.VerifyFields{ 496 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 497 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, 498 IsCA: false, 499 Host: subjectID, 500 }, 501 expectedError: "", 502 }, 503 "CA uses RSA": { 504 forCA: true, 505 certOpts: util.CertOptions{ 506 RSAKeySize: 2048, 507 IsCA: true, 508 }, 509 maxTTL: 365 * 24 * time.Hour, 510 requestedTTL: 30 * 24 * time.Hour, 511 verifyFields: util.VerifyFields{ 512 KeyUsage: x509.KeyUsageCertSign, 513 IsCA: true, 514 Host: subjectID, 515 }, 516 expectedError: "", 517 }, 518 "CA uses EC": { 519 forCA: true, 520 certOpts: util.CertOptions{ 521 ECSigAlg: util.EcdsaSigAlg, 522 IsCA: true, 523 }, 524 maxTTL: 365 * 24 * time.Hour, 525 requestedTTL: 30 * 24 * time.Hour, 526 verifyFields: util.VerifyFields{ 527 KeyUsage: x509.KeyUsageCertSign, 528 IsCA: true, 529 Host: subjectID, 530 }, 531 expectedError: "", 532 }, 533 "CSR uses RSA TTL error": { 534 forCA: false, 535 certOpts: util.CertOptions{ 536 Org: "istio.io", 537 RSAKeySize: 2048, 538 }, 539 maxTTL: 2 * time.Hour, 540 requestedTTL: 3 * time.Hour, 541 expectedError: "requested TTL 3h0m0s is greater than the max allowed TTL 2h0m0s", 542 }, 543 "CSR uses EC TTL error": { 544 forCA: false, 545 certOpts: util.CertOptions{ 546 Org: "istio.io", 547 ECSigAlg: util.EcdsaSigAlg, 548 }, 549 maxTTL: 2 * time.Hour, 550 requestedTTL: 3 * time.Hour, 551 expectedError: "requested TTL 3h0m0s is greater than the max allowed TTL 2h0m0s", 552 }, 553 } 554 555 for id, tc := range cases { 556 csrPEM, keyPEM, err := util.GenCSR(tc.certOpts) 557 if err != nil { 558 t.Errorf("%s: GenCSR error: %v", id, err) 559 } 560 561 ca, err := createCA(tc.maxTTL, tc.certOpts.ECSigAlg) 562 if err != nil { 563 t.Errorf("%s: createCA error: %v", id, err) 564 } 565 566 caCertOpts := CertOpts{ 567 SubjectIDs: []string{subjectID}, 568 TTL: tc.requestedTTL, 569 ForCA: tc.forCA, 570 } 571 certPEM, signErr := ca.Sign(csrPEM, caCertOpts) 572 if signErr != nil { 573 if tc.expectedError == "" { 574 t.Errorf("%s: Sign error: %v", id, err) 575 } 576 if certPEM != nil { 577 t.Errorf("%s: Expected null cert be obtained a non-null cert.", id) 578 } 579 if signErr.(*caerror.Error).Error() != tc.expectedError { 580 t.Errorf("%s: Expected error: %s but got error: %s.", id, tc.expectedError, signErr.(*caerror.Error).Error()) 581 } 582 continue 583 } 584 585 _, _, certChainBytes, rootCertBytes := ca.GetCAKeyCertBundle().GetAll() 586 if err = util.VerifyCertificate( 587 keyPEM, append(certPEM, certChainBytes...), rootCertBytes, &tc.verifyFields); err != nil { 588 t.Errorf("%s: VerifyCertificate error: %v", id, err) 589 } 590 591 cert, err := util.ParsePemEncodedCertificate(certPEM) 592 if err != nil { 593 t.Errorf("%s: ParsePemEncodedCertificate error: %v", id, err) 594 } 595 596 if ttl := cert.NotAfter.Sub(cert.NotBefore) - util.ClockSkewGracePeriod; ttl != tc.requestedTTL { 597 t.Errorf("%s: Unexpected certificate TTL (expecting %v, actual %v)", id, tc.requestedTTL, ttl) 598 } 599 san := util.ExtractSANExtension(cert.Extensions) 600 if san == nil { 601 t.Errorf("%s: No SAN extension is found in the certificate", id) 602 } 603 expected, err := util.BuildSubjectAltNameExtension(subjectID) 604 if err != nil { 605 t.Errorf("%s: BuildSubjectAltNameExtension error: %v", id, err) 606 } 607 if !reflect.DeepEqual(expected, san) { 608 t.Errorf("%s: Unexpected extensions: wanted %v but got %v", id, expected, san) 609 } 610 } 611 } 612 613 func TestAppendRootCerts(t *testing.T) { 614 root1 := "root-cert-1" 615 expRootCerts := `root-cert-1 616 root-cert-2 617 root-cert-3` 618 rootCerts, err := util.AppendRootCerts([]byte(root1), "./root-certs-for-testing.pem") 619 if err != nil { 620 t.Errorf("AppendRootCerts() returns an error: %v", err) 621 } else if expRootCerts != string(rootCerts) { 622 t.Errorf("the root certificates do not match. Expect:%v. Actual:%v.", 623 expRootCerts, string(rootCerts)) 624 } 625 } 626 627 func TestAppendRootCertsToNullCert(t *testing.T) { 628 // nil certificate 629 var root1 []byte 630 expRootCerts := `root-cert-2 631 root-cert-3` 632 rootCerts, err := util.AppendRootCerts(root1, "./root-certs-for-testing.pem") 633 if err != nil { 634 t.Errorf("AppendRootCerts() returns an error: %v", err) 635 } else if expRootCerts != string(rootCerts) { 636 t.Errorf("the root certificates do not match. Expect:%v. Actual:%v.", 637 expRootCerts, string(rootCerts)) 638 } 639 } 640 641 func TestSignWithCertChain(t *testing.T) { 642 rootCertFile := "../testdata/multilevelpki/root-cert.pem" 643 certChainFile := []string{"../testdata/multilevelpki/int-cert-chain.pem"} 644 signingCertFile := "../testdata/multilevelpki/int-cert.pem" 645 signingKeyFile := "../testdata/multilevelpki/int-key.pem" 646 rsaKeySize := 2048 647 648 defaultWorkloadCertTTL := 30 * time.Minute 649 maxWorkloadCertTTL := time.Hour 650 651 caopts, err := NewPluggedCertIstioCAOptions(SigningCAFileBundle{rootCertFile, certChainFile, signingCertFile, signingKeyFile}, 652 defaultWorkloadCertTTL, maxWorkloadCertTTL, rsaKeySize) 653 if err != nil { 654 t.Fatalf("Failed to create a plugged-cert CA Options: %v", err) 655 } 656 657 ca, err := NewIstioCA(caopts) 658 if err != nil { 659 t.Errorf("Got error while creating plugged-cert CA: %v", err) 660 } 661 if ca == nil { 662 t.Fatalf("Failed to create a plugged-cert CA.") 663 } 664 665 opts := util.CertOptions{ 666 // This value is not used, instead, subjectID should be used in certificate. 667 Host: "spiffe://different.com/test", 668 RSAKeySize: 2048, 669 IsCA: false, 670 } 671 csrPEM, privPEM, err := util.GenCSR(opts) 672 if err != nil { 673 t.Error(err) 674 } 675 676 caCertOpts := CertOpts{ 677 SubjectIDs: []string{"localhost"}, 678 TTL: time.Hour, 679 ForCA: false, 680 } 681 certPEM, signErr := ca.signWithCertChain(csrPEM, caCertOpts.SubjectIDs, caCertOpts.TTL, true, caCertOpts.ForCA) 682 683 if signErr != nil { 684 t.Error(err) 685 } 686 687 cert, err := tls.X509KeyPair(certPEM, privPEM) 688 if err != nil { 689 t.Error(err) 690 } 691 692 if len(cert.Certificate) != 3 { 693 t.Errorf("Unexpected number of certificates returned: %d (expected 4)", len(cert.Certificate)) 694 } 695 } 696 697 func TestGenKeyCert(t *testing.T) { 698 cases := map[string]struct { 699 rootCertFile string 700 certChainFile []string 701 signingCertFile string 702 signingKeyFile string 703 certLifetime time.Duration 704 checkCertLifetime bool 705 expectedError string 706 }{ 707 "RSA cryptography": { 708 rootCertFile: "../testdata/multilevelpki/root-cert.pem", 709 certChainFile: []string{"../testdata/multilevelpki/int-cert-chain.pem"}, 710 signingCertFile: "../testdata/multilevelpki/int-cert.pem", 711 signingKeyFile: "../testdata/multilevelpki/int-key.pem", 712 certLifetime: 3650 * 24 * time.Hour, 713 checkCertLifetime: false, 714 expectedError: "", 715 }, 716 "EC cryptography": { 717 rootCertFile: "../testdata/multilevelpki/ecc-root-cert.pem", 718 certChainFile: []string{"../testdata/multilevelpki/ecc-int-cert-chain.pem"}, 719 signingCertFile: "../testdata/multilevelpki/ecc-int-cert.pem", 720 signingKeyFile: "../testdata/multilevelpki/ecc-int-key.pem", 721 certLifetime: 3650 * 24 * time.Hour, 722 checkCertLifetime: false, 723 expectedError: "", 724 }, 725 "Pass lifetime check": { 726 rootCertFile: "../testdata/multilevelpki/ecc-root-cert.pem", 727 certChainFile: []string{"../testdata/multilevelpki/ecc-int-cert-chain.pem"}, 728 signingCertFile: "../testdata/multilevelpki/ecc-int-cert.pem", 729 signingKeyFile: "../testdata/multilevelpki/ecc-int-key.pem", 730 certLifetime: 24 * time.Hour, 731 checkCertLifetime: true, 732 expectedError: "", 733 }, 734 "Error lifetime check": { 735 rootCertFile: "../testdata/multilevelpki/ecc-root-cert.pem", 736 certChainFile: []string{"../testdata/multilevelpki/ecc-int-cert-chain.pem"}, 737 signingCertFile: "../testdata/multilevelpki/ecc-int-cert.pem", 738 signingKeyFile: "../testdata/multilevelpki/ecc-int-key.pem", 739 certLifetime: 25 * time.Hour, 740 checkCertLifetime: true, 741 expectedError: "requested TTL 25h0m0s is greater than the max allowed TTL 24h0m0s", 742 }, 743 } 744 defaultWorkloadCertTTL := 30 * time.Minute 745 maxWorkloadCertTTL := 24 * time.Hour 746 rsaKeySize := 2048 747 748 for id, tc := range cases { 749 caopts, err := NewPluggedCertIstioCAOptions(SigningCAFileBundle{tc.rootCertFile, tc.certChainFile, tc.signingCertFile, tc.signingKeyFile}, 750 defaultWorkloadCertTTL, maxWorkloadCertTTL, rsaKeySize) 751 if err != nil { 752 t.Fatalf("%s: failed to create a plugged-cert CA Options: %v", id, err) 753 } 754 755 ca, err := NewIstioCA(caopts) 756 if err != nil { 757 t.Fatalf("%s: got error while creating plugged-cert CA: %v", id, err) 758 } 759 if ca == nil { 760 t.Fatalf("failed to create a plugged-cert CA.") 761 } 762 763 certPEM, privPEM, err := ca.GenKeyCert([]string{"host1", "host2"}, tc.certLifetime, tc.checkCertLifetime) 764 if err != nil { 765 if tc.expectedError == "" { 766 t.Fatalf("[%s] Unexpected error: %v", id, err) 767 } 768 if err.Error() != tc.expectedError { 769 t.Fatalf("[%s] Error returned does not match expectation: %v VS (expected) %v", id, err, tc.expectedError) 770 } 771 continue 772 } else if tc.expectedError != "" { 773 t.Fatalf("[%s] GenKeyCert succeeded but expected error: %v", id, tc.expectedError) 774 } 775 776 cert, err := tls.X509KeyPair(certPEM, privPEM) 777 if err != nil { 778 t.Fatalf("[%s] X509KeyPair error: %v", id, err) 779 } 780 781 if len(cert.Certificate) != 3 { 782 t.Fatalf("[%s] unexpected number of certificates returned: %d (expected 3)", id, len(cert.Certificate)) 783 } 784 } 785 } 786 787 // TestBuildSecret verifies that BuildSecret returns expected secret. 788 func TestBuildSecret(t *testing.T) { 789 CertPem := []byte(cert1Pem) 790 KeyPem := []byte(key1Pem) 791 namespace := "default" 792 secretType := "secret-type" 793 794 caSecret := BuildSecret(CASecret, namespace, nil, nil, nil, CertPem, KeyPem, v1.SecretType(secretType)) 795 if caSecret.ObjectMeta.Annotations != nil { 796 t.Fatalf("Annotation should be nil but got %v", caSecret) 797 } 798 if _, ok := caSecret.Data[IstioGenerated]; ok { 799 t.Fatal("IstioGenerated key should not exist") 800 } 801 if caSecret.Data[CertChainFile] != nil { 802 t.Fatalf("Cert chain should be nil but got %v", caSecret.Data[CertChainFile]) 803 } 804 if caSecret.Data[PrivateKeyFile] != nil { 805 t.Fatalf("Private key should be nil but got %v", caSecret.Data[PrivateKeyFile]) 806 } 807 if !bytes.Equal(caSecret.Data[CACertFile], CertPem) { 808 t.Fatalf("CA cert does not match, want %v got %v", CertPem, caSecret.Data[CACertFile]) 809 } 810 if !bytes.Equal(caSecret.Data[CAPrivateKeyFile], KeyPem) { 811 t.Fatalf("CA cert does not match, want %v got %v", KeyPem, caSecret.Data[CAPrivateKeyFile]) 812 } 813 serverSecret := BuildSecret(CACertsSecret, namespace, CertPem, KeyPem, nil, nil, nil, v1.SecretType(secretType)) 814 if serverSecret.ObjectMeta.Annotations != nil { 815 t.Fatalf("Annotation should be nil but got %v", serverSecret) 816 } 817 if _, ok := serverSecret.Data[IstioGenerated]; !ok { 818 t.Fatal("IstioGenerated key should exist") 819 } 820 if serverSecret.Data[CACertFile] != nil { 821 t.Fatalf("CA Cert should be nil but got %v", serverSecret.Data[CACertFile]) 822 } 823 if serverSecret.Data[CAPrivateKeyFile] != nil { 824 t.Fatalf("CA private key should be nil but got %v", serverSecret.Data[CAPrivateKeyFile]) 825 } 826 if !bytes.Equal(serverSecret.Data[CertChainFile], CertPem) { 827 t.Fatalf("Cert chain does not match, want %v got %v", CertPem, serverSecret.Data[CertChainFile]) 828 } 829 if !bytes.Equal(serverSecret.Data[PrivateKeyFile], KeyPem) { 830 t.Fatalf("Private key does not match, want %v got %v", KeyPem, serverSecret.Data[PrivateKeyFile]) 831 } 832 } 833 834 func createCA(maxTTL time.Duration, ecSigAlg util.SupportedECSignatureAlgorithms) (*IstioCA, error) { 835 // Generate root CA key and cert. 836 rootCAOpts := util.CertOptions{ 837 IsCA: true, 838 IsSelfSigned: true, 839 TTL: time.Hour, 840 Org: "Root CA", 841 RSAKeySize: 2048, 842 ECSigAlg: ecSigAlg, 843 } 844 845 rootCertBytes, rootKeyBytes, err := util.GenCertKeyFromOptions(rootCAOpts) 846 if err != nil { 847 return nil, err 848 } 849 850 rootCert, err := util.ParsePemEncodedCertificate(rootCertBytes) 851 if err != nil { 852 return nil, err 853 } 854 855 rootKey, err := util.ParsePemEncodedKey(rootKeyBytes) 856 if err != nil { 857 return nil, err 858 } 859 860 intermediateCAOpts := util.CertOptions{ 861 IsCA: true, 862 IsSelfSigned: false, 863 TTL: time.Hour, 864 Org: "Intermediate CA", 865 RSAKeySize: 2048, 866 SignerCert: rootCert, 867 SignerPriv: rootKey, 868 ECSigAlg: ecSigAlg, 869 } 870 871 intermediateCert, intermediateKey, err := util.GenCertKeyFromOptions(intermediateCAOpts) 872 if err != nil { 873 return nil, err 874 } 875 876 bundle, err := util.NewVerifiedKeyCertBundleFromPem( 877 intermediateCert, intermediateKey, intermediateCert, rootCertBytes) 878 if err != nil { 879 return nil, err 880 } 881 // Disable root cert rotator by setting root cert check interval to 0ns. 882 rootCertCheckInverval := time.Duration(0) 883 caOpts := &IstioCAOptions{ 884 DefaultCertTTL: time.Hour, 885 MaxCertTTL: maxTTL, 886 KeyCertBundle: bundle, 887 RotatorConfig: &SelfSignedCARootCertRotatorConfig{ 888 CheckInterval: rootCertCheckInverval, 889 }, 890 } 891 892 return NewIstioCA(caOpts) 893 } 894 895 func comparePem(expectedBytes []byte, file string) bool { 896 fileBytes, err := os.ReadFile(file) 897 if err != nil { 898 return false 899 } 900 if !bytes.Equal(fileBytes, expectedBytes) { 901 return false 902 } 903 return true 904 }