Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions policy/lib/tekton/trusted.rego
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# 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.
Expand Down Expand Up @@ -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,
}
Expand Down
138 changes: 138 additions & 0 deletions policy/lib/tekton/trusted_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions policy/release/trusted_task/trusted_task.rego
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions policy/release/trusted_task/trusted_task_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -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
#####################################################
Expand Down
Loading