Skip to content

Commit 79ada71

Browse files
simonbairdclaude
andcommitted
Add --skip-att-sig-check flag
Implement --skip-att-sig-check flag to skip attestation signature validation checks, mirroring the existing --skip-image-sig-check flag. When enabled, attestation signature verification is bypassed during image validation. Why? Often I'm debugging/troubleshooting something and I get given an image ref to look at. We can use cosign download attestation to inspect the attestation, which is very useful, but if we want to try running Conforma against it, we must either guess, find, or ask to be provided with the public key. Sometimes that's not so difficult, but other times it may be very difficult or even impossible. (Consider for example if the image was built on an ephemeral cluster and the signing secret used is gone forever.) Now we can use --skip-image-sig-check and --skip-att-sig-check and carry on with the debugging. Note that we added the --skip-image-sig-check recently for other reasons, see https://redhat.atlassian.net/browse/EC-1647. The --skip-att-sig-check is a little more complicated because we needed to add a new function that can download the attestation without verifying it. The argument against this is that it may encourage less secure practices, but I would say it's acceptable because we're not changing the default behavior, which is always to require signature verification. Ref: https://redhat.atlassian.net/browse/EC-1815 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 05c7b17 commit 79ada71

12 files changed

Lines changed: 577 additions & 71 deletions

File tree

cmd/validate/image.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
211211
},
212212
IgnoreRekor: data.ignoreRekor,
213213
SkipImageSigCheck: data.skipImageSigCheck,
214+
SkipAttSigCheck: data.skipAttSigCheck,
214215
PolicyRef: data.policyConfiguration,
215216
PublicKey: data.publicKey,
216217
RekorURL: data.rekorURL,
@@ -499,6 +500,9 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
499500
cmd.Flags().BoolVar(&data.skipImageSigCheck, "skip-image-sig-check", data.skipImageSigCheck,
500501
"Skip image signature validation checks.")
501502

503+
cmd.Flags().BoolVar(&data.skipAttSigCheck, "skip-att-sig-check", data.skipAttSigCheck,
504+
"Skip attestation signature validation checks.")
505+
502506
cmd.Flags().StringVar(&data.certificateIdentity, "certificate-identity", data.certificateIdentity,
503507
"URL of the certificate identity for keyless verification")
504508

@@ -637,6 +641,7 @@ type imageData struct {
637641
input string
638642
ignoreRekor bool
639643
skipImageSigCheck bool
644+
skipAttSigCheck bool
640645
output []string
641646
outputFile string
642647
policy policy.Policy

docs/modules/ROOT/pages/ec_validate_image.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ mark (?) sign, for example: --output text=output.txt?show-successes=false
151151
* inline JSON ('{sources: {...}, identity: {...}}')")
152152
-k, --public-key:: path to the public key. Overrides publicKey from EnterpriseContractPolicy
153153
-r, --rekor-url:: Rekor URL. Overrides rekorURL from EnterpriseContractPolicy
154+
--skip-att-sig-check:: Skip attestation signature validation checks. (Default: false)
154155
--skip-image-sig-check:: Skip image signature validation checks. (Default: false)
155156
--snapshot:: Provide the AppStudio Snapshot as a source of the images to validate, as inline
156157
JSON of the "spec" or a reference to a Kubernetes object [<namespace>/]<name>

features/__snapshots__/validate_image.snap

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5529,6 +5529,7 @@ Error: success criteria not met
55295529
---
55305530

55315531
[TestFeatures/invalid image signature with valid att sig and skip-image-sig-check flag:stderr - 1]
5532+
time="${TIMESTAMP}" level=warning msg="Image signature check skipped"
55325533

55335534
---
55345535

@@ -5594,6 +5595,7 @@ Error: success criteria not met
55945595
---
55955596

55965597
[TestFeatures/happy day with skip-image-sig-check flag:stderr - 1]
5598+
time="${TIMESTAMP}" level=warning msg="Image signature check skipped"
55975599

55985600
---
55995601

@@ -5780,3 +5782,137 @@ Error: success criteria not met
57805782
[TestFeatures/discover artifact referrers via OCI Referrers API:stderr - 1]
57815783

57825784
---
5785+
5786+
[TestFeatures/invalid att signature with valid image sig and skip-att-sig-check flag:stdout - 1]
5787+
{
5788+
"success": true,
5789+
"components": [
5790+
{
5791+
"name": "Unnamed",
5792+
"containerImage": "${REGISTRY}/acceptance/invalid-att-signature@sha256:${REGISTRY_acceptance/invalid-att-signature:latest_DIGEST}",
5793+
"source": {},
5794+
"successes": [
5795+
{
5796+
"msg": "Pass",
5797+
"metadata": {
5798+
"code": "builtin.attestation.syntax_check"
5799+
}
5800+
},
5801+
{
5802+
"msg": "Pass",
5803+
"metadata": {
5804+
"code": "builtin.image.signature_check"
5805+
}
5806+
},
5807+
{
5808+
"msg": "Pass",
5809+
"metadata": {
5810+
"code": "main.acceptor"
5811+
}
5812+
}
5813+
],
5814+
"success": true,
5815+
"signatures": [
5816+
{
5817+
"keyid": "",
5818+
"sig": "${IMAGE_SIGNATURE_acceptance/invalid-att-signature}"
5819+
}
5820+
],
5821+
"attestations": [
5822+
{
5823+
"type": "https://in-toto.io/Statement/v0.1",
5824+
"predicateType": "https://slsa.dev/provenance/v0.2",
5825+
"predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2",
5826+
"signatures": [
5827+
{
5828+
"keyid": "",
5829+
"sig": "${ATTESTATION_SIGNATURE_acceptance/invalid-att-signature}"
5830+
}
5831+
]
5832+
}
5833+
]
5834+
}
5835+
],
5836+
"key": "${known_PUBLIC_KEY_JSON}",
5837+
"policy": {
5838+
"sources": [
5839+
{
5840+
"policy": [
5841+
"git::${GITHOST}/git/invalid-att-signature.git?ref=${LATEST_COMMIT}"
5842+
]
5843+
}
5844+
],
5845+
"rekorUrl": "${REKOR}",
5846+
"publicKey": "${known_PUBLIC_KEY}"
5847+
},
5848+
"ec-version": "${EC_VERSION}",
5849+
"effective-time": "${TIMESTAMP}"
5850+
}
5851+
---
5852+
5853+
[TestFeatures/invalid att signature with valid image sig and skip-att-sig-check flag:stderr - 1]
5854+
time="${TIMESTAMP}" level=warning msg="Attestation signature check skipped, fetching attestations without verification"
5855+
5856+
---
5857+
5858+
[TestFeatures/skip both image and att sig checks for debugging:stdout - 1]
5859+
{
5860+
"success": true,
5861+
"components": [
5862+
{
5863+
"name": "Unnamed",
5864+
"containerImage": "${REGISTRY}/acceptance/skip-both-sig-checks@sha256:${REGISTRY_acceptance/skip-both-sig-checks:latest_DIGEST}",
5865+
"source": {},
5866+
"successes": [
5867+
{
5868+
"msg": "Pass",
5869+
"metadata": {
5870+
"code": "builtin.attestation.syntax_check"
5871+
}
5872+
},
5873+
{
5874+
"msg": "Pass",
5875+
"metadata": {
5876+
"code": "main.acceptor"
5877+
}
5878+
}
5879+
],
5880+
"success": true,
5881+
"attestations": [
5882+
{
5883+
"type": "https://in-toto.io/Statement/v0.1",
5884+
"predicateType": "https://slsa.dev/provenance/v0.2",
5885+
"predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2",
5886+
"signatures": [
5887+
{
5888+
"keyid": "",
5889+
"sig": "${ATTESTATION_SIGNATURE_acceptance/skip-both-sig-checks}"
5890+
}
5891+
]
5892+
}
5893+
]
5894+
}
5895+
],
5896+
"key": "${known_PUBLIC_KEY_JSON}",
5897+
"policy": {
5898+
"sources": [
5899+
{
5900+
"policy": [
5901+
"git::${GITHOST}/git/skip-both-sig-checks.git?ref=${LATEST_COMMIT}"
5902+
]
5903+
}
5904+
],
5905+
"rekorUrl": "${REKOR}",
5906+
"publicKey": "${known_PUBLIC_KEY}"
5907+
},
5908+
"ec-version": "${EC_VERSION}",
5909+
"effective-time": "${TIMESTAMP}"
5910+
}
5911+
---
5912+
5913+
[TestFeatures/skip both image and att sig checks for debugging:stderr - 1]
5914+
time="${TIMESTAMP}" level=warning msg="Image signature check skipped"
5915+
time="${TIMESTAMP}" level=warning msg="Attestation signature check skipped, fetching attestations without verification"
5916+
time="${TIMESTAMP}" level=warning msg="Both --skip-image-sig-check and --skip-att-sig-check are active, all cryptographic verification is disabled"
5917+
5918+
---

features/validate_image.feature

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,65 @@ Feature: evaluate enterprise contract
5656
# builtin.attestation.image_check is not found in the success output
5757
Then the output should match the snapshot
5858

59+
Scenario: invalid att signature with valid image sig and skip-att-sig-check flag
60+
Given a key pair named "known"
61+
Given a key pair named "unknown"
62+
Given an image named "acceptance/invalid-att-signature"
63+
64+
# We're going to use the "known" public key when validating, so the image
65+
# sig check should pass but the att sig check should fail (if it wasn't skipped).
66+
Given a valid image signature of "acceptance/invalid-att-signature" image signed by the "known" key
67+
Given a valid attestation of "acceptance/invalid-att-signature" signed by the "unknown" key
68+
69+
Given a git repository named "invalid-att-signature" with
70+
| main.rego | examples/happy_day.rego |
71+
Given policy configuration named "invalid-att-signature-policy" with specification
72+
"""
73+
{
74+
"sources": [
75+
{
76+
"policy": [
77+
"git::https://${GITHOST}/git/invalid-att-signature.git"
78+
]
79+
}
80+
]
81+
}
82+
"""
83+
# Notice we're using the --skip-att-sig-check flag, which is the reason this is
84+
# green even though attestation is signed with a different key to the one we're providing
85+
When ec command is run with "validate image --image ${REGISTRY}/acceptance/invalid-att-signature --policy acceptance/invalid-att-signature-policy --public-key ${known_PUBLIC_KEY} --skip-att-sig-check --rekor-url ${REKOR} --show-successes --output json"
86+
Then the exit status should be 0
87+
Then the output should match the snapshot
88+
89+
Scenario: skip both image and att sig checks for debugging
90+
Given a key pair named "known"
91+
Given a key pair named "unknown"
92+
Given an image named "acceptance/skip-both-sig-checks"
93+
94+
# Both signatures use the wrong key. Without the skip flags this would fail.
95+
Given a valid image signature of "acceptance/skip-both-sig-checks" image signed by the "unknown" key
96+
Given a valid attestation of "acceptance/skip-both-sig-checks" signed by the "unknown" key
97+
98+
Given a git repository named "skip-both-sig-checks" with
99+
| main.rego | examples/happy_day.rego |
100+
Given policy configuration named "skip-both-sig-checks-policy" with specification
101+
"""
102+
{
103+
"sources": [
104+
{
105+
"policy": [
106+
"git::https://${GITHOST}/git/skip-both-sig-checks.git"
107+
]
108+
}
109+
]
110+
}
111+
"""
112+
# This is the expected real-world debugging usage: skip all signature checks
113+
# to focus on policy evaluation when the signing key is unavailable
114+
When ec command is run with "validate image --image ${REGISTRY}/acceptance/skip-both-sig-checks --policy acceptance/skip-both-sig-checks-policy --public-key ${known_PUBLIC_KEY} --skip-image-sig-check --skip-att-sig-check --rekor-url ${REKOR} --show-successes --output json"
115+
Then the exit status should be 0
116+
Then the output should match the snapshot
117+
59118
Scenario: happy day with git config and yaml
60119
Given a key pair named "known"
61120
Given an image named "acceptance/ec-happy-day"

internal/evaluation_target/application_snapshot_image/application_snapshot_image.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,32 @@ func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Cont
201201
return a.parseAttestationsFromBundles(layers)
202202
}
203203

204-
// Extract the signatures from the attestations here in order to also validate that
205-
// the signatures do exist in the expected format.
206-
for _, sig := range layers {
204+
return a.parseAttestationsFromSignatures(layers)
205+
}
206+
207+
// FetchAttestationsWithoutVerification fetches attestations from the registry
208+
// without performing signature verification. This is used when --skip-att-sig-check
209+
// is enabled but we still need the attestation data for policy evaluation.
210+
func (a *ApplicationSnapshotImage) FetchAttestationsWithoutVerification(ctx context.Context) error {
211+
if trace.IsEnabled() {
212+
region := trace.StartRegion(ctx, "ec:fetch-attestations-without-verification")
213+
defer region.End()
214+
}
215+
216+
sigs, err := oci.NewClient(ctx).FetchAttestationLayers(a.reference)
217+
if err != nil {
218+
return err
219+
}
220+
221+
if a.hasBundles(ctx) {
222+
return a.parseAttestationsFromBundles(sigs)
223+
}
224+
225+
return a.parseAttestationsFromSignatures(sigs)
226+
}
227+
228+
func (a *ApplicationSnapshotImage) parseAttestationsFromSignatures(sigs []cosignOCI.Signature) error {
229+
for _, sig := range sigs {
207230
att, err := attestation.ProvenanceFromSignature(sig)
208231
if err != nil {
209232
return fmt.Errorf("unable to parse untyped provenance: %w", err)
@@ -283,7 +306,8 @@ func (a *ApplicationSnapshotImage) parseAttestationsFromBundles(layers []cosignO
283306
// ValidateAttestationSyntax validates the attestations against known JSON
284307
// schemas, errors out if there are no attestations to check to prevent
285308
// successful syntax check of no inputs, must invoke
286-
// [ValidateAttestationSignature] to prefill the attestations.
309+
// [ValidateAttestationSignature] or [FetchAttestationsWithoutVerification]
310+
// to prefill the attestations.
287311
func (a ApplicationSnapshotImage) ValidateAttestationSyntax(ctx context.Context) error {
288312
if trace.IsEnabled() {
289313
region := trace.StartRegion(ctx, "ec:validate-attestation-syntax")

0 commit comments

Comments
 (0)