| // 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 util |
| |
| import ( |
| "fmt" |
| "strings" |
| "testing" |
| "time" |
| ) |
| |
| const ( |
| rootCertFile = "../testdata/multilevelpki/root-cert.pem" |
| rootKeyFile = "../testdata/multilevelpki/root-key.pem" |
| intCertFile = "../testdata/multilevelpki/int-cert.pem" |
| intKeyFile = "../testdata/multilevelpki/int-key.pem" |
| intCertChainFile = "../testdata/multilevelpki/int-cert-chain.pem" |
| int2CertFile = "../testdata/multilevelpki/int2-cert.pem" |
| int2KeyFile = "../testdata/multilevelpki/int2-key.pem" |
| int2CertChainFile = "../testdata/multilevelpki/int2-cert-chain.pem" |
| badCertFile = "../testdata/cert-parse-fail.pem" |
| badKeyFile = "../testdata/key-parse-fail.pem" |
| anotherKeyFile = "../testdata/key.pem" |
| anotherRootCertFile = "../testdata/cert.pem" |
| // These key/cert contain workload key/cert, and a self-signed root cert, |
| // all with TTL 100 years. |
| rootCertFile1 = "../testdata/self-signed-root-cert.pem" |
| certChainFile1 = "../testdata/workload-cert.pem" |
| keyFile1 = "../testdata/workload-key.pem" |
| ecRootCertFile = "../testdata/ec-root-cert.pem" |
| ecRootKeyFile = "../testdata/ec-root-key.pem" |
| ecClientCertFile = "../testdata/ec-workload-cert.pem" |
| ecClientKeyFile = "../testdata/ec-workload-key.pem" |
| ) |
| |
| func TestKeyCertBundleWithRootCertFromFile(t *testing.T) { |
| testCases := map[string]struct { |
| rootCertFile string |
| expectedErr string |
| }{ |
| "File not found": { |
| rootCertFile: "bad.pem", |
| expectedErr: "open bad.pem: no such file or directory", |
| }, |
| "With RSA root cert": { |
| rootCertFile: rootCertFile, |
| expectedErr: "", |
| }, |
| "With EC root cert": { |
| rootCertFile: rootCertFile1, |
| expectedErr: "", |
| }, |
| } |
| for id, tc := range testCases { |
| bundle, err := NewKeyCertBundleWithRootCertFromFile(tc.rootCertFile) |
| if err != nil { |
| if tc.expectedErr == "" { |
| t.Errorf("%s: Unexpected error: %v", id, err) |
| } else if strings.Compare(err.Error(), tc.expectedErr) != 0 { |
| t.Errorf("%s: Unexpected error: %v VS (expected) %s", id, err, tc.expectedErr) |
| } |
| } else if tc.expectedErr != "" { |
| t.Errorf("%s: Expected error %s but succeeded", id, tc.expectedErr) |
| } else if bundle == nil { |
| t.Errorf("%s: the bundle should not be empty", id) |
| } else { |
| cert, key, chain, root := bundle.GetAllPem() |
| if len(cert) != 0 { |
| t.Errorf("%s: certBytes should be empty", id) |
| } |
| if len(key) != 0 { |
| t.Errorf("%s: privateKeyBytes should be empty", id) |
| } |
| if len(chain) != 0 { |
| t.Errorf("%s: certChainBytes should be empty", id) |
| } |
| if len(root) == 0 { |
| t.Errorf("%s: rootCertBytes should not be empty", id) |
| } |
| |
| chain = bundle.GetCertChainPem() |
| if len(chain) != 0 { |
| t.Errorf("%s: certChainBytes should be empty", id) |
| } |
| |
| root = bundle.GetRootCertPem() |
| if len(root) == 0 { |
| t.Errorf("%s: rootCertBytes should not be empty", id) |
| } |
| |
| x509Cert, privKey, chain, root := bundle.GetAll() |
| if x509Cert != nil { |
| t.Errorf("%s: cert should be nil", id) |
| } |
| if privKey != nil { |
| t.Errorf("%s: private key should be nil", id) |
| } |
| if len(chain) != 0 { |
| t.Errorf("%s: certChainBytes should be empty", id) |
| } |
| if len(root) == 0 { |
| t.Errorf("%s: rootCertBytes should not be empty", id) |
| } |
| } |
| } |
| } |
| |
| // The test of CertOptions |
| func TestCertOptionsAndRetrieveID(t *testing.T) { |
| testCases := map[string]struct { |
| caCertFile string |
| caKeyFile string |
| certChainFile []string |
| rootCertFile string |
| certOptions *CertOptions |
| expectedErr string |
| }{ |
| "No SAN RSA": { |
| caCertFile: rootCertFile, |
| caKeyFile: rootKeyFile, |
| certChainFile: nil, |
| rootCertFile: rootCertFile, |
| certOptions: &CertOptions{ |
| Host: "test_ca.com", |
| TTL: time.Hour, |
| Org: "MyOrg", |
| IsCA: true, |
| RSAKeySize: 2048, |
| }, |
| expectedErr: "failed to extract id the SAN extension does not exist", |
| }, |
| "RSA Success": { |
| caCertFile: certChainFile1, |
| caKeyFile: keyFile1, |
| certChainFile: nil, |
| rootCertFile: rootCertFile1, |
| certOptions: &CertOptions{ |
| Host: "watt", |
| TTL: 100 * 365 * 24 * time.Hour, |
| Org: "Juju org", |
| IsCA: false, |
| RSAKeySize: 2048, |
| }, |
| expectedErr: "", |
| }, |
| "No SAN EC": { |
| caCertFile: ecRootCertFile, |
| caKeyFile: ecRootKeyFile, |
| certChainFile: nil, |
| rootCertFile: ecRootCertFile, |
| certOptions: &CertOptions{ |
| Host: "watt", |
| TTL: 100 * 365 * 24 * time.Hour, |
| Org: "Juju org", |
| IsCA: true, |
| ECSigAlg: EcdsaSigAlg, |
| }, |
| expectedErr: "failed to extract id the SAN extension does not exist", |
| }, |
| "EC Success": { |
| caCertFile: ecClientCertFile, |
| caKeyFile: ecClientKeyFile, |
| certChainFile: nil, |
| rootCertFile: ecRootCertFile, |
| certOptions: &CertOptions{ |
| Host: "watt", |
| TTL: 10 * 365 * 24 * time.Hour, |
| Org: "Juju org", |
| IsCA: false, |
| ECSigAlg: EcdsaSigAlg, |
| }, |
| expectedErr: "", |
| }, |
| } |
| for id, tc := range testCases { |
| k, err := NewVerifiedKeyCertBundleFromFile(tc.caCertFile, tc.caKeyFile, tc.certChainFile, tc.rootCertFile) |
| if err != nil { |
| t.Fatalf("%s: Unexpected error: %v", id, err) |
| } |
| opts, err := k.CertOptions() |
| if err != nil { |
| if tc.expectedErr == "" { |
| t.Errorf("%s: Unexpected error: %v", id, err) |
| } else if strings.Compare(err.Error(), tc.expectedErr) != 0 { |
| t.Errorf("%s: Unexpected error: %v VS (expected) %s", id, err, tc.expectedErr) |
| } |
| } else if tc.expectedErr != "" { |
| t.Errorf("%s: expected error %s but have error %v", id, tc.expectedErr, err) |
| } else { |
| compareCertOptions(opts, tc.certOptions, t) |
| } |
| } |
| } |
| |
| func compareCertOptions(actual, expected *CertOptions, t *testing.T) { |
| if actual.Host != expected.Host { |
| t.Errorf("host does not match, %s vs %s", actual.Host, expected.Host) |
| } |
| if actual.TTL != expected.TTL { |
| t.Errorf("TTL does not match") |
| } |
| if actual.Org != expected.Org { |
| t.Errorf("Org does not match") |
| } |
| if actual.IsCA != expected.IsCA { |
| t.Errorf("IsCA does not match") |
| } |
| if actual.RSAKeySize != expected.RSAKeySize { |
| t.Errorf("RSAKeySize does not match") |
| } |
| } |
| |
| // The test of NewVerifiedKeyCertBundleFromPem, VerifyAndSetAll can be covered by this test. |
| func TestNewVerifiedKeyCertBundleFromFile(t *testing.T) { |
| testCases := map[string]struct { |
| caCertFile string |
| caKeyFile string |
| certChainFile []string |
| rootCertFile string |
| expectedErr string |
| }{ |
| "Success - 1 level CA": { |
| caCertFile: rootCertFile, |
| caKeyFile: rootKeyFile, |
| certChainFile: nil, |
| rootCertFile: rootCertFile, |
| expectedErr: "", |
| }, |
| "Success - 2 level CA": { |
| caCertFile: intCertFile, |
| caKeyFile: intKeyFile, |
| certChainFile: []string{intCertChainFile}, |
| rootCertFile: rootCertFile, |
| expectedErr: "", |
| }, |
| "Success - 3 level CA": { |
| caCertFile: int2CertFile, |
| caKeyFile: int2KeyFile, |
| certChainFile: []string{int2CertChainFile}, |
| rootCertFile: rootCertFile, |
| expectedErr: "", |
| }, |
| "Success - 2 level CA without cert chain file": { |
| caCertFile: intCertFile, |
| caKeyFile: intKeyFile, |
| certChainFile: nil, |
| rootCertFile: rootCertFile, |
| expectedErr: "", |
| }, |
| "Failure - invalid cert chain file": { |
| caCertFile: intCertFile, |
| caKeyFile: intKeyFile, |
| certChainFile: []string{"bad.pem"}, |
| rootCertFile: rootCertFile, |
| expectedErr: "open bad.pem: no such file or directory", |
| }, |
| "Failure - no root cert file": { |
| caCertFile: intCertFile, |
| caKeyFile: intKeyFile, |
| certChainFile: nil, |
| rootCertFile: "bad.pem", |
| expectedErr: "open bad.pem: no such file or directory", |
| }, |
| "Failure - cert and key do not match": { |
| caCertFile: int2CertFile, |
| caKeyFile: anotherKeyFile, |
| certChainFile: []string{int2CertChainFile}, |
| rootCertFile: rootCertFile, |
| expectedErr: "the cert does not match the key", |
| }, |
| "Failure - 3 level CA without cert chain file": { |
| caCertFile: int2CertFile, |
| caKeyFile: int2KeyFile, |
| certChainFile: nil, |
| rootCertFile: rootCertFile, |
| expectedErr: "cannot verify the cert with the provided root chain and " + |
| "cert pool with error: x509: certificate signed by unknown authority", |
| }, |
| "Failure - cert not verifiable from root cert": { |
| caCertFile: intCertFile, |
| caKeyFile: intKeyFile, |
| certChainFile: []string{intCertChainFile}, |
| rootCertFile: anotherRootCertFile, |
| expectedErr: "cannot verify the cert with the provided root chain and " + |
| "cert pool with error: x509: certificate is not authorized to sign " + |
| "other certificates", |
| }, |
| "Failure - invalid cert": { |
| caCertFile: badCertFile, |
| caKeyFile: intKeyFile, |
| certChainFile: nil, |
| rootCertFile: rootCertFile, |
| expectedErr: "failed to parse cert PEM: invalid PEM encoded certificate", |
| }, |
| "Failure - not existing private key": { |
| caCertFile: intCertFile, |
| caKeyFile: "bad.pem", |
| certChainFile: nil, |
| rootCertFile: rootCertFile, |
| expectedErr: "open bad.pem: no such file or directory", |
| }, |
| "Failure - invalid private key": { |
| caCertFile: intCertFile, |
| caKeyFile: badKeyFile, |
| certChainFile: nil, |
| rootCertFile: rootCertFile, |
| expectedErr: "failed to parse private key PEM: invalid PEM-encoded key", |
| }, |
| "Failure - file does not exist": { |
| caCertFile: "random/path/does/not/exist", |
| caKeyFile: intKeyFile, |
| certChainFile: nil, |
| rootCertFile: rootCertFile, |
| expectedErr: "open random/path/does/not/exist: no such file or directory", |
| }, |
| } |
| for id, tc := range testCases { |
| _, err := NewVerifiedKeyCertBundleFromFile( |
| tc.caCertFile, tc.caKeyFile, tc.certChainFile, tc.rootCertFile) |
| if err != nil { |
| if tc.expectedErr == "" { |
| t.Errorf("%s: Unexpected error: %v", id, err) |
| } else if strings.Compare(err.Error(), tc.expectedErr) != 0 { |
| t.Errorf("%s: Unexpected error: %v VS (expected) %s", id, err, tc.expectedErr) |
| } |
| } else if tc.expectedErr != "" { |
| t.Errorf("%s: Expected error %s but succeeded", id, tc.expectedErr) |
| } |
| } |
| } |
| |
| // Test the root cert expiry timestamp can be extracted correctly. |
| func TestExtractRootCertExpiryTimestamp(t *testing.T) { |
| t0 := time.Now() |
| cert, key, err := GenCertKeyFromOptions(CertOptions{ |
| Host: "citadel.testing.istio.io", |
| NotBefore: t0, |
| TTL: time.Minute, |
| Org: "MyOrg", |
| IsCA: true, |
| IsSelfSigned: true, |
| IsServer: true, |
| RSAKeySize: 2048, |
| }) |
| if err != nil { |
| t.Errorf("failed to gen cert for Citadel self signed cert %v", err) |
| } |
| kb, err := NewVerifiedKeyCertBundleFromPem(cert, key, nil, cert) |
| if err != nil { |
| t.Errorf("failed to create key cert bundle: %v", err) |
| } |
| testCases := []struct { |
| name string |
| ttl float64 |
| time time.Time |
| }{ |
| { |
| name: "ttl valid", |
| ttl: 30, |
| time: t0.Add(time.Second * 30), |
| }, |
| { |
| name: "ttl almost expired", |
| ttl: 2, |
| time: t0.Add(time.Second * 58), |
| }, |
| { |
| name: "ttl just expired", |
| ttl: 0, |
| time: t0.Add(time.Second * 60), |
| }, |
| { |
| name: "ttl-invalid", |
| ttl: -30, |
| time: t0.Add(time.Second * 90), |
| }, |
| } |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| expiryTimestamp, _ := kb.ExtractRootCertExpiryTimestamp() |
| // Ignore error; it just indicates cert is expired which we check via `tc.ttl` |
| |
| sec := expiryTimestamp - float64(tc.time.Unix()) |
| if sec != tc.ttl { |
| t.Fatalf("expected ttl %v, got %v", tc.ttl, sec) |
| } |
| }) |
| } |
| } |
| |
| // Test the CA cert expiry timestamp can be extracted correctly. |
| func TestExtractCACertExpiryTimestamp(t *testing.T) { |
| t0 := time.Now() |
| rootCertBytes, rootKeyBytes, err := GenCertKeyFromOptions(CertOptions{ |
| Host: "citadel.testing.istio.io", |
| Org: "MyOrg", |
| NotBefore: t0, |
| IsCA: true, |
| IsSelfSigned: true, |
| TTL: time.Hour, |
| RSAKeySize: 2048, |
| }) |
| if err != nil { |
| t.Errorf("failed to gen root cert for Citadel self signed cert %v", err) |
| } |
| |
| rootCert, err := ParsePemEncodedCertificate(rootCertBytes) |
| if err != nil { |
| t.Errorf("failed to parsing pem for root cert %v", err) |
| } |
| |
| rootKey, err := ParsePemEncodedKey(rootKeyBytes) |
| if err != nil { |
| t.Errorf("failed to parsing pem for root key cert %v", err) |
| } |
| |
| caCertBytes, caCertKeyBytes, err := GenCertKeyFromOptions(CertOptions{ |
| Host: "citadel.testing.istio.io", |
| Org: "MyOrg", |
| NotBefore: t0, |
| TTL: time.Second * 60, |
| IsServer: true, |
| IsCA: true, |
| IsSelfSigned: false, |
| RSAKeySize: 2048, |
| SignerCert: rootCert, |
| SignerPriv: rootKey, |
| }) |
| if err != nil { |
| t.Fatalf("failed to gen CA cert for Citadel self signed cert %v", err) |
| } |
| |
| kb, err := NewVerifiedKeyCertBundleFromPem( |
| caCertBytes, caCertKeyBytes, caCertBytes, rootCertBytes) |
| if err != nil { |
| t.Fatalf("failed to create key cert bundle: %v", err) |
| } |
| |
| testCases := []struct { |
| name string |
| ttl float64 |
| time time.Time |
| }{ |
| { |
| name: "ttl valid", |
| ttl: 30, |
| time: t0.Add(time.Second * 30), |
| }, |
| { |
| name: "ttl almost expired", |
| ttl: 2, |
| time: t0.Add(time.Second * 58), |
| }, |
| { |
| name: "ttl just expired", |
| ttl: 0, |
| time: t0.Add(time.Second * 60), |
| }, |
| { |
| name: "ttl-invalid", |
| ttl: -30, |
| time: t0.Add(time.Second * 90), |
| }, |
| } |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| expiryTimestamp, _ := kb.ExtractCACertExpiryTimestamp() |
| // Ignore error; it just indicates cert is expired which we check via `tc.ttl` |
| |
| sec := expiryTimestamp - float64(tc.time.Unix()) |
| if sec != tc.ttl { |
| t.Fatalf("expected ttl %v, got %v", tc.ttl, sec) |
| } |
| }) |
| } |
| } |
| |
| func TestTimeBeforeCertExpires(t *testing.T) { |
| t0 := time.Now() |
| certTTL := time.Second * 60 |
| rootCertBytes, _, err := GenCertKeyFromOptions(CertOptions{ |
| Host: "citadel.testing.istio.io", |
| Org: "MyOrg", |
| NotBefore: t0, |
| IsCA: true, |
| IsSelfSigned: true, |
| TTL: certTTL, |
| RSAKeySize: 2048, |
| }) |
| if err != nil { |
| t.Errorf("failed to gen root cert for Citadel self signed cert %v", err) |
| } |
| |
| testCases := []struct { |
| name string |
| cert []byte |
| expectedTime time.Duration |
| timeNow time.Time |
| expectedErr error |
| }{ |
| { |
| name: "TTL left should be equal to cert TTL", |
| cert: rootCertBytes, |
| timeNow: t0, |
| expectedTime: certTTL, |
| }, |
| { |
| name: "TTL left should be ca cert ttl minus 5 seconds", |
| cert: rootCertBytes, |
| timeNow: t0.Add(5 * time.Second), |
| expectedTime: 55 * time.Second, |
| }, |
| { |
| name: "TTL left should be negative because already got expired", |
| cert: rootCertBytes, |
| timeNow: t0.Add(120 * time.Second), |
| expectedTime: -60 * time.Second, |
| }, |
| { |
| name: "no cert, so it should return an error", |
| cert: nil, |
| timeNow: t0, |
| expectedErr: fmt.Errorf("no certificate found"), |
| }, |
| { |
| name: "invalid cert", |
| cert: []byte("invalid cert"), |
| timeNow: t0, |
| expectedErr: fmt.Errorf("failed to extract cert expiration timestamp: failed to parse the cert: invalid PEM encoded certificate"), |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| time, err := TimeBeforeCertExpires(tc.cert, tc.timeNow) |
| if err != nil { |
| if tc.expectedErr == nil { |
| t.Fatalf("Unexpected error: %v", err) |
| } else if strings.Compare(err.Error(), tc.expectedErr.Error()) != 0 { |
| t.Errorf("expected error: %v got %v", err, tc.expectedErr) |
| } |
| return |
| } |
| |
| if time != tc.expectedTime { |
| t.Fatalf("expected time %v, got %v", tc.expectedTime, time) |
| } |
| }) |
| } |
| } |