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) }