istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/pki/util/keycertbundle_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 util 16 17 import ( 18 "fmt" 19 "strings" 20 "testing" 21 "time" 22 ) 23 24 const ( 25 rootCertFile = "../testdata/multilevelpki/root-cert.pem" 26 rootKeyFile = "../testdata/multilevelpki/root-key.pem" 27 intCertFile = "../testdata/multilevelpki/int-cert.pem" 28 intKeyFile = "../testdata/multilevelpki/int-key.pem" 29 intCertChainFile = "../testdata/multilevelpki/int-cert-chain.pem" 30 int2CertFile = "../testdata/multilevelpki/int2-cert.pem" 31 int2KeyFile = "../testdata/multilevelpki/int2-key.pem" 32 int2CertChainFile = "../testdata/multilevelpki/int2-cert-chain.pem" 33 badCertFile = "../testdata/cert-parse-fail.pem" 34 badKeyFile = "../testdata/key-parse-fail.pem" 35 anotherKeyFile = "../testdata/key.pem" 36 anotherRootCertFile = "../testdata/cert.pem" 37 // These key/cert contain workload key/cert, and a self-signed root cert, 38 // all with TTL 100 years. 39 rootCertFile1 = "../testdata/self-signed-root-cert.pem" 40 certChainFile1 = "../testdata/workload-cert.pem" 41 keyFile1 = "../testdata/workload-key.pem" 42 ecRootCertFile = "../testdata/ec-root-cert.pem" 43 ecRootKeyFile = "../testdata/ec-root-key.pem" 44 ecClientCertFile = "../testdata/ec-workload-cert.pem" 45 ecClientKeyFile = "../testdata/ec-workload-key.pem" 46 ) 47 48 func TestKeyCertBundleWithRootCertFromFile(t *testing.T) { 49 testCases := map[string]struct { 50 rootCertFile string 51 expectedErr string 52 }{ 53 "File not found": { 54 rootCertFile: "bad.pem", 55 expectedErr: "open bad.pem: no such file or directory", 56 }, 57 "With RSA root cert": { 58 rootCertFile: rootCertFile, 59 expectedErr: "", 60 }, 61 "With EC root cert": { 62 rootCertFile: rootCertFile1, 63 expectedErr: "", 64 }, 65 } 66 for id, tc := range testCases { 67 bundle, err := NewKeyCertBundleWithRootCertFromFile(tc.rootCertFile) 68 if err != nil { 69 if tc.expectedErr == "" { 70 t.Errorf("%s: Unexpected error: %v", id, err) 71 } else if strings.Compare(err.Error(), tc.expectedErr) != 0 { 72 t.Errorf("%s: Unexpected error: %v VS (expected) %s", id, err, tc.expectedErr) 73 } 74 } else if tc.expectedErr != "" { 75 t.Errorf("%s: Expected error %s but succeeded", id, tc.expectedErr) 76 } else if bundle == nil { 77 t.Errorf("%s: the bundle should not be empty", id) 78 } else { 79 cert, key, chain, root := bundle.GetAllPem() 80 if len(cert) != 0 { 81 t.Errorf("%s: certBytes should be empty", id) 82 } 83 if len(key) != 0 { 84 t.Errorf("%s: privateKeyBytes should be empty", id) 85 } 86 if len(chain) != 0 { 87 t.Errorf("%s: certChainBytes should be empty", id) 88 } 89 if len(root) == 0 { 90 t.Errorf("%s: rootCertBytes should not be empty", id) 91 } 92 93 chain = bundle.GetCertChainPem() 94 if len(chain) != 0 { 95 t.Errorf("%s: certChainBytes should be empty", id) 96 } 97 98 root = bundle.GetRootCertPem() 99 if len(root) == 0 { 100 t.Errorf("%s: rootCertBytes should not be empty", id) 101 } 102 103 x509Cert, privKey, chain, root := bundle.GetAll() 104 if x509Cert != nil { 105 t.Errorf("%s: cert should be nil", id) 106 } 107 if privKey != nil { 108 t.Errorf("%s: private key should be nil", id) 109 } 110 if len(chain) != 0 { 111 t.Errorf("%s: certChainBytes should be empty", id) 112 } 113 if len(root) == 0 { 114 t.Errorf("%s: rootCertBytes should not be empty", id) 115 } 116 } 117 } 118 } 119 120 // The test of CertOptions 121 func TestCertOptionsAndRetrieveID(t *testing.T) { 122 testCases := map[string]struct { 123 caCertFile string 124 caKeyFile string 125 certChainFile []string 126 rootCertFile string 127 certOptions *CertOptions 128 expectedErr string 129 }{ 130 "No SAN RSA": { 131 caCertFile: rootCertFile, 132 caKeyFile: rootKeyFile, 133 certChainFile: nil, 134 rootCertFile: rootCertFile, 135 certOptions: &CertOptions{ 136 Host: "test_ca.com", 137 TTL: time.Hour, 138 Org: "MyOrg", 139 IsCA: true, 140 RSAKeySize: 2048, 141 }, 142 expectedErr: "failed to extract id the SAN extension does not exist", 143 }, 144 "RSA Success": { 145 caCertFile: certChainFile1, 146 caKeyFile: keyFile1, 147 certChainFile: nil, 148 rootCertFile: rootCertFile1, 149 certOptions: &CertOptions{ 150 Host: "watt", 151 TTL: 100 * 365 * 24 * time.Hour, 152 Org: "Juju org", 153 IsCA: false, 154 RSAKeySize: 2048, 155 }, 156 expectedErr: "", 157 }, 158 "No SAN EC": { 159 caCertFile: ecRootCertFile, 160 caKeyFile: ecRootKeyFile, 161 certChainFile: nil, 162 rootCertFile: ecRootCertFile, 163 certOptions: &CertOptions{ 164 Host: "watt", 165 TTL: 100 * 365 * 24 * time.Hour, 166 Org: "Juju org", 167 IsCA: true, 168 ECSigAlg: EcdsaSigAlg, 169 }, 170 expectedErr: "failed to extract id the SAN extension does not exist", 171 }, 172 "EC Success": { 173 caCertFile: ecClientCertFile, 174 caKeyFile: ecClientKeyFile, 175 certChainFile: nil, 176 rootCertFile: ecRootCertFile, 177 certOptions: &CertOptions{ 178 Host: "watt", 179 TTL: 10 * 365 * 24 * time.Hour, 180 Org: "Juju org", 181 IsCA: false, 182 ECSigAlg: EcdsaSigAlg, 183 }, 184 expectedErr: "", 185 }, 186 } 187 for id, tc := range testCases { 188 k, err := NewVerifiedKeyCertBundleFromFile(tc.caCertFile, tc.caKeyFile, tc.certChainFile, tc.rootCertFile) 189 if err != nil { 190 t.Fatalf("%s: Unexpected error: %v", id, err) 191 } 192 opts, err := k.CertOptions() 193 if err != nil { 194 if tc.expectedErr == "" { 195 t.Errorf("%s: Unexpected error: %v", id, err) 196 } else if strings.Compare(err.Error(), tc.expectedErr) != 0 { 197 t.Errorf("%s: Unexpected error: %v VS (expected) %s", id, err, tc.expectedErr) 198 } 199 } else if tc.expectedErr != "" { 200 t.Errorf("%s: expected error %s but have error %v", id, tc.expectedErr, err) 201 } else { 202 compareCertOptions(opts, tc.certOptions, t) 203 } 204 } 205 } 206 207 func compareCertOptions(actual, expected *CertOptions, t *testing.T) { 208 if actual.Host != expected.Host { 209 t.Errorf("host does not match, %s vs %s", actual.Host, expected.Host) 210 } 211 if actual.TTL != expected.TTL { 212 t.Errorf("TTL does not match") 213 } 214 if actual.Org != expected.Org { 215 t.Errorf("Org does not match") 216 } 217 if actual.IsCA != expected.IsCA { 218 t.Errorf("IsCA does not match") 219 } 220 if actual.RSAKeySize != expected.RSAKeySize { 221 t.Errorf("RSAKeySize does not match") 222 } 223 } 224 225 // The test of NewVerifiedKeyCertBundleFromPem, VerifyAndSetAll can be covered by this test. 226 func TestNewVerifiedKeyCertBundleFromFile(t *testing.T) { 227 testCases := map[string]struct { 228 caCertFile string 229 caKeyFile string 230 certChainFile []string 231 rootCertFile string 232 expectedErr string 233 }{ 234 "Success - 1 level CA": { 235 caCertFile: rootCertFile, 236 caKeyFile: rootKeyFile, 237 certChainFile: nil, 238 rootCertFile: rootCertFile, 239 expectedErr: "", 240 }, 241 "Success - 2 level CA": { 242 caCertFile: intCertFile, 243 caKeyFile: intKeyFile, 244 certChainFile: []string{intCertChainFile}, 245 rootCertFile: rootCertFile, 246 expectedErr: "", 247 }, 248 "Success - 3 level CA": { 249 caCertFile: int2CertFile, 250 caKeyFile: int2KeyFile, 251 certChainFile: []string{int2CertChainFile}, 252 rootCertFile: rootCertFile, 253 expectedErr: "", 254 }, 255 "Success - 2 level CA without cert chain file": { 256 caCertFile: intCertFile, 257 caKeyFile: intKeyFile, 258 certChainFile: nil, 259 rootCertFile: rootCertFile, 260 expectedErr: "", 261 }, 262 "Failure - invalid cert chain file": { 263 caCertFile: intCertFile, 264 caKeyFile: intKeyFile, 265 certChainFile: []string{"bad.pem"}, 266 rootCertFile: rootCertFile, 267 expectedErr: "open bad.pem: no such file or directory", 268 }, 269 "Failure - no root cert file": { 270 caCertFile: intCertFile, 271 caKeyFile: intKeyFile, 272 certChainFile: nil, 273 rootCertFile: "bad.pem", 274 expectedErr: "open bad.pem: no such file or directory", 275 }, 276 "Failure - cert and key do not match": { 277 caCertFile: int2CertFile, 278 caKeyFile: anotherKeyFile, 279 certChainFile: []string{int2CertChainFile}, 280 rootCertFile: rootCertFile, 281 expectedErr: "the cert does not match the key", 282 }, 283 "Failure - 3 level CA without cert chain file": { 284 caCertFile: int2CertFile, 285 caKeyFile: int2KeyFile, 286 certChainFile: nil, 287 rootCertFile: rootCertFile, 288 expectedErr: "cannot verify the cert with the provided root chain and " + 289 "cert pool with error: x509: certificate signed by unknown authority", 290 }, 291 "Failure - cert not verifiable from root cert": { 292 caCertFile: intCertFile, 293 caKeyFile: intKeyFile, 294 certChainFile: []string{intCertChainFile}, 295 rootCertFile: anotherRootCertFile, 296 expectedErr: "cannot verify the cert with the provided root chain and " + 297 "cert pool with error: x509: certificate is not authorized to sign " + 298 "other certificates", 299 }, 300 "Failure - invalid cert": { 301 caCertFile: badCertFile, 302 caKeyFile: intKeyFile, 303 certChainFile: nil, 304 rootCertFile: rootCertFile, 305 expectedErr: "failed to parse cert PEM: invalid PEM encoded certificate", 306 }, 307 "Failure - not existing private key": { 308 caCertFile: intCertFile, 309 caKeyFile: "bad.pem", 310 certChainFile: nil, 311 rootCertFile: rootCertFile, 312 expectedErr: "open bad.pem: no such file or directory", 313 }, 314 "Failure - invalid private key": { 315 caCertFile: intCertFile, 316 caKeyFile: badKeyFile, 317 certChainFile: nil, 318 rootCertFile: rootCertFile, 319 expectedErr: "failed to parse private key PEM: invalid PEM-encoded key", 320 }, 321 "Failure - file does not exist": { 322 caCertFile: "random/path/does/not/exist", 323 caKeyFile: intKeyFile, 324 certChainFile: nil, 325 rootCertFile: rootCertFile, 326 expectedErr: "open random/path/does/not/exist: no such file or directory", 327 }, 328 } 329 for id, tc := range testCases { 330 _, err := NewVerifiedKeyCertBundleFromFile( 331 tc.caCertFile, tc.caKeyFile, tc.certChainFile, tc.rootCertFile) 332 if err != nil { 333 if tc.expectedErr == "" { 334 t.Errorf("%s: Unexpected error: %v", id, err) 335 } else if !strings.HasPrefix(err.Error(), tc.expectedErr) { 336 t.Errorf("%s: Unexpected error: %v VS (expected) %s", id, err, tc.expectedErr) 337 } 338 } else if tc.expectedErr != "" { 339 t.Errorf("%s: Expected error %s but succeeded", id, tc.expectedErr) 340 } 341 } 342 } 343 344 // Test the root cert expiry timestamp can be extracted correctly. 345 func TestExtractRootCertExpiryTimestamp(t *testing.T) { 346 t0 := time.Now() 347 cert, key, err := GenCertKeyFromOptions(CertOptions{ 348 Host: "citadel.testing.istio.io", 349 NotBefore: t0, 350 TTL: time.Minute, 351 Org: "MyOrg", 352 IsCA: true, 353 IsSelfSigned: true, 354 IsServer: true, 355 RSAKeySize: 2048, 356 }) 357 if err != nil { 358 t.Errorf("failed to gen cert for Citadel self signed cert %v", err) 359 } 360 kb, err := NewVerifiedKeyCertBundleFromPem(cert, key, nil, cert) 361 if err != nil { 362 t.Errorf("failed to create key cert bundle: %v", err) 363 } 364 testCases := []struct { 365 name string 366 ttl float64 367 time time.Time 368 }{ 369 { 370 name: "ttl valid", 371 ttl: 30, 372 time: t0.Add(time.Second * 30), 373 }, 374 { 375 name: "ttl almost expired", 376 ttl: 2, 377 time: t0.Add(time.Second * 58), 378 }, 379 { 380 name: "ttl just expired", 381 ttl: 0, 382 time: t0.Add(time.Second * 60), 383 }, 384 { 385 name: "ttl-invalid", 386 ttl: -30, 387 time: t0.Add(time.Second * 90), 388 }, 389 } 390 for _, tc := range testCases { 391 t.Run(tc.name, func(t *testing.T) { 392 expiryTimestamp, _ := kb.ExtractRootCertExpiryTimestamp() 393 // Ignore error; it just indicates cert is expired which we check via `tc.ttl` 394 395 sec := expiryTimestamp - float64(tc.time.Unix()) 396 if sec != tc.ttl { 397 t.Fatalf("expected ttl %v, got %v", tc.ttl, sec) 398 } 399 }) 400 } 401 } 402 403 // Test the CA cert expiry timestamp can be extracted correctly. 404 func TestExtractCACertExpiryTimestamp(t *testing.T) { 405 t0 := time.Now() 406 rootCertBytes, rootKeyBytes, err := GenCertKeyFromOptions(CertOptions{ 407 Host: "citadel.testing.istio.io", 408 Org: "MyOrg", 409 NotBefore: t0, 410 IsCA: true, 411 IsSelfSigned: true, 412 TTL: time.Hour, 413 RSAKeySize: 2048, 414 }) 415 if err != nil { 416 t.Errorf("failed to gen root cert for Citadel self signed cert %v", err) 417 } 418 419 rootCert, err := ParsePemEncodedCertificate(rootCertBytes) 420 if err != nil { 421 t.Errorf("failed to parsing pem for root cert %v", err) 422 } 423 424 rootKey, err := ParsePemEncodedKey(rootKeyBytes) 425 if err != nil { 426 t.Errorf("failed to parsing pem for root key cert %v", err) 427 } 428 429 caCertBytes, caCertKeyBytes, err := GenCertKeyFromOptions(CertOptions{ 430 Host: "citadel.testing.istio.io", 431 Org: "MyOrg", 432 NotBefore: t0, 433 TTL: time.Second * 60, 434 IsServer: true, 435 IsCA: true, 436 IsSelfSigned: false, 437 RSAKeySize: 2048, 438 SignerCert: rootCert, 439 SignerPriv: rootKey, 440 }) 441 if err != nil { 442 t.Fatalf("failed to gen CA cert for Citadel self signed cert %v", err) 443 } 444 445 kb, err := NewVerifiedKeyCertBundleFromPem( 446 caCertBytes, caCertKeyBytes, caCertBytes, rootCertBytes) 447 if err != nil { 448 t.Fatalf("failed to create key cert bundle: %v", err) 449 } 450 451 testCases := []struct { 452 name string 453 ttl float64 454 time time.Time 455 }{ 456 { 457 name: "ttl valid", 458 ttl: 30, 459 time: t0.Add(time.Second * 30), 460 }, 461 { 462 name: "ttl almost expired", 463 ttl: 2, 464 time: t0.Add(time.Second * 58), 465 }, 466 { 467 name: "ttl just expired", 468 ttl: 0, 469 time: t0.Add(time.Second * 60), 470 }, 471 { 472 name: "ttl-invalid", 473 ttl: -30, 474 time: t0.Add(time.Second * 90), 475 }, 476 } 477 for _, tc := range testCases { 478 t.Run(tc.name, func(t *testing.T) { 479 expiryTimestamp, _ := kb.ExtractCACertExpiryTimestamp() 480 // Ignore error; it just indicates cert is expired which we check via `tc.ttl` 481 482 sec := expiryTimestamp - float64(tc.time.Unix()) 483 if sec != tc.ttl { 484 t.Fatalf("expected ttl %v, got %v", tc.ttl, sec) 485 } 486 }) 487 } 488 } 489 490 func TestTimeBeforeCertExpires(t *testing.T) { 491 t0 := time.Now() 492 certTTL := time.Second * 60 493 rootCertBytes, _, err := GenCertKeyFromOptions(CertOptions{ 494 Host: "citadel.testing.istio.io", 495 Org: "MyOrg", 496 NotBefore: t0, 497 IsCA: true, 498 IsSelfSigned: true, 499 TTL: certTTL, 500 RSAKeySize: 2048, 501 }) 502 if err != nil { 503 t.Errorf("failed to gen root cert for Citadel self signed cert %v", err) 504 } 505 506 testCases := []struct { 507 name string 508 cert []byte 509 expectedTime time.Duration 510 timeNow time.Time 511 expectedErr error 512 }{ 513 { 514 name: "TTL left should be equal to cert TTL", 515 cert: rootCertBytes, 516 timeNow: t0, 517 expectedTime: certTTL, 518 }, 519 { 520 name: "TTL left should be ca cert ttl minus 5 seconds", 521 cert: rootCertBytes, 522 timeNow: t0.Add(5 * time.Second), 523 expectedTime: 55 * time.Second, 524 }, 525 { 526 name: "TTL left should be negative because already got expired", 527 cert: rootCertBytes, 528 timeNow: t0.Add(120 * time.Second), 529 expectedTime: -60 * time.Second, 530 }, 531 { 532 name: "no cert, so it should return an error", 533 cert: nil, 534 timeNow: t0, 535 expectedErr: fmt.Errorf("no certificate found"), 536 }, 537 { 538 name: "invalid cert", 539 cert: []byte("invalid cert"), 540 timeNow: t0, 541 expectedErr: fmt.Errorf("failed to extract cert expiration timestamp: failed to parse the cert: invalid PEM encoded certificate"), 542 }, 543 } 544 545 for _, tc := range testCases { 546 t.Run(tc.name, func(t *testing.T) { 547 time, err := TimeBeforeCertExpires(tc.cert, tc.timeNow) 548 if err != nil { 549 if tc.expectedErr == nil { 550 t.Fatalf("Unexpected error: %v", err) 551 } else if strings.Compare(err.Error(), tc.expectedErr.Error()) != 0 { 552 t.Errorf("expected error: %v got %v", err, tc.expectedErr) 553 } 554 return 555 } 556 557 if time != tc.expectedTime { 558 t.Fatalf("expected time %v, got %v", tc.expectedTime, time) 559 } 560 }) 561 } 562 }