/* 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) }