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 }