Skip to content

Commit

Permalink
Extract CA chain from tls.crt if ca.crt is missing for secret-type tl…
Browse files Browse the repository at this point in the history
…s artifact

Co-authored-by: antoniolago <45375617+antoniolago@users.noreply.github.com>
Co-authored-by: Piyush Tiwari <piyush.s.tiwari@oracle.com>
  • Loading branch information
piyush-tiwari and antoniolago committed Jun 17, 2024
1 parent c1c2c24 commit d602ace
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 11 deletions.
4 changes: 3 additions & 1 deletion GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
40 changes: 38 additions & 2 deletions pkg/controllers/ingress/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ package ingress

import (
"context"
"crypto/tls"
"encoding/pem"
"errors"
"fmt"
"reflect"
"strings"
Expand Down Expand Up @@ -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 ""
Expand Down
110 changes: 109 additions & 1 deletion pkg/controllers/ingress/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{}
}
Expand Down
15 changes: 8 additions & 7 deletions pkg/testutil/testutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down

0 comments on commit d602ace

Please sign in to comment.