diff --git a/cmd/cosign/cli/attach/attach.go b/cmd/cosign/cli/attach/attach.go index 7c24235a428..2ac519936ef 100644 --- a/cmd/cosign/cli/attach/attach.go +++ b/cmd/cosign/cli/attach/attach.go @@ -32,7 +32,7 @@ func Attach() *ffcli.Command { ShortUsage: "cosign attach", ShortHelp: "attach contains tools to attach artifacts to other artifacts in a registry", FlagSet: flagset, - Subcommands: []*ffcli.Command{Signature()}, + Subcommands: []*ffcli.Command{Signature(), SBOM()}, Exec: func(ctx context.Context, args []string) error { return flag.ErrHelp }, diff --git a/cmd/cosign/cli/attach/sbom.go b/cmd/cosign/cli/attach/sbom.go new file mode 100644 index 00000000000..6bf36c89804 --- /dev/null +++ b/cmd/cosign/cli/attach/sbom.go @@ -0,0 +1,112 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package attach + +import ( + "context" + "flag" + "io/ioutil" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/peterbourgon/ff/v3/ffcli" + + "github.com/sigstore/cosign/pkg/cosign" + cremote "github.com/sigstore/cosign/pkg/cosign/remote" +) + +var mediaTypes = map[string]string{ + "cyclonedx": "application/vnd.cyclonedx", + "spdx": "text/spdx", +} + +func SBOM() *ffcli.Command { + var ( + flagset = flag.NewFlagSet("cosign attach sbom", flag.ExitOnError) + sbom = flagset.String("sbom", "", "path to the sbom, or {-} for stdin") + sbomType = flagset.String("type", "spdx", "type of sbom (spdx|cyclonedx), default spdx") + ) + return &ffcli.Command{ + Name: "sbom", + ShortUsage: "cosign attach sbom ", + ShortHelp: "attach sbom to the supplied container image", + FlagSet: flagset, + Exec: func(ctx context.Context, args []string) error { + if len(args) != 1 { + return flag.ErrHelp + } + + mt, ok := mediaTypes[*sbomType] + if !ok { + return flag.ErrHelp + } + + return SBOMCmd(ctx, *sbom, mt, args[0]) + }, + } +} + +func SBOMCmd(ctx context.Context, sbomRef, sbomType, imageRef string) error { + + ref, err := name.ParseReference(imageRef) + if err != nil { + return err + } + + b, err := ioutil.ReadFile(sbomRef) + if err != nil { + return err + } + s := &cremote.StaticLayer{ + B: b, + Mt: types.MediaType(sbomType), + } + + img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) + img, err = mutate.Append(img, mutate.Addendum{ + Layer: s, + }) + if err != nil { + return err + } + + // This doesn't work on DockerHub + m, err := img.Manifest() + if err != nil { + return err + } + // Setting it to an artifact type doesn't work on media types + m.Config.MediaType = types.OCIConfigJSON + + auth := remote.WithAuthFromKeychain(authn.DefaultKeychain) + + get, err := remote.Get(ref, auth) + if err != nil { + return err + } + repo := ref.Context() + + dstRef := cosign.AttachedImageTag(repo, get, cosign.SuffixSBOM) + + if err := remote.Write(dstRef, img, remote.WithAuthFromKeychain(authn.DefaultKeychain)); err != nil { + return err + } + return nil +} diff --git a/cmd/cosign/cli/download/download.go b/cmd/cosign/cli/download/download.go index 22ac495e85b..ceb2643eef2 100644 --- a/cmd/cosign/cli/download/download.go +++ b/cmd/cosign/cli/download/download.go @@ -32,7 +32,7 @@ func Download() *ffcli.Command { ShortUsage: "cosign download", ShortHelp: "download contains tools to download artifacts and attached artifacts in a registry", FlagSet: flagset, - Subcommands: []*ffcli.Command{Signature()}, + Subcommands: []*ffcli.Command{Signature(), SBOM()}, Exec: func(ctx context.Context, args []string) error { return flag.ErrHelp }, diff --git a/cmd/cosign/cli/download/sbom.go b/cmd/cosign/cli/download/sbom.go new file mode 100644 index 00000000000..d063ea1dd9d --- /dev/null +++ b/cmd/cosign/cli/download/sbom.go @@ -0,0 +1,90 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package download + +import ( + "context" + "flag" + "fmt" + "io/ioutil" + "os" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/peterbourgon/ff/v3/ffcli" + "github.com/sigstore/cosign/pkg/cosign" +) + +func SBOM() *ffcli.Command { + var ( + flagset = flag.NewFlagSet("cosign download sbom", flag.ExitOnError) + ) + return &ffcli.Command{ + Name: "sbom", + ShortUsage: "cosign download sbom ", + ShortHelp: "Download SBOMs from the supplied container image", + FlagSet: flagset, + Exec: func(ctx context.Context, args []string) error { + if len(args) != 1 { + return flag.ErrHelp + } + return SBOMCmd(ctx, args[0]) + }, + } +} + +func SBOMCmd(ctx context.Context, imageRef string) error { + ref, err := name.ParseReference(imageRef) + if err != nil { + return err + } + + auth := remote.WithAuthFromKeychain(authn.DefaultKeychain) + + get, err := remote.Get(ref, auth) + if err != nil { + return err + } + + repo := ref.Context() + dstRef := cosign.AttachedImageTag(repo, get, cosign.SuffixSBOM) + img, err := remote.Image(dstRef, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return err + } + layers, err := img.Layers() + if err != nil { + return err + } + for _, l := range layers { + mt, err := l.MediaType() + if err != nil { + return err + } + fmt.Fprintf(os.Stderr, "Found SBOM of media type: %s\n", mt) + r, err := l.Compressed() + if err != nil { + return err + } + sbom, err := ioutil.ReadAll(r) + if err != nil { + return err + } + fmt.Println(string(sbom)) + } + return nil +} diff --git a/pkg/cosign/fetch.go b/pkg/cosign/fetch.go index 72a2a5e9453..0f64919a946 100644 --- a/pkg/cosign/fetch.go +++ b/pkg/cosign/fetch.go @@ -51,12 +51,13 @@ type SignedPayload struct { const ( SuffixSignature = ".sig" + SuffixSBOM = ".sbom" ) -func AttachedImageTag(sigRepo name.Repository, signedImgDesc *remote.Descriptor, suffix string) name.Tag { +func AttachedImageTag(repo name.Repository, imgDesc *remote.Descriptor, suffix string) name.Tag { // sha256:d34db33f -> sha256-d34db33f.sig - tagStr := strings.ReplaceAll(signedImgDesc.Digest.String(), ":", "-") + suffix - return sigRepo.Tag(tagStr) + tagStr := strings.ReplaceAll(imgDesc.Digest.String(), ":", "-") + suffix + return repo.Tag(tagStr) } func GetAttachedManifestForImage(imgDesc *remote.Descriptor, repo name.Repository, suffix string, opts ...remote.Option) (*remote.Descriptor, error) {