diff --git a/policy/lib/tekton/trusted.rego b/policy/lib/tekton/trusted.rego index 6756d28f6..d10a64ad3 100644 --- a/policy/lib/tekton/trusted.rego +++ b/policy/lib/tekton/trusted.rego @@ -399,6 +399,22 @@ denial_reason(task, bundle_manifests) := reason if { "pattern": deny_info.patterns, "messages": deny_info.messages, } +} else := reason if { + # Case: Matches pattern+version on an allow rule but fails signature verification + ref := task_ref(task) + not _task_matches_deny_rule(ref, bundle_manifests) + + some rule in _effective_allow_rules + _task_matches_allow_rule_pattern_version(ref, rule, bundle_manifests) + + # But no allow rule passes signature verification + not _task_matches_allow_rule(ref, bundle_manifests) + + reason := { + "type": "signature_verification_failed", + "pattern": [], + "messages": [sprintf("Task bundle %s failed signature verification", [ref.bundle])], + } } else := reason if { # Case 2: Doesn't match any allow rule # Only applies if there are effective allow rules defined @@ -445,10 +461,51 @@ _denying_rules_info(task, bundle_manifests) := {"patterns": patterns, "messages" # bundle_manifests is a map of bundle_ref -> manifest from ec.oci.image_manifests _task_matches_allow_rule(ref, bundle_manifests) if { some rule in _effective_allow_rules + _task_matches_allow_rule_pattern_version(ref, rule, bundle_manifests) + _signature_verified_for_rule(ref, rule) +} + +_task_matches_allow_rule_pattern_version(ref, rule, bundle_manifests) if { _pattern_matches(ref.key, rule.pattern) _version_satisfies_all_rule_constraints(ref, rule, bundle_manifests) } +# Build sigstore opts object from a rule's signature_verification config. +# Only includes keys that are explicitly set to non-default values to avoid +# empty strings being interpreted as "no constraint" by Sigstore. +_sigstore_opts_for_rule(rule) := opts if { + sv := rule.signature_verification + is_object(sv) + opts := {k: v | + some k, v in sv + v != "" + v != false + } +} + +# A ref is signature-verified for a given rule if: +# 1. The rule has no signature_verification config (pass through), OR +# 2. The ref is not an OCI bundle (git tasks are exempt), OR +# 3. The bundle passes sigstore verification with the rule's opts +_signature_verified_for_rule(_, rule) if { + not rule.signature_verification +} + +_signature_verified_for_rule(ref, _) if { + not ref.bundle +} + +_signature_verified_for_rule(ref, rule) if { + ref.bundle + opts := _sigstore_opts_for_rule(rule) + not _sigstore_verify_has_errors(ref.bundle, opts) +} + +_sigstore_verify_has_errors(bundle, opts) if { + info := ec.sigstore.verify_image(bundle, opts) + some _ in info.errors +} + # Checks if the key matches the wildcard pattern using glob matching. # Wildcards (*) match any sequence of characters. Patterns without a # wildcard also match keys that have a :tag suffix appended (e.g. @@ -488,6 +545,27 @@ _trusted_task_rule_entry_schema := { "description": "List of version constraints", "items": {"type": "string"}, }, + "signature_verification": { + "type": "object", + # regal ignore:line-length + "description": "Sigstore verification options. When present, bundles matching this allow rule must also have a verified signature.", + "properties": { + "certificate_identity": {"type": "string", "minLength": 1}, + "certificate_identity_regexp": {"type": "string", "minLength": 1}, + "certificate_oidc_issuer": {"type": "string", "minLength": 1}, + "certificate_oidc_issuer_regexp": {"type": "string", "minLength": 1}, + "ignore_rekor": {"type": "boolean"}, + "public_key": {"type": "string", "minLength": 1}, + "rekor_url": {"type": "string", "minLength": 1}, + }, + "additionalProperties": false, + # regal ignore:line-length + "anyOf": [ + {"required": ["certificate_identity"]}, + {"required": ["certificate_identity_regexp"]}, + {"required": ["public_key"]}, + ], + }, }, "additionalProperties": true, } diff --git a/policy/lib/tekton/trusted_test.rego b/policy/lib/tekton/trusted_test.rego index 6168120a4..013317806 100644 --- a/policy/lib/tekton/trusted_test.rego +++ b/policy/lib/tekton/trusted_test.rego @@ -1188,6 +1188,144 @@ untrusted_git_task := { ]}}, } +# ============================================================================= +# SIGNATURE VERIFICATION TESTS +# ============================================================================= + +# Test: Allow rule without signature_verification works as before (backward compat) +test_allow_rule_without_signature_verification if { + rules := {"allow": {"test-group": [{"pattern": "oci://registry.local/trusty*"}]}} + + tekton.is_trusted_task(trusted_bundle_task, _empty_bundle_manifests) with data.trusted_task_rules as rules + with data.rule_data.trusted_task_rules_enabled as true +} + +# Test: Allow rule with signature_verification passes when signature is valid +test_allow_rule_with_valid_signature if { + rules := {"allow": {"signed-catalog": [{ + "pattern": "oci://registry.local/trusty*", + "signature_verification": { + "certificate_identity_regexp": "https://tekton.dev/chains/.*", + "certificate_oidc_issuer": "https://accounts.google.com", + }, + }]}} + + tekton.is_trusted_task(trusted_bundle_task, _empty_bundle_manifests) with data.trusted_task_rules as rules + with data.rule_data.trusted_task_rules_enabled as true + with ec.sigstore.verify_image as _mock_verify_image_success +} + +# Test: Allow rule with signature_verification fails when signature is invalid +test_allow_rule_with_invalid_signature if { + rules := {"allow": {"signed-catalog": [{ + "pattern": "oci://registry.local/trusty*", + "signature_verification": { + "certificate_identity_regexp": "https://tekton.dev/chains/.*", + "certificate_oidc_issuer": "https://accounts.google.com", + }, + }]}} + + not tekton.is_trusted_task(trusted_bundle_task, _empty_bundle_manifests) with data.trusted_task_rules as rules + with data.rule_data.trusted_task_rules_enabled as true + with ec.sigstore.verify_image as _mock_verify_image_failure +} + +# Test: Multiple allow rules - task passes if any rule's pattern+signature match +test_multiple_allow_rules_different_sig_configs if { + rules := {"allow": { + "signed-catalog": [{ + "pattern": "oci://registry.local/trusty*", + "signature_verification": { + "certificate_identity_regexp": "https://tekton.dev/chains/.*", + "certificate_oidc_issuer": "https://accounts.google.com", + }, + }], + "unsigned-catalog": [{"pattern": "oci://registry.local/trusty*"}], + }} + + # Passes because the unsigned-catalog rule has no sig verification requirement + tekton.is_trusted_task(trusted_bundle_task, _empty_bundle_manifests) with data.trusted_task_rules as rules + with data.rule_data.trusted_task_rules_enabled as true + with ec.sigstore.verify_image as _mock_verify_image_failure +} + +# Test: Git-resolved tasks are exempt from signature verification +test_git_tasks_exempt_from_signature_verification if { + rules := {"allow": {"signed-catalog": [{ + "pattern": "git\\+git\\.local/repo\\.git//*", + "signature_verification": { + "certificate_identity_regexp": "https://tekton.dev/chains/.*", + "certificate_oidc_issuer": "https://accounts.google.com", + }, + }]}} + + # Git task should pass even though ec.sigstore.verify_image would fail, + # because git tasks are exempt from signature verification + tekton.is_trusted_task(trusted_git_task, _empty_bundle_manifests) with data.trusted_task_rules as rules + with data.rule_data.trusted_task_rules_enabled as true + with ec.sigstore.verify_image as _mock_verify_image_failure +} + +# Test: denial_reason returns signature_verification_failed when pattern matches but sig fails +test_denial_reason_signature_verification_failed if { + rules := {"allow": {"signed-catalog": [{ + "pattern": "oci://registry.local/trusty*", + "signature_verification": { + "certificate_identity_regexp": "https://tekton.dev/chains/.*", + "certificate_oidc_issuer": "https://accounts.google.com", + }, + }]}} + + reason := tekton.denial_reason(trusted_bundle_task, _empty_bundle_manifests) with data.trusted_task_rules as rules + with data.rule_data.trusted_task_rules_enabled as true + with ec.sigstore.verify_image as _mock_verify_image_failure + + assertions.assert_equal("signature_verification_failed", reason.type) + assertions.assert_equal([], reason.pattern) + count(reason.messages) == 1 + contains(reason.messages[0], "failed signature verification") +} + +# Test: Schema validation accepts valid signature_verification on allow rules +test_schema_accepts_signature_verification if { + rules := {"allow": {"signed-catalog": [{ + "pattern": "oci://registry.local/trusty*", + "signature_verification": { + "certificate_identity_regexp": "https://tekton.dev/chains/.*", + "certificate_oidc_issuer": "https://accounts.google.com", + "ignore_rekor": true, + }, + }]}} + + # No data errors should be produced for valid signature_verification + assertions.assert_empty(tekton.data_errors) with data.rule_data.trusted_task_rules as rules +} + +# Test: Schema validation rejects empty signature_verification (would be silently permissive) +test_schema_rejects_empty_signature_verification if { + rules := {"allow": {"signed-catalog": [{ + "pattern": "oci://registry.local/trusty*", + "signature_verification": {}, + }]}} + + count(tekton.data_errors) > 0 with data.rule_data.trusted_task_rules as rules +} + +# Test: Schema validation rejects signature_verification with only non-identity fields +test_schema_rejects_signature_verification_without_identity if { + rules := {"allow": {"signed-catalog": [{ + "pattern": "oci://registry.local/trusty*", + "signature_verification": {"ignore_rekor": true}, + }]}} + + count(tekton.data_errors) > 0 with data.rule_data.trusted_task_rules as rules +} + +# Mock helpers for signature verification tests +_mock_verify_image_success(_, _) := {"success": true, "errors": []} + +_mock_verify_image_failure(_, _) := {"success": false, "errors": ["signature verification failed"]} + # ============================================================================= # BEGIN LEGACY TEST DATA (trusted_tasks) # DELETE THIS SECTION when removing legacy support. diff --git a/policy/release/trusted_task/trusted_task.rego b/policy/release/trusted_task/trusted_task.rego index 9b79ddf9e..4ed35cdda 100644 --- a/policy/release/trusted_task/trusted_task.rego +++ b/policy/release/trusted_task/trusted_task.rego @@ -455,6 +455,11 @@ _format_denial_reason(reason) := msg if { pattern_lines := [sprintf(" - %s", [pattern]) | some pattern in reason.pattern] msg := sprintf("%s\n%s", [reason.type, concat("\n", pattern_lines)]) +} else := msg if { + reason.type == "signature_verification_failed" + count(reason.messages) > 0 + message_lines := [sprintf(" - %s", [m]) | some m in reason.messages] + msg := sprintf("%s\n%s", [reason.type, concat("\n", message_lines)]) } else := reason.type # Format error for rules system with Trusted Artifacts diff --git a/policy/release/trusted_task/trusted_task_test.rego b/policy/release/trusted_task/trusted_task_test.rego index f633b5f0d..71cfa260c 100644 --- a/policy/release/trusted_task/trusted_task_test.rego +++ b/policy/release/trusted_task/trusted_task_test.rego @@ -1358,6 +1358,38 @@ test_mixed_trusted_and_untrusted_tasks if { with ec.oci.image_manifest as _mock_image_manifest } +test_signature_verification_failed_error_rules if { + att := {"statement": { + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "buildType": lib.tekton_pipeline_run, + "buildConfig": {"tasks": [trusted_bundle_pipeline_task]}, + }, + }} + + rules := {"allow": {"signed-catalog": [{ + "pattern": "oci://registry.local/trusty*", + "signature_verification": { + "certificate_identity_regexp": "https://tekton.dev/chains/.*", + "certificate_oidc_issuer": "https://accounts.google.com", + }, + }]}} + + results := trusted_task.deny with input.attestations as [att] + with data.trusted_task_rules as rules + with data.rule_data.trusted_task_rules_enabled as true + with ec.sigstore.verify_image as _mock_verify_image_failure + with ec.oci.image_manifests as _mock_empty_manifests + + count(results) > 0 + some result in results + contains(result.msg, "signature_verification_failed") +} + +_mock_verify_image_failure(_, _) := {"success": false, "errors": ["signature verification failed"]} + +_mock_empty_manifests(_) := {} + ##################################################### # Helper Functions for trusted_task_rules tests #####################################################