Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validation around licenseKey field for extended kubernetes version support #9218

Merged
merged 3 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion controllers/cluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 11 additions & 27 deletions controllers/cluster_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
Expand Down Expand Up @@ -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{},
Expand Down Expand Up @@ -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{},
Expand Down Expand Up @@ -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{},
Expand Down Expand Up @@ -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{},
Expand All @@ -1693,6 +1695,7 @@ func createBundle() *releasev1.Bundles {
ExternalEtcdBootstrap: releasev1.EtcdadmBootstrapBundle{},
ExternalEtcdController: releasev1.EtcdadmControllerBundle{},
Tinkerbell: releasev1.TinkerbellBundle{},
EndOfStandardSupport: "2030-06-30",
},
},
},
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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,
Expand Down
35 changes: 8 additions & 27 deletions controllers/cluster_controller_test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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{
Expand Down Expand Up @@ -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{
Expand All @@ -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{
Expand All @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
2 changes: 2 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 45 additions & 12 deletions pkg/signature/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"strings"
"text/template"

"github.com/golang-jwt/jwt/v5"
"github.com/itchyny/gojq"
"sigs.k8s.io/yaml"

Expand All @@ -50,19 +51,9 @@
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
Expand Down Expand Up @@ -162,3 +153,45 @@
}
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")
}

Check warning on line 194 in pkg/signature/manifest.go

View check run for this annotation

Codecov / codecov/patch

pkg/signature/manifest.go#L193-L194

Added lines #L193 - L194 were not covered by tests

return token, nil
}
84 changes: 83 additions & 1 deletion pkg/signature/manifest_test.go
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
}
Loading
Loading