diff --git a/GettingStarted.md b/GettingStarted.md index bd9d1b80..4e82c826 100644 --- a/GettingStarted.md +++ b/GettingStarted.md @@ -443,7 +443,7 @@ We will be able to configure ingress routes those are HTTPS enabled. Customers c - Customer can use the same credentials in their pods to make this an end to end SSL support. ##### Sample configuration : Using Secret -We create OCI certificate service certificates and cabundles for each kubernetes secret. Hence the content of the secret (ca.crt, tls.crt, tlskey) should conform to the certificate service standards. +We create OCI certificate service certificates and cabundles for each kubernetes secret. Hence the content of the secret (ca.crt, tls.crt, tls.key) should conform to the certificate service standards. Ref Documents: - [Validation of Certificate chain](https://docs.oracle.com/en-us/iaas/Content/certificates/invalidcertificatechain.htm) - [Validation of CA Bundle](https://docs.oracle.com/en-us/iaas/Content/certificates/invalidcabundlepem.htm) @@ -461,6 +461,8 @@ metadata: name: demo-tls-secret type: kubernetes.io/tls ``` +Note: If `ca.crt` field is missing/empty, the entire certificate chain is expected to be present in `tls.crt`. The server certificate MUST be first, followed by chain of CAs. + Ingress Format: ``` apiVersion: networking.k8s.io/v1 diff --git a/pkg/controllers/ingress/util.go b/pkg/controllers/ingress/util.go index e95a0cd1..b6cff8cf 100644 --- a/pkg/controllers/ingress/util.go +++ b/pkg/controllers/ingress/util.go @@ -11,6 +11,9 @@ package ingress import ( "context" + "crypto/tls" + "encoding/pem" + "errors" "fmt" "reflect" "strings" @@ -205,12 +208,45 @@ func getTlsSecretContent(namespace string, secretName string, client kubernetes. if err != nil { return nil, err } - caCertificateChain := string(secret.Data["ca.crt"]) - serverCertificate := string(secret.Data["tls.crt"]) + + var serverCertificate string privateKey := string(secret.Data["tls.key"]) + caCertificateChain := string(secret.Data["ca.crt"]) + + if caCertificateChain != "" { + serverCertificate = string(secret.Data["tls.crt"]) + } else { + // If ca.crt is not available, we will assume tls.crt has the entire chain, leaf first + serverCertificate, caCertificateChain, err = splitLeafAndCaCertChain(secret.Data["tls.crt"], secret.Data["tls.key"]) + if err != nil { + return nil, err + } + } + return &TLSSecretData{CaCertificateChain: &caCertificateChain, ServerCertificate: &serverCertificate, PrivateKey: &privateKey}, nil } +func splitLeafAndCaCertChain(certChainPEMBlock []byte, keyPEMBlock []byte) (string, string, error) { + certs, err := tls.X509KeyPair(certChainPEMBlock, keyPEMBlock) + if err != nil { + return "", "", fmt.Errorf("unable to parse cert chain, %w", err) + } + + if len(certs.Certificate) <= 1 { + return "", "", errors.New("tls.crt chain has less than two certificates") + } + + leafCertString := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certs.Certificate[0]})) + + caCertChainString := "" + for _, cert := range certs.Certificate[1:] { + caCertString := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert})) + caCertChainString += caCertString + } + + return leafCertString, caCertChainString, nil +} + func getCertificateNameFromSecret(secretName string) string { if secretName == "" { return "" diff --git a/pkg/controllers/ingress/util_test.go b/pkg/controllers/ingress/util_test.go index 17997ce6..1bbc19ab 100644 --- a/pkg/controllers/ingress/util_test.go +++ b/pkg/controllers/ingress/util_test.go @@ -6,11 +6,19 @@ * * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ * */ + package ingress import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "errors" + "math/big" + "net" "net/http" "testing" "time" @@ -123,7 +131,7 @@ const ( func initsUtil() (*client.ClientProvider, ociloadbalancer.LoadBalancer) { k8client := fakeclientset.NewSimpleClientset() - secret := testutil.GetSampleCertSecret() + secret := testutil.GetSampleCertSecret("test", "oci-cert", "chain", "cert", "key") action := "get" resource := "secrets" obj := secret @@ -285,6 +293,106 @@ func TestGetCertificate(t *testing.T) { Expect(err).Should(BeNil()) } +func TestGetTlsSecretContent(t *testing.T) { + RegisterTestingT(t) + + testCaChain, testCert, testKey := generateTestCertsAndKey() + + secretWithCaCrt := testutil.GetSampleCertSecret("test", "secretWithCaCrt", testCaChain, testCert, testKey) + secretWithCorrectChain := testutil.GetSampleCertSecret("test", "secretWithCorrectChain", "", testCert+testCaChain, testKey) + secretWithWrongChain := testutil.GetSampleCertSecret("test", "secretWithWrongChain", "", testCaChain+testCert, testKey) + secretWithoutCaCrt := testutil.GetSampleCertSecret("test", "secretWithoutCaCrt", "", testCert, testKey) + + k8client := fakeclientset.NewSimpleClientset() + testutil.FakeClientGetCall(k8client, "get", "secrets", secretWithCaCrt) + + secretData1, err := getTlsSecretContent("test", "secretWithCaCrt", k8client) + Expect(err).ToNot(HaveOccurred()) + Expect(*secretData1.CaCertificateChain).To(Equal(testCaChain)) + Expect(*secretData1.ServerCertificate).To(Equal(testCert)) + Expect(*secretData1.PrivateKey).To(Equal(testKey)) + + testutil.FakeClientGetCall(k8client, "get", "secrets", secretWithCorrectChain) + secretData2, err := getTlsSecretContent("test", "secretWithCorrectChain", k8client) + Expect(err).ToNot(HaveOccurred()) + Expect(*secretData2.CaCertificateChain).To(Equal(testCaChain)) + Expect(*secretData2.ServerCertificate).To(Equal(testCert)) + Expect(*secretData2.PrivateKey).To(Equal(testKey)) + + testutil.FakeClientGetCall(k8client, "get", "secrets", secretWithWrongChain) + _, err = getTlsSecretContent("test", "secretWithWrongChain", k8client) + Expect(err).To(HaveOccurred()) + + testutil.FakeClientGetCall(k8client, "get", "secrets", secretWithoutCaCrt) + _, err = getTlsSecretContent("test", "secretWithoutCaCrt", k8client) + Expect(err).To(HaveOccurred()) +} + +func TestSplitLeafAndCaCertChain(t *testing.T) { + RegisterTestingT(t) + + // tls.X509KeyPair validates key against leaf cert, so need to create an actual pair + // Will create a CA and a test cert + testCaChain, testCert, testKey := generateTestCertsAndKey() + + // Adding dummy intermediate certs to chain + testCaChain = `-----BEGIN CERTIFICATE----- +intermediatecert +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +intermediatecert +-----END CERTIFICATE----- +` + testCaChain + + serverCert, caCertChain, err := splitLeafAndCaCertChain([]byte(testCert+testCaChain), []byte(testKey)) + Expect(err).ToNot(HaveOccurred()) + Expect(serverCert).To(Equal(testCert)) + Expect(caCertChain).To(Equal(testCaChain)) + + serverCert, caCertChain, err = splitLeafAndCaCertChain([]byte(testCert), []byte(testKey)) + Expect(err).To(HaveOccurred()) + + noCertString := "Has no certificates" + serverCert, caCertChain, err = splitLeafAndCaCertChain([]byte(noCertString), []byte(testKey)) + Expect(err).To(HaveOccurred()) +} + +func generateTestCertsAndKey() (string, string, string) { + caCert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"MyOrg, INC."}, + Country: []string{"US"}, + Province: []string{"CA"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + BasicConstraintsValid: true, + } + caPrivKey, _ := rsa.GenerateKey(rand.Reader, 4096) + caBytes, _ := x509.CreateCertificate(rand.Reader, caCert, caCert, &caPrivKey.PublicKey, caPrivKey) + testCaChain := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caBytes})) + + cert := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{ + Organization: []string{"MyOrg, INC."}, + Country: []string{"US"}, + Province: []string{"CA"}, + }, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + } + certPrivKey, _ := rsa.GenerateKey(rand.Reader, 4096) + certBytes, _ := x509.CreateCertificate(rand.Reader, cert, caCert, &certPrivKey.PublicKey, caPrivKey) + testCert := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})) + testKey := string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey)})) + + return testCaChain, testCert, testKey +} + func GetCertManageClient() ociclient.CertificateManagementInterface { return &MockCertificateManagerClient{} } diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index 669a2306..498d0303 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -548,21 +548,22 @@ func GetSampleSecret(configName string, privateKey string, data string, privateK return secret } -func GetSampleCertSecret() *v1.Secret { - namespace := "test" - name := "oci-cert" - s := "some-random" +func GetSampleCertSecret(namespace, name, caChain, cert, key string) *v1.Secret { secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, }, Data: map[string][]byte{ - "ca.crt": []byte(s), - "tls.crt": []byte(s), - "tls.key": []byte(s), + "tls.crt": []byte(cert), + "tls.key": []byte(key), }, } + + if caChain != "" { + secret.Data["ca.crt"] = []byte(caChain) + } + return secret }