475 lines
16 KiB
Go
475 lines
16 KiB
Go
/*
|
|
Copyright 2023 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package clustertrustbundle
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/informers"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
"k8s.io/client-go/tools/cache"
|
|
)
|
|
|
|
func TestBeforeSynced(t *testing.T) {
|
|
kc := fake.NewSimpleClientset()
|
|
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(kc, 0)
|
|
|
|
ctbInformer := informerFactory.Certificates().V1alpha1().ClusterTrustBundles()
|
|
ctbManager, _ := NewInformerManager(ctbInformer, 256, 5*time.Minute)
|
|
|
|
_, err := ctbManager.GetTrustAnchorsByName("foo", false)
|
|
if err == nil {
|
|
t.Fatalf("Got nil error, wanted non-nil")
|
|
}
|
|
}
|
|
|
|
func TestGetTrustAnchorsByName(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
ctb1 := &certificatesv1alpha1.ClusterTrustBundle{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "ctb1",
|
|
},
|
|
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
|
|
TrustBundle: mustMakeRoot(t, "root1"),
|
|
},
|
|
}
|
|
|
|
ctb2 := &certificatesv1alpha1.ClusterTrustBundle{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "ctb2",
|
|
},
|
|
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
|
|
TrustBundle: mustMakeRoot(t, "root2"),
|
|
},
|
|
}
|
|
|
|
kc := fake.NewSimpleClientset(ctb1, ctb2)
|
|
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(kc, 0)
|
|
|
|
ctbInformer := informerFactory.Certificates().V1alpha1().ClusterTrustBundles()
|
|
ctbManager, _ := NewInformerManager(ctbInformer, 256, 5*time.Minute)
|
|
|
|
informerFactory.Start(ctx.Done())
|
|
if !cache.WaitForCacheSync(ctx.Done(), ctbInformer.Informer().HasSynced) {
|
|
t.Fatalf("Timed out waiting for informer to sync")
|
|
}
|
|
|
|
gotBundle, err := ctbManager.GetTrustAnchorsByName("ctb1", false)
|
|
if err != nil {
|
|
t.Fatalf("Error while calling GetTrustAnchorsByName: %v", err)
|
|
}
|
|
|
|
if diff := diffBundles(gotBundle, []byte(ctb1.Spec.TrustBundle)); diff != "" {
|
|
t.Fatalf("Got bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
|
|
gotBundle, err = ctbManager.GetTrustAnchorsByName("ctb2", false)
|
|
if err != nil {
|
|
t.Fatalf("Error while calling GetTrustAnchorsByName: %v", err)
|
|
}
|
|
|
|
if diff := diffBundles(gotBundle, []byte(ctb2.Spec.TrustBundle)); diff != "" {
|
|
t.Fatalf("Got bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
|
|
_, err = ctbManager.GetTrustAnchorsByName("not-found", false)
|
|
if err == nil { // EQUALS nil
|
|
t.Fatalf("While looking up nonexisting ClusterTrustBundle, got nil error, wanted non-nil")
|
|
}
|
|
|
|
_, err = ctbManager.GetTrustAnchorsByName("not-found", true)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error while calling GetTrustAnchorsByName for nonexistent CTB with allowMissing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetTrustAnchorsByNameCaching(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
defer cancel()
|
|
|
|
ctb1 := &certificatesv1alpha1.ClusterTrustBundle{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
},
|
|
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
|
|
TrustBundle: mustMakeRoot(t, "root1"),
|
|
},
|
|
}
|
|
|
|
ctb2 := &certificatesv1alpha1.ClusterTrustBundle{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
},
|
|
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
|
|
TrustBundle: mustMakeRoot(t, "root2"),
|
|
},
|
|
}
|
|
|
|
kc := fake.NewSimpleClientset(ctb1)
|
|
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(kc, 0)
|
|
|
|
ctbInformer := informerFactory.Certificates().V1alpha1().ClusterTrustBundles()
|
|
ctbManager, _ := NewInformerManager(ctbInformer, 256, 5*time.Minute)
|
|
|
|
informerFactory.Start(ctx.Done())
|
|
if !cache.WaitForCacheSync(ctx.Done(), ctbInformer.Informer().HasSynced) {
|
|
t.Fatalf("Timed out waiting for informer to sync")
|
|
}
|
|
|
|
t.Run("foo should yield the first certificate", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsByName("foo", false)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
wantBundle := ctb1.Spec.TrustBundle
|
|
|
|
if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
|
|
t.Run("foo should still yield the first certificate", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsByName("foo", false)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
wantBundle := ctb1.Spec.TrustBundle
|
|
|
|
if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
|
|
if err := kc.CertificatesV1alpha1().ClusterTrustBundles().Delete(ctx, ctb1.ObjectMeta.Name, metav1.DeleteOptions{}); err != nil {
|
|
t.Fatalf("Error while deleting the old CTB: %v", err)
|
|
}
|
|
if _, err := kc.CertificatesV1alpha1().ClusterTrustBundles().Create(ctx, ctb2, metav1.CreateOptions{}); err != nil {
|
|
t.Fatalf("Error while adding new CTB: %v", err)
|
|
}
|
|
|
|
// We need to sleep long enough for the informer to notice the new
|
|
// ClusterTrustBundle, but much less than the 5 minutes of the cache TTL.
|
|
// This shows us that the informer is properly clearing the cache.
|
|
time.Sleep(5 * time.Second)
|
|
|
|
t.Run("foo should yield the new certificate", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsByName("foo", false)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
wantBundle := ctb2.Spec.TrustBundle
|
|
|
|
if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetTrustAnchorsBySignerName(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
ctb1 := mustMakeCTB("signer-a-label-a-1", "foo.bar/a", map[string]string{"label": "a"}, mustMakeRoot(t, "0"))
|
|
ctb2 := mustMakeCTB("signer-a-label-a-2", "foo.bar/a", map[string]string{"label": "a"}, mustMakeRoot(t, "1"))
|
|
ctb2dup := mustMakeCTB("signer-a-label-2-dup", "foo.bar/a", map[string]string{"label": "a"}, ctb2.Spec.TrustBundle)
|
|
ctb3 := mustMakeCTB("signer-a-label-b-1", "foo.bar/a", map[string]string{"label": "b"}, mustMakeRoot(t, "2"))
|
|
ctb4 := mustMakeCTB("signer-b-label-a-1", "foo.bar/b", map[string]string{"label": "a"}, mustMakeRoot(t, "3"))
|
|
|
|
kc := fake.NewSimpleClientset(ctb1, ctb2, ctb2dup, ctb3, ctb4)
|
|
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(kc, 0)
|
|
|
|
ctbInformer := informerFactory.Certificates().V1alpha1().ClusterTrustBundles()
|
|
ctbManager, _ := NewInformerManager(ctbInformer, 256, 5*time.Minute)
|
|
|
|
informerFactory.Start(ctx.Done())
|
|
if !cache.WaitForCacheSync(ctx.Done(), ctbInformer.Informer().HasSynced) {
|
|
t.Fatalf("Timed out waiting for informer to sync")
|
|
}
|
|
|
|
t.Run("big labelselector should cause error", func(t *testing.T) {
|
|
longString := strings.Builder{}
|
|
for i := 0; i < 63; i++ {
|
|
longString.WriteString("v")
|
|
}
|
|
matchLabels := map[string]string{}
|
|
for i := 0; i < 100*1024/63+1; i++ {
|
|
matchLabels[fmt.Sprintf("key-%d", i)] = longString.String()
|
|
}
|
|
|
|
_, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: matchLabels}, false)
|
|
if err == nil || !strings.Contains(err.Error(), "label selector length") {
|
|
t.Fatalf("Bad error, got %v, wanted it to contain \"label selector length\"", err)
|
|
}
|
|
})
|
|
|
|
t.Run("signer-a label-a should yield two sorted certificates", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "a"}}, false)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
wantBundle := ctb1.Spec.TrustBundle + ctb2.Spec.TrustBundle
|
|
|
|
if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
|
|
t.Run("signer-a with nil selector should yield zero certificates", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", nil, true)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
wantBundle := ""
|
|
|
|
if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
|
|
t.Run("signer-b with empty selector should yield one certificates", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/b", &metav1.LabelSelector{}, false)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
if diff := diffBundles(gotBundle, []byte(ctb4.Spec.TrustBundle)); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
|
|
t.Run("signer-a label-b should yield one certificate", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "b"}}, false)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
if diff := diffBundles(gotBundle, []byte(ctb3.Spec.TrustBundle)); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
|
|
t.Run("signer-b label-a should yield one certificate", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/b", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "a"}}, false)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
if diff := diffBundles(gotBundle, []byte(ctb4.Spec.TrustBundle)); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
|
|
t.Run("signer-b label-b allowMissing=true should yield zero certificates", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/b", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "b"}}, true)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
if diff := diffBundles(gotBundle, []byte{}); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
|
|
t.Run("signer-b label-b allowMissing=false should yield zero certificates (error)", func(t *testing.T) {
|
|
_, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/b", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "b"}}, false)
|
|
if err == nil { // EQUALS nil
|
|
t.Fatalf("Got nil error while calling GetTrustAnchorsBySigner, wanted non-nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetTrustAnchorsBySignerNameCaching(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
defer cancel()
|
|
|
|
ctb1 := mustMakeCTB("signer-a-label-a-1", "foo.bar/a", map[string]string{"label": "a"}, mustMakeRoot(t, "0"))
|
|
ctb2 := mustMakeCTB("signer-a-label-a-2", "foo.bar/a", map[string]string{"label": "a"}, mustMakeRoot(t, "1"))
|
|
|
|
kc := fake.NewSimpleClientset(ctb1)
|
|
|
|
informerFactory := informers.NewSharedInformerFactoryWithOptions(kc, 0)
|
|
|
|
ctbInformer := informerFactory.Certificates().V1alpha1().ClusterTrustBundles()
|
|
ctbManager, _ := NewInformerManager(ctbInformer, 256, 5*time.Minute)
|
|
|
|
informerFactory.Start(ctx.Done())
|
|
if !cache.WaitForCacheSync(ctx.Done(), ctbInformer.Informer().HasSynced) {
|
|
t.Fatalf("Timed out waiting for informer to sync")
|
|
}
|
|
|
|
t.Run("signer-a label-a should yield one certificate", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "a"}}, false)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
wantBundle := ctb1.Spec.TrustBundle
|
|
|
|
if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
|
|
t.Run("signer-a label-a should yield the same result when called again", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "a"}}, false)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
wantBundle := ctb1.Spec.TrustBundle
|
|
|
|
if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
|
|
if err := kc.CertificatesV1alpha1().ClusterTrustBundles().Delete(ctx, ctb1.ObjectMeta.Name, metav1.DeleteOptions{}); err != nil {
|
|
t.Fatalf("Error while deleting the old CTB: %v", err)
|
|
}
|
|
if _, err := kc.CertificatesV1alpha1().ClusterTrustBundles().Create(ctx, ctb2, metav1.CreateOptions{}); err != nil {
|
|
t.Fatalf("Error while adding new CTB: %v", err)
|
|
}
|
|
|
|
// We need to sleep long enough for the informer to notice the new
|
|
// ClusterTrustBundle, but much less than the 5 minutes of the cache TTL.
|
|
// This shows us that the informer is properly clearing the cache.
|
|
time.Sleep(5 * time.Second)
|
|
|
|
t.Run("signer-a label-a should return the new certificate", func(t *testing.T) {
|
|
gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "a"}}, false)
|
|
if err != nil {
|
|
t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err)
|
|
}
|
|
|
|
wantBundle := ctb2.Spec.TrustBundle
|
|
|
|
if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" {
|
|
t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
func mustMakeRoot(t *testing.T, cn string) string {
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("Error while generating key: %v", err)
|
|
}
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(0),
|
|
Subject: pkix.Name{
|
|
CommonName: cn,
|
|
},
|
|
IsCA: true,
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
cert, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
|
|
if err != nil {
|
|
t.Fatalf("Error while making certificate: %v", err)
|
|
}
|
|
|
|
return string(pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Headers: nil,
|
|
Bytes: cert,
|
|
}))
|
|
}
|
|
|
|
func mustMakeCTB(name, signerName string, labels map[string]string, bundle string) *certificatesv1alpha1.ClusterTrustBundle {
|
|
return &certificatesv1alpha1.ClusterTrustBundle{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Labels: labels,
|
|
},
|
|
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
|
|
SignerName: signerName,
|
|
TrustBundle: bundle,
|
|
},
|
|
}
|
|
}
|
|
|
|
func diffBundles(a, b []byte) string {
|
|
var block *pem.Block
|
|
|
|
aBlocks := []*pem.Block{}
|
|
for {
|
|
block, a = pem.Decode(a)
|
|
if block == nil {
|
|
break
|
|
}
|
|
aBlocks = append(aBlocks, block)
|
|
}
|
|
sort.Slice(aBlocks, func(i, j int) bool {
|
|
if aBlocks[i].Type < aBlocks[j].Type {
|
|
return true
|
|
} else if aBlocks[i].Type == aBlocks[j].Type {
|
|
comp := bytes.Compare(aBlocks[i].Bytes, aBlocks[j].Bytes)
|
|
return comp <= 0
|
|
} else {
|
|
return false
|
|
}
|
|
})
|
|
|
|
bBlocks := []*pem.Block{}
|
|
for {
|
|
block, b = pem.Decode(b)
|
|
if block == nil {
|
|
break
|
|
}
|
|
bBlocks = append(bBlocks, block)
|
|
}
|
|
sort.Slice(bBlocks, func(i, j int) bool {
|
|
if bBlocks[i].Type < bBlocks[j].Type {
|
|
return true
|
|
} else if bBlocks[i].Type == bBlocks[j].Type {
|
|
comp := bytes.Compare(bBlocks[i].Bytes, bBlocks[j].Bytes)
|
|
return comp <= 0
|
|
} else {
|
|
return false
|
|
}
|
|
})
|
|
|
|
return cmp.Diff(aBlocks, bBlocks)
|
|
}
|