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
111 changes: 108 additions & 3 deletions policy/lib/tekton/trusted.rego
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,18 @@ untrusted_task_refs_rules(tasks, bundle_manifests) := {task |
}

# Returns true if the task uses a trusted Task reference according to trusted_task_rules.
# 1. If task matches a deny rule, it's not trusted
# 2. If task matches an allow rule, it's trusted
# 3. Otherwise, it's not trusted
# To be trusted, a task should:
# 1. Match at least one allow rule
# 2. Not match any deny rule
# 3. If a matched allow rule specifies signature details, the
# task bundle must successfully pass a signature check
# Otherwise, it's not trusted.
# bundle_manifests is a map of bundle_ref -> manifest from ec.oci.image_manifests
is_trusted_task_rules(task, bundle_manifests) if {
ref := task_ref(task)
not _task_matches_deny_rule(ref, bundle_manifests)
_task_matches_allow_rule(ref, bundle_manifests)
_task_bundle_sig_check_okay(ref, bundle_manifests)
}

# Merging in the trusted_task_rules rule data
Expand Down Expand Up @@ -399,6 +403,33 @@ denial_reason(task, bundle_manifests) := reason if {
"pattern": deny_info.patterns,
"messages": deny_info.messages,
}
} else := reason if {
# Case: Matches allow rule but fails signature verification
ref := task_ref(task)
not _task_matches_deny_rule(ref, bundle_manifests)
_task_matches_allow_rule(ref, bundle_manifests)
not _task_bundle_sig_check_okay(ref, bundle_manifests)
Comment thread
qodo-for-conforma[bot] marked this conversation as resolved.

sig_rules := [rule |
some rule in _effective_allow_rules
_pattern_matches(ref.key, rule.pattern)
_version_satisfies_all_rule_constraints(ref, rule, bundle_manifests)
rule.signing_identity
]
messages := [msg |
some rule in sig_rules
opts := _sigstore_opts_for_rule(rule)
msg := sprintf(
"Task bundle %s failed signature verification for pattern %s with options %v",
[ref.bundle, rule.pattern, opts],
)
]

reason := {
Comment thread
simonbaird marked this conversation as resolved.
"type": "signing_identity_failed",
"pattern": [rule.pattern | some rule in sig_rules],
"messages": messages,
}
} else := reason if {
# Case 2: Doesn't match any allow rule
# Only applies if there are effective allow rules defined
Expand Down Expand Up @@ -449,6 +480,59 @@ _task_matches_allow_rule(ref, bundle_manifests) if {
_version_satisfies_all_rule_constraints(ref, rule, bundle_manifests)
}

# Returns true if the task's signature is verified against all matching allow rules
# that require signature verification. True when:
# - No matching allow rule has a signing_identity config, OR
# - The ref is not an OCI bundle (git tasks are exempt), OR
# - At least one matching allow rule's signature verification passes
_task_bundle_sig_check_okay(ref, bundle_manifests) if {
matching_rules := [rule |
some rule in _effective_allow_rules
_pattern_matches(ref.key, rule.pattern)
_version_satisfies_all_rule_constraints(ref, rule, bundle_manifests)
]
_signature_verified_for_rules(ref, matching_rules)
}

_signature_verified_for_rules(_, matching_rules) if {
some rule in matching_rules
not rule.signing_identity
}

_signature_verified_for_rules(ref, _) if {
not ref.bundle
}

_signature_verified_for_rules(ref, matching_rules) if {
ref.bundle
some rule in matching_rules
rule.signing_identity
opts := _sigstore_opts_for_rule(rule)
not _sigstore_verify_has_errors(ref.bundle, opts)
}

# Omit empty/false values so Sigstore doesn't treat them as "no constraint".
_sigstore_opts_for_rule(rule) := opts if {
sv := rule.signing_identity
is_object(sv)
opts := {k: v |
some k, v in sv
v != ""
v != false
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it might be possible for this part to go away once Joe's other PR is merged.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔


# Todo: This gets "called" first to check if the task is trusted and
# then again when generating a denial reason text. Generally OPA memoizes
# everything so it would not be re-run when called the second time, but
# I'm not sure if the call to ec.sigstore.verify_image makes OPA think it
# can't memoize it. If we are really calling the (possibly expensive)
# sigstore verify twice then we might want to refactor and try to avoid that.
_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.
Expand Down Expand Up @@ -488,6 +572,27 @@ _trusted_task_rule_entry_schema := {
"description": "List of version constraints",
"items": {"type": "string"},
},
"signing_identity": {
"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
140 changes: 140 additions & 0 deletions policy/lib/tekton/trusted_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,146 @@ untrusted_git_task := {
]}},
}

# =============================================================================
# SIGNATURE VERIFICATION TESTS
# =============================================================================

# Test: Allow rule without signing_identity works as before (backward compat)
test_allow_rule_without_signing_identity 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 signing_identity passes when signature is valid
test_allow_rule_with_valid_signature if {
rules := {"allow": {"signed-catalog": [{
"pattern": "oci://registry.local/trusty*",
"signing_identity": {
"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 signing_identity fails when signature is invalid
test_allow_rule_with_invalid_signature if {
rules := {"allow": {"signed-catalog": [{
"pattern": "oci://registry.local/trusty*",
"signing_identity": {
"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*",
"signing_identity": {
"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_signing_identity if {
rules := {"allow": {"signed-catalog": [{
"pattern": "git\\+git\\.local/repo\\.git//*",
"signing_identity": {
"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 signing_identity_failed when pattern matches but sig fails
test_denial_reason_signing_identity_failed if {
rules := {"allow": {"signed-catalog": [{
"pattern": "oci://registry.local/trusty*",
"signing_identity": {
"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("signing_identity_failed", reason.type)
assertions.assert_equal(["oci://registry.local/trusty*"], reason.pattern)
count(reason.messages) == 1
contains(reason.messages[0], "failed signature verification")
contains(reason.messages[0], "oci://registry.local/trusty*")
contains(reason.messages[0], "certificate_identity_regexp")
}

# Test: Schema validation accepts valid signing_identity on allow rules
test_schema_accepts_signing_identity if {
rules := {"allow": {"signed-catalog": [{
"pattern": "oci://registry.local/trusty*",
"signing_identity": {
"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 signing_identity
assertions.assert_empty(tekton.data_errors) with data.rule_data.trusted_task_rules as rules
}

# Test: Schema validation rejects empty signing_identity (would be silently permissive)
test_schema_rejects_empty_signing_identity if {
rules := {"allow": {"signed-catalog": [{
"pattern": "oci://registry.local/trusty*",
"signing_identity": {},
}]}}

count(tekton.data_errors) > 0 with data.rule_data.trusted_task_rules as rules
}

# Test: Schema validation rejects signing_identity with only non-identity fields
test_schema_rejects_signing_identity_without_identity if {
rules := {"allow": {"signed-catalog": [{
"pattern": "oci://registry.local/trusty*",
"signing_identity": {"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
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_signing_identity_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*",
"signing_identity": {
"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, "signing_identity_failed")
}

_mock_verify_image_failure(_, _) := {"success": false, "errors": ["signature verification failed"]}

_mock_empty_manifests(_) := {}

#####################################################
# Helper Functions for trusted_task_rules tests
#####################################################
Expand Down
Loading