istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/pki/ra/k8s_ra_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 ra
    16  
    17  import (
    18  	"os"
    19  	"path"
    20  	"testing"
    21  	"time"
    22  
    23  	cert "k8s.io/api/certificates/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  
    26  	meshconfig "istio.io/api/mesh/v1alpha1"
    27  	"istio.io/istio/pkg/kube"
    28  	"istio.io/istio/pkg/spiffe"
    29  	"istio.io/istio/pkg/test"
    30  	"istio.io/istio/pkg/test/env"
    31  	"istio.io/istio/security/pkg/pki/ca"
    32  	pkiutil "istio.io/istio/security/pkg/pki/util"
    33  )
    34  
    35  const (
    36  	TestCertificatePEM = `-----BEGIN CERTIFICATE-----
    37  MIIDGDCCAgCgAwIBAgIRAKvYcPLFqnJcwtshCGfNzTswDQYJKoZIhvcNAQELBQAw
    38  LzEtMCsGA1UEAxMkYTc1OWM3MmQtZTY3Mi00MDM2LWFjM2MtZGMwMTAwZjE1ZDVl
    39  MB4XDTE5MDgwNjE5NTU0NVoXDTI0MDgwNDE5NTU0NVowCzEJMAcGA1UEChMAMIIB
    40  IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyLIFJU5yJ5VXhbmizir+7Glm
    41  1tVEYXKGiqYbMRbfsFm7V6Z4l00D9/eHvfTXaFpqhv6HBm31MArjYB3OaaV6krvT
    42  whBUEPSkGBFe/eMPSFWBW27a0nw0cK2s/5yuFhTRtcUrZ9+ojJg4IS3oSm2UZ6UJ
    43  DuNI3qwB6OlPQOcWX8uEp4eAaolD1lIbLRQYvxYrBqnyCZBLE+MJgA1/VB3dAECB
    44  TxPtAqcwLFcvsM5ABys8yK8FrqRn5Bx54NiztgG+yU30W33xjdqzmEmuIIk4JjPU
    45  ZQRsug7XClDvQKM6lbYcYS1td2zT08hdgURFXJ9VR64ALFp00/bvglpryu8FmQID
    46  AQABo1MwUTAMBgNVHRMBAf8EAjAAMEEGA1UdEQQ6MDiCHHByb3RvbXV0YXRlLmlz
    47  dGlvLXN5c3RlbS5zdmOCGHByb3RvbXV0YXRlLmlzdGlvLXN5c3RlbTANBgkqhkiG
    48  9w0BAQsFAAOCAQEAhcVEZSuNMqMUJrWVb3b+6pmw9o1f7j6a51KWxOiIl6YuTYFS
    49  WaR0lHSW8wLesjsjm1awWO/F3QRuYWbalANy7434GMAGF53u/uc+Z8aE3EItER9o
    50  SpAJos6OfJqyok7JXDdOYRDD5/hBerj68R9llWzNJd27/1jZ0NF2sIE1W4QFddy/
    51  +8YA4+IqwkWB5/LbeRznl3EjFZDpCEJk0gg5XwAR5eIEy4QU8GueTwrDkssFdBGq
    52  0naco7/Es7CWQscYdKHAgYgk0UAyu8sGV235Uw3hlOrbZ/kqvyUmsSujgT8irmDV
    53  e+5z6MTAO6ktvHdQlSuH6ARn47bJrZOlkttAhg==
    54  -----END CERTIFICATE-----
    55  `
    56  )
    57  
    58  var (
    59  	testCsrHostName       = spiffe.Identity{TrustDomain: "cluster.local", Namespace: "default", ServiceAccount: "bookinfo-productpage"}.String()
    60  	TestCACertFile        = "../testdata/example-ca-cert.pem"
    61  	mismatchCertChainFile = "../testdata/cert-chain.pem"
    62  )
    63  
    64  func TestK8sSignWithMeshConfig(t *testing.T) {
    65  	cases := []struct {
    66  		name                          string
    67  		rootCertForMeshConfig         string
    68  		certChain                     string
    69  		updatedRootCertForMeshConfig  string
    70  		expectedFail                  bool
    71  		expectedFailOnUpdatedRootCert bool
    72  	}{
    73  		{
    74  			name:                  "Root cert from mesh config and cert chain does not match",
    75  			rootCertForMeshConfig: path.Join(env.IstioSrc, "samples/certs", "root-cert.pem"),
    76  			certChain:             mismatchCertChainFile,
    77  			expectedFail:          true,
    78  		},
    79  		{
    80  			name:                  "Root cert is specified in mesh config and Root cert from cert chain is empty(only one leaf cert)",
    81  			rootCertForMeshConfig: path.Join(env.IstioSrc, "samples/certs", "root-cert.pem"),
    82  			certChain:             path.Join(env.IstioSrc, "samples/certs", "leaf-workload-foo-cert.pem"),
    83  			expectedFail:          true,
    84  		},
    85  		{
    86  			name:                  "Root cert and intermediate CA are specified in mesh config and Root cert from cert chain is empty(only one leaf cert)",
    87  			rootCertForMeshConfig: path.Join(env.IstioSrc, "samples/certs", "workload-foo-root-certs.pem"),
    88  			certChain:             path.Join(env.IstioSrc, "samples/certs", "leaf-workload-foo-cert.pem"),
    89  		},
    90  		{
    91  			name:                  "Root cert is specified in mesh config and cert chain contains only intermediate CA(only leaf cert + intermediate CA)",
    92  			rootCertForMeshConfig: path.Join(env.IstioSrc, "samples/certs", "root-cert.pem"),
    93  			certChain:             path.Join(env.IstioSrc, "samples/certs", "workload-foo-cert.pem"),
    94  		},
    95  		{
    96  			name:                          "Root cert is specified in mesh config and be updated to an invalid value",
    97  			rootCertForMeshConfig:         path.Join(env.IstioSrc, "samples/certs", "root-cert.pem"),
    98  			certChain:                     path.Join(env.IstioSrc, "samples/certs", "cert-chain.pem"),
    99  			updatedRootCertForMeshConfig:  TestCACertFile,
   100  			expectedFailOnUpdatedRootCert: true,
   101  		},
   102  		{
   103  			name:      "Root cert is not specified in mesh config and cert chain contains only intermediate CA(only leaf cert + intermediate CA)",
   104  			certChain: path.Join(env.IstioSrc, "samples/certs", "workload-foo-cert.pem"),
   105  		},
   106  		{
   107  			name:         "Root cert is not specified in mesh config and Root cert from cert chain is empty(only one leaf cert)",
   108  			certChain:    path.Join(env.IstioSrc, "samples/certs", "leaf-workload-foo-cert.pem"),
   109  			expectedFail: true,
   110  		},
   111  	}
   112  	for _, tc := range cases {
   113  		t.Run(tc.name, func(t *testing.T) {
   114  			csrPEM := createFakeCsr(t)
   115  			certChainPem, err := os.ReadFile(tc.certChain)
   116  			if err != nil {
   117  				t.Errorf("Failed to read sample %s", tc.certChain)
   118  			}
   119  			client := initFakeKubeClient(t, certChainPem)
   120  			ra, err := createFakeK8sRA(client, "")
   121  			if err != nil {
   122  				t.Errorf("Failed to create Fake K8s RA")
   123  			}
   124  			signer := "kubernetes.io/kube-apiserver-client"
   125  			ra.certSignerDomain = "kubernetes.io"
   126  			if tc.rootCertForMeshConfig != "" {
   127  				rootCertPem, err := os.ReadFile(tc.rootCertForMeshConfig)
   128  				if err != nil {
   129  					t.Errorf("Failed to read sample %s", tc.rootCertForMeshConfig)
   130  				}
   131  				caCertificates := []*meshconfig.MeshConfig_CertificateData{
   132  					{CertificateData: &meshconfig.MeshConfig_CertificateData_Pem{Pem: string(rootCertPem)}, CertSigners: []string{signer}},
   133  				}
   134  				ra.SetCACertificatesFromMeshConfig(caCertificates)
   135  			}
   136  			subjectID := spiffe.Identity{TrustDomain: "cluster.local", Namespace: "default", ServiceAccount: "bookinfo-productpage"}.String()
   137  			certOptions := ca.CertOpts{
   138  				SubjectIDs: []string{subjectID},
   139  				TTL:        60 * time.Second, ForCA: false,
   140  				CertSigner: "kube-apiserver-client",
   141  			}
   142  			_, err = ra.SignWithCertChain(csrPEM, certOptions)
   143  			if (tc.expectedFail && err == nil) || (!tc.expectedFail && err != nil) {
   144  				t.Fatalf("expected failure: %t, got %v", tc.expectedFail, err)
   145  			}
   146  			if tc.updatedRootCertForMeshConfig != "" {
   147  				testCACert, err := os.ReadFile(tc.updatedRootCertForMeshConfig)
   148  				if err != nil {
   149  					t.Errorf("Failed to read test CA Cert file")
   150  				}
   151  				updatedCACertificates := []*meshconfig.MeshConfig_CertificateData{
   152  					{CertificateData: &meshconfig.MeshConfig_CertificateData_Pem{Pem: string(testCACert)}, CertSigners: []string{signer}},
   153  				}
   154  				ra.SetCACertificatesFromMeshConfig(updatedCACertificates)
   155  				// expect failure in sign since root cert in mesh config does not match
   156  				_, err = ra.SignWithCertChain(csrPEM, certOptions)
   157  				if err == nil && !tc.expectedFailOnUpdatedRootCert {
   158  					t.Fatalf("expected failed, got none")
   159  				}
   160  			}
   161  		})
   162  	}
   163  }
   164  
   165  func createFakeCsr(t *testing.T) []byte {
   166  	options := pkiutil.CertOptions{
   167  		Host:       testCsrHostName,
   168  		RSAKeySize: 2048,
   169  		PKCS8Key:   false,
   170  		ECSigAlg:   pkiutil.SupportedECSignatureAlgorithms("ECDSA"),
   171  	}
   172  	csrPEM, _, err := pkiutil.GenCSR(options)
   173  	if err != nil {
   174  		t.Fatalf("Error creating Mock CA client: %v", err)
   175  		return nil
   176  	}
   177  	return csrPEM
   178  }
   179  
   180  func initFakeKubeClient(t test.Failer, certificate []byte) kube.CLIClient {
   181  	client := kube.NewFakeClient()
   182  	client.RunAndWait(test.NewStop(t))
   183  	ctx := test.NewContext(t)
   184  	w, _ := client.Kube().CertificatesV1().CertificateSigningRequests().Watch(ctx, metav1.ListOptions{})
   185  	go func() {
   186  		for {
   187  			select {
   188  			case <-ctx.Done():
   189  				return
   190  			case r := <-w.ResultChan():
   191  				csr := r.Object.(*cert.CertificateSigningRequest).DeepCopy()
   192  				if csr.Status.Certificate != nil {
   193  					continue
   194  				}
   195  				csr.Status.Certificate = certificate
   196  				// fake clientset doesn't handle resource version, so we need to delay the update
   197  				// to make sure watchers can catch the event
   198  				time.Sleep(time.Millisecond)
   199  				client.Kube().CertificatesV1().CertificateSigningRequests().UpdateStatus(ctx, csr, metav1.UpdateOptions{})
   200  			}
   201  		}
   202  	}()
   203  	return client
   204  }
   205  
   206  func createFakeK8sRA(client kube.Client, caCertFile string) (*KubernetesRA, error) {
   207  	defaultCertTTL := 30 * time.Minute
   208  	maxCertTTL := time.Hour
   209  	caSigner := "kubernates.io/kube-apiserver-client"
   210  	raOpts := &IstioRAOptions{
   211  		ExternalCAType: ExtCAK8s,
   212  		DefaultCertTTL: defaultCertTTL,
   213  		MaxCertTTL:     maxCertTTL,
   214  		CaSigner:       caSigner,
   215  		CaCertFile:     caCertFile,
   216  		VerifyAppendCA: true,
   217  		K8sClient:      client.Kube(),
   218  	}
   219  	return NewKubernetesRA(raOpts)
   220  }
   221  
   222  // TestK8sSign : Verify that ra.k8sSign returns a valid certPEM while using k8s Fake Client to create a CSR
   223  func TestK8sSign(t *testing.T) {
   224  	csrPEM := createFakeCsr(t)
   225  	client := initFakeKubeClient(t, []byte(TestCertificatePEM))
   226  	r, err := createFakeK8sRA(client, TestCACertFile)
   227  	if err != nil {
   228  		t.Errorf("Validation CSR failed")
   229  	}
   230  	subjectID := spiffe.Identity{TrustDomain: "cluster.local", Namespace: "default", ServiceAccount: "bookinfo-productpage"}.String()
   231  	_, err = r.Sign(csrPEM, ca.CertOpts{
   232  		SubjectIDs: []string{subjectID},
   233  		TTL:        60 * time.Second, ForCA: false,
   234  	})
   235  	if err != nil {
   236  		t.Errorf("K8s CA Signing CSR failed")
   237  	}
   238  }
   239  
   240  func TestValidateCSR(t *testing.T) {
   241  	csrPEM := createFakeCsr(t)
   242  	client := initFakeKubeClient(t, []byte(TestCertificatePEM))
   243  	_, err := createFakeK8sRA(client, TestCACertFile)
   244  	if err != nil {
   245  		t.Errorf("Validation CSR failed")
   246  	}
   247  	var testSubjectIDs []string
   248  
   249  	// Test Case 1
   250  	testSubjectIDs = []string{testCsrHostName, "Random-Host-Name"}
   251  	if !ValidateCSR(csrPEM, testSubjectIDs) {
   252  		t.Errorf("Test 1: CSR Validation failed")
   253  	}
   254  
   255  	// Test Case 2
   256  	testSubjectIDs = []string{"Random-Host-Name"}
   257  	if ValidateCSR(csrPEM, testSubjectIDs) {
   258  		t.Errorf("Test 2: CSR Validation failed")
   259  	}
   260  }