Skip to content

Commit

Permalink
Have download sbom use the Attachment API.
Browse files Browse the repository at this point in the history
This changes the `download sbom` command to use the `Attachment("sbom")` accessor, and expands `oci.File` with several useful methods for interacting with the underlying data.

Signed-off-by: Matt Moore <[email protected]>
  • Loading branch information
mattmoor committed Oct 29, 2021
1 parent 0bf537f commit c99f4dd
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 34 deletions.
38 changes: 14 additions & 24 deletions cmd/cosign/cli/download/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,43 +36,33 @@ func SBOMCmd(ctx context.Context, regOpts options.RegistryOptions, imageRef stri
if err != nil {
return nil, err
}
ociremoteOpts = append(ociremoteOpts,
// TODO(mattmoor): This isn't really "signatures", consider shifting to
// an SBOMs accessor?
ociremote.WithSignatureSuffix(ociremote.SBOMTagSuffix))

se, err := ociremote.SignedEntity(ref, ociremoteOpts...)
if err != nil {
return nil, err
}

// TODO(mattmoor): This logic does a shallow walk, we should use `mutate.Map`
// if we want to collect all of the SBOMs attached at any level of an index.
img, err := se.Signatures()
file, err := se.Attachment("sbom")
if err != nil {
return nil, err
}
sigs, err := img.Get()

// "attach sbom" attaches a single static.NewFile
sboms := make([]string, 0, 1)

mt, err := file.MediaType()
if err != nil {
return nil, err
}
if len(sigs) == 0 {
return nil, fmt.Errorf("no signatures associated with %v", ref)
}

sboms := make([]string, 0, len(sigs))
for _, l := range sigs {
mt, err := l.MediaType()
if err != nil {
return nil, err
}
fmt.Fprintf(os.Stderr, "Found SBOM of media type: %s\n", mt)
sbom, err := l.Payload()
if err != nil {
return nil, err
}
sboms = append(sboms, string(sbom))
fmt.Fprintln(out, string(sbom))
fmt.Fprintf(os.Stderr, "Found SBOM of media type: %s\n", mt)
sbom, err := file.Payload()
if err != nil {
return nil, err
}

sboms = append(sboms, string(sbom))
fmt.Fprintln(out, string(sbom))

return sboms, nil
}
9 changes: 8 additions & 1 deletion pkg/oci/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@

package oci

import "github.com/google/go-containerregistry/pkg/v1/types"

// File is a degenerate form of SignedImage that stores a single file as a v1.Layer
type File interface {
SignedImage

// TODO(mattmoor): Consider adding useful helpers.
// MediaType retrieves the media type of the File
MediaType() (types.MediaType, error)

// Payload fetches the opaque data that is being signed.
// This will always return data when there is no error.
Payload() ([]byte, error)
}
37 changes: 37 additions & 0 deletions pkg/oci/remote/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,40 @@ func TestSignedImage(t *testing.T) {
t.Errorf("len(Get()) = %d, wanted %d", got, wantLayers)
}
}

func TestSignedImageWithAttachment(t *testing.T) {
ri := remote.Image
t.Cleanup(func() {
remoteImage = ri
})
wantLayers := int64(1) // File must have a single layer

remoteImage = func(ref name.Reference, options ...remote.Option) (v1.Image, error) {
// Only called for signature images
return random.Image(300 /* byteSize */, wantLayers)
}

ref, err := name.ParseReference("gcr.io/distroless/static:nonroot")
if err != nil {
t.Fatalf("ParseRef() = %v", err)
}

si, err := SignedImage(ref)
if err != nil {
t.Fatalf("Signatures() = %v", err)
}

file, err := si.Attachment("sbom")
if err != nil {
t.Fatalf("Signatures() = %v", err)
}

payload, err := file.Payload()
if err != nil {
t.Errorf("Payload() = %v", err)
}
// We check greater than because it's wrapped in a tarball with `random.Layer`
if len(payload) < 300 {
t.Errorf("Payload() = %d bytes, wanted %d", len(payload), 300)
}
}
36 changes: 35 additions & 1 deletion pkg/oci/remote/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package remote

import (
"fmt"
"io"

"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
Expand Down Expand Up @@ -154,5 +155,38 @@ func attachment(digestable digestable, attName string, o *options) (oci.File, er
if err != nil {
return nil, err
}
return oci.File(img), nil
ls, err := img.Layers()
if err != nil {
return nil, err
}
if len(ls) != 1 {
return nil, fmt.Errorf("expected exactly one layer in attachment, got %d", len(ls))
}

return &attache{
SignedImage: img,
layer: ls[0],
}, nil
}

type attache struct {
oci.SignedImage
layer v1.Layer
}

var _ oci.File = (*attache)(nil)

// MediaType implements oci.File
func (f *attache) MediaType() (types.MediaType, error) {
return f.layer.MediaType()
}

// Payload implements oci.File
func (f *attache) Payload() ([]byte, error) {
rc, err := f.layer.Uncompressed()
if err != nil {
return nil, err
}
defer rc.Close()
return io.ReadAll(rc)
}
39 changes: 34 additions & 5 deletions pkg/oci/static/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
package static

import (
"io"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/types"
Expand All @@ -31,14 +34,40 @@ func NewFile(payload []byte, opts ...Option) (oci.File, error) {
}
base := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
base = mutate.ConfigMediaType(base, o.ConfigMediaType)
layer := &staticLayer{
b: payload,
opts: o,
}
img, err := mutate.Append(base, mutate.Addendum{
Layer: &staticLayer{
b: payload,
opts: o,
},
Layer: layer,
})
if err != nil {
return nil, err
}
return signed.Image(img), nil
return &file{
SignedImage: signed.Image(img),
layer: layer,
}, nil
}

type file struct {
oci.SignedImage
layer v1.Layer
}

var _ oci.File = (*file)(nil)

// MediaType implements oci.File
func (f *file) MediaType() (types.MediaType, error) {
return f.layer.MediaType()
}

// Payload implements oci.File
func (f *file) Payload() ([]byte, error) {
rc, err := f.layer.Uncompressed()
if err != nil {
return nil, err
}
defer rc.Close()
return io.ReadAll(rc)
}
14 changes: 11 additions & 3 deletions pkg/oci/static/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ import (

func TestNewFile(t *testing.T) {
payload := "this is the content!"
img, err := NewFile([]byte(payload), WithLayerMediaType("foo"))
file, err := NewFile([]byte(payload), WithLayerMediaType("foo"))
if err != nil {
t.Fatalf("NewFile() = %v", err)
}

layers, err := img.Layers()
layers, err := file.Layers()
if err != nil {
t.Fatalf("Layers() = %v", err)
} else if got, want := len(layers), 1; got != want {
Expand All @@ -53,7 +53,7 @@ func TestNewFile(t *testing.T) {

t.Run("check media type", func(t *testing.T) {
wantMT := types.MediaType("foo")
gotMT, err := l.MediaType()
gotMT, err := file.MediaType()
if err != nil {
t.Fatalf("MediaType() = %v", err)
}
Expand Down Expand Up @@ -111,5 +111,13 @@ func TestNewFile(t *testing.T) {
if got, want := string(uncompContent), payload; got != want {
t.Errorf("Uncompressed() = %s, wanted %s", got, want)
}

gotPayload, err := file.Payload()
if err != nil {
t.Fatalf("Payload() = %v", err)
}
if got, want := string(gotPayload), payload; got != want {
t.Errorf("Payload() = %s, wanted %s", got, want)
}
})
}

0 comments on commit c99f4dd

Please sign in to comment.