diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go index c16db8736a7..81d0f2065fe 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go @@ -83,6 +83,12 @@ func (authority *Authority) Validate(ctx context.Context) *apis.FieldError { errs = errs.Also(authority.Keyless.Validate(ctx).ViaField("keyless")) } + if len(authority.Sources) > 0 { + for _, source := range authority.Sources { + errs = errs.Also(source.Validate(ctx).ViaField("source")) + } + } + return errs } @@ -130,6 +136,14 @@ func (keyless *KeylessRef) Validate(ctx context.Context) *apis.FieldError { return errs } +func (source *Source) Validate(ctx context.Context) *apis.FieldError { + var errs *apis.FieldError + if source.OCI == "" { + errs = errs.Also(apis.ErrMissingField("oci")) + } + return errs +} + func (identity *Identity) Validate(ctx context.Context) *apis.FieldError { var errs *apis.FieldError if identity.Issuer == "" && identity.Subject == "" { diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go index b3213cc67a8..5c288188e9d 100644 --- a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go +++ b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation_test.go @@ -397,7 +397,57 @@ func TestAuthoritiesValidation(t *testing.T) { }, }, }, + { + name: "Should pass when source oci is present", + expectErr: false, + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{{OCI: "registry.example.com"}}, + }, + }, + }, + }, + }, + { + name: "Should fail when source oci is empty", + expectErr: true, + errorString: "missing field(s): spec.authorities[0].source.oci", + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{{OCI: ""}}, + }, + }, + }, + }, + }, + { + name: "Should pass with multiple source oci is present", + expectErr: false, + policy: ClusterImagePolicy{ + Spec: ClusterImagePolicySpec{ + Images: []ImagePattern{{Regex: ".*"}}, + Authorities: []Authority{ + { + Key: &KeyRef{KMS: "kms://key/path"}, + Sources: []Source{ + {OCI: "registry1"}, + {OCI: "registry2"}, + }, + }, + }, + }, + }, + }, } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := test.policy.Validate(context.TODO()) diff --git a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go index 4a06e9a2f67..5da8c7f83f2 100644 --- a/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go +++ b/pkg/cosign/kubernetes/webhook/clusterimagepolicy/clusterimagepolicy_types.go @@ -20,7 +20,10 @@ import ( "encoding/json" "encoding/pem" + "github.com/google/go-containerregistry/pkg/name" + "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" + "github.com/sigstore/cosign/pkg/oci/remote" "knative.dev/pkg/apis" ) @@ -44,6 +47,10 @@ type Authority struct { Sources []v1alpha1.Source `json:"source,omitempty"` // +optional CTLog *v1alpha1.TLog `json:"ctlog,omitempty"` + // RemoteOpts are not marshalled because they are an unsupported type + // RemoteOpts will be populated by the Authority UnmarshalJSON override + // +optional + RemoteOpts []remote.Option `json:"-"` } // This references a public verification key stored in @@ -92,6 +99,34 @@ func (k *KeyRef) UnmarshalJSON(data []byte) error { return nil } +// UnmarshalJSON populates the authority with the remoteOpts +// from authority sources +func (a *Authority) UnmarshalJSON(data []byte) error { + // Create a new type to avoid recursion + type RawAuthority Authority + + var rawAuthority RawAuthority + err := json.Unmarshal(data, &rawAuthority) + if err != nil { + return err + } + + // Determine additional RemoteOpts + if len(rawAuthority.Sources) > 0 { + for _, source := range rawAuthority.Sources { + if targetRepoOverride, err := name.NewRepository(source.OCI); err != nil { + return errors.Wrap(err, "failed to determine source") + } else if (targetRepoOverride != name.Repository{}) { + rawAuthority.RemoteOpts = append(rawAuthority.RemoteOpts, remote.WithTargetRepository(targetRepoOverride)) + } + } + } + + // Set the new type instance to casted original + *a = Authority(rawAuthority) + return nil +} + func ConvertClusterImagePolicyV1alpha1ToWebhook(in *v1alpha1.ClusterImagePolicy) *ClusterImagePolicy { copyIn := in.DeepCopy() diff --git a/pkg/cosign/kubernetes/webhook/validator.go b/pkg/cosign/kubernetes/webhook/validator.go index bd95f87623f..dc9ad4c037e 100644 --- a/pkg/cosign/kubernetes/webhook/validator.go +++ b/pkg/cosign/kubernetes/webhook/validator.go @@ -257,15 +257,15 @@ func validatePolicies(ctx context.Context, ref name.Reference, kc authn.Keychain // return a success if at least one of the Authorities validated the signatures. // Returns the validated signatures, or the errors encountered. func ValidatePolicy(ctx context.Context, ref name.Reference, kc authn.Keychain, authorities []webhookcip.Authority, remoteOpts ...ociremote.Option) ([]oci.Signature, []error) { + remoteOpts = append(remoteOpts, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(kc))) + // If none of the Authorities for a given policy pass the checks, gather // the errors here. If one passes, do not return the errors. authorityErrors := []error{} for _, authority := range authorities { - logging.FromContext(ctx).Debugf("Checking Authority: %+v", authority) - // TODO(vaikas): We currently only use the kc, we have to look - // at authority.Sources to determine additional information for the - // WithRemoteOptions below, at least the 'TargetRepository' - // https://github.com/sigstore/cosign/issues/1651 + // Assignment for appendAssign lint error + authorityRemoteOpts := remoteOpts + authorityRemoteOpts = append(authorityRemoteOpts, authority.RemoteOpts...) switch { case authority.Key != nil && len(authority.Key.PublicKeys) > 0: @@ -273,7 +273,7 @@ func ValidatePolicy(ctx context.Context, ref name.Reference, kc authn.Keychain, // Is it even allowed? 'valid' returns success if any key // matches. // https://github.com/sigstore/cosign/issues/1652 - sps, err := valid(ctx, ref, authority.Key.PublicKeys, remoteOpts...) + sps, err := valid(ctx, ref, authority.Key.PublicKeys, authorityRemoteOpts...) if err != nil { authorityErrors = append(authorityErrors, errors.Wrap(err, "failed to validate keys")) continue @@ -303,7 +303,7 @@ func ValidatePolicy(ctx context.Context, ref name.Reference, kc authn.Keychain, continue } } - sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroot, rekorClient, authority.Keyless.Identities, remoteOpts...) + sps, err := validSignaturesWithFulcio(ctx, ref, fulcioroot, rekorClient, authority.Keyless.Identities, authorityRemoteOpts...) if err != nil { logging.FromContext(ctx).Errorf("failed validSignatures with fulcio for %s: %v", ref.Name(), err) authorityErrors = append(authorityErrors, errors.Wrap(err, "validate signatures with fulcio")) diff --git a/test/e2e_test_cluster_image_policy.sh b/test/e2e_test_cluster_image_policy.sh index 0f53f7ae141..45eb1712bb5 100755 --- a/test/e2e_test_cluster_image_policy.sh +++ b/test/e2e_test_cluster_image_policy.sh @@ -165,14 +165,22 @@ kubectl delete cip image-policy-keyless-with-identities-mismatch sleep 5 echo '::endgroup::' -echo '::group:: Generate signing key' +echo '::group:: Generate New Signing Key For Colocated Signature' COSIGN_PASSWORD="" ./cosign generate-key-pair +mv cosign.key cosign-colocated-signing.key +mv cosign.pub cosign-colocated-signing.pub echo '::endgroup::' echo '::group:: Deploy ClusterImagePolicy With Key Signing' -yq '. | .spec.authorities[0].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-key.yaml | kubectl apply -f - +yq '. | .spec.authorities[0].key.data |= load_str("cosign-colocated-signing.pub")' \ + ./test/testdata/cosigned/e2e/cip-key.yaml | \ + kubectl apply -f - echo '::endgroup::' +echo '::group:: Create and label new namespace for verification' +kubectl create namespace demo-key-signing +kubectl label namespace demo-key-signing cosigned.sigstore.dev/include=true + echo '::group:: Verify blocks unsigned with the key' if kubectl create -n demo-key-signing job demo --image=${demoimage}; then echo Failed to block unsigned Job creation! @@ -181,17 +189,13 @@ fi echo '::endgroup::' echo '::group:: Sign demoimage with cosign key' -COSIGN_PASSWORD="" ./cosign sign --key cosign.key --force --allow-insecure-registry ${demoimage} +COSIGN_PASSWORD="" ./cosign sign --key cosign-colocated-signing.key --force --allow-insecure-registry ${demoimage} echo '::endgroup::' echo '::group:: Verify demoimage with cosign key' -./cosign verify --key cosign.pub --allow-insecure-registry ${demoimage} +./cosign verify --key cosign-colocated-signing.pub --allow-insecure-registry ${demoimage} echo '::endgroup::' -echo '::group:: Create and label new namespace for verification' -kubectl create namespace demo-key-signing -kubectl label namespace demo-key-signing cosigned.sigstore.dev/include=true - echo '::group:: test job success' # We signed this above, this should work if ! kubectl create -n demo-key-signing job demo --image=${demoimage} ; then @@ -212,7 +216,66 @@ else fi echo '::endgroup::' +echo '::group:: Generate New Signing Key For Remote Signature' +COSIGN_PASSWORD="" ./cosign generate-key-pair +mv cosign.key cosign-remote-signing.key +mv cosign.pub cosign-remote-signing.pub +echo '::endgroup::' + +echo '::group:: Deploy ClusterImagePolicy With Remote Public Key But Missing Source' +yq '. | .metadata.name = "image-policy-remote-source" + | .spec.authorities[0].key.data |= load_str("cosign-remote-signing.pub")' \ + ./test/testdata/cosigned/e2e/cip-key.yaml | \ + kubectl apply -f - +echo '::endgroup::' + +echo '::group:: Sign demoimage with cosign remote key' +COSIGN_REPOSITORY="${KO_DOCKER_REPO}/remote-signature" ./cosign sign --key cosign-remote-signing.key --force --allow-insecure-registry ${demoimage} +echo '::endgroup::' + +echo '::group:: Verify demoimage with cosign remote key' +if ./cosign verify --key cosign-remote-signing.pub --allow-insecure-registry ${demoimage}; then + echo "Signature should not have been verified unless COSIGN_REPOSITORY was defined" + exit 1 +fi + +if ! COSIGN_REPOSITORY="${KO_DOCKER_REPO}/remote-signature" ./cosign verify --key cosign-remote-signing.pub --allow-insecure-registry ${demoimage}; then + echo "Signature should have been verified when COSIGN_REPOSITORY was defined" + exit 1 +fi +echo '::endgroup::' + +echo '::group:: Create test namespace and label for remote key verification' +kubectl create namespace demo-key-remote +kubectl label namespace demo-key-remote cosigned.sigstore.dev/include=true +echo '::endgroup::' + +echo '::group:: Verify with three CIP, one without correct Source set' +if kubectl create -n demo-key-remote job demo --image=${demoimage}; then + echo Failed to block unsigned Job creation! + exit 1 +fi +echo '::endgroup::' + +echo '::group:: Deploy ClusterImagePolicy With Remote Public Key With Source' +yq '. | .metadata.name = "image-policy-remote-source" + | .spec.authorities[0].key.data |= load_str("cosign-remote-signing.pub") + | .spec.authorities[0] += {"source": [{"oci": env(KO_DOCKER_REPO)+"/remote-signature"}]}' \ + ./test/testdata/cosigned/e2e/cip-key.yaml | \ + kubectl apply -f - +echo '::endgroup::' + +echo '::group:: Verify with three CIP, one with correct Source set' +# We signed this above and applied remote signature source location above +if ! kubectl create -n demo-key-remote job demo --image=${demoimage}; then + echo Failed to create Job in namespace without label! + exit 1 +else + echo Succcessfully created Job with signed image +fi +echo '::endgroup::' + echo '::group::' Cleanup kubectl delete cip --all kubectl delete ns demo-key-signing demo-keyless-signing -rm cosign.key cosign.pub +rm cosign*.key cosign*.pub