From 4a9579078ac0e344a84e519b745514523e658e73 Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Thu, 23 Apr 2026 16:25:55 -0400 Subject: [PATCH 1/3] 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 --- cmd/validate/image.go | 5 ++ .../modules/ROOT/pages/ec_validate_image.adoc | 1 + .../application_snapshot_image.go | 69 +++++++++++++++++++ internal/image/validate.go | 15 +++- internal/policy/policy.go | 8 +++ 5 files changed, 95 insertions(+), 3 deletions(-) diff --git a/cmd/validate/image.go b/cmd/validate/image.go index 336f1080d..961794e0b 100644 --- a/cmd/validate/image.go +++ b/cmd/validate/image.go @@ -211,6 +211,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { }, IgnoreRekor: data.ignoreRekor, SkipImageSigCheck: data.skipImageSigCheck, + SkipAttSigCheck: data.skipAttSigCheck, PolicyRef: data.policyConfiguration, PublicKey: data.publicKey, RekorURL: data.rekorURL, @@ -498,6 +499,9 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { cmd.Flags().BoolVar(&data.skipImageSigCheck, "skip-image-sig-check", data.skipImageSigCheck, "Skip image signature validation checks.") + cmd.Flags().BoolVar(&data.skipAttSigCheck, "skip-att-sig-check", data.skipAttSigCheck, + "Skip attestation signature validation checks.") + cmd.Flags().StringVar(&data.certificateIdentity, "certificate-identity", data.certificateIdentity, "URL of the certificate identity for keyless verification") @@ -636,6 +640,7 @@ type imageData struct { input string ignoreRekor bool skipImageSigCheck bool + skipAttSigCheck bool output []string outputFile string policy policy.Policy diff --git a/docs/modules/ROOT/pages/ec_validate_image.adoc b/docs/modules/ROOT/pages/ec_validate_image.adoc index d2d67263f..2eadb4315 100644 --- a/docs/modules/ROOT/pages/ec_validate_image.adoc +++ b/docs/modules/ROOT/pages/ec_validate_image.adoc @@ -151,6 +151,7 @@ mark (?) sign, for example: --output text=output.txt?show-successes=false * inline JSON ('{sources: {...}, identity: {...}}')") -k, --public-key:: path to the public key. Overrides publicKey from EnterpriseContractPolicy -r, --rekor-url:: Rekor URL. Overrides rekorURL from EnterpriseContractPolicy +--skip-att-sig-check:: Skip attestation signature validation checks. (Default: false) --skip-image-sig-check:: Skip image signature validation checks. (Default: false) --snapshot:: Provide the AppStudio Snapshot as a source of the images to validate, as inline JSON of the "spec" or a reference to a Kubernetes object [/] diff --git a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go index eaecddc05..c28ac2ad2 100644 --- a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go +++ b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go @@ -32,6 +32,7 @@ import ( "github.com/santhosh-tekuri/jsonschema/v5" "github.com/sigstore/cosign/v3/pkg/cosign" cosignOCI "github.com/sigstore/cosign/v3/pkg/oci" + ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" log "github.com/sirupsen/logrus" "github.com/spf13/afero" @@ -246,6 +247,74 @@ func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Cont return nil } +// FetchAttestationsWithoutVerification fetches attestations from the registry +// without performing signature verification. This is used when --skip-att-sig-check +// is enabled but we still need the attestation data for policy evaluation. +func (a *ApplicationSnapshotImage) FetchAttestationsWithoutVerification(ctx context.Context) error { + if trace.IsEnabled() { + region := trace.StartRegion(ctx, "ec:fetch-attestations-without-verification") + defer region.End() + } + + remoteOpts := oci.CreateRemoteOptions(ctx) + signedEntity, err := ociremote.SignedEntity(a.reference, ociremote.WithRemoteOptions(remoteOpts...)) + if err != nil { + return fmt.Errorf("failed to fetch signed entity: %w", err) + } + + layers, err := signedEntity.Attestations() + if err != nil { + return fmt.Errorf("failed to fetch attestations: %w", err) + } + + // Check if using bundles + useBundles := a.hasBundles(ctx) + if useBundles { + sigs, err := layers.Get() + if err != nil { + return fmt.Errorf("failed to get attestation signatures: %w", err) + } + return a.parseAttestationsFromBundles(sigs) + } + + // Parse non-bundle attestations + sigs, err := layers.Get() + if err != nil { + return fmt.Errorf("failed to get attestation signatures: %w", err) + } + + for _, sig := range sigs { + att, err := attestation.ProvenanceFromSignature(sig) + if err != nil { + return fmt.Errorf("unable to parse untyped provenance: %w", err) + } + t := att.PredicateType() + log.Debugf("Found attestation with predicateType: %s", t) + switch t { + case attestation.PredicateSLSAProvenance: + sp, err := attestation.SLSAProvenanceFromSignature(sig) + if err != nil { + return fmt.Errorf("unable to parse as SLSA v0.2: %w", err) + } + a.attestations = append(a.attestations, sp) + + case attestation.PredicateSLSAProvenanceV1: + sp, err := attestation.SLSAProvenanceFromSignatureV1(sig) + if err != nil { + return fmt.Errorf("unable to parse as SLSA v1: %w", err) + } + a.attestations = append(a.attestations, sp) + + case attestation.PredicateSpdxDocument: + a.attestations = append(a.attestations, att) + + default: + a.attestations = append(a.attestations, att) + } + } + return nil +} + // parseAttestationsFromBundles extracts attestations from Sigstore bundles. // Bundle-wrapped layers report an incorrect media type, so we unmarshal the // DSSE envelope from the raw payload directly. diff --git a/internal/image/validate.go b/internal/image/validate.go index cf80840fa..8824d12bc 100644 --- a/internal/image/validate.go +++ b/internal/image/validate.go @@ -81,9 +81,18 @@ func ValidateImage(ctx context.Context, comp app.SnapshotComponent, snap *app.Sn out.SetImageSignatureCheckFromError(a.ValidateImageSignature(ctx)) } - out.SetAttestationSignatureCheckFromError(a.ValidateAttestationSignature(ctx)) - if !out.AttestationSignatureCheck.Passed { - return out, nil + // Handle attestation signature validation + if p.SkipAttSigCheck() { + log.Debug("Attestation signature check skipped, fetching attestations without verification") + if err := a.FetchAttestationsWithoutVerification(ctx); err != nil { + log.Debugf("Failed to fetch attestations without verification: %v", err) + // Continue with validation even if attestation fetch fails + } + } else { + out.SetAttestationSignatureCheckFromError(a.ValidateAttestationSignature(ctx)) + if !out.AttestationSignatureCheck.Passed { + return out, nil + } } out.Signatures = a.Signatures() diff --git a/internal/policy/policy.go b/internal/policy/policy.go index b8122bf6c..b76fbd260 100644 --- a/internal/policy/policy.go +++ b/internal/policy/policy.go @@ -76,6 +76,7 @@ type Policy interface { Spec() ecc.EnterpriseContractPolicySpec EffectiveTime() time.Time SkipImageSigCheck() bool + SkipAttSigCheck() bool AttestationTime(time.Time) Identity() cosign.Identity Keyless() bool @@ -91,6 +92,7 @@ type policy struct { identity cosign.Identity ignoreRekor bool skipImageSigCheck bool + skipAttSigCheck bool } // PublicKeyPEM returns the PublicKey in PEM format. When SigVerifier is not @@ -169,6 +171,7 @@ type Options struct { Identity cosign.Identity IgnoreRekor bool SkipImageSigCheck bool + SkipAttSigCheck bool PolicyRef string PublicKey string RekorURL string @@ -266,6 +269,7 @@ func NewPolicy(ctx context.Context, opts Options) (Policy, error) { p.ignoreRekor = opts.IgnoreRekor p.skipImageSigCheck = opts.SkipImageSigCheck + p.skipAttSigCheck = opts.SkipAttSigCheck if opts.PublicKey != "" && opts.PublicKey != p.PublicKey { p.PublicKey = opts.PublicKey @@ -409,6 +413,10 @@ func (p policy) SkipImageSigCheck() bool { return p.skipImageSigCheck } +func (p policy) SkipAttSigCheck() bool { + return p.skipAttSigCheck +} + func isNow(choosenTime string) bool { return strings.EqualFold(choosenTime, Now) } From bc7ed38530a121db5fc9467ea34a8980b16d79a5 Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Mon, 18 May 2026 13:35:23 -0400 Subject: [PATCH 2/3] Test coverage for --skip-att-sig-check Also refactor some duplicated code Co-authored-by: Claude Code --- features/__snapshots__/validate_image.snap | 71 +++++++++ features/validate_image.feature | 25 +++ .../application_snapshot_image.go | 53 ++----- .../application_snapshot_image_test.go | 150 ++++++++++++++++++ internal/image/validate_test.go | 119 ++++++++++++++ internal/policy/policy_test.go | 35 ++++ 6 files changed, 409 insertions(+), 44 deletions(-) diff --git a/features/__snapshots__/validate_image.snap b/features/__snapshots__/validate_image.snap index 8aca1ad7d..6e4c935cc 100644 --- a/features/__snapshots__/validate_image.snap +++ b/features/__snapshots__/validate_image.snap @@ -5780,3 +5780,74 @@ Error: success criteria not met [TestFeatures/discover artifact referrers via OCI Referrers API:stderr - 1] --- + +[TestFeatures/happy day with skip-att-sig-check flag:stdout - 1] +{ + "success": true, + "components": [ + { + "name": "Unnamed", + "containerImage": "${REGISTRY}/acceptance/ec-happy-day@sha256:${REGISTRY_acceptance/ec-happy-day:latest_DIGEST}", + "source": {}, + "successes": [ + { + "msg": "Pass", + "metadata": { + "code": "builtin.attestation.syntax_check" + } + }, + { + "msg": "Pass", + "metadata": { + "code": "builtin.image.signature_check" + } + }, + { + "msg": "Pass", + "metadata": { + "code": "main.acceptor" + } + } + ], + "success": true, + "signatures": [ + { + "keyid": "", + "sig": "${IMAGE_SIGNATURE_acceptance/ec-happy-day}" + } + ], + "attestations": [ + { + "type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", + "signatures": [ + { + "keyid": "", + "sig": "${ATTESTATION_SIGNATURE_acceptance/ec-happy-day}" + } + ] + } + ] + } + ], + "key": "${known_PUBLIC_KEY_JSON}", + "policy": { + "sources": [ + { + "policy": [ + "git::${GITHOST}/git/happy-day-policy.git?ref=${LATEST_COMMIT}" + ] + } + ], + "rekorUrl": "${REKOR}", + "publicKey": "${known_PUBLIC_KEY}" + }, + "ec-version": "${EC_VERSION}", + "effective-time": "${TIMESTAMP}" +} +--- + +[TestFeatures/happy day with skip-att-sig-check flag:stderr - 1] + +--- diff --git a/features/validate_image.feature b/features/validate_image.feature index 373fcb5c1..7b89f9de7 100644 --- a/features/validate_image.feature +++ b/features/validate_image.feature @@ -56,6 +56,31 @@ Feature: evaluate enterprise contract # builtin.attestation.image_check is not found in the success output Then the output should match the snapshot + Scenario: happy day with skip-att-sig-check flag + Given a key pair named "known" + Given an image named "acceptance/ec-happy-day" + Given a valid image signature of "acceptance/ec-happy-day" image signed by the "known" key + Given a valid attestation of "acceptance/ec-happy-day" signed by the "known" key + Given a git repository named "happy-day-policy" with + | main.rego | examples/happy_day.rego | + Given policy configuration named "ec-policy" with specification + """ + { + "sources": [ + { + "policy": [ + "git::https://${GITHOST}/git/happy-day-policy.git" + ] + } + ] + } + """ + When ec command is run with "validate image --image ${REGISTRY}/acceptance/ec-happy-day --policy acceptance/ec-policy --public-key ${known_PUBLIC_KEY} --skip-att-sig-check --rekor-url ${REKOR} --show-successes --output json" + Then the exit status should be 0 + # The only difference to the happy day scenario is that + # builtin.attestation.signature_check is not found in the success output + Then the output should match the snapshot + Scenario: happy day with git config and yaml Given a key pair named "known" Given an image named "acceptance/ec-happy-day" diff --git a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go index c28ac2ad2..739044106 100644 --- a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go +++ b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go @@ -202,49 +202,7 @@ func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Cont return a.parseAttestationsFromBundles(layers) } - // Extract the signatures from the attestations here in order to also validate that - // the signatures do exist in the expected format. - for _, sig := range layers { - att, err := attestation.ProvenanceFromSignature(sig) - if err != nil { - return fmt.Errorf("unable to parse untyped provenance: %w", err) - } - t := att.PredicateType() - log.Debugf("Found attestation with predicateType: %s", t) - switch t { - case attestation.PredicateSLSAProvenance: - // SLSAProvenanceFromSignature does the payload extraction - // and decoding that was done in ProvenanceFromSignature - // over again. We could refactor so we're not doing that twice, - // but it's not super important IMO. - sp, err := attestation.SLSAProvenanceFromSignature(sig) - if err != nil { - return fmt.Errorf("unable to parse as SLSA v0.2: %w", err) - } - a.attestations = append(a.attestations, sp) - - case attestation.PredicateSLSAProvenanceV1: - // SLSA Provenance v1.0 - sp, err := attestation.SLSAProvenanceFromSignatureV1(sig) - if err != nil { - return fmt.Errorf("unable to parse as SLSA v1: %w", err) - } - a.attestations = append(a.attestations, sp) - - case attestation.PredicateSpdxDocument: - // It's an SPDX format SBOM - // Todo maybe: We could unmarshal it into a suitable SPDX struct - // similar to how it's done for SLSA above - a.attestations = append(a.attestations, att) - - // Todo: CycloneDX format SBOM - - default: - // It's some other kind of attestation - a.attestations = append(a.attestations, att) - } - } - return nil + return a.parseAttestationsFromSignatures(layers) } // FetchAttestationsWithoutVerification fetches attestations from the registry @@ -277,12 +235,15 @@ func (a *ApplicationSnapshotImage) FetchAttestationsWithoutVerification(ctx cont return a.parseAttestationsFromBundles(sigs) } - // Parse non-bundle attestations sigs, err := layers.Get() if err != nil { return fmt.Errorf("failed to get attestation signatures: %w", err) } + return a.parseAttestationsFromSignatures(sigs) +} + +func (a *ApplicationSnapshotImage) parseAttestationsFromSignatures(sigs []cosignOCI.Signature) error { for _, sig := range sigs { att, err := attestation.ProvenanceFromSignature(sig) if err != nil { @@ -292,6 +253,8 @@ func (a *ApplicationSnapshotImage) FetchAttestationsWithoutVerification(ctx cont log.Debugf("Found attestation with predicateType: %s", t) switch t { case attestation.PredicateSLSAProvenance: + // SLSAProvenanceFromSignature re-does the payload extraction from + // ProvenanceFromSignature. Could be deduplicated but not important. sp, err := attestation.SLSAProvenanceFromSignature(sig) if err != nil { return fmt.Errorf("unable to parse as SLSA v0.2: %w", err) @@ -308,6 +271,8 @@ func (a *ApplicationSnapshotImage) FetchAttestationsWithoutVerification(ctx cont case attestation.PredicateSpdxDocument: a.attestations = append(a.attestations, att) + // Todo: CycloneDX format SBOM + default: a.attestations = append(a.attestations, att) } diff --git a/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go b/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go index e010ef9cb..02510dcc7 100644 --- a/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go +++ b/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go @@ -1081,6 +1081,156 @@ func createBundleDSSESignature(t *testing.T, statement any) oci.Signature { } // createDSSESignature creates a test signature with a DSSE envelope containing the given statement +func TestParseAttestationsFromSignatures(t *testing.T) { + ref := name.MustParseReference("registry.io/repository/image:tag") + + //nolint:staticcheck + slsaV02Statement := in_toto.ProvenanceStatementSLSA02{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: v02.PredicateSLSAProvenance, + Subject: []in_toto.Subject{ + { + Name: "test-image", + Digest: common.DigestSet{"sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"}, + }, + }, + }, + Predicate: v02.ProvenancePredicate{ + BuildType: pipelineRunBuildType, + Builder: common.ProvenanceBuilder{ID: "https://tekton.dev/chains/v2"}, + }, + } + + //nolint:staticcheck + slsaV1Statement := in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: "https://slsa.dev/provenance/v1", + Subject: []in_toto.Subject{ + { + Name: "test-image", + Digest: common.DigestSet{"sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"}, + }, + }, + }, + Predicate: slsav1.ProvenancePredicate{ + BuildDefinition: slsav1.ProvenanceBuildDefinition{ + BuildType: "https://tekton.dev/attestations/chains/pipelinerun@v2", + ExternalParameters: json.RawMessage(`{}`), + }, + RunDetails: slsav1.ProvenanceRunDetails{ + Builder: slsav1.Builder{ID: "https://tekton.dev/chains/v2"}, + }, + }, + } + + //nolint:staticcheck + spdxStatement := in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: "https://spdx.dev/Document", + Subject: []in_toto.Subject{ + { + Name: "test-image", + Digest: common.DigestSet{"sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"}, + }, + }, + }, + Predicate: json.RawMessage(`{"spdxVersion":"SPDX-2.3"}`), + } + + //nolint:staticcheck + unknownStatement := in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: "https://example.com/unknown/v1", + Subject: []in_toto.Subject{ + { + Name: "test-image", + Digest: common.DigestSet{"sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"}, + }, + }, + }, + Predicate: json.RawMessage(`{"custom":"data"}`), + } + + cases := []struct { + name string + signatures []oci.Signature + expectErr bool + expectedAttCount int + expectedPredTypes []string + }{ + { + name: "no signatures", + signatures: []oci.Signature{}, + expectedAttCount: 0, + }, + { + name: "SLSA v0.2", + signatures: []oci.Signature{createDSSESignature(t, slsaV02Statement)}, + expectedAttCount: 1, + expectedPredTypes: []string{v02.PredicateSLSAProvenance}, + }, + { + name: "SLSA v1.0", + signatures: []oci.Signature{createDSSESignature(t, slsaV1Statement)}, + expectedAttCount: 1, + expectedPredTypes: []string{"https://slsa.dev/provenance/v1"}, + }, + { + name: "SPDX document", + signatures: []oci.Signature{createDSSESignature(t, spdxStatement)}, + expectedAttCount: 1, + expectedPredTypes: []string{"https://spdx.dev/Document"}, + }, + { + name: "unknown predicate type", + signatures: []oci.Signature{createDSSESignature(t, unknownStatement)}, + expectedAttCount: 1, + expectedPredTypes: []string{"https://example.com/unknown/v1"}, + }, + { + name: "multiple mixed types", + signatures: []oci.Signature{ + createDSSESignature(t, slsaV02Statement), + createDSSESignature(t, slsaV1Statement), + createDSSESignature(t, spdxStatement), + createDSSESignature(t, unknownStatement), + }, + expectedAttCount: 4, + expectedPredTypes: []string{ + v02.PredicateSLSAProvenance, + "https://slsa.dev/provenance/v1", + "https://spdx.dev/Document", + "https://example.com/unknown/v1", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + a := ApplicationSnapshotImage{reference: ref} + + err := a.parseAttestationsFromSignatures(tc.signatures) + + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedAttCount, len(a.attestations)) + + if tc.expectedPredTypes != nil { + for i, expectedType := range tc.expectedPredTypes { + assert.Equal(t, expectedType, a.attestations[i].PredicateType()) + } + } + } + }) + } +} + func createDSSESignature(t *testing.T, statement any) oci.Signature { t.Helper() diff --git a/internal/image/validate_test.go b/internal/image/validate_test.go index 1c49077cf..5941bdd0a 100644 --- a/internal/image/validate_test.go +++ b/internal/image/validate_test.go @@ -708,3 +708,122 @@ func TestValidateImageSkipImageSigCheck(t *testing.T) { }) } } + +func TestValidateImageSkipAttSigCheck(t *testing.T) { + tests := []struct { + name string + skipAttSigCheck bool + expectAttSigResult bool + expectImageSigCheckCall bool + }{ + { + name: "skip attestation signature check disabled (default)", + skipAttSigCheck: false, + expectAttSigResult: true, + expectImageSigCheckCall: true, + }, + { + name: "skip attestation signature check enabled", + skipAttSigCheck: true, + expectAttSigResult: false, + expectImageSigCheckCall: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := afero.NewMemMapFs() + ctx := utils.WithFS(context.Background(), fs) + + comp := app.SnapshotComponent{ + ContainerImage: imageRef, + } + + ctx = withImageConfig(ctx, comp.ContainerImage) + client := ecoci.NewClient(ctx) + fakeClient := client.(*fake.FakeClient) + + fakeClient.On("Head", ref).Return(&v1.Descriptor{MediaType: types.OCIManifestSchema1}, nil) + fakeClient.On("HasBundles", mock.Anything, refNoTag).Return(false, nil) + fakeClient.On("VerifyImageSignatures", refNoTag, mock.Anything).Return([]oci.Signature{}, false, fmt.Errorf("no signatures found")) + fakeClient.On("VerifyImageAttestations", refNoTag, mock.Anything).Return([]oci.Signature{}, false, fmt.Errorf("no attestations found")) + + opts := policy.Options{ + EffectiveTime: policy.Now, + SkipAttSigCheck: tt.skipAttSigCheck, + Identity: cosign.Identity{ + Issuer: "https://example.com/oidc", + Subject: "test@example.com", + }, + } + + updatedPolicy, _, err := policy.PreProcessPolicy(ctx, opts) + require.NoError(t, err) + + snap := &app.SnapshotSpec{} + evaluators := []evaluator.Evaluator{} + + output, err := ValidateImage(ctx, comp, snap, updatedPolicy, evaluators, false) + + require.NoError(t, err) + require.NotNil(t, output) + + if tt.expectAttSigResult { + assert.NotNil(t, output.AttestationSignatureCheck.Result, "Expected AttestationSignatureCheck to have a result") + } else { + assert.Nil(t, output.AttestationSignatureCheck.Result, "Expected AttestationSignatureCheck to not have a result when skipped") + } + + // ImageSignatureCheck should always have a result (not affected by this flag) + assert.NotNil(t, output.ImageSignatureCheck.Result, "ImageSignatureCheck should always have a result") + + violations := output.Violations() + successes := output.Successes() + + if tt.skipAttSigCheck { + for _, violation := range violations { + if violation.Metadata != nil { + code, ok := violation.Metadata["code"].(string) + if ok { + assert.NotEqual(t, "builtin.attestation.signature_check", code, + "Skipped attestation signature check should not appear in violations") + } + } + } + + for _, success := range successes { + if success.Metadata != nil { + code, ok := success.Metadata["code"].(string) + if ok { + assert.NotEqual(t, "builtin.attestation.signature_check", code, + "Skipped attestation signature check should not appear in successes") + } + } + } + } + + if !tt.skipAttSigCheck { + foundAttestationCheck := false + for _, violation := range violations { + if violation.Metadata != nil { + code, ok := violation.Metadata["code"].(string) + if ok && code == "builtin.attestation.signature_check" { + foundAttestationCheck = true + break + } + } + } + for _, success := range successes { + if success.Metadata != nil { + code, ok := success.Metadata["code"].(string) + if ok && code == "builtin.attestation.signature_check" { + foundAttestationCheck = true + break + } + } + } + assert.True(t, foundAttestationCheck, "AttestationSignatureCheck should appear in results when not skipped") + } + }) + } +} diff --git a/internal/policy/policy_test.go b/internal/policy/policy_test.go index d1a8c3a76..c528ffb31 100644 --- a/internal/policy/policy_test.go +++ b/internal/policy/policy_test.go @@ -1615,6 +1615,41 @@ type FakeCosignClient struct { publicKey string } +func TestSkipAttSigCheck(t *testing.T) { + ctx := context.Background() + ctx = withSignatureClient(ctx, &FakeCosignClient{publicKey: utils.TestPublicKey}) + utils.SetTestRekorPublicKey(t) + utils.SetTestFulcioRoots(t) + utils.SetTestCTLogPublicKey(t) + + t.Run("disabled by default", func(t *testing.T) { + p, err := NewPolicy(ctx, Options{ + PolicyRef: toJson(&ecc.EnterpriseContractPolicySpec{PublicKey: utils.TestPublicKey}), + EffectiveTime: Now, + Identity: cosign.Identity{ + Issuer: "https://example.com/oidc", + Subject: "test@example.com", + }, + }) + require.NoError(t, err) + assert.False(t, p.SkipAttSigCheck()) + }) + + t.Run("enabled when set", func(t *testing.T) { + p, err := NewPolicy(ctx, Options{ + PolicyRef: toJson(&ecc.EnterpriseContractPolicySpec{PublicKey: utils.TestPublicKey}), + EffectiveTime: Now, + SkipAttSigCheck: true, + Identity: cosign.Identity{ + Issuer: "https://example.com/oidc", + Subject: "test@example.com", + }, + }) + require.NoError(t, err) + assert.True(t, p.SkipAttSigCheck()) + }) +} + func (c *FakeCosignClient) publicKeyFromKeyRef(ctx context.Context, publicKey string) (sigstoreSig.Verifier, error) { if strings.Contains(publicKey, "invalid:") { return nil, fmt.Errorf("invalid public key reference format") From 5b6e0644db7d0e0702f78b269c4912559ac9338b Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Tue, 19 May 2026 14:05:32 -0400 Subject: [PATCH 3/3] squash me (failure handling) Co-authored-by: Stefano Pentassuglia --- internal/image/validate.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/image/validate.go b/internal/image/validate.go index 8824d12bc..beb4a182c 100644 --- a/internal/image/validate.go +++ b/internal/image/validate.go @@ -85,8 +85,9 @@ func ValidateImage(ctx context.Context, comp app.SnapshotComponent, snap *app.Sn if p.SkipAttSigCheck() { log.Debug("Attestation signature check skipped, fetching attestations without verification") if err := a.FetchAttestationsWithoutVerification(ctx); err != nil { - log.Debugf("Failed to fetch attestations without verification: %v", err) - // Continue with validation even if attestation fetch fails + log.Warnf("Failed to fetch attestations without verification: %v", err) + out.SetAttestationSignatureCheckFromError(fmt.Errorf("failed to fetch attestations (signature check skipped): %w", err)) + return out, nil } } else { out.SetAttestationSignatureCheckFromError(a.ValidateAttestationSignature(ctx))