diff --git a/pkg/cosign/kubernetes/webhook/validator_test.go b/pkg/cosign/kubernetes/webhook/validator_test.go index 7b7ea96513b..4dedd275ac5 100644 --- a/pkg/cosign/kubernetes/webhook/validator_test.go +++ b/pkg/cosign/kubernetes/webhook/validator_test.go @@ -381,10 +381,10 @@ UoJou2P8sbDxpLiE/v3yLw1/jyOrCPWYHWFXnyyeGlkgSVefG54tNoK7Uw== want: func() *apis.FieldError { var errs *apis.FieldError fe := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("initContainers", 0) - fe.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : string literal not terminated", digest.String()) + fe.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : failed to compile the cue policy with error: string literal not terminated", digest.String()) errs = errs.Also(fe) fe2 := apis.ErrGeneric("failed policy: cluster-image-policy-keyless", "image").ViaFieldIndex("containers", 0) - fe2.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : string literal not terminated", digest.String()) + fe2.Details = fmt.Sprintf("%s failed evaluating cue policy for ClusterImagePolicy : failed to compile the cue policy with error: string literal not terminated", digest.String()) errs = errs.Also(fe2) return errs }(), diff --git a/pkg/policy/eval.go b/pkg/policy/eval.go index 9ef6e82fd02..27861b4329d 100644 --- a/pkg/policy/eval.go +++ b/pkg/policy/eval.go @@ -20,7 +20,6 @@ import ( "fmt" "cuelang.org/go/cue/cuecontext" - cuejson "cuelang.org/go/encoding/json" "knative.dev/pkg/logging" ) @@ -54,9 +53,22 @@ func EvaluatePolicyAgainstJSON(ctx context.Context, name, policyType string, pol // evaluateCue evaluates a cue policy `evaluator` against `attestation` func evaluateCue(ctx context.Context, attestation []byte, evaluator string) error { logging.FromContext(ctx).Infof("Evaluating attestation: %s", string(attestation)) + logging.FromContext(ctx).Infof("Evaluator: %s", evaluator) + cueCtx := cuecontext.New() - v := cueCtx.CompileString(evaluator) - return cuejson.Validate(attestation, v) + cueEvaluator := cueCtx.CompileString(evaluator) + if cueEvaluator.Err() != nil { + return fmt.Errorf("failed to compile the cue policy with error: %w", cueEvaluator.Err()) + } + cueAtt := cueCtx.CompileBytes(attestation) + if cueAtt.Err() != nil { + return fmt.Errorf("failed to compile the attestation data with error: %w", cueAtt.Err()) + } + result := cueEvaluator.Unify(cueAtt) + if err := result.Validate(); err != nil { + return fmt.Errorf("failed to evaluate the policy with error: %w", err) + } + return nil } // evaluateRego evaluates a rego policy `evaluator` against `attestation` diff --git a/pkg/policy/eval_test.go b/pkg/policy/eval_test.go index 9c1a3cae74f..c44729bc265 100644 --- a/pkg/policy/eval_test.go +++ b/pkg/policy/eval_test.go @@ -75,8 +75,7 @@ const ( } }` - // TODO(vaikas): Enable tests once we sort this out. - // cipAttestation = "authorityMatches:{\"key-att\":{\"signatures\":null,\"attestations\":{\"custom-match-predicate\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}},\"key-signature\":{\"signatures\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}],\"attestations\":null},\"keyless-att\":{\"signatures\":null,\"attestations\":{\"custom-keyless\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}},\"keyless-signature\":{\"signatures\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}],\"attestations\":null}}" + cipAttestation = "{\"authorityMatches\":{\"keyatt\":{\"signatures\":null,\"attestations\":{\"vuln-key\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}},\"keysignature\":{\"signatures\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}],\"attestations\":null},\"keylessatt\":{\"signatures\":null,\"attestations\":{\"custom-keyless\":[{\"subject\":\"PLACEHOLDER\",\"issuer\":\"PLACEHOLDER\"}]}}}}" ) func TestEvalPolicy(t *testing.T) { @@ -124,6 +123,51 @@ func TestEvalPolicy(t *testing.T) { policyType: "cue", policyFile: `predicateType: "cosign.sigstore.dev/attestation/vuln/v1" predicate: invocation: uri: "invocation.example.com/cosign-testing"`, + }, { + name: "cluster image policy main policy, checks out", + json: cipAttestation, + policyType: "cue", + policyFile: `package sigstore + import "struct" + import "list" + authorityMatches: { + keyatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keysignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + }, + keylessatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keylesssignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + } + }`, + }, { + name: "cluster image policy main policy, fails", + json: cipAttestation, + policyType: "cue", + wantErr: true, + wantErrSub: `failed evaluating cue policy for cluster image policy main policy, fails : failed to evaluate the policy with error: authorityMatches.keylessattMinAttestations: conflicting values 2 and "Error" (mismatched types int and string)`, + policyFile: `package sigstore + import "struct" + import "list" + authorityMatches: { + keyatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keysignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + }, + if( len(authorityMatches.keylessatt.attestations) < 2) { + keylessattMinAttestations: 2 + keylessattMinAttestations: "Error" + }, + keylesssignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + } + }`, }} for _, tc := range tests { ctx := context.Background() diff --git a/test/e2e_test_cluster_image_policy_with_attestations.sh b/test/e2e_test_cluster_image_policy_with_attestations.sh index e16f535fcbd..ce95ff76f2a 100755 --- a/test/e2e_test_cluster_image_policy_with_attestations.sh +++ b/test/e2e_test_cluster_image_policy_with_attestations.sh @@ -62,6 +62,7 @@ assert_error() { local KUBECTL_OUT_FILE="/tmp/kubectl.failure.out" match="$@" echo looking for ${match} + kubectl delete job demo -n ${NS} --ignore-not-found=true if kubectl create -n ${NS} job demo --image=${demoimage} 2> ${KUBECTL_OUT_FILE} ; then echo Failed to block unsigned Job creation! exit 1 @@ -206,17 +207,14 @@ echo '::endgroup::' # Note we have to bake in the inline data from the keys above echo '::group:: Add cip for two signatures and two attestations' yq '. | .spec.authorities[1].key.data |= load_str("cosign.pub") | .spec.authorities[3].key.data |= load_str("cosign.pub")' ./test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml | kubectl apply -f - +# allow things to propagate +sleep 5 echo '::endgroup::' -# TODO(vaikas): Enable the remaining tests once we sort out how to write -# a valid CUE policy, or once #1787 goes in try implementing a Rego one. -echo 'Not testing the CIP policy evaluation yet' -exit 0 - # The CIP policy is the one that should fail now because it doesn't have enough # attestations echo '::group:: test job rejection' -expected_error='no matching attestations' +expected_error='failed to evaluate the policy with error: authorityMatches.keylessattMinAttestations' assert_error ${expected_error} echo '::endgroup::' @@ -229,9 +227,9 @@ echo '::endgroup::' echo '::group:: test job success' # We signed this with key and keyless and it has two keyless attestations and # it has one key attestation, so it should succeed. -if ! kubectl create -n ${NS} job demo3 --image=${demoimage} 2> ./${KUBECTL_OUT_FILE} ; then +if ! kubectl create -n ${NS} job demo3 --image=${demoimage} 2> ${KUBECTL_SUCCESS_FILE} ; then echo Failed to create job that has two signatures and 3 attestations - cat ${KUBECTL_OUT_FILE} + cat ${KUBECTL_SUCCESS_FILE} exit 1 fi echo '::endgroup::' diff --git a/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml b/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml index 80b44ece51a..6e0d32f8866 100644 --- a/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml +++ b/test/testdata/cosigned/e2e/cip-requires-two-signatures-and-two-attestations.yaml @@ -20,14 +20,14 @@ spec: images: - glob: registry.local:5000/cosigned/demo* authorities: - - name: keyless-att + - name: keylessatt keyless: url: http://fulcio.fulcio-system.svc ctlog: url: http://rekor.rekor-system.svc attestations: - predicateType: custom - name: custom-keyless + name: customkeyless policy: type: cue data: | @@ -39,7 +39,7 @@ spec: Timestamp: after } } - - name: key-att + - name: keyatt key: data: | -----BEGIN PUBLIC KEY----- @@ -78,12 +78,12 @@ spec: data: | predicateType: "cosign.sigstore.dev/attestation/v1" predicate: Data: "foobar key e2e test" - - name: keyless-signature + - name: keylesssignature keyless: url: http://fulcio.fulcio-system.svc ctlog: url: http://rekor.rekor-system.svc - - name: key-signature + - name: keysignature key: data: | -----BEGIN PUBLIC KEY----- @@ -95,48 +95,21 @@ spec: policy: type: cue data: | - if len(authorityMatches."keyless-att".attestations) < 2 { - keylessAttestationsErr: "error" - keylessAttestationsErr: "Did not get both keyless attestations" - } - if len(authorityMatches."key-att".attestations) < 1 { - keyAttestationsErr: 1 - keyAttestationsErr: "Did not get key attestation" - } - if len(authorityMatches."keyless-signature".signatures) < 1 { - keylessSignatureErr: 1 - keylessSignatureErr: "Did not get keyless signature" - } - if len(authorityMatches."key-signature".signatures) < 1 { - keySignatureErr: 1 - keySignatureErr: "Did not get key signature" - } + package sigstore + import "struct" + import "list" authorityMatches: { - key-att: { - attestations: { - "vuln-key": [ - {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, - ] - } - } - keyless-att: { - attestations: { - "vuln-keyless": [ - {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, - ], - "custom-keyless": [ - {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, - ], - } - } - keyless-signature: { - signatures: [ - {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, - ] - } - key-signature: { - signatures: [ - {subject: "PLACEHOLDER", issuer: "PLACEHOLDER"}, - ] + keyatt: { + attestations: struct.MaxFields(1) & struct.MinFields(1) + }, + keysignature: { + signatures: list.MaxItems(1) & list.MinItems(1) + }, + if (len(authorityMatches.keylessatt.attestations) < 2) { + keylessattMinAttestations: 2 + keylessattMinAttestations: "Error" + }, + keylesssignature: { + signatures: list.MaxItems(1) & list.MinItems(1) } }