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  }