istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/k8s/chiron/utils_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 chiron
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"net/http"
    21  	"net/http/httptest"
    22  	"os"
    23  	"path/filepath"
    24  	"strconv"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	cert "k8s.io/api/certificates/v1"
    30  	corev1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/client-go/kubernetes/fake"
    34  	kt "k8s.io/client-go/testing"
    35  
    36  	"istio.io/istio/pkg/kube"
    37  	"istio.io/istio/pkg/log"
    38  	"istio.io/istio/pkg/test"
    39  	csrctrl "istio.io/istio/pkg/test/csrctrl/controllers"
    40  	"istio.io/istio/pkg/test/util/assert"
    41  	pkiutil "istio.io/istio/security/pkg/pki/util"
    42  )
    43  
    44  const (
    45  	// exampleCACert copied from samples/certs/ca-cert.pem
    46  	exampleCACert = `-----BEGIN CERTIFICATE-----
    47  MIIDnzCCAoegAwIBAgIJAON1ifrBZ2/BMA0GCSqGSIb3DQEBCwUAMIGLMQswCQYD
    48  VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJU3Vubnl2YWxl
    49  MQ4wDAYDVQQKDAVJc3RpbzENMAsGA1UECwwEVGVzdDEQMA4GA1UEAwwHUm9vdCBD
    50  QTEiMCAGCSqGSIb3DQEJARYTdGVzdHJvb3RjYUBpc3Rpby5pbzAgFw0xODAxMjQx
    51  OTE1NTFaGA8yMTE3MTIzMTE5MTU1MVowWTELMAkGA1UEBhMCVVMxEzARBgNVBAgT
    52  CkNhbGlmb3JuaWExEjAQBgNVBAcTCVN1bm55dmFsZTEOMAwGA1UEChMFSXN0aW8x
    53  ETAPBgNVBAMTCElzdGlvIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
    54  AQEAyzCxr/xu0zy5rVBiso9ffgl00bRKvB/HF4AX9/ytmZ6Hqsy13XIQk8/u/By9
    55  iCvVwXIMvyT0CbiJq/aPEj5mJUy0lzbrUs13oneXqrPXf7ir3HzdRw+SBhXlsh9z
    56  APZJXcF93DJU3GabPKwBvGJ0IVMJPIFCuDIPwW4kFAI7R/8A5LSdPrFx6EyMXl7K
    57  M8jekC0y9DnTj83/fY72WcWX7YTpgZeBHAeeQOPTZ2KYbFal2gLsar69PgFS0Tom
    58  ESO9M14Yit7mzB1WDK2z9g3r+zLxENdJ5JG/ZskKe+TO4Diqi5OJt/h8yspS1ck8
    59  LJtCole9919umByg5oruflqIlQIDAQABozUwMzALBgNVHQ8EBAMCAgQwDAYDVR0T
    60  BAUwAwEB/zAWBgNVHREEDzANggtjYS5pc3Rpby5pbzANBgkqhkiG9w0BAQsFAAOC
    61  AQEAltHEhhyAsve4K4bLgBXtHwWzo6SpFzdAfXpLShpOJNtQNERb3qg6iUGQdY+w
    62  A2BpmSkKr3Rw/6ClP5+cCG7fGocPaZh+c+4Nxm9suMuZBZCtNOeYOMIfvCPcCS+8
    63  PQ/0hC4/0J3WJKzGBssaaMufJxzgFPPtDJ998kY8rlROghdSaVt423/jXIAYnP3Y
    64  05n8TGERBj7TLdtIVbtUIx3JHAo3PWJywA6mEDovFMJhJERp9sDHIr1BbhXK1TFN
    65  Z6HNH6gInkSSMtvC4Ptejb749PTaePRPF7ID//eq/3AH8UK50F3TQcLjEqWUsJUn
    66  aFKltOc+RAjzDklcUPeG4Y6eMA==
    67  -----END CERTIFICATE-----`
    68  
    69  	// exampleIssuedCert copied from samples/certs/cert-chain.pem
    70  	exampleIssuedCert = `-----BEGIN CERTIFICATE-----
    71  MIIDnzCCAoegAwIBAgIJAON1ifrBZ2/BMA0GCSqGSIb3DQEBCwUAMIGLMQswCQYD
    72  VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJU3Vubnl2YWxl
    73  MQ4wDAYDVQQKDAVJc3RpbzENMAsGA1UECwwEVGVzdDEQMA4GA1UEAwwHUm9vdCBD
    74  QTEiMCAGCSqGSIb3DQEJARYTdGVzdHJvb3RjYUBpc3Rpby5pbzAgFw0xODAxMjQx
    75  OTE1NTFaGA8yMTE3MTIzMTE5MTU1MVowWTELMAkGA1UEBhMCVVMxEzARBgNVBAgT
    76  CkNhbGlmb3JuaWExEjAQBgNVBAcTCVN1bm55dmFsZTEOMAwGA1UEChMFSXN0aW8x
    77  ETAPBgNVBAMTCElzdGlvIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
    78  AQEAyzCxr/xu0zy5rVBiso9ffgl00bRKvB/HF4AX9/ytmZ6Hqsy13XIQk8/u/By9
    79  iCvVwXIMvyT0CbiJq/aPEj5mJUy0lzbrUs13oneXqrPXf7ir3HzdRw+SBhXlsh9z
    80  APZJXcF93DJU3GabPKwBvGJ0IVMJPIFCuDIPwW4kFAI7R/8A5LSdPrFx6EyMXl7K
    81  M8jekC0y9DnTj83/fY72WcWX7YTpgZeBHAeeQOPTZ2KYbFal2gLsar69PgFS0Tom
    82  ESO9M14Yit7mzB1WDK2z9g3r+zLxENdJ5JG/ZskKe+TO4Diqi5OJt/h8yspS1ck8
    83  LJtCole9919umByg5oruflqIlQIDAQABozUwMzALBgNVHQ8EBAMCAgQwDAYDVR0T
    84  BAUwAwEB/zAWBgNVHREEDzANggtjYS5pc3Rpby5pbzANBgkqhkiG9w0BAQsFAAOC
    85  AQEAltHEhhyAsve4K4bLgBXtHwWzo6SpFzdAfXpLShpOJNtQNERb3qg6iUGQdY+w
    86  A2BpmSkKr3Rw/6ClP5+cCG7fGocPaZh+c+4Nxm9suMuZBZCtNOeYOMIfvCPcCS+8
    87  PQ/0hC4/0J3WJKzGBssaaMufJxzgFPPtDJ998kY8rlROghdSaVt423/jXIAYnP3Y
    88  05n8TGERBj7TLdtIVbtUIx3JHAo3PWJywA6mEDovFMJhJERp9sDHIr1BbhXK1TFN
    89  Z6HNH6gInkSSMtvC4Ptejb749PTaePRPF7ID//eq/3AH8UK50F3TQcLjEqWUsJUn
    90  aFKltOc+RAjzDklcUPeG4Y6eMA==
    91  -----END CERTIFICATE-----
    92  `
    93  	DefaulCertTTL = 24 * time.Hour
    94  )
    95  
    96  type mockTLSServer struct {
    97  	httpServer *httptest.Server
    98  }
    99  
   100  func defaultReactionFunc(obj runtime.Object) kt.ReactionFunc {
   101  	return func(act kt.Action) (bool, runtime.Object, error) {
   102  		return true, obj, nil
   103  	}
   104  }
   105  
   106  const testSigner = "test-signer"
   107  
   108  func runTestSigner(t test.Failer) ([]csrctrl.SignerRootCert, kube.CLIClient) {
   109  	c := kube.NewFakeClient()
   110  	signers, err := csrctrl.RunCSRController(testSigner, test.NewStop(t), []kube.Client{c})
   111  	if err != nil {
   112  		t.Fatal(err)
   113  	}
   114  	return signers, c
   115  }
   116  
   117  func TestGenKeyCertK8sCA(t *testing.T) {
   118  	log.FindScope("default").SetOutputLevel(log.DebugLevel)
   119  	signers, client := runTestSigner(t)
   120  	ca := filepath.Join(t.TempDir(), "root-cert.pem")
   121  	os.WriteFile(ca, []byte(signers[0].Rootcert), 0o666)
   122  
   123  	_, _, _, err := GenKeyCertK8sCA(client.Kube(), "foo", ca, testSigner, true, DefaulCertTTL)
   124  	assert.NoError(t, err)
   125  }
   126  
   127  func TestReadCACert(t *testing.T) {
   128  	testCases := map[string]struct {
   129  		certPath     string
   130  		shouldFail   bool
   131  		expectedCert []byte
   132  	}{
   133  		"cert not exist": {
   134  			certPath:   "./invalid-path/invalid-file",
   135  			shouldFail: true,
   136  		},
   137  		"cert valid": {
   138  			certPath:     "./test-data/example-ca-cert.pem",
   139  			shouldFail:   false,
   140  			expectedCert: []byte(exampleCACert),
   141  		},
   142  		"cert invalid": {
   143  			certPath:   "./test-data/example-invalid-ca-cert.pem",
   144  			shouldFail: true,
   145  		},
   146  	}
   147  
   148  	for _, tc := range testCases {
   149  		t.Run(tc.certPath, func(t *testing.T) {
   150  			cert, err := readCACert(tc.certPath)
   151  			if tc.shouldFail {
   152  				if err == nil {
   153  					t.Errorf("should have failed at readCACert()")
   154  				} else {
   155  					// Should fail, skip the current case.
   156  					return
   157  				}
   158  			} else if err != nil {
   159  				t.Errorf("failed at readCACert(): %v", err)
   160  			}
   161  
   162  			if !bytes.Equal(tc.expectedCert, cert) {
   163  				t.Error("the certificate read is unexpected")
   164  			}
   165  		})
   166  	}
   167  }
   168  
   169  func TestIsTCPReachable(t *testing.T) {
   170  	server1 := newMockTLSServer(t)
   171  	defer server1.httpServer.Close()
   172  	server2 := newMockTLSServer(t)
   173  	defer server2.httpServer.Close()
   174  
   175  	host := "127.0.0.1"
   176  	port1, err := getServerPort(server1.httpServer)
   177  	if err != nil {
   178  		t.Fatalf("error to get the server 1 port: %v", err)
   179  	}
   180  	port2, err := getServerPort(server2.httpServer)
   181  	if err != nil {
   182  		t.Fatalf("error to get the server 2 port: %v", err)
   183  	}
   184  
   185  	// Server 1 should be reachable, since it is not closed.
   186  	if !isTCPReachable(host, port1) {
   187  		t.Fatal("server 1 is unreachable")
   188  	}
   189  
   190  	// After closing server 2, server 2 should not be reachable
   191  	server2.httpServer.Close()
   192  	if isTCPReachable(host, port2) {
   193  		t.Fatal("server 2 is reachable")
   194  	}
   195  }
   196  
   197  func TestSubmitCSR(t *testing.T) {
   198  	testCases := map[string]struct {
   199  		gracePeriodRatio float32
   200  		minGracePeriod   time.Duration
   201  		k8sCaCertFile    string
   202  		dnsNames         []string
   203  		secretNames      []string
   204  
   205  		secretName      string
   206  		secretNameSpace string
   207  		expectFail      bool
   208  	}{
   209  		"submitting a CSR without duplicate should succeed": {
   210  			gracePeriodRatio: 0.6,
   211  			k8sCaCertFile:    "./test-data/example-ca-cert.pem",
   212  			dnsNames:         []string{"foo"},
   213  			secretNames:      []string{"istio.webhook.foo"},
   214  			secretName:       "mock-secret",
   215  			secretNameSpace:  "mock-secret-namespace",
   216  			expectFail:       false,
   217  		},
   218  	}
   219  
   220  	for tcName, tc := range testCases {
   221  		t.Run(tcName, func(t *testing.T) {
   222  			client := fake.NewSimpleClientset()
   223  			csr := &cert.CertificateSigningRequest{
   224  				ObjectMeta: metav1.ObjectMeta{
   225  					Name: "domain-cluster.local-ns--secret-mock-secret",
   226  				},
   227  				Status: cert.CertificateSigningRequestStatus{
   228  					Certificate: []byte(exampleIssuedCert),
   229  				},
   230  			}
   231  			client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr))
   232  
   233  			usages := []cert.KeyUsage{
   234  				cert.UsageDigitalSignature,
   235  				cert.UsageKeyEncipherment,
   236  				cert.UsageServerAuth,
   237  				cert.UsageClientAuth,
   238  			}
   239  			r, err := submitCSR(client, []byte("test-pem"), "test-signer",
   240  				usages, DefaulCertTTL)
   241  			if tc.expectFail {
   242  				assert.Error(t, err)
   243  			} else if err != nil || r == nil {
   244  				t.Errorf("test case (%s) failed unexpectedly: %v", tcName, err)
   245  			}
   246  		})
   247  	}
   248  }
   249  
   250  func TestReadSignedCertificate(t *testing.T) {
   251  	testCases := []struct {
   252  		name              string
   253  		gracePeriodRatio  float32
   254  		minGracePeriod    time.Duration
   255  		k8sCaCertFile     string
   256  		secretNames       []string
   257  		dnsNames          []string
   258  		serviceNamespaces []string
   259  
   260  		secretName      string
   261  		secretNameSpace string
   262  
   263  		invalidCert     bool
   264  		expectFail      bool
   265  		certificateData []byte
   266  	}{
   267  		{
   268  			name:              "read signed cert should succeed",
   269  			gracePeriodRatio:  0.6,
   270  			k8sCaCertFile:     "./test-data/example-ca-cert.pem",
   271  			dnsNames:          []string{"foo"},
   272  			secretNames:       []string{"istio.webhook.foo"},
   273  			serviceNamespaces: []string{"foo.ns"},
   274  			secretName:        "mock-secret",
   275  			secretNameSpace:   "mock-secret-namespace",
   276  			invalidCert:       false,
   277  			expectFail:        false,
   278  			certificateData:   []byte(exampleIssuedCert),
   279  		},
   280  		{
   281  			name:              "read invalid signed cert should fail",
   282  			gracePeriodRatio:  0.6,
   283  			k8sCaCertFile:     "./test-data/example-ca-cert.pem",
   284  			dnsNames:          []string{"foo"},
   285  			secretNames:       []string{"istio.webhook.foo"},
   286  			serviceNamespaces: []string{"foo.ns"},
   287  			secretName:        "mock-secret",
   288  			secretNameSpace:   "mock-secret-namespace",
   289  			invalidCert:       true,
   290  			expectFail:        true,
   291  			certificateData:   []byte("invalid-cert"),
   292  		},
   293  		{
   294  			name:              "read empty signed cert should fail",
   295  			gracePeriodRatio:  0.6,
   296  			k8sCaCertFile:     "./test-data/example-ca-cert.pem",
   297  			dnsNames:          []string{"foo"},
   298  			secretNames:       []string{"istio.webhook.foo"},
   299  			serviceNamespaces: []string{"foo.ns"},
   300  			secretName:        "mock-secret",
   301  			secretNameSpace:   "mock-secret-namespace",
   302  			invalidCert:       true,
   303  			expectFail:        true,
   304  			certificateData:   []byte(""),
   305  		},
   306  	}
   307  
   308  	for _, tc := range testCases {
   309  		t.Run(tc.name, func(t *testing.T) {
   310  			log.FindScope("default").SetOutputLevel(log.DebugLevel)
   311  			client := initFakeKubeClient(t, tc.certificateData)
   312  
   313  			// 4. Read the signed certificate
   314  			_, _, err := SignCSRK8s(client.Kube(), createFakeCsr(t), "fake-signer", []cert.KeyUsage{cert.UsageAny}, "fake.com",
   315  				tc.k8sCaCertFile, true, true, 1*time.Second)
   316  
   317  			if tc.expectFail {
   318  				if err == nil {
   319  					t.Fatalf("should have failed at updateMutatingWebhookConfig")
   320  				}
   321  			} else if err != nil {
   322  				t.Fatalf("failed at updateMutatingWebhookConfig: %v", err)
   323  			}
   324  		})
   325  	}
   326  }
   327  
   328  func createFakeCsr(t *testing.T) []byte {
   329  	options := pkiutil.CertOptions{
   330  		Host:       "fake.com",
   331  		RSAKeySize: 2048,
   332  		PKCS8Key:   false,
   333  		ECSigAlg:   pkiutil.SupportedECSignatureAlgorithms("ECDSA"),
   334  	}
   335  	csrPEM, _, err := pkiutil.GenCSR(options)
   336  	if err != nil {
   337  		t.Fatalf("Error creating Mock CA client: %v", err)
   338  		return nil
   339  	}
   340  	return csrPEM
   341  }
   342  
   343  // newMockTLSServer creates a mock TLS server for testing purpose.
   344  func newMockTLSServer(t *testing.T) *mockTLSServer {
   345  	server := &mockTLSServer{}
   346  
   347  	handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
   348  		t.Logf("request: %+v", *req)
   349  		switch req.URL.Path {
   350  		default:
   351  			t.Logf("The request contains path: %v", req.URL)
   352  			resp.WriteHeader(http.StatusOK)
   353  		}
   354  	})
   355  
   356  	server.httpServer = httptest.NewTLSServer(handler)
   357  
   358  	t.Logf("Serving TLS at: %v", server.httpServer.URL)
   359  
   360  	return server
   361  }
   362  
   363  // Get the server port from server.URL (e.g., https://127.0.0.1:36253)
   364  func getServerPort(server *httptest.Server) (int, error) {
   365  	strs := strings.Split(server.URL, ":")
   366  	if len(strs) < 2 {
   367  		return 0, fmt.Errorf("server.URL is invalid: %v", server.URL)
   368  	}
   369  	port, err := strconv.Atoi(strs[len(strs)-1])
   370  	if err != nil {
   371  		return 0, fmt.Errorf("error to extract port from URL: %v", server.URL)
   372  	}
   373  	return port, nil
   374  }
   375  
   376  func initFakeKubeClient(t test.Failer, certificate []byte) kube.CLIClient {
   377  	client := kube.NewFakeClient()
   378  	ctx := test.NewContext(t)
   379  	w, _ := client.Kube().CertificatesV1().CertificateSigningRequests().Watch(ctx, metav1.ListOptions{})
   380  	go func() {
   381  		for {
   382  			select {
   383  			case <-ctx.Done():
   384  				return
   385  			case r := <-w.ResultChan():
   386  				csr := r.Object.(*cert.CertificateSigningRequest).DeepCopy()
   387  				if csr.Status.Certificate != nil {
   388  					log.Debugf("test signer skip, already signed: %v", csr.Name)
   389  					continue
   390  				}
   391  				if approved(csr) {
   392  					// This is a pretty terrible hack, but client-go fake doesn't properly support list+watch,
   393  					// so any updates in between the list and watch would be missed. So give some time for the watch to start
   394  					time.Sleep(time.Millisecond * 25)
   395  					csr.Status.Certificate = certificate
   396  					_, err := client.Kube().CertificatesV1().CertificateSigningRequests().UpdateStatus(ctx, csr, metav1.UpdateOptions{})
   397  					log.Debugf("test signer sign %v: %v", csr.Name, err)
   398  				} else {
   399  					log.Debugf("test signer skip, not approved: %v", csr.Name)
   400  				}
   401  			}
   402  		}
   403  	}()
   404  	return client
   405  }
   406  
   407  func approved(csr *cert.CertificateSigningRequest) bool {
   408  	return GetCondition(csr.Status.Conditions, cert.CertificateApproved).Status == corev1.ConditionTrue
   409  }
   410  
   411  func GetCondition(conditions []cert.CertificateSigningRequestCondition, condition cert.RequestConditionType) cert.CertificateSigningRequestCondition {
   412  	for _, cond := range conditions {
   413  		if cond.Type == condition {
   414  			return cond
   415  		}
   416  	}
   417  	return cert.CertificateSigningRequestCondition{}
   418  }