diff --git a/.github/workflows/kind-cluster-image-policy.yaml b/.github/workflows/kind-cluster-image-policy.yaml index 963f5f6a4be..779528baa58 100644 --- a/.github/workflows/kind-cluster-image-policy.yaml +++ b/.github/workflows/kind-cluster-image-policy.yaml @@ -197,13 +197,15 @@ jobs: kubectl delete cip image-policy-keyless-with-identities-mismatch sleep 5 - - name: Generate New Signing Key + - name: Generate New Signing Key For Colocated Signature run: | COSIGN_PASSWORD="" ./cosign generate-key-pair + mv cosign.key cosign-colocated-signing.key + mv cosign.pub cosign-colocated-signing.pub - name: Deploy ClusterImagePolicy With Key Signing run: | - yq '. | .spec.authorities[0].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-key.yaml | \ + yq '. | .spec.authorities[0].key.data |= load_str("cosign-colocated-signing.pub")' ./test/testdata/cosigned/e2e/cip-key.yaml | \ kubectl apply -f - - name: Verify with two CIP, one not signed with public key @@ -215,11 +217,11 @@ jobs: - name: Sign demoimage with cosign key run: | - ./cosign sign --key cosign.key --force --allow-insecure-registry ${{ env.demoimage }} + ./cosign sign --key cosign-colocated-signing.key --force --allow-insecure-registry ${{ env.demoimage }} - name: Verify with cosign run: | - ./cosign verify --key cosign.pub --allow-insecure-registry ${{ env.demoimage }} + ./cosign verify --key cosign-colocated-signing.pub --allow-insecure-registry ${{ env.demoimage }} - name: Deploy jobs and verify signed works, unsigned fails run: | @@ -246,6 +248,63 @@ jobs: fi echo '::endgroup::' + - name: Generate New Signing Key For Remote Signature + run: | + COSIGN_PASSWORD="" ./cosign generate-key-pair + mv cosign.key cosign-remote-signing.key + mv cosign.pub cosign-remote-signing.pub + + - name: Deploy ClusterImagePolicy With Remote Public Key But Missing Source + run: | + 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 - + + - name: Sign demoimage with cosign key + run: | + COSIGN_REPOSITORY="${{ env.KO_DOCKER_REPO }}/remote-signature" ./cosign sign --key cosign-remote-signing.key --force --allow-insecure-registry ${{ env.demoimage }} + + - name: Verify with cosign + run: | + if ./cosign verify --key cosign-remote-signing.pub --allow-insecure-registry ${{ env.demoimage }}; then + echo "Signature should not have been verified unless COSIGN_REPOSITORY was defined" + exit 1 + fi + + if ! COSIGN_REPOSITORY="${{ env.KO_DOCKER_REPO }}/remote-signature" ./cosign verify --key cosign-remote-signing.pub --allow-insecure-registry ${{ env.demoimage }}; then + echo "Signature should have been verified when COSIGN_REPOSITORY was defined" + exit 1 + fi + + - name: Verify with three CIP, one without correct Source set + run: | + kubectl create namespace demo-key-remote + kubectl label namespace demo-key-remote cosigned.sigstore.dev/include=true + + if kubectl create -n demo-key-remote job demo --image=${{ env.demoimage }}; then + echo Failed to block unsigned Job creation! + exit 1 + fi + + - name: Deploy ClusterImagePolicy With Remote Public Key With Source + run: | + 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 | tee image-policy-remote-source.yaml + kubectl apply -f image-policy-remote-source.yaml + + - name: Verify with three CIP, one with correct Source set + run: | + echo '::group:: test job success' + # We signed this above, this should work + if ! kubectl create -n demo-key-remote job demo --image=${{ env.demoimage }} ; then + echo Failed to create Job in namespace without label! + exit 1 + else + echo Succcessfully created Job with signed image + fi + echo '::endgroup:: test job success' + - name: Collect diagnostics if: ${{ failure() }} uses: chainguard-dev/actions/kind-diag@84c993eaf02da1c325854fb272a4df9184bd80fc # main diff --git a/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go b/pkg/apis/cosigned/v1alpha1/clusterimagepolicy_validation.go index edc12b907ac..4b76b04b80b 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 5d04662e836..794fdc84ff9 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 41706d53fc1..787004135f6 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,9 @@ 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 + // +optional + RemoteOpts []remote.Option `json:"-"` } // This references a public verification key stored in @@ -92,6 +98,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"))