From 8d300825d8ebde1e213d6609619985e5ca50eef4 Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Thu, 23 Apr 2026 16:25:55 -0400 Subject: [PATCH] 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 perhaps a little more complicated because we need to add a function that can download the attestation without verifying it. The argument against this is that it may encourage less practices, but I would say it's acceptable because we're not changing the default behavior, which is always to require signature verification. Co-Authored-By: Claude Sonnet 4.5 --- cmd/validate/image.go | 5 ++ .../modules/ROOT/pages/ec_validate_image.adoc | 1 + .../application_snapshot_image.go | 68 +++++++++++++++++++ internal/image/validate.go | 15 +++- internal/policy/policy.go | 8 +++ 5 files changed, 94 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 3149d6839..a4f6e27c3 100644 --- a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go +++ b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go @@ -248,6 +248,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) }