diff --git a/pkg/apis/policy/v1alpha1/clusterimagepolicy_types.go b/pkg/apis/policy/v1alpha1/clusterimagepolicy_types.go index 682f368512f..e382d39ae14 100644 --- a/pkg/apis/policy/v1alpha1/clusterimagepolicy_types.go +++ b/pkg/apis/policy/v1alpha1/clusterimagepolicy_types.go @@ -104,6 +104,7 @@ type KeyRef struct { // +optional Data string `json:"data,omitempty"` // KMS contains the KMS url of the public key + // Supported formats differ based on the KMS system used. // +optional KMS string `json:"kms,omitempty"` } diff --git a/pkg/apis/policy/v1alpha1/clusterimagepolicy_validation.go b/pkg/apis/policy/v1alpha1/clusterimagepolicy_validation.go index 7881a9c816d..82960be58b7 100644 --- a/pkg/apis/policy/v1alpha1/clusterimagepolicy_validation.go +++ b/pkg/apis/policy/v1alpha1/clusterimagepolicy_validation.go @@ -17,13 +17,18 @@ package v1alpha1 import ( "context" "fmt" + "net" "path/filepath" "regexp" + "strings" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/sigstore/cosign/pkg/apis/utils" "knative.dev/pkg/apis" ) +const awsKMSPrefix = "awskms://" + // Validate implements apis.Validatable func (c *ClusterImagePolicy) Validate(ctx context.Context) *apis.FieldError { return c.Spec.Validate(ctx).ViaField("spec") @@ -54,6 +59,7 @@ func (image *ImagePattern) Validate(ctx context.Context) *apis.FieldError { } errs = errs.Also(ValidateGlob(image.Glob).ViaField("glob")) + return errs } @@ -104,6 +110,11 @@ func (key *KeyRef) Validate(ctx context.Context) *apis.FieldError { } else if key.KMS != "" && key.SecretRef != nil { errs = errs.Also(apis.ErrMultipleOneOf("data", "kms", "secretref")) } + if key.KMS != "" { + if strings.HasPrefix(key.KMS, awsKMSPrefix) { + errs = errs.Also(validateAWSKMS(key.KMS).ViaField("kms")) + } + } return errs } @@ -209,3 +220,36 @@ func ValidateRegex(regex string) *apis.FieldError { return nil } + +// validateAWSKMS validates that the KMS conforms to AWS +// KMS format: +// awskms://$ENDPOINT/$KEYID +// Where: +// $ENDPOINT is optional +// $KEYID is either the key ARN or an alias ARN +// Reasoning for only supporting these formats is that other +// formats require additional configuration via ENV variables. +func validateAWSKMS(kms string) *apis.FieldError { + parts := strings.Split(kms, "/") + if len(parts) < 4 { + return apis.ErrInvalidValue(kms, apis.CurrentField, "malformed AWS KMS format, should be: 'awskms://$ENDPOINT/$KEYID'") + } + endpoint := parts[2] + // missing endpoint is fine, only validate if not empty + if endpoint != "" { + _, _, err := net.SplitHostPort(endpoint) + if err != nil { + return apis.ErrInvalidValue(kms, apis.CurrentField, fmt.Sprintf("malformed endpoint: %s", err)) + } + } + keyID := parts[3] + arn, err := arn.Parse(keyID) + if err != nil { + return apis.ErrInvalidValue(kms, apis.CurrentField, fmt.Sprintf("failed to parse either key or alias arn: %s", err)) + } + // Only support key or alias ARN. + if arn.Resource != "key" && arn.Resource != "alias" { + return apis.ErrInvalidValue(kms, apis.CurrentField, fmt.Sprintf("Got ARN: %+v Resource: %s", arn, arn.Resource)) + } + return nil +} diff --git a/pkg/apis/policy/v1alpha1/clusterimagepolicy_validation_test.go b/pkg/apis/policy/v1alpha1/clusterimagepolicy_validation_test.go index 679f835b534..91ef792e336 100644 --- a/pkg/apis/policy/v1alpha1/clusterimagepolicy_validation_test.go +++ b/pkg/apis/policy/v1alpha1/clusterimagepolicy_validation_test.go @@ -710,3 +710,66 @@ func TestIdentitiesValidation(t *testing.T) { }) } } + +func TestAWSKMSValidation(t *testing.T) { + tests := []struct { + name string + expectErr bool + errorString string + kms string + }{ + { + name: "malformed, only 2 slashes ", + expectErr: true, + errorString: "invalid value: awskms://1234abcd-12ab-34cd-56ef-1234567890ab: kms\nmalformed AWS KMS format, should be: 'awskms://$ENDPOINT/$KEYID'", + kms: "awskms://1234abcd-12ab-34cd-56ef-1234567890ab", + }, + { + name: "fails with invalid host", + expectErr: true, + errorString: "invalid value: awskms://localhost:::4566/alias/exampleAlias: kms\nmalformed endpoint: address localhost:::4566: too many colons in address", + kms: "awskms://localhost:::4566/alias/exampleAlias", + }, + { + name: "fails with non-arn alias", + expectErr: true, + errorString: "invalid value: awskms://localhost:4566/alias/exampleAlias: kms\nfailed to parse either key or alias arn: arn: invalid prefix", + kms: "awskms://localhost:4566/alias/exampleAlias", + }, + { + name: "Should fail when arn is invalid", + expectErr: true, + errorString: "invalid value: awskms://localhost:4566/arn:sonotvalid: kms\nfailed to parse either key or alias arn: arn: not enough sections", + kms: "awskms://localhost:4566/arn:sonotvalid", + }, + { + name: "works with valid arn key and endpoint", + kms: "awskms://localhost:4566/arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + }, + { + name: "works with valid arn key and no endpoint", + kms: "awskms:///arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + }, + { + name: "works with valid arn alias and endpoint", + kms: "awskms://localhost:4566/arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias", + }, + { + name: "works with valid arn alias and no endpoint", + kms: "awskms:///arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + keyRef := KeyRef{KMS: test.kms} + err := keyRef.Validate(context.TODO()) + if test.expectErr { + require.NotNil(t, err) + require.EqualError(t, err, test.errorString) + } else { + require.Nil(t, err) + } + }) + } + +} diff --git a/pkg/apis/policy/v1beta1/clusterimagepolicy_types.go b/pkg/apis/policy/v1beta1/clusterimagepolicy_types.go index 3840189755d..2f1210c38a9 100644 --- a/pkg/apis/policy/v1beta1/clusterimagepolicy_types.go +++ b/pkg/apis/policy/v1beta1/clusterimagepolicy_types.go @@ -104,6 +104,7 @@ type KeyRef struct { // +optional Data string `json:"data,omitempty"` // KMS contains the KMS url of the public key + // Supported formats differ based on the KMS system used. // +optional KMS string `json:"kms,omitempty"` } diff --git a/pkg/apis/policy/v1beta1/clusterimagepolicy_validation.go b/pkg/apis/policy/v1beta1/clusterimagepolicy_validation.go index 86a05e664ea..b76fd084a09 100644 --- a/pkg/apis/policy/v1beta1/clusterimagepolicy_validation.go +++ b/pkg/apis/policy/v1beta1/clusterimagepolicy_validation.go @@ -17,13 +17,18 @@ package v1beta1 import ( "context" "fmt" + "net" "path/filepath" "regexp" + "strings" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/sigstore/cosign/pkg/apis/utils" "knative.dev/pkg/apis" ) +const awsKMSPrefix = "awskms://" + // Validate implements apis.Validatable func (c *ClusterImagePolicy) Validate(ctx context.Context) *apis.FieldError { return c.Spec.Validate(ctx).ViaField("spec") @@ -105,6 +110,11 @@ func (key *KeyRef) Validate(ctx context.Context) *apis.FieldError { } else if key.KMS != "" && key.SecretRef != nil { errs = errs.Also(apis.ErrMultipleOneOf("data", "kms", "secretref")) } + if key.KMS != "" { + if strings.HasPrefix(key.KMS, awsKMSPrefix) { + errs = errs.Also(validateAWSKMS(key.KMS).ViaField("kms")) + } + } return errs } @@ -211,3 +221,36 @@ func ValidateRegex(regex string) *apis.FieldError { return nil } + +// validateAWSKMS validates that the KMS conforms to AWS +// KMS format: +// awskms://$ENDPOINT/$KEYID +// Where: +// $ENDPOINT is optional +// $KEYID is either the key ARN or an alias ARN +// Reasoning for only supporting these formats is that other +// formats require additional configuration via ENV variables. +func validateAWSKMS(kms string) *apis.FieldError { + parts := strings.Split(kms, "/") + if len(parts) < 4 { + return apis.ErrInvalidValue(kms, apis.CurrentField, "malformed AWS KMS format, should be: 'awskms://$ENDPOINT/$KEYID'") + } + endpoint := parts[2] + // missing endpoint is fine, only validate if not empty + if endpoint != "" { + _, _, err := net.SplitHostPort(endpoint) + if err != nil { + return apis.ErrInvalidValue(kms, apis.CurrentField, fmt.Sprintf("malformed endpoint: %s", err)) + } + } + keyID := parts[3] + arn, err := arn.Parse(keyID) + if err != nil { + return apis.ErrInvalidValue(kms, apis.CurrentField, fmt.Sprintf("failed to parse either key or alias arn: %s", err)) + } + // Only support key or alias ARN. + if arn.Resource != "key" && arn.Resource != "alias" { + return apis.ErrInvalidValue(kms, apis.CurrentField, fmt.Sprintf("Got ARN: %+v Resource: %s", arn, arn.Resource)) + } + return nil +} diff --git a/pkg/apis/policy/v1beta1/clusterimagepolicy_validation_test.go b/pkg/apis/policy/v1beta1/clusterimagepolicy_validation_test.go index 3dad6b9b51d..5a8c3c1a738 100644 --- a/pkg/apis/policy/v1beta1/clusterimagepolicy_validation_test.go +++ b/pkg/apis/policy/v1beta1/clusterimagepolicy_validation_test.go @@ -676,3 +676,65 @@ func TestIdentitiesValidation(t *testing.T) { }) } } + +func TestAWSKMSValidation(t *testing.T) { + tests := []struct { + name string + expectErr bool + errorString string + kms string + }{ + { + name: "malformed, only 2 slashes ", + expectErr: true, + errorString: "invalid value: awskms://1234abcd-12ab-34cd-56ef-1234567890ab: kms\nmalformed AWS KMS format, should be: 'awskms://$ENDPOINT/$KEYID'", + kms: "awskms://1234abcd-12ab-34cd-56ef-1234567890ab", + }, + { + name: "fails with invalid host", + expectErr: true, + errorString: "invalid value: awskms://localhost:::4566/alias/exampleAlias: kms\nmalformed endpoint: address localhost:::4566: too many colons in address", + kms: "awskms://localhost:::4566/alias/exampleAlias", + }, + { + name: "fails with non-arn alias", + expectErr: true, + errorString: "invalid value: awskms://localhost:4566/alias/exampleAlias: kms\nfailed to parse either key or alias arn: arn: invalid prefix", + kms: "awskms://localhost:4566/alias/exampleAlias", + }, + { + name: "Should fail when arn is invalid", + expectErr: true, + errorString: "invalid value: awskms://localhost:4566/arn:sonotvalid: kms\nfailed to parse either key or alias arn: arn: not enough sections", + kms: "awskms://localhost:4566/arn:sonotvalid", + }, + { + name: "works with valid arn key and endpoint", + kms: "awskms://localhost:4566/arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + }, + { + name: "works with valid arn key and no endpoint", + kms: "awskms:///arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + }, + { + name: "works with valid arn alias and endpoint", + kms: "awskms://localhost:4566/arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias", + }, + { + name: "works with valid arn alias and no endpoint", + kms: "awskms:///arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + keyRef := KeyRef{KMS: test.kms} + err := keyRef.Validate(context.TODO()) + if test.expectErr { + require.NotNil(t, err) + require.EqualError(t, err, test.errorString) + } else { + require.Nil(t, err) + } + }) + } +} diff --git a/test/testdata/policy-controller/invalid/invalid-keyref-awskms.yaml b/test/testdata/policy-controller/invalid/invalid-keyref-awskms.yaml new file mode 100644 index 00000000000..6fdec0b04f2 --- /dev/null +++ b/test/testdata/policy-controller/invalid/invalid-keyref-awskms.yaml @@ -0,0 +1,33 @@ +# Copyright 2022 The Sigstore 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. +--- +apiVersion: policy.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy +spec: + images: + - glob: images.* + - key: + # keyid is not supported + kms: "awskms:///1234abcd-12ab-34cd-56ef-1234567890ab" + - key: + # keyid with hostname is still not supported + kms: "awskms://localhost:4566/1234abcd-12ab-34cd-56ef-1234567890ab" + - key: + # alias is not supported + kms: "awskms:///alias/ExampleAlias" + = key: + # alias is not supported, even if you give a hostname + kms: "awskms://localhost:4566/alias/ExampleAlias" diff --git a/test/testdata/policy-controller/valid/valid-keyref-awskms.yaml b/test/testdata/policy-controller/valid/valid-keyref-awskms.yaml new file mode 100644 index 00000000000..62b7ad135a2 --- /dev/null +++ b/test/testdata/policy-controller/valid/valid-keyref-awskms.yaml @@ -0,0 +1,29 @@ +# Copyright 2022 The Sigstore 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. +--- +apiVersion: policy.sigstore.dev/v1alpha1 +kind: ClusterImagePolicy +metadata: + name: image-policy +spec: + images: + - glob: images.* + - key: + kms: "awskms:///arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" + - key: + kms: "awskms://localhost:4566/arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" + - key: + kms: "awskms:///arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias" + = key: + kms: "awskms://localhost:4566/arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias"