From 6e52317ff37cfedb23ae5565b1c09046c0c0c0f4 Mon Sep 17 00:00:00 2001 From: panktishah26 Date: Tue, 28 Jan 2025 19:11:05 -0800 Subject: [PATCH 1/3] Verify bundle signature is valid and is not modified --- pkg/signature/manifets_test.go | 338 +++++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 pkg/signature/manifets_test.go diff --git a/pkg/signature/manifets_test.go b/pkg/signature/manifets_test.go new file mode 100644 index 000000000000..05016abea91c --- /dev/null +++ b/pkg/signature/manifets_test.go @@ -0,0 +1,338 @@ +package signature + +import ( + "fmt" + "strings" + "testing" + + "github.com/onsi/gomega" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aws/eks-anywhere/pkg/constants" + anywherev1alpha1 "github.com/aws/eks-anywhere/release/api/v1alpha1" +) + +func TestValidateSignature(t *testing.T) { + tests := []struct { + name string + bundle *anywherev1alpha1.Bundles + publicKey string + valid bool + wantErr error + }{ + { + name: "empty bundle with signature field", + bundle: &anywherev1alpha1.Bundles{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + constants.SignatureAnnotation: "MEUCICV1iiNA4owIUdZBIowSgWjTKx+JT5/CE8PzmF2CBD5+AiEAk8Fcc1X/LNGm0YCyZISWFhbh4qdc7ENyYCU3DB0u4b0=", + }, + }, + }, + valid: false, + wantErr: fmt.Errorf("filtering excluded fields: gojq execution error"), + }, + { + name: "no bundle signature", + bundle: &anywherev1alpha1.Bundles{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + "eks.amazonaws.com/no-signature": "", + }, + }, + }, + valid: false, + wantErr: fmt.Errorf("missing signature annotation"), + }, + { + name: "invalid signature", + bundle: &anywherev1alpha1.Bundles{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + constants.SignatureAnnotation: "invalid", + }, + }, + Spec: anywherev1alpha1.BundlesSpec{ + Number: 1, + VersionsBundles: []anywherev1alpha1.VersionsBundle{ + { + KubeVersion: "1.31", + }, + }, + }, + }, + valid: false, + wantErr: fmt.Errorf("signature in metadata isn't base64 encoded"), + }, + { + name: "invalid public key", + bundle: &anywherev1alpha1.Bundles{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + constants.SignatureAnnotation: "MEUCICV1iiNA4owIUdZBIowSgWjTKx+JT5/CE8PzmF2CBD5+AiEAk8Fcc1X/LNGm0YCyZISWFhbh4qdc7ENyYCU3DB0u4b0=", + }, + }, + Spec: anywherev1alpha1.BundlesSpec{ + Number: 1, + VersionsBundles: []anywherev1alpha1.VersionsBundle{ + { + KubeVersion: "1.31", + }, + }, + }, + }, + publicKey: "invalid", + valid: false, + wantErr: fmt.Errorf("decoding the public key as string"), + }, + { + name: "invalid encoded public key", + bundle: &anywherev1alpha1.Bundles{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + constants.SignatureAnnotation: "MEUCICV1iiNA4owIUdZBIowSgWjTKx+JT5/CE8PzmF2CBD5+AiEAk8Fcc1X/LNGm0YCyZISWFhbh4qdc7ENyYCU3DB0u4b0=", + }, + }, + Spec: anywherev1alpha1.BundlesSpec{ + Number: 1, + VersionsBundles: []anywherev1alpha1.VersionsBundle{ + { + KubeVersion: "1.31", + }, + }, + }, + }, + publicKey: "TUVVQ0lDVjFpaU5BNG93SVVkWkJJb3dTZ1dqVEt4K0pUNS9DRThQem1GMkNCRDUrQWlFQWs4RmNjMVgvTE5HbTBZQ3laSVNXRmhiaDRxZGM3RU55WUNVM0RCMHU0YjA9Cg==", + valid: false, + wantErr: fmt.Errorf("parsing the public key (not PKIX)"), + }, + { + name: "signature verification fail", + bundle: &anywherev1alpha1.Bundles{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + constants.SignatureAnnotation: "MEUCICV1iiNA4owIUdZBIowSgWjTKx+JT5/CE8PzmF2CBD5+AiEAk8Fcc1X/LNGm0YCyZISWFhbh4qdc7ENyYCU3DB0u4b0=", + }, + }, + Spec: anywherev1alpha1.BundlesSpec{ + Number: 1, + VersionsBundles: []anywherev1alpha1.VersionsBundle{ + { + KubeVersion: "1.31", + }, + }, + }, + }, + publicKey: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+JHaQBRHL76XoZvFeIbYDCPDFONnXM+cP307iq3L/pmqnj0EhoERnbKkJHISYBkOu2MH7LUVGcC0hMw1SxcVpg==", + valid: false, + wantErr: nil, + }, + { + name: "signature verification succeeded", + bundle: &anywherev1alpha1.Bundles{ + TypeMeta: v1.TypeMeta{ + Kind: "Bundles", + APIVersion: anywherev1alpha1.GroupVersion.String(), + }, + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + constants.SignatureAnnotation: "MEYCIQCiWwxw/Nchkgtan47FzagXHgB45Op7YWxvSZjFzHau8wIhALG2kbm+H8HJEfN/rUQ0ldo298MnzyhukBptUm0jCtZZ", + }, + }, + Spec: anywherev1alpha1.BundlesSpec{ + Number: 1, + VersionsBundles: []anywherev1alpha1.VersionsBundle{ + { + KubeVersion: "1.31", + }, + }, + }, + }, + publicKey: constants.KMSPublicKey, + valid: true, + wantErr: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(_ *testing.T) { + valid, err := ValidateSignature(tc.bundle, tc.publicKey) + fmt.Println(err) + if err != nil && !strings.Contains(err.Error(), tc.wantErr.Error()) { + t.Errorf("%v got = %v, \nwant %v", tc.name, err, tc.wantErr) + } + if valid != tc.valid { + t.Errorf("%v got = %v, \nwant %v", tc.name, valid, tc.valid) + } + }) + } +} + +func TestGetDigest(t *testing.T) { + testCases := []struct { + testName string + bundle *anywherev1alpha1.Bundles + expectErrSubstr string + }{ + { + testName: "Simple valid bundle", + bundle: &anywherev1alpha1.Bundles{ + Spec: anywherev1alpha1.BundlesSpec{ + Number: 1, + VersionsBundles: []anywherev1alpha1.VersionsBundle{ + { + KubeVersion: "1.31", + }, + }, + }, + }, + expectErrSubstr: "", + }, + { + testName: "Another valid bundle with more fields", + bundle: &anywherev1alpha1.Bundles{ + Spec: anywherev1alpha1.BundlesSpec{ + Number: 10, + CliMinVersion: "v0.0.1", + VersionsBundles: []anywherev1alpha1.VersionsBundle{ + { + KubeVersion: "1.28", + }, + { + KubeVersion: "1.29", + }, + }, + }, + }, + expectErrSubstr: "", + }, + } + + for _, tt := range testCases { + t.Run(tt.testName, func(t *testing.T) { + g := gomega.NewWithT(t) + + digest, filtered, err := getDigest(tt.bundle) + if tt.expectErrSubstr == "" { + g.Expect(err).NotTo(gomega.HaveOccurred(), "Expected success but got error") + g.Expect(digest).NotTo(gomega.BeZero(), + "Expected digest to be non-zero array") + g.Expect(filtered).NotTo(gomega.BeEmpty(), + "Expected filtered bytes to be non-empty") + } else { + g.Expect(err).To(gomega.HaveOccurred(), + "Expected error but got none") + g.Expect(err.Error()).To(gomega.ContainSubstring(tt.expectErrSubstr), + "Error message should contain substring %q, got: %v", + tt.expectErrSubstr, err) + g.Expect(digest).To(gomega.BeZero()) + g.Expect(filtered).To(gomega.BeNil()) + } + }) + } +} + +func TestFilterExcludes(t *testing.T) { + testCases := []struct { + testName string + jsonPayload string + expectErrSubstr string + expectExclude []string // substrings we expect to NOT be present + expectInclude []string // substrings we expect to be present + }{ + { + testName: "Valid JSON with known excludes", + jsonPayload: `{ + "metadata": { + "creationTimestamp": "2021-09-01T00:00:00Z", + "annotations": { "key": "value" } + }, + "status": { + "someStatus": "info" + }, + "spec": { + "versionsBundles": [{ + "kubeVersion": "1.28", + "endOfExtendedSupport": "2024-12-31", + "eksD": { + "channel": "1-28", + "components": "https://distro.eks.amazonaws.com/crds/releases.distro.eks.amazonaws.com-v1alpha1.yaml", + "gitCommit": "3c3ff5d3aaa7417b906549756da44f60af5df03d", + "kubeVersion": "v1.28.15", + "manifestUrl": "https://distro.eks.amazonaws.com/kubernetes-1-28/kubernetes-1-28-eks-37.yaml", + "name": "kubernetes-1-28-eks-37" + }, + "eksa": "someValue" + }], + "otherField": "otherValue" + } + }`, + expectErrSubstr: "", + expectExclude: []string{ + "creationTimestamp", + "annotations", + "status", + "eksa", + }, + expectInclude: []string{ + "kubeVersion", + "endOfExtendedSupport", + "eksD", + }, + }, + { + testName: "Invalid JSON payload", + jsonPayload: `{"unclosed": [`, + expectErrSubstr: "unmarshalling JSON:", + }, + { + testName: "empty JSON payload", + jsonPayload: `{}`, + expectErrSubstr: "gojq execution error", + }, + { + testName: "Excludes with minimal JSON", + jsonPayload: `{ + "metadata": {"creationTimestamp": "2021-09-01T00:00:00Z"}, + "spec": { + "versionsBundles": [{ + "kubeVersion": "1.31" + }] + } + }`, + expectErrSubstr: "", + expectExclude: []string{"creationTimestamp"}, + expectInclude: []string{"spec"}, + }, + } + + for _, tt := range testCases { + t.Run(tt.testName, func(t *testing.T) { + g := gomega.NewWithT(t) + + filtered, err := filterExcludes([]byte(tt.jsonPayload)) + + if tt.expectErrSubstr == "" { + g.Expect(err).NotTo(gomega.HaveOccurred(), + "Expected success but got error: %v", err) + g.Expect(filtered).NotTo(gomega.BeEmpty(), "Expected non-empty filtered output") + + // Convert filtered output back to string for substring checks + filteredStr := string(filtered) + for _, excl := range tt.expectExclude { + g.Expect(filteredStr).NotTo(gomega.ContainSubstring(excl), + "Expected %q to be excluded but it was present", excl) + } + for _, incl := range tt.expectInclude { + g.Expect(filteredStr).To(gomega.ContainSubstring(incl), + "Expected %q to be included but it was not found", incl) + } + } else { + g.Expect(err).To(gomega.HaveOccurred(), + "Expected error but got none") + g.Expect(err.Error()).To(gomega.ContainSubstring(tt.expectErrSubstr), + "Error should contain substring %q", tt.expectErrSubstr) + } + }) + } +} From 1b680d9a379351c2b3451350cb747854afcc100a Mon Sep 17 00:00:00 2001 From: panktishah26 Date: Fri, 31 Jan 2025 14:58:22 -0800 Subject: [PATCH 2/3] Verify bundle signature is valid and is not modified --- pkg/signature/manifets_test.go | 338 --------------------------------- 1 file changed, 338 deletions(-) delete mode 100644 pkg/signature/manifets_test.go diff --git a/pkg/signature/manifets_test.go b/pkg/signature/manifets_test.go deleted file mode 100644 index 05016abea91c..000000000000 --- a/pkg/signature/manifets_test.go +++ /dev/null @@ -1,338 +0,0 @@ -package signature - -import ( - "fmt" - "strings" - "testing" - - "github.com/onsi/gomega" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/aws/eks-anywhere/pkg/constants" - anywherev1alpha1 "github.com/aws/eks-anywhere/release/api/v1alpha1" -) - -func TestValidateSignature(t *testing.T) { - tests := []struct { - name string - bundle *anywherev1alpha1.Bundles - publicKey string - valid bool - wantErr error - }{ - { - name: "empty bundle with signature field", - bundle: &anywherev1alpha1.Bundles{ - ObjectMeta: v1.ObjectMeta{ - Annotations: map[string]string{ - constants.SignatureAnnotation: "MEUCICV1iiNA4owIUdZBIowSgWjTKx+JT5/CE8PzmF2CBD5+AiEAk8Fcc1X/LNGm0YCyZISWFhbh4qdc7ENyYCU3DB0u4b0=", - }, - }, - }, - valid: false, - wantErr: fmt.Errorf("filtering excluded fields: gojq execution error"), - }, - { - name: "no bundle signature", - bundle: &anywherev1alpha1.Bundles{ - ObjectMeta: v1.ObjectMeta{ - Annotations: map[string]string{ - "eks.amazonaws.com/no-signature": "", - }, - }, - }, - valid: false, - wantErr: fmt.Errorf("missing signature annotation"), - }, - { - name: "invalid signature", - bundle: &anywherev1alpha1.Bundles{ - ObjectMeta: v1.ObjectMeta{ - Annotations: map[string]string{ - constants.SignatureAnnotation: "invalid", - }, - }, - Spec: anywherev1alpha1.BundlesSpec{ - Number: 1, - VersionsBundles: []anywherev1alpha1.VersionsBundle{ - { - KubeVersion: "1.31", - }, - }, - }, - }, - valid: false, - wantErr: fmt.Errorf("signature in metadata isn't base64 encoded"), - }, - { - name: "invalid public key", - bundle: &anywherev1alpha1.Bundles{ - ObjectMeta: v1.ObjectMeta{ - Annotations: map[string]string{ - constants.SignatureAnnotation: "MEUCICV1iiNA4owIUdZBIowSgWjTKx+JT5/CE8PzmF2CBD5+AiEAk8Fcc1X/LNGm0YCyZISWFhbh4qdc7ENyYCU3DB0u4b0=", - }, - }, - Spec: anywherev1alpha1.BundlesSpec{ - Number: 1, - VersionsBundles: []anywherev1alpha1.VersionsBundle{ - { - KubeVersion: "1.31", - }, - }, - }, - }, - publicKey: "invalid", - valid: false, - wantErr: fmt.Errorf("decoding the public key as string"), - }, - { - name: "invalid encoded public key", - bundle: &anywherev1alpha1.Bundles{ - ObjectMeta: v1.ObjectMeta{ - Annotations: map[string]string{ - constants.SignatureAnnotation: "MEUCICV1iiNA4owIUdZBIowSgWjTKx+JT5/CE8PzmF2CBD5+AiEAk8Fcc1X/LNGm0YCyZISWFhbh4qdc7ENyYCU3DB0u4b0=", - }, - }, - Spec: anywherev1alpha1.BundlesSpec{ - Number: 1, - VersionsBundles: []anywherev1alpha1.VersionsBundle{ - { - KubeVersion: "1.31", - }, - }, - }, - }, - publicKey: "TUVVQ0lDVjFpaU5BNG93SVVkWkJJb3dTZ1dqVEt4K0pUNS9DRThQem1GMkNCRDUrQWlFQWs4RmNjMVgvTE5HbTBZQ3laSVNXRmhiaDRxZGM3RU55WUNVM0RCMHU0YjA9Cg==", - valid: false, - wantErr: fmt.Errorf("parsing the public key (not PKIX)"), - }, - { - name: "signature verification fail", - bundle: &anywherev1alpha1.Bundles{ - ObjectMeta: v1.ObjectMeta{ - Annotations: map[string]string{ - constants.SignatureAnnotation: "MEUCICV1iiNA4owIUdZBIowSgWjTKx+JT5/CE8PzmF2CBD5+AiEAk8Fcc1X/LNGm0YCyZISWFhbh4qdc7ENyYCU3DB0u4b0=", - }, - }, - Spec: anywherev1alpha1.BundlesSpec{ - Number: 1, - VersionsBundles: []anywherev1alpha1.VersionsBundle{ - { - KubeVersion: "1.31", - }, - }, - }, - }, - publicKey: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+JHaQBRHL76XoZvFeIbYDCPDFONnXM+cP307iq3L/pmqnj0EhoERnbKkJHISYBkOu2MH7LUVGcC0hMw1SxcVpg==", - valid: false, - wantErr: nil, - }, - { - name: "signature verification succeeded", - bundle: &anywherev1alpha1.Bundles{ - TypeMeta: v1.TypeMeta{ - Kind: "Bundles", - APIVersion: anywherev1alpha1.GroupVersion.String(), - }, - ObjectMeta: v1.ObjectMeta{ - Annotations: map[string]string{ - constants.SignatureAnnotation: "MEYCIQCiWwxw/Nchkgtan47FzagXHgB45Op7YWxvSZjFzHau8wIhALG2kbm+H8HJEfN/rUQ0ldo298MnzyhukBptUm0jCtZZ", - }, - }, - Spec: anywherev1alpha1.BundlesSpec{ - Number: 1, - VersionsBundles: []anywherev1alpha1.VersionsBundle{ - { - KubeVersion: "1.31", - }, - }, - }, - }, - publicKey: constants.KMSPublicKey, - valid: true, - wantErr: nil, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(_ *testing.T) { - valid, err := ValidateSignature(tc.bundle, tc.publicKey) - fmt.Println(err) - if err != nil && !strings.Contains(err.Error(), tc.wantErr.Error()) { - t.Errorf("%v got = %v, \nwant %v", tc.name, err, tc.wantErr) - } - if valid != tc.valid { - t.Errorf("%v got = %v, \nwant %v", tc.name, valid, tc.valid) - } - }) - } -} - -func TestGetDigest(t *testing.T) { - testCases := []struct { - testName string - bundle *anywherev1alpha1.Bundles - expectErrSubstr string - }{ - { - testName: "Simple valid bundle", - bundle: &anywherev1alpha1.Bundles{ - Spec: anywherev1alpha1.BundlesSpec{ - Number: 1, - VersionsBundles: []anywherev1alpha1.VersionsBundle{ - { - KubeVersion: "1.31", - }, - }, - }, - }, - expectErrSubstr: "", - }, - { - testName: "Another valid bundle with more fields", - bundle: &anywherev1alpha1.Bundles{ - Spec: anywherev1alpha1.BundlesSpec{ - Number: 10, - CliMinVersion: "v0.0.1", - VersionsBundles: []anywherev1alpha1.VersionsBundle{ - { - KubeVersion: "1.28", - }, - { - KubeVersion: "1.29", - }, - }, - }, - }, - expectErrSubstr: "", - }, - } - - for _, tt := range testCases { - t.Run(tt.testName, func(t *testing.T) { - g := gomega.NewWithT(t) - - digest, filtered, err := getDigest(tt.bundle) - if tt.expectErrSubstr == "" { - g.Expect(err).NotTo(gomega.HaveOccurred(), "Expected success but got error") - g.Expect(digest).NotTo(gomega.BeZero(), - "Expected digest to be non-zero array") - g.Expect(filtered).NotTo(gomega.BeEmpty(), - "Expected filtered bytes to be non-empty") - } else { - g.Expect(err).To(gomega.HaveOccurred(), - "Expected error but got none") - g.Expect(err.Error()).To(gomega.ContainSubstring(tt.expectErrSubstr), - "Error message should contain substring %q, got: %v", - tt.expectErrSubstr, err) - g.Expect(digest).To(gomega.BeZero()) - g.Expect(filtered).To(gomega.BeNil()) - } - }) - } -} - -func TestFilterExcludes(t *testing.T) { - testCases := []struct { - testName string - jsonPayload string - expectErrSubstr string - expectExclude []string // substrings we expect to NOT be present - expectInclude []string // substrings we expect to be present - }{ - { - testName: "Valid JSON with known excludes", - jsonPayload: `{ - "metadata": { - "creationTimestamp": "2021-09-01T00:00:00Z", - "annotations": { "key": "value" } - }, - "status": { - "someStatus": "info" - }, - "spec": { - "versionsBundles": [{ - "kubeVersion": "1.28", - "endOfExtendedSupport": "2024-12-31", - "eksD": { - "channel": "1-28", - "components": "https://distro.eks.amazonaws.com/crds/releases.distro.eks.amazonaws.com-v1alpha1.yaml", - "gitCommit": "3c3ff5d3aaa7417b906549756da44f60af5df03d", - "kubeVersion": "v1.28.15", - "manifestUrl": "https://distro.eks.amazonaws.com/kubernetes-1-28/kubernetes-1-28-eks-37.yaml", - "name": "kubernetes-1-28-eks-37" - }, - "eksa": "someValue" - }], - "otherField": "otherValue" - } - }`, - expectErrSubstr: "", - expectExclude: []string{ - "creationTimestamp", - "annotations", - "status", - "eksa", - }, - expectInclude: []string{ - "kubeVersion", - "endOfExtendedSupport", - "eksD", - }, - }, - { - testName: "Invalid JSON payload", - jsonPayload: `{"unclosed": [`, - expectErrSubstr: "unmarshalling JSON:", - }, - { - testName: "empty JSON payload", - jsonPayload: `{}`, - expectErrSubstr: "gojq execution error", - }, - { - testName: "Excludes with minimal JSON", - jsonPayload: `{ - "metadata": {"creationTimestamp": "2021-09-01T00:00:00Z"}, - "spec": { - "versionsBundles": [{ - "kubeVersion": "1.31" - }] - } - }`, - expectErrSubstr: "", - expectExclude: []string{"creationTimestamp"}, - expectInclude: []string{"spec"}, - }, - } - - for _, tt := range testCases { - t.Run(tt.testName, func(t *testing.T) { - g := gomega.NewWithT(t) - - filtered, err := filterExcludes([]byte(tt.jsonPayload)) - - if tt.expectErrSubstr == "" { - g.Expect(err).NotTo(gomega.HaveOccurred(), - "Expected success but got error: %v", err) - g.Expect(filtered).NotTo(gomega.BeEmpty(), "Expected non-empty filtered output") - - // Convert filtered output back to string for substring checks - filteredStr := string(filtered) - for _, excl := range tt.expectExclude { - g.Expect(filteredStr).NotTo(gomega.ContainSubstring(excl), - "Expected %q to be excluded but it was present", excl) - } - for _, incl := range tt.expectInclude { - g.Expect(filteredStr).To(gomega.ContainSubstring(incl), - "Expected %q to be included but it was not found", incl) - } - } else { - g.Expect(err).To(gomega.HaveOccurred(), - "Expected error but got none") - g.Expect(err.Error()).To(gomega.ContainSubstring(tt.expectErrSubstr), - "Error should contain substring %q", tt.expectErrSubstr) - } - }) - } -} From 78910a3a9cdb05380a70c93e1dd29433ee19a4e2 Mon Sep 17 00:00:00 2001 From: panktishah26 Date: Mon, 3 Feb 2025 22:23:02 -0800 Subject: [PATCH 3/3] Validation around licenseKey field for extended kubernetes version support --- controllers/cluster_controller.go | 2 +- controllers/cluster_controller_test.go | 38 +++------- controllers/cluster_controller_test_test.go | 35 ++------- go.mod | 1 + go.sum | 3 + pkg/constants/constants.go | 2 + pkg/signature/manifest.go | 57 +++++++++++--- pkg/signature/manifest_test.go | 84 ++++++++++++++++++++- pkg/validations/extendedversion.go | 75 +++++++++++++++++- pkg/validations/extendedversion_test.go | 77 +++++++++++++++++-- 10 files changed, 297 insertions(+), 77 deletions(-) diff --git a/controllers/cluster_controller.go b/controllers/cluster_controller.go index 719f4bbd2915..a854a9d54473 100644 --- a/controllers/cluster_controller.go +++ b/controllers/cluster_controller.go @@ -649,7 +649,7 @@ func validateExtendedK8sVersionSupport(ctx context.Context, client client.Client cluster.Status.FailureReason = &reason return fmt.Errorf("getting bundle for cluster: %w", err) } - if err = validations.ValidateExtendedK8sVersionSupport(ctx, cluster, bundle, clientutil.NewKubeClient(client)); err != nil { + if err = validations.ValidateExtendedK8sVersionSupport(ctx, *cluster, bundle, clientutil.NewKubeClient(client)); err != nil { reason := anywherev1.ExtendedK8sVersionSupportNotSupportedReason cluster.Status.FailureMessage = ptr.String(err.Error()) cluster.Status.FailureReason = &reason diff --git a/controllers/cluster_controller_test.go b/controllers/cluster_controller_test.go index 8fdf5333012a..dae51dfc6fa6 100644 --- a/controllers/cluster_controller_test.go +++ b/controllers/cluster_controller_test.go @@ -151,7 +151,8 @@ func TestClusterReconcilerReconcileSelfManagedCluster(t *testing.T) { Name: "my-management-cluster", }, Spec: anywherev1.ClusterSpec{ - EksaVersion: &version, + KubernetesVersion: anywherev1.Kube132, + EksaVersion: &version, ClusterNetwork: anywherev1.ClusterNetwork{ CNIConfig: &anywherev1.CNIConfig{ Cilium: &anywherev1.CiliumConfig{}, @@ -1082,6 +1083,7 @@ func TestClusterReconcilerReconcileSelfManagedClusterRegAuthFailNoSecret(t *test Name: "my-management-cluster", }, Spec: anywherev1.ClusterSpec{ + KubernetesVersion: anywherev1.Kube132, ClusterNetwork: anywherev1.ClusterNetwork{ CNIConfig: &anywherev1.CNIConfig{ Cilium: &anywherev1.CiliumConfig{}, @@ -1252,7 +1254,7 @@ func TestClusterReconcilerSkipDontInstallPackagesOnSelfManaged(t *testing.T) { Namespace: "default", }, Spec: anywherev1.ClusterSpec{ - KubernetesVersion: "v1.25", + KubernetesVersion: anywherev1.Kube132, ClusterNetwork: anywherev1.ClusterNetwork{ CNIConfig: &anywherev1.CNIConfig{ Cilium: &anywherev1.CiliumConfig{}, @@ -1402,7 +1404,7 @@ func TestClusterReconcilerPackagesInstall(s *testing.T) { Namespace: "default", }, Spec: anywherev1.ClusterSpec{ - KubernetesVersion: "v1.25", + KubernetesVersion: anywherev1.Kube132, ClusterNetwork: anywherev1.ClusterNetwork{ CNIConfig: &anywherev1.CNIConfig{ Cilium: &anywherev1.CiliumConfig{}, @@ -1667,17 +1669,17 @@ func createBundle() *releasev1.Bundles { Name: "bundles-1", Namespace: "default", Annotations: map[string]string{ - constants.SignatureAnnotation: "MEUCIQDbVAB+yy+pdCOFet/vWMoHQA2FYiiQtq1zltBRRhRo2QIgGQopCHraD/HpvpSh4Q7rVdesXeVriJv2ucEnoidoZlg=", + constants.SignatureAnnotation: "MEQCIG8DZfnqQtx1fF5x2assfSUEvuJ9BqaCN8jaoBHxKU8SAiBwR2B/T2BC3nzmnT2uEvwyemOy+A7V/K+PkGuKGX0E1Q==", }, }, Spec: releasev1.BundlesSpec{ VersionsBundles: []releasev1.VersionsBundle{ { - KubeVersion: "1.30", + KubeVersion: "1.32", EksD: releasev1.EksDRelease{ Name: "test", EksDReleaseUrl: "testdata/release.yaml", - KubeVersion: "1.30", + KubeVersion: "1.32", }, CertManager: releasev1.CertManagerBundle{}, ClusterAPI: releasev1.CoreClusterAPI{}, @@ -1693,6 +1695,7 @@ func createBundle() *releasev1.Bundles { ExternalEtcdBootstrap: releasev1.EtcdadmBootstrapBundle{}, ExternalEtcdController: releasev1.EtcdadmControllerBundle{}, Tinkerbell: releasev1.TinkerbellBundle{}, + EndOfStandardSupport: "2030-06-30", }, }, }, @@ -1745,7 +1748,7 @@ func vsphereCluster() *anywherev1.Cluster { Kind: "VSphereDatacenterConfig", Name: "datacenter", }, - KubernetesVersion: "1.20", + KubernetesVersion: anywherev1.Kube132, ControlPlaneConfiguration: anywherev1.ControlPlaneConfiguration{ Count: 1, Endpoint: &anywherev1.Endpoint{ @@ -1864,26 +1867,7 @@ func baseTestVsphereCluster() (*cluster.Config, *releasev1.Bundles) { config.AWSIAMConfigs[awsIAM.Name] = awsIAM config.OIDCConfigs[oidc.Name] = oidc - bundles := &releasev1.Bundles{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-bundles-ref", - Namespace: config.Cluster.Namespace, - Annotations: map[string]string{ - constants.SignatureAnnotation: "MEYCIQDA40Bizd/0mdCwRCIKq10gjLdJMT0s0y57RPW/zOyWZwIhALPOFS+NZZ7QCwI7wiC1TiArMHMq4TbzIJcx85H/zjU4", - }, - }, - Spec: releasev1.BundlesSpec{ - VersionsBundles: []releasev1.VersionsBundle{ - { - KubeVersion: "v1.30", - PackageController: releasev1.PackageBundle{ - HelmChart: releasev1.Image{}, - }, - }, - }, - }, - } - + bundles := createBundle() config.Cluster.Spec.BundlesRef = &anywherev1.BundlesRef{ Name: bundles.Name, Namespace: bundles.Namespace, diff --git a/controllers/cluster_controller_test_test.go b/controllers/cluster_controller_test_test.go index 73c00e4cf607..4f87a0d2ced6 100644 --- a/controllers/cluster_controller_test_test.go +++ b/controllers/cluster_controller_test_test.go @@ -27,7 +27,6 @@ import ( "github.com/aws/eks-anywhere/pkg/controller" "github.com/aws/eks-anywhere/pkg/controller/clientutil" "github.com/aws/eks-anywhere/pkg/controller/clusters" - "github.com/aws/eks-anywhere/release/api/v1alpha1" ) func TestClusterReconcilerEnsureOwnerReferences(t *testing.T) { @@ -54,7 +53,7 @@ func TestClusterReconcilerEnsureOwnerReferences(t *testing.T) { Namespace: "default", }, Spec: anywherev1.ClusterSpec{ - KubernetesVersion: "v1.25", + KubernetesVersion: anywherev1.Kube132, EksaVersion: &version, }, Status: anywherev1.ClusterStatus{ @@ -244,8 +243,8 @@ func TestClusterReconcilerSetBundlesRef(t *testing.T) { }, Spec: anywherev1.ClusterSpec{ BundlesRef: &anywherev1.BundlesRef{ - Name: "my-bundles-ref", - Namespace: "my-namespace", + Name: "bundles-1", + Namespace: "default", }, }, Status: anywherev1.ClusterStatus{ @@ -258,10 +257,10 @@ func TestClusterReconcilerSetBundlesRef(t *testing.T) { Name: "my-cluster", }, Spec: anywherev1.ClusterSpec{ - KubernetesVersion: "v1.25", + KubernetesVersion: anywherev1.Kube132, BundlesRef: &anywherev1.BundlesRef{ - Name: "my-bundles-ref", - Namespace: "my-namespace", + Name: "bundles-1", + Namespace: "default", }, }, Status: anywherev1.ClusterStatus{ @@ -275,25 +274,7 @@ func TestClusterReconcilerSetBundlesRef(t *testing.T) { Namespace: constants.EksaSystemNamespace, }, } - bundles := &v1alpha1.Bundles{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-bundles-ref", - Namespace: cluster.Spec.BundlesRef.Namespace, - Annotations: map[string]string{ - constants.SignatureAnnotation: "MEYCIQDA40Bizd/0mdCwRCIKq10gjLdJMT0s0y57RPW/zOyWZwIhALPOFS+NZZ7QCwI7wiC1TiArMHMq4TbzIJcx85H/zjU4", - }, - }, - Spec: v1alpha1.BundlesSpec{ - VersionsBundles: []v1alpha1.VersionsBundle{ - { - KubeVersion: "v1.30", - PackageController: v1alpha1.PackageBundle{ - HelmChart: v1alpha1.Image{}, - }, - }, - }, - }, - } + bundles := createBundle() objs := []runtime.Object{cluster, managementCluster, secret, bundles} cb := fake.NewClientBuilder() @@ -347,7 +328,7 @@ func TestClusterReconcilerSetDefaultEksaVersion(t *testing.T) { Namespace: "default", }, Spec: anywherev1.ClusterSpec{ - KubernetesVersion: "v1.25", + KubernetesVersion: anywherev1.Kube132, }, Status: anywherev1.ClusterStatus{ ReconciledGeneration: 1, diff --git a/go.mod b/go.mod index b56dcd41c917..3fbaf433365d 100644 --- a/go.mod +++ b/go.mod @@ -124,6 +124,7 @@ require ( github.com/go-openapi/swag v0.22.3 // indirect github.com/gobuffalo/flect v1.0.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect diff --git a/go.sum b/go.sum index 3ec736dc5680..6b4355d8ac17 100644 --- a/go.sum +++ b/go.sum @@ -437,7 +437,10 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 9c184cc0d60d..d91c55d2a81b 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -122,6 +122,8 @@ const ( Excludes = "LnNwZWMudmVyc2lvbnNCdW5kbGVzW10uYm9vdHN0cmFwCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmJvdHRsZXJvY2tldEhvc3RDb250YWluZXJzCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmNlcnRNYW5hZ2VyCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmNpbGl1bQouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS5jbG91ZFN0YWNrCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmNsdXN0ZXJBUEkKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uY29udHJvbFBsYW5lCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmRvY2tlcgouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS5la3NhCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmV0Y2RhZG1Cb290c3RyYXAKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uZXRjZGFkbUNvbnRyb2xsZXIKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uZmx1eAouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS5oYXByb3h5Ci5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmtpbmRuZXRkCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLm51dGFuaXgKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10ucGFja2FnZUNvbnRyb2xsZXIKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uc25vdwouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS50aW5rZXJiZWxsCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLnVwZ3JhZGVyCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLnZTcGhlcmU=" // KMSPublicKey to verify bundle signature. KMSPublicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFZU/Z6VVMU9HioT7rGkPdJg3frC2xyQZhWFIrz5HeZEfeQ2nAdnJMLrs2Qr3V9xVrJrHA54wnIHDoPGbEhojqg==" + // LincesePublicKey is to verify the licenseKey field. + LincesePublicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE96Xb67YUq+at8gFOioYlf1kxOIPio7i3Y8sFrG3a3sn/MzqQmTO9K82psqOuN+E4NdE8VajOtbyfcLo+Ojax/w==" ) type Operation int diff --git a/pkg/signature/manifest.go b/pkg/signature/manifest.go index e0f8434b294a..4508d17503df 100644 --- a/pkg/signature/manifest.go +++ b/pkg/signature/manifest.go @@ -26,6 +26,7 @@ import ( "strings" "text/template" + "github.com/golang-jwt/jwt/v5" "github.com/itchyny/gojq" "sigs.k8s.io/yaml" @@ -50,19 +51,9 @@ func ValidateSignature(bundle *anywherev1alpha1.Bundles, pubKey string) (valid b return false, fmt.Errorf("signature in metadata isn't base64 encoded: %w", err) } - pubdecoded, err := base64.StdEncoding.DecodeString(pubKey) + pubkey, err := parsePublicKey(pubKey) if err != nil { - return false, fmt.Errorf("decoding the public key as string: %w", err) - } - - pubparsed, err := x509.ParsePKIXPublicKey(pubdecoded) - if err != nil { - return false, fmt.Errorf("parsing the public key (not PKIX): %w", err) - } - - pubkey, ok := pubparsed.(*ecdsa.PublicKey) - if !ok { - return false, fmt.Errorf("parsing the public key (not ECDSA): %T", pubparsed) + return false, err } return ecdsa.VerifyASN1(pubkey, digest[:], sig), nil @@ -162,3 +153,45 @@ func filterExcludes(jsonBytes []byte) ([]byte, error) { } return filtered, nil } + +func parsePublicKey(key string) (*ecdsa.PublicKey, error) { + pubdecoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return nil, fmt.Errorf("decoding the public key as string: %w", err) + } + + pubparsed, err := x509.ParsePKIXPublicKey(pubdecoded) + if err != nil { + return nil, fmt.Errorf("parsing the public key (not PKIX): %w", err) + } + + pubkey, ok := pubparsed.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("parsing the public key (not ECDSA): %T", pubparsed) + } + return pubkey, nil +} + +// ParseLicense parses licenseKey jwt token using the public key and returns token fields. +func ParseLicense(licenseToken string, key string) (*jwt.Token, error) { + tokenKey, err := parsePublicKey(key) + if err != nil { + return nil, err + } + + token, err := jwt.Parse(licenseToken, func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodECDSA); !ok { + return nil, fmt.Errorf("signing method not supported: %v", t.Header["alg"]) + } + return tokenKey, nil + }) + if err != nil { + return nil, fmt.Errorf("parsing licenseToken: %w", err) + } + + if !token.Valid { + return nil, errors.New("licenseToken is not valid") + } + + return token, nil +} diff --git a/pkg/signature/manifest_test.go b/pkg/signature/manifest_test.go index 05016abea91c..37efa15db8f7 100644 --- a/pkg/signature/manifest_test.go +++ b/pkg/signature/manifest_test.go @@ -1,10 +1,17 @@ package signature import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/base64" "fmt" "strings" "testing" + "time" + "github.com/golang-jwt/jwt/v5" "github.com/onsi/gomega" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -157,7 +164,6 @@ func TestValidateSignature(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(_ *testing.T) { valid, err := ValidateSignature(tc.bundle, tc.publicKey) - fmt.Println(err) if err != nil && !strings.Contains(err.Error(), tc.wantErr.Error()) { t.Errorf("%v got = %v, \nwant %v", tc.name, err, tc.wantErr) } @@ -336,3 +342,79 @@ func TestFilterExcludes(t *testing.T) { }) } } + +func TestParseLicense(t *testing.T) { + tests := []struct { + name string + licenseKey string + key string + wantErr error + }{ + { + name: "malformed token", + licenseKey: "invalid.token.string", + key: constants.LincesePublicKey, + wantErr: fmt.Errorf("parsing licenseToken"), + }, + { + name: "invalid public key", + licenseKey: "invalid.token.string", + key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7XtGi5M5nUyoXZpZWg5e9YgQaVbUq4DbxFkGn7yM9rIg+45dQ1pJwYQd/Z9RDZ3umTZHfdmVfaMT8E/2jpa6vYh5AroOn75tN8qaGmG2OqEBoA8k84zK98qNdOJow7CcIWjHQGk6Tr/dSfdTC6ydmBdRMX/7bBYcKylOFf2P65HOMQCB5YdZJAYzvlXEXzoc1o7DD3pT68BOHHTJp6h7+GGXZoNlHJeq1+AKq38Ra6tuI8EUV2S/5+75FFJzMTLVlJ20Jlhh3fuWJtn6a2hGeD/fbZ1w6CMi0dCTGEX6wUOmL5FJ4RFSVthqZCZ7Ap0G2/5Mu3pxVR9glAxThOw61QIDAQAB", + wantErr: fmt.Errorf("parsing the public key (not ECDSA)"), + }, + { + name: "signing method not supported", + licenseKey: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleGFtcGxlIjoiZGF0YSJ9.UKzt6DArjTtHk_Nch6TwbdgVni6FwLJ1fdbVNYikE_kFGTzMZC82m_0qY7l27LtN0J6b_5D8hLLFk3pTZHYGBX5kB2XKH5e5syRkGh6uZHDkGtRjTMoD5sPMZJ0rG4m80k8cgI37UsIt66hoK_45FzSMlTwxogJ2nJk5G1dH10", + key: constants.LincesePublicKey, + wantErr: fmt.Errorf("signing method not supported"), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := ParseLicense(tc.licenseKey, tc.key) + if err != nil && !strings.Contains(err.Error(), tc.wantErr.Error()) { + t.Errorf("%v got = %v, \nwant %v", tc.name, err, tc.wantErr) + } + }) + } +} + +func generateTestKeys() (string, *ecdsa.PrivateKey, error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return "", nil, fmt.Errorf("failed to generate private key: %w", err) + } + publicKey := &privateKey.PublicKey + + publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return "", nil, fmt.Errorf("failed to marshal public key: %w", err) + } + publicKeyBase64 := base64.StdEncoding.EncodeToString(publicKeyBytes) + + return publicKeyBase64, privateKey, nil +} + +func TestParseLicense_Success(t *testing.T) { + publicKeyBase64, privateKey, err := generateTestKeys() + if err != nil { + t.Errorf("Failed to generate test keys: %v", err) + } + + claims := jwt.MapClaims{ + "iss": "test", + "sub": "12345", + "exp": time.Now().Add(time.Hour * 24).Unix(), + } + token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) + signedToken, err := token.SignedString(privateKey) + if err != nil { + t.Errorf("Failed to sign token: %v", err) + } + + _, err = ParseLicense(signedToken, publicKeyBase64) + if err != nil { + t.Errorf("ParseLicense failed: %v", err) + } +} diff --git a/pkg/validations/extendedversion.go b/pkg/validations/extendedversion.go index f027033161e4..3848ec3816de 100644 --- a/pkg/validations/extendedversion.go +++ b/pkg/validations/extendedversion.go @@ -2,21 +2,41 @@ package validations import ( "context" + "errors" "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" anywherev1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1" "github.com/aws/eks-anywhere/pkg/clients/kubernetes" + "github.com/aws/eks-anywhere/pkg/cluster" "github.com/aws/eks-anywhere/pkg/constants" "github.com/aws/eks-anywhere/pkg/signature" "github.com/aws/eks-anywhere/release/api/v1alpha1" ) // ValidateExtendedK8sVersionSupport validates all the validations needed for the support of extended kubernetes support. -func ValidateExtendedK8sVersionSupport(_ context.Context, _ *anywherev1.Cluster, bundle *v1alpha1.Bundles, _ kubernetes.Client) error { +func ValidateExtendedK8sVersionSupport(_ context.Context, clusterSpec anywherev1.Cluster, bundle *v1alpha1.Bundles, _ kubernetes.Client) error { // Validate EKS-A bundle has not been modified by verifying the signature in the bundle annotation if err := validateBundleSignature(bundle); err != nil { return fmt.Errorf("validating bundle signature: %w", err) } + + // Check whether the kubernetes version for the cluster is currently under extended support by comparing the endOfStandardSupport date from the bundle with the current date. + isExtended, err := isExtendedSupport(clusterSpec.Spec.KubernetesVersion, bundle) + if err != nil { + return err + } + if isExtended { + token, err := getLicense(clusterSpec.Spec.LicenseToken) + if err != nil { + return fmt.Errorf("getting licenseToken: %w", err) + } + if err = validateLicense(token); err != nil { + return fmt.Errorf("validating licenseToken: %w", err) + } + } return nil } @@ -31,3 +51,56 @@ func validateBundleSignature(bundle *v1alpha1.Bundles) error { } return nil } + +func isExtendedSupport(kubernetesVersion anywherev1.KubernetesVersion, bundle *v1alpha1.Bundles) (bool, error) { + versionsBundle, err := cluster.GetVersionsBundle(kubernetesVersion, bundle) + if err != nil { + return false, fmt.Errorf("getting versions bundle for %s kubernetes version: %w", kubernetesVersion, err) + } + + endOfStandardSupport, err := time.Parse("2006-01-02", versionsBundle.EndOfStandardSupport) + if err != nil { + return false, fmt.Errorf("parsing EndOfStandardSupport field format: %w", err) + } + + return isPastDateThanToday(endOfStandardSupport), nil +} + +func getLicense(licenseToken string) (*jwt.Token, error) { + if licenseToken == "" { + return nil, errors.New("licenseToken is required for extended kubernetes support") + } + token, err := signature.ParseLicense(licenseToken, constants.LincesePublicKey) + if err != nil { + return nil, fmt.Errorf("parsing licenseToken: %w", err) + } + + return token, nil +} + +func validateLicense(token *jwt.Token) error { + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return errors.New("could not parse the licenseToken claims") + } + endValidity, ok := claims["endValidity"].(string) + if !ok { + return errors.New("license validity field missing from the licenseToken, not a valid license") + } + + validity, err := time.Parse(time.RFC3339, endValidity) + if err != nil { + return fmt.Errorf("parsing endValidity field from licenseToken: %w", err) + } + + if isPastDateThanToday(validity) { + return errors.New("license is expired, please renew the license") + } + + return nil +} + +func isPastDateThanToday(dateToCompare time.Time) bool { + today := time.Now().Truncate(24 * time.Hour) + return dateToCompare.Before(today) +} diff --git a/pkg/validations/extendedversion_test.go b/pkg/validations/extendedversion_test.go index 4aa94e307423..f78d5c3d99dc 100644 --- a/pkg/validations/extendedversion_test.go +++ b/pkg/validations/extendedversion_test.go @@ -19,16 +19,17 @@ import ( func TestValidateExtendedK8sVersionSupport(t *testing.T) { ctx := context.Background() client := test.NewFakeKubeClient() + tests := []struct { name string - cluster *anywherev1.Cluster + cluster anywherev1.Cluster bundle *v1alpha1.Bundles client kubernetes.Client wantErr error }{ { - name: "No bundle signature", - cluster: &anywherev1.Cluster{}, + name: "no bundle signature", + cluster: anywherev1.Cluster{}, bundle: &v1alpha1.Bundles{ ObjectMeta: v1.ObjectMeta{ Annotations: map[string]string{ @@ -39,8 +40,22 @@ func TestValidateExtendedK8sVersionSupport(t *testing.T) { wantErr: fmt.Errorf("missing signature annotation"), }, { - name: "bundle verification succeeded", - cluster: &anywherev1.Cluster{}, + name: "kubernetes version not supported", + cluster: anywherev1.Cluster{ + Spec: anywherev1.ClusterSpec{ + KubernetesVersion: "1.22", + }, + }, + bundle: validBundle(), + wantErr: fmt.Errorf("getting versions bundle for 1.22 kubernetes version"), + }, + { + name: "unsupported EndOfStandardSupport format", + cluster: anywherev1.Cluster{ + Spec: anywherev1.ClusterSpec{ + KubernetesVersion: "1.28", + }, + }, bundle: &v1alpha1.Bundles{ TypeMeta: v1.TypeMeta{ Kind: "Bundles", @@ -48,19 +63,42 @@ func TestValidateExtendedK8sVersionSupport(t *testing.T) { }, ObjectMeta: v1.ObjectMeta{ Annotations: map[string]string{ - constants.SignatureAnnotation: "MEYCIQCiWwxw/Nchkgtan47FzagXHgB45Op7YWxvSZjFzHau8wIhALG2kbm+H8HJEfN/rUQ0ldo298MnzyhukBptUm0jCtZZ", + constants.SignatureAnnotation: "MEYCIQCYJwrDjICgUQImFpJdOLjQlC7OSQutCsqBk+0jUheZTQIhALSj7peTLSTSy9rvNfYwyqbP0fOi3elggWwPcAz89csc", }, }, Spec: v1alpha1.BundlesSpec{ Number: 1, VersionsBundles: []v1alpha1.VersionsBundle{ { - KubeVersion: "1.31", + KubeVersion: "1.28", + EndOfStandardSupport: "2024-31-12", }, }, }, }, - wantErr: fmt.Errorf("missing signature annotation"), + wantErr: fmt.Errorf("parsing EndOfStandardSupport field format"), + }, + { + name: "missing license token", + cluster: anywherev1.Cluster{ + Spec: anywherev1.ClusterSpec{ + KubernetesVersion: "1.28", + LicenseToken: "", + }, + }, + bundle: validBundle(), + wantErr: fmt.Errorf("licenseToken is required for extended kubernetes support"), + }, + { + name: "invalid licenseKey", + cluster: anywherev1.Cluster{ + Spec: anywherev1.ClusterSpec{ + KubernetesVersion: "1.28", + LicenseToken: "invalid-token", + }, + }, + bundle: validBundle(), + wantErr: fmt.Errorf("getting licenseToken"), }, } @@ -73,3 +111,26 @@ func TestValidateExtendedK8sVersionSupport(t *testing.T) { }) } } + +func validBundle() *v1alpha1.Bundles { + return &v1alpha1.Bundles{ + TypeMeta: v1.TypeMeta{ + Kind: "Bundles", + APIVersion: v1alpha1.GroupVersion.String(), + }, + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + constants.SignatureAnnotation: "MEYCIQC8Fuo81dxibtkvrOFZpbFXZGmJnhLN6bkJjx4YB0fGIQIhAJIxIAl3s26eXqcmS6kAyjDd0NXDlBbM0d/GCHcL2Xoo", + }, + }, + Spec: v1alpha1.BundlesSpec{ + Number: 1, + VersionsBundles: []v1alpha1.VersionsBundle{ + { + KubeVersion: "1.28", + EndOfStandardSupport: "2024-12-31", + }, + }, + }, + } +}