refactor tlsconfigutil to return a caBundle type

Signed-off-by: Ashish Amarnath <ashish.amarnath@broadcom.com>
This commit is contained in:
Ashish Amarnath
2024-07-25 19:20:57 -07:00
committed by Ryan Richard
parent a1dcba4731
commit 005dbf3aa8
7 changed files with 155 additions and 48 deletions

View File

@@ -4,6 +4,7 @@
package tlsconfigutil
import (
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"fmt"
@@ -80,18 +81,54 @@ func TLSSpecForConcierge(source *authenticationv1alpha1.TLSSpec) *TLSSpec {
return dest
}
// CABundle abstracts the internal representation of CA certificate bundles.
type CABundle struct {
caBundle []byte
caCertPool *x509.CertPool
}
// GetCABundle returns the CA certificate bundle PEM bytes.
func (c *CABundle) GetCABundle() []byte {
return c.caBundle
}
// GetCABundlePemString returns the certificate bundle PEM formatted as a string.
func (c *CABundle) GetCABundlePemString() string {
return string(c.caBundle)
}
// GetCertPool returns a X509 cert pool with the CA certificate bundle.
func (c *CABundle) GetCertPool() *x509.CertPool {
return c.caCertPool
}
// GetCABundleHash returns a sha256 sum of the CA bundle bytes.
func (c *CABundle) GetCABundleHash() [32]byte {
return sha256.Sum256(c.caBundle) // note that this will always return the same hash for nil input
}
// IsEqual returns whether a CABundle has the same CA certificate bundle as another.
func (l *CABundle) IsEqual(r *CABundle) bool {
if l == nil && r == nil {
return true
}
if l == nil || r == nil {
return false
}
return sha256.Sum256(l.caBundle) == sha256.Sum256(r.GetCABundle())
}
// ValidateTLSConfig reads ca bundle in the tlsSpec, supplied either inline using the CertificateAuthorityDate
// or as a reference to a kubernetes secret or configmap using the CertificateAuthorityDataSource, and returns
// - a condition of type TLSConfigurationValid based on the validity of the ca bundle,
// - a pem encoded ca bundle
// - a X509 cert pool with the ca bundle.
// - a CABundle - an abstraction of internal representation of CA certificate bundles.
func ValidateTLSConfig(
tlsSpec *TLSSpec,
conditionPrefix string,
namespace string,
secretInformer corev1informers.SecretInformer,
configMapInformer corev1informers.ConfigMapInformer,
) (*metav1.Condition, []byte, *x509.CertPool) {
) (*metav1.Condition, *CABundle) {
// TODO: This func should return a struct that abstracts away the internals of how a CA bundle is held in memory
// and can return the CA bundle as string PEM, []byte base64-encoded, CertPool, hash, etc, as well as compare itself
// to either a different struct instance or a hash.
@@ -100,13 +137,14 @@ func ValidateTLSConfig(
certPool, bundle, err := getCertPool(tlsSpec, conditionPrefix, namespace, secretInformer, configMapInformer)
if err != nil {
return invalidTLSCondition(err.Error()), nil, nil
return invalidTLSCondition(err.Error()), &CABundle{}
}
if bundle == nil {
// An empty or nil CA bundle results in a valid TLS condition which indicates that no CA data was supplied.
return validTLSCondition(fmt.Sprintf("%s is valid: %s", conditionPrefix, noTLSConfigurationMessage)), nil, nil
return validTLSCondition(fmt.Sprintf("%s is valid: %s", conditionPrefix, noTLSConfigurationMessage)), nil
}
return validTLSCondition(fmt.Sprintf("%s is valid: %s", conditionPrefix, loadedTLSConfigurationMessage)), bundle, certPool
return validTLSCondition(fmt.Sprintf("%s is valid: %s", conditionPrefix, loadedTLSConfigurationMessage)),
&CABundle{bundle, certPool}
}
// getCertPool reads the unified tlsSpec and returns an X509 cert pool with the CA data that is read either from

View File

@@ -27,18 +27,16 @@ import (
func TestValidateTLSConfig(t *testing.T) {
testCA, err := certauthority.New("Test CA", 1*time.Hour)
require.NoError(t, err)
bundle := testCA.Bundle()
certPool := x509.NewCertPool()
require.True(t, certPool.AppendCertsFromPEM(bundle))
base64EncodedBundle := base64.StdEncoding.EncodeToString(bundle)
require.True(t, certPool.AppendCertsFromPEM(testCA.Bundle()))
base64EncodedBundle := base64.StdEncoding.EncodeToString(testCA.Bundle())
tests := []struct {
name string
tlsSpec *TLSSpec
namespace string
k8sObjects []runtime.Object
expectedBundle []byte
expectedCertPool *x509.CertPool
expectedCABundle *CABundle
expectedCondition *metav1.Condition
}{
{
@@ -66,8 +64,10 @@ func TestValidateTLSConfig(t *testing.T) {
tlsSpec: &TLSSpec{
CertificateAuthorityData: base64EncodedBundle,
},
expectedBundle: bundle,
expectedCertPool: certPool,
expectedCABundle: &CABundle{
caBundle: testCA.Bundle(),
caCertPool: certPool,
},
expectedCondition: &metav1.Condition{
Type: typeTLSConfigurationValid,
Status: metav1.ConditionTrue,
@@ -134,12 +134,14 @@ func TestValidateTLSConfig(t *testing.T) {
},
Type: corev1.SecretTypeTLS,
Data: map[string][]byte{
"ca-bundle": bundle,
"ca-bundle": testCA.Bundle(),
},
},
},
expectedBundle: bundle,
expectedCertPool: certPool,
expectedCABundle: &CABundle{
caBundle: testCA.Bundle(),
caCertPool: certPool,
},
expectedCondition: &metav1.Condition{
Type: typeTLSConfigurationValid,
Status: metav1.ConditionTrue,
@@ -165,12 +167,14 @@ func TestValidateTLSConfig(t *testing.T) {
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"ca-bundle": bundle,
"ca-bundle": testCA.Bundle(),
},
},
},
expectedBundle: bundle,
expectedCertPool: certPool,
expectedCABundle: &CABundle{
caBundle: testCA.Bundle(),
caCertPool: certPool,
},
expectedCondition: &metav1.Condition{
Type: typeTLSConfigurationValid,
Status: metav1.ConditionTrue,
@@ -196,7 +200,7 @@ func TestValidateTLSConfig(t *testing.T) {
},
Type: corev1.SecretTypeBasicAuth,
Data: map[string][]byte{
"ca-bundle": bundle,
"ca-bundle": testCA.Bundle(),
},
},
},
@@ -225,7 +229,7 @@ func TestValidateTLSConfig(t *testing.T) {
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"wrong-key": bundle,
"wrong-key": testCA.Bundle(),
},
},
},
@@ -311,7 +315,7 @@ func TestValidateTLSConfig(t *testing.T) {
Namespace: "awesome-namespace",
},
Data: map[string]string{
"wrong-key": string(bundle),
"wrong-key": string(testCA.Bundle()),
},
},
},
@@ -395,12 +399,14 @@ func TestValidateTLSConfig(t *testing.T) {
Namespace: "awesome-namespace",
},
Data: map[string]string{
"ca-bundle": string(bundle),
"ca-bundle": string(testCA.Bundle()),
},
},
},
expectedBundle: bundle,
expectedCertPool: certPool,
expectedCABundle: &CABundle{
caBundle: testCA.Bundle(),
caCertPool: certPool,
},
expectedCondition: &metav1.Condition{
Type: typeTLSConfigurationValid,
Status: metav1.ConditionTrue,
@@ -461,7 +467,7 @@ func TestValidateTLSConfig(t *testing.T) {
Namespace: "awesome-namespace",
},
Data: map[string]string{
"ca-bundle": string(bundle),
"ca-bundle": string(testCA.Bundle()),
},
},
},
@@ -499,11 +505,12 @@ func TestValidateTLSConfig(t *testing.T) {
// which would do this same call for us.
sharedInformers.WaitForCacheSync(ctx.Done())
actualCondition, actualBundle, actualCertPool := ValidateTLSConfig(tt.tlsSpec, "spec.foo.tls", tt.namespace, secretsInformer, configMapInformer)
actualCondition, actualBundle := ValidateTLSConfig(tt.tlsSpec, "spec.foo.tls", tt.namespace, secretsInformer, configMapInformer)
require.Equal(t, tt.expectedCondition, actualCondition)
require.Equal(t, tt.expectedBundle, actualBundle)
require.True(t, tt.expectedCertPool.Equal(actualCertPool), "expectedCertPool did not equal actualCertPool")
if tt.expectedCABundle != nil {
require.True(t, tt.expectedCABundle.IsEqual(actualBundle), "expectedCertPool did not equal actualCertPool")
}
})
}
}
@@ -655,3 +662,68 @@ func TestTLSSpecForConcierge(t *testing.T) {
})
}
}
func TestCABundleIsEqual(t *testing.T) {
testCA, err := certauthority.New("Test CA", 1*time.Hour)
require.NoError(t, err)
certPool := x509.NewCertPool()
require.True(t, certPool.AppendCertsFromPEM(testCA.Bundle()))
tests := []struct {
name string
left *CABundle
right *CABundle
expected bool
}{
{
name: "should return equal when left and right are nil",
left: nil,
right: nil,
expected: true,
},
{
name: "should return not equal when left is nil and right is not",
left: nil,
right: &CABundle{},
expected: false,
},
{
name: "should return not equal when right is nil and left is not",
left: &CABundle{},
right: nil,
expected: false,
},
{
name: "should return equal when both left and right have same CA certificate bytes",
left: &CABundle{
caBundle: testCA.Bundle(),
caCertPool: certPool,
},
right: &CABundle{
caBundle: testCA.Bundle(),
caCertPool: certPool,
},
expected: true,
},
{
name: "should return not equal when both left and right do not have same CA certificate bytes",
left: &CABundle{
caBundle: testCA.Bundle(),
caCertPool: certPool,
},
right: &CABundle{
caBundle: []byte("something that is not a cert"),
caCertPool: nil,
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := tt.left.IsEqual(tt.right)
require.Equal(t, tt.expected, actual)
})
}
}