diff --git a/cmd/cosign/cli/download/attestation.go b/cmd/cosign/cli/download/attestation.go index 46a4d67cc30..92a2f6603d9 100644 --- a/cmd/cosign/cli/download/attestation.go +++ b/cmd/cosign/cli/download/attestation.go @@ -21,8 +21,11 @@ import ( "fmt" "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/cosign/v2/pkg/oci" + ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" ) func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, attOptions options.AttestationDownloadOptions, imageRef string) error { @@ -43,7 +46,50 @@ func AttestationCmd(ctx context.Context, regOpts options.RegistryOptions, attOpt } } - attestations, err := cosign.FetchAttestationsForReference(ctx, ref, predicateType, ociremoteOpts...) + se, err := ociremote.SignedEntity(ref, ociremoteOpts...) + if err != nil { + return err + } + + idx, isIndex := se.(oci.SignedImageIndex) + + // We only allow --platform on multiarch indexes + if attOptions.Platform != "" && !isIndex { + return fmt.Errorf("specified reference is not a multiarch image") + } + + if attOptions.Platform != "" && isIndex { + targetPlatform, err := v1.ParsePlatform(attOptions.Platform) + if err != nil { + return fmt.Errorf("parsing platform: %w", err) + } + platforms, err := getIndexPlatforms(idx) + if err != nil { + return fmt.Errorf("getting available platforms: %w", err) + } + + platforms = matchPlatform(targetPlatform, platforms) + if len(platforms) == 0 { + return fmt.Errorf("unable to find an attestation for %s", targetPlatform.String()) + } + if len(platforms) > 1 { + return fmt.Errorf( + "platform spec matches more than one image architecture: %s", + platforms.String(), + ) + } + + nse, err := idx.SignedImage(platforms[0].hash) + if err != nil { + return fmt.Errorf("searching for %s image: %w", platforms[0].hash.String(), err) + } + if nse == nil { + return fmt.Errorf("unable to find image %s", platforms[0].hash.String()) + } + se = nse + } + + attestations, err := cosign.FetchAttestations(se, predicateType) if err != nil { return err } diff --git a/cmd/cosign/cli/options/download.go b/cmd/cosign/cli/options/download.go index ce2fe613072..5a28196c3e7 100644 --- a/cmd/cosign/cli/options/download.go +++ b/cmd/cosign/cli/options/download.go @@ -24,6 +24,7 @@ type SBOMDownloadOptions struct { type AttestationDownloadOptions struct { PredicateType string // Predicate type of attestation to retrieve + Platform string // Platform to download attestations } var _ Interface = (*SBOMDownloadOptions)(nil) @@ -40,4 +41,6 @@ func (o *SBOMDownloadOptions) AddFlags(cmd *cobra.Command) { func (o *AttestationDownloadOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.PredicateType, "predicate-type", "", "download attestation with matching predicateType annotation") + cmd.Flags().StringVar(&o.Platform, "platform", "", + "download attestation for a specific platform image") } diff --git a/doc/cosign_download_attestation.md b/doc/cosign_download_attestation.md index 87364a34983..7f9d7ea5af7 100644 --- a/doc/cosign_download_attestation.md +++ b/doc/cosign_download_attestation.md @@ -20,6 +20,7 @@ cosign download attestation [flags] --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] -h, --help help for attestation --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). + --platform string download attestation for a specific platform image --predicate-type string download attestation with matching predicateType annotation ``` diff --git a/pkg/cosign/fetch.go b/pkg/cosign/fetch.go index 88bfeee23b2..ed789d9fbe5 100644 --- a/pkg/cosign/fetch.go +++ b/pkg/cosign/fetch.go @@ -19,6 +19,7 @@ import ( "context" "crypto/x509" "encoding/json" + "errors" "fmt" "os" "runtime" @@ -26,6 +27,7 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/sigstore/cosign/v2/pkg/cosign/bundle" + "github.com/sigstore/cosign/v2/pkg/oci" ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" "golang.org/x/sync/errgroup" ) @@ -121,12 +123,15 @@ func FetchSignaturesForReference(_ context.Context, ref name.Reference, opts ... } func FetchAttestationsForReference(_ context.Context, ref name.Reference, predicateType string, opts ...ociremote.Option) ([]AttestationPayload, error) { - simg, err := ociremote.SignedEntity(ref, opts...) + se, err := ociremote.SignedEntity(ref, opts...) if err != nil { return nil, err } + return FetchAttestations(se, predicateType) +} - atts, err := simg.Attestations() +func FetchAttestations(se oci.SignedEntity, predicateType string) ([]AttestationPayload, error) { + atts, err := se.Attestations() if err != nil { return nil, fmt.Errorf("remote image: %w", err) } @@ -135,7 +140,7 @@ func FetchAttestationsForReference(_ context.Context, ref name.Reference, predic return nil, fmt.Errorf("fetching attestations: %w", err) } if len(l) == 0 { - return nil, fmt.Errorf("no attestations associated with %s", ref) + return nil, errors.New("found no attestations") } attestations := make([]AttestationPayload, 0, len(l))