diff --git a/cmd/cosign/cli/download/sbom.go b/cmd/cosign/cli/download/sbom.go index eb362a9624a..ba6208c704d 100644 --- a/cmd/cosign/cli/download/sbom.go +++ b/cmd/cosign/cli/download/sbom.go @@ -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.FileMediaType() 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 } diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 073303fb7e2..9139423dd35 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -174,35 +174,35 @@ func SignCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOptions, a if digest, ok := ref.(name.Digest); ok && !recursive { se, err := ociempty.SignedImage(ref) if err != nil { - return err + return errors.Wrap(err, "accessing image") } err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, force, dd, sv, se) if err != nil { - return err + return errors.Wrap(err, "signing digest") } continue } se, err := ociremote.SignedEntity(ref, opts...) if err != nil { - return err + return errors.Wrap(err, "accessing entity") } if err := walk.SignedEntity(ctx, se, func(ctx context.Context, se oci.SignedEntity) error { // Get the digest for this entity in our walk. d, err := se.(interface{ Digest() (v1.Hash, error) }).Digest() if err != nil { - return err + return errors.Wrap(err, "computing digest") } digest := ref.Context().Digest(d.String()) err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, force, dd, sv, se) if err != nil { - return err + return errors.Wrap(err, "signing digest") } return ErrDone }); err != nil { - return err + return errors.Wrap(err, "recursively signing") } } diff --git a/pkg/oci/file.go b/pkg/oci/file.go index 5fc68cec16a..2d354ff9757 100644 --- a/pkg/oci/file.go +++ b/pkg/oci/file.go @@ -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. + // FileMediaType retrieves the media type of the File + FileMediaType() (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) } diff --git a/pkg/oci/remote/image_test.go b/pkg/oci/remote/image_test.go index 0854b1939ea..ba840f9a094 100644 --- a/pkg/oci/remote/image_test.go +++ b/pkg/oci/remote/image_test.go @@ -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) + } +} diff --git a/pkg/oci/remote/remote.go b/pkg/oci/remote/remote.go index 06bff3d17d2..ab4c63b77e8 100644 --- a/pkg/oci/remote/remote.go +++ b/pkg/oci/remote/remote.go @@ -17,6 +17,7 @@ package remote import ( "fmt" + "io" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -154,5 +155,42 @@ 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) + +// FileMediaType implements oci.File +func (f *attache) FileMediaType() (types.MediaType, error) { + return f.layer.MediaType() +} + +// Payload implements oci.File +func (f *attache) Payload() ([]byte, error) { + // remote layers are believed to be stored + // compressed, but we don't compress attachments + // so use "Compressed" to access the raw byte + // stream. + rc, err := f.layer.Compressed() + if err != nil { + return nil, err + } + defer rc.Close() + return io.ReadAll(rc) } diff --git a/pkg/oci/static/file.go b/pkg/oci/static/file.go index 7542af874db..270b5a6b763 100644 --- a/pkg/oci/static/file.go +++ b/pkg/oci/static/file.go @@ -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" @@ -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) + +// FileMediaType implements oci.File +func (f *file) FileMediaType() (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) } diff --git a/pkg/oci/static/file_test.go b/pkg/oci/static/file_test.go index 4666ed06c08..8c074fbc7f8 100644 --- a/pkg/oci/static/file_test.go +++ b/pkg/oci/static/file_test.go @@ -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 { @@ -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.FileMediaType() if err != nil { t.Fatalf("MediaType() = %v", err) } @@ -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) + } }) } diff --git a/test/e2e_test.go b/test/e2e_test.go index 36b41166f81..3c77e1047e2 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -622,7 +622,7 @@ func TestAttachSBOM(t *testing.T) { if err == nil { t.Fatal("Expected error") } - t.Log(out) + t.Log(out.String()) out.Reset() // Upload it! @@ -632,7 +632,7 @@ func TestAttachSBOM(t *testing.T) { if err != nil { t.Fatal(err) } - t.Log(out) + t.Log(out.String()) if len(sboms) != 1 { t.Fatalf("Expected one sbom, got %d", len(sboms)) }