diff --git a/cmd/cosign/cli/attest.go b/cmd/cosign/cli/attest.go index 5cd22a43ef4..4fcaec2be8c 100644 --- a/cmd/cosign/cli/attest.go +++ b/cmd/cosign/cli/attest.go @@ -209,27 +209,22 @@ func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOpt opts = append(opts, static.WithBundle(bundle)) } - clientOpts := append( - regOpts.ClientOpts(ctx), - ociremote.WithSignatureSuffix(ociremote.AttestationTagSuffix), - ) - sig, err := static.NewAttestation(signedPayload, opts...) if err != nil { return err } - se, err := ociremote.SignedEntity(digest, clientOpts...) + se, err := ociremote.SignedEntity(digest, regOpts.ClientOpts(ctx)...) if err != nil { return err } - // Attach the signature to the entity. - newSE, err := mutate.AttachSignatureToEntity(se, sig, mutate.WithDupeDetector(dd)) + // Attach the attestation to the entity. + newSE, err := mutate.AttachAttestationToEntity(se, sig, mutate.WithDupeDetector(dd)) if err != nil { return err } - // Publish the signatures associated with this entity - return ociremote.WriteSignatures(digest.Repository, newSE, clientOpts...) + // Publish the attestations associated with this entity + return ociremote.WriteAttestations(digest.Repository, newSE, regOpts.ClientOpts(ctx)...) } diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index bbae375e8ef..41dfa2c2bb6 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -163,6 +163,8 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, args []string) (err return err } + // TODO(mattmoor): Add some sort of configuration to have this + // use Attestations() in place of Signatures(). verified, bundleVerified, err := cosign.Verify(ctx, ref, co) if err != nil { return err diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 0ca2c8a852d..b20b7daed0a 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -82,9 +82,7 @@ func Verify(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (ch } } - opts := co.RegistryClientOpts - - se, err := ociremote.SignedEntity(signedImgRef, opts...) + se, err := ociremote.SignedEntity(signedImgRef, co.RegistryClientOpts...) if err != nil { return nil, false, err } diff --git a/pkg/oci/interface.go b/pkg/oci/interface.go index e5ebaa3aafe..ad182672380 100644 --- a/pkg/oci/interface.go +++ b/pkg/oci/interface.go @@ -19,4 +19,8 @@ type SignedEntity interface { // Signatures returns the set of signatures currently associated with this // entity, or the empty equivalent if none are found. Signatures() (Signatures, error) + + // Attestations returns the set of attestations currently associated with this + // entity, or the empty equivalent if none are found. + Attestations() (Signatures, error) } diff --git a/pkg/oci/mutate/mutate.go b/pkg/oci/mutate/mutate.go index 02837284fee..18c46742dc7 100644 --- a/pkg/oci/mutate/mutate.go +++ b/pkg/oci/mutate/mutate.go @@ -72,6 +72,11 @@ func (i *indexWrapper) Signatures() (oci.Signatures, error) { return empty.Signatures(), nil } +// Attestations implements oic.SignedImageIndex +func (i *indexWrapper) Attestations() (oci.Signatures, error) { + return empty.Signatures(), nil +} + // SignedImage implements oic.SignedImageIndex func (i *indexWrapper) SignedImage(h v1.Hash) (oci.SignedImage, error) { for _, add := range i.addendum { @@ -128,6 +133,18 @@ func AttachSignatureToEntity(se oci.SignedEntity, sig oci.Signature, opts ...Sig } } +// AttachAttestationToEntity attaches the provided attestation to the provided entity. +func AttachAttestationToEntity(se oci.SignedEntity, att oci.Signature, opts ...SignOption) (oci.SignedEntity, error) { + switch obj := se.(type) { + case oci.SignedImage: + return AttachAttestationToImage(obj, att, opts...) + case oci.SignedImageIndex: + return AttachAttestationToImageIndex(obj, att, opts...) + default: + return nil, fmt.Errorf("unsupported type: %T", se) + } +} + // AttachSignatureToImage attaches the provided signature to the provided image. func AttachSignatureToImage(si oci.SignedImage, sig oci.Signature, opts ...SignOption) (oci.SignedImage, error) { return &signedImage{ @@ -137,9 +154,19 @@ func AttachSignatureToImage(si oci.SignedImage, sig oci.Signature, opts ...SignO }, nil } +// AttachAttestationToImage attaches the provided signature to the provided image. +func AttachAttestationToImage(si oci.SignedImage, att oci.Signature, opts ...SignOption) (oci.SignedImage, error) { + return &signedImage{ + SignedImage: si, + att: att, + so: makeSignOpts(opts...), + }, nil +} + type signedImage struct { oci.SignedImage sig oci.Signature + att oci.Signature so *signOpts } @@ -148,6 +175,8 @@ func (si *signedImage) Signatures() (oci.Signatures, error) { base, err := si.SignedImage.Signatures() if err != nil { return nil, err + } else if si.sig == nil { + return base, nil } if si.so.dd != nil { if existing, err := si.so.dd.Find(base, si.sig); err != nil { @@ -160,6 +189,25 @@ func (si *signedImage) Signatures() (oci.Signatures, error) { return AppendSignatures(base, si.sig) } +// Attestations implements oci.SignedImage +func (si *signedImage) Attestations() (oci.Signatures, error) { + base, err := si.SignedImage.Attestations() + if err != nil { + return nil, err + } else if si.att == nil { + return base, nil + } + if si.so.dd != nil { + if existing, err := si.so.dd.Find(base, si.att); err != nil { + return nil, err + } else if existing != nil { + // Just return base if the signature is redundant + return base, nil + } + } + return AppendSignatures(base, si.att) +} + // AttachSignatureToImageIndex attaches the provided signature to the provided image index. func AttachSignatureToImageIndex(sii oci.SignedImageIndex, sig oci.Signature, opts ...SignOption) (oci.SignedImageIndex, error) { return &signedImageIndex{ @@ -169,11 +217,21 @@ func AttachSignatureToImageIndex(sii oci.SignedImageIndex, sig oci.Signature, op }, nil } +// AttachAttestationToImageIndex attaches the provided attestation to the provided image index. +func AttachAttestationToImageIndex(sii oci.SignedImageIndex, att oci.Signature, opts ...SignOption) (oci.SignedImageIndex, error) { + return &signedImageIndex{ + ociSignedImageIndex: sii, + att: att, + so: makeSignOpts(opts...), + }, nil +} + type ociSignedImageIndex oci.SignedImageIndex type signedImageIndex struct { ociSignedImageIndex sig oci.Signature + att oci.Signature so *signOpts } @@ -182,6 +240,8 @@ func (sii *signedImageIndex) Signatures() (oci.Signatures, error) { base, err := sii.ociSignedImageIndex.Signatures() if err != nil { return nil, err + } else if sii.sig == nil { + return base, nil } if sii.so.dd != nil { if existing, err := sii.so.dd.Find(base, sii.sig); err != nil { @@ -193,3 +253,22 @@ func (sii *signedImageIndex) Signatures() (oci.Signatures, error) { } return AppendSignatures(base, sii.sig) } + +// Attestations implements oci.SignedImageIndex +func (sii *signedImageIndex) Attestations() (oci.Signatures, error) { + base, err := sii.ociSignedImageIndex.Attestations() + if err != nil { + return nil, err + } else if sii.att == nil { + return base, nil + } + if sii.so.dd != nil { + if existing, err := sii.so.dd.Find(base, sii.att); err != nil { + return nil, err + } else if existing != nil { + // Just return base if the signature is redundant + return base, nil + } + } + return AppendSignatures(base, sii.att) +} diff --git a/pkg/oci/mutate/mutate_test.go b/pkg/oci/mutate/mutate_test.go index 383b7ed7ab1..623375030e8 100644 --- a/pkg/oci/mutate/mutate_test.go +++ b/pkg/oci/mutate/mutate_test.go @@ -98,6 +98,14 @@ func TestAppendManifests(t *testing.T) { t.Errorf("len(Get()) = %d, wanted 0", len(sl)) } + if atts, err := ni.Attestations(); err != nil { + t.Errorf("Attestations() = %v", err) + } else if al, err := atts.Get(); err != nil { + t.Errorf("Get() = %v", err) + } else if len(al) != 0 { + t.Errorf("len(Get()) = %d, wanted 0", len(al)) + } + d1, err := i1.Digest() if err != nil { t.Fatalf("Digest() = %v", err) @@ -153,7 +161,7 @@ func TestSignEntity(t *testing.T) { } sii := signed.ImageIndex(ii) - t.Run("without duplicate detector", func(t *testing.T) { + t.Run("without duplicate detector (signature)", func(t *testing.T) { for _, se := range []oci.SignedEntity{si, sii} { orig, err := static.NewSignature(nil, "") if err != nil { @@ -161,7 +169,7 @@ func TestSignEntity(t *testing.T) { } se, err = AttachSignatureToEntity(se, orig) if err != nil { - t.Fatalf("SignEntity() = %v", err) + t.Fatalf("AttachSignatureToEntity() = %v", err) } for i := 2; i < 10; i++ { @@ -172,7 +180,7 @@ func TestSignEntity(t *testing.T) { se, err = AttachSignatureToEntity(se, sig) if err != nil { - t.Fatalf("SignEntity() = %v", err) + t.Fatalf("AttachSignatureToEntity() = %v", err) } sigs, err := se.Signatures() @@ -188,7 +196,42 @@ func TestSignEntity(t *testing.T) { } }) - t.Run("with duplicate detector", func(t *testing.T) { + t.Run("without duplicate detector (attestation)", func(t *testing.T) { + for _, se := range []oci.SignedEntity{si, sii} { + orig, err := static.NewAttestation([]byte("payload")) + if err != nil { + t.Fatalf("static.NewAttestation() = %v", err) + } + se, err = AttachAttestationToEntity(se, orig) + if err != nil { + t.Fatalf("AttachAttestationToEntity() = %v", err) + } + + for i := 2; i < 10; i++ { + sig, err := static.NewAttestation([]byte(fmt.Sprintf("%d", i))) + if err != nil { + t.Fatalf("static.NewAttestation() = %v", err) + } + + se, err = AttachAttestationToEntity(se, sig) + if err != nil { + t.Fatalf("AttachAttestationToEntity() = %v", err) + } + + atts, err := se.Attestations() + if err != nil { + t.Fatalf("Attestations() = %v", err) + } + if al, err := atts.Get(); err != nil { + t.Fatalf("Get() = %v", err) + } else if len(al) != i { + t.Errorf("len(Get()) = %d, wanted %d", len(al), i) + } + } + } + }) + + t.Run("with duplicate detector (signature)", func(t *testing.T) { for _, se := range []oci.SignedEntity{si, sii} { orig, err := static.NewSignature(nil, "") if err != nil { @@ -196,7 +239,7 @@ func TestSignEntity(t *testing.T) { } se, err = AttachSignatureToEntity(se, orig) if err != nil { - t.Fatalf("SignEntity() = %v", err) + t.Fatalf("AttachSignatureToEntity() = %v", err) } dd := &dupe{ @@ -211,7 +254,7 @@ func TestSignEntity(t *testing.T) { se, err = AttachSignatureToEntity(se, sig, WithDupeDetector(dd)) if err != nil { - t.Fatalf("SignEntity() = %v", err) + t.Fatalf("AttachSignatureToEntity() = %v", err) } sigs, err := se.Signatures() @@ -227,7 +270,46 @@ func TestSignEntity(t *testing.T) { } }) - t.Run("with erroring duplicate detector", func(t *testing.T) { + t.Run("with duplicate detector (attestation)", func(t *testing.T) { + for _, se := range []oci.SignedEntity{si, sii} { + orig, err := static.NewAttestation([]byte("blah")) + if err != nil { + t.Fatalf("static.NewAttestation() = %v", err) + } + se, err = AttachAttestationToEntity(se, orig) + if err != nil { + t.Fatalf("AttachAttestationToEntity() = %v", err) + } + + dd := &dupe{ + sig: orig, + } + + for i := 2; i < 10; i++ { + sig, err := static.NewAttestation([]byte(fmt.Sprintf("%d", i))) + if err != nil { + t.Fatalf("static.NewAttestation() = %v", err) + } + + se, err = AttachAttestationToEntity(se, sig, WithDupeDetector(dd)) + if err != nil { + t.Fatalf("AttachAttestationToEntity() = %v", err) + } + + atts, err := se.Attestations() + if err != nil { + t.Fatalf("Attestations() = %v", err) + } + if al, err := atts.Get(); err != nil { + t.Fatalf("Get() = %v", err) + } else if len(al) != 1 { + t.Errorf("len(Get()) = %d, wanted %d", len(al), i) + } + } + } + }) + + t.Run("with erroring duplicate detector (signature)", func(t *testing.T) { for _, se := range []oci.SignedEntity{si, sii} { orig, err := static.NewSignature(nil, "") if err != nil { @@ -235,7 +317,7 @@ func TestSignEntity(t *testing.T) { } se, err = AttachSignatureToEntity(se, orig) if err != nil { - t.Fatalf("SignEntity() = %v", err) + t.Fatalf("AttachSignatureToEntity() = %v", err) } want := errors.New("expected error") @@ -251,7 +333,7 @@ func TestSignEntity(t *testing.T) { se, err = AttachSignatureToEntity(se, sig, WithDupeDetector(dd)) if err != nil { - t.Fatalf("SignEntity() = %v", err) + t.Fatalf("AttachSignatureToEntity() = %v", err) } if _, got := se.Signatures(); !errors.Is(got, want) { @@ -260,6 +342,40 @@ func TestSignEntity(t *testing.T) { } } }) + + t.Run("with erroring duplicate detector (attestation)", func(t *testing.T) { + for _, se := range []oci.SignedEntity{si, sii} { + orig, err := static.NewAttestation([]byte("blah")) + if err != nil { + t.Fatalf("static.NewAttestation() = %v", err) + } + se, err = AttachAttestationToEntity(se, orig) + if err != nil { + t.Fatalf("AttachAttestationToEntity() = %v", err) + } + + want := errors.New("expected error") + dd := &dupe{ + err: want, + } + + for i := 2; i < 10; i++ { + sig, err := static.NewAttestation([]byte(fmt.Sprintf("%d", i))) + if err != nil { + t.Fatalf("static.NewAttestation() = %v", err) + } + + se, err = AttachAttestationToEntity(se, sig, WithDupeDetector(dd)) + if err != nil { + t.Fatalf("AttachAttestationToEntity() = %v", err) + } + + if _, got := se.Attestations(); !errors.Is(got, want) { + t.Fatalf("Attestations() = %v, wanted %v", got, want) + } + } + } + }) } type dupe struct { diff --git a/pkg/oci/remote/image.go b/pkg/oci/remote/image.go index f5dd853f3a0..f5bb6dcb4f9 100644 --- a/pkg/oci/remote/image.go +++ b/pkg/oci/remote/image.go @@ -48,3 +48,8 @@ var _ oci.SignedImage = (*image)(nil) func (i *image) Signatures() (oci.Signatures, error) { return signatures(i, i.opt) } + +// Attestations implements oic.SignedImage +func (i *image) Attestations() (oci.Signatures, error) { + return attestations(i, i.opt) +} diff --git a/pkg/oci/remote/image_test.go b/pkg/oci/remote/image_test.go index 36f64c8edb9..0854b1939ea 100644 --- a/pkg/oci/remote/image_test.go +++ b/pkg/oci/remote/image_test.go @@ -56,4 +56,15 @@ func TestSignedImage(t *testing.T) { } else if got := int64(len(sl)); got != wantLayers { t.Errorf("len(Get()) = %d, wanted %d", got, wantLayers) } + + atts, err := si.Attestations() + if err != nil { + t.Fatalf("Signatures() = %v", err) + } + + if al, err := atts.Get(); err != nil { + t.Errorf("Get() = %v", err) + } else if got := int64(len(al)); got != wantLayers { + t.Errorf("len(Get()) = %d, wanted %d", got, wantLayers) + } } diff --git a/pkg/oci/remote/index.go b/pkg/oci/remote/index.go index dc8447df00f..ff5fa0665ba 100644 --- a/pkg/oci/remote/index.go +++ b/pkg/oci/remote/index.go @@ -55,6 +55,11 @@ func (i *index) Signatures() (oci.Signatures, error) { return signatures(i, i.opt) } +// Attestations implements oic.SignedImageIndex +func (i *index) Attestations() (oci.Signatures, error) { + return attestations(i, i.opt) +} + // SignedImage implements oic.SignedImageIndex func (i *index) SignedImage(h v1.Hash) (oci.SignedImage, error) { img, err := i.Image(h) diff --git a/pkg/oci/remote/index_test.go b/pkg/oci/remote/index_test.go index f1606f325e3..f761894c71c 100644 --- a/pkg/oci/remote/index_test.go +++ b/pkg/oci/remote/index_test.go @@ -118,5 +118,16 @@ func TestSignedImageIndex(t *testing.T) { } else if got := int64(len(sl)); got != wantLayers { t.Errorf("len(Get()) = %d, wanted %d", got, wantLayers) } + + atts, err := se.Attestations() + if err != nil { + t.Fatalf("Signatures() = %v", err) + } + + if al, err := atts.Get(); err != nil { + t.Errorf("Get() = %v", err) + } else if got := int64(len(al)); got != wantLayers { + t.Errorf("len(Get()) = %d, wanted %d", got, wantLayers) + } } } diff --git a/pkg/oci/remote/remote.go b/pkg/oci/remote/remote.go index 7230ae65643..d8fa33f13b2 100644 --- a/pkg/oci/remote/remote.go +++ b/pkg/oci/remote/remote.go @@ -142,3 +142,12 @@ func signatures(digestable digestable, o *options) (oci.Signatures, error) { } return Signatures(o.TargetRepository.Tag(normalize(h, o.SignatureSuffix)), o.OriginalOptions...) } + +// attestations is a shared implementation of the oci.Signed* Signatures method. +func attestations(digestable digestable, o *options) (oci.Signatures, error) { + h, err := digestable.Digest() + if err != nil { + return nil, err + } + return Signatures(o.TargetRepository.Tag(normalize(h, o.AttestationSuffix)), o.OriginalOptions...) +} diff --git a/pkg/oci/remote/write.go b/pkg/oci/remote/write.go index 2c1ba7f6cc4..746c2c97314 100644 --- a/pkg/oci/remote/write.go +++ b/pkg/oci/remote/write.go @@ -20,7 +20,7 @@ import ( "github.com/sigstore/cosign/pkg/oci" ) -// WriteSignature publishes the signatures attaches to the given entity +// WriteSignature publishes the signatures attached to the given entity // into the provided repository. func WriteSignatures(repo name.Repository, se oci.SignedEntity, opts ...Option) error { o, err := makeOptions(repo, opts...) @@ -44,3 +44,28 @@ func WriteSignatures(repo name.Repository, se oci.SignedEntity, opts ...Option) // Write the Signatures image to the tag, with the provided remote.Options return remoteWrite(tag, sigs, o.ROpt...) } + +// WriteAttestations publishes the attestations attached to the given entity +// into the provided repository. +func WriteAttestations(repo name.Repository, se oci.SignedEntity, opts ...Option) error { + o, err := makeOptions(repo, opts...) + if err != nil { + return err + } + + // Access the signature list to publish + atts, err := se.Attestations() + if err != nil { + return err + } + + // Determine the tag to which these signatures should be published. + h, err := se.(digestable).Digest() + if err != nil { + return err + } + tag := o.TargetRepository.Tag(normalize(h, o.AttestationSuffix)) + + // Write the Signatures image to the tag, with the provided remote.Options + return remoteWrite(tag, atts, o.ROpt...) +} diff --git a/pkg/oci/remote/write_test.go b/pkg/oci/remote/write_test.go index 0ecfb1320d7..f978ac52818 100644 --- a/pkg/oci/remote/write_test.go +++ b/pkg/oci/remote/write_test.go @@ -69,3 +69,45 @@ func TestWriteSignatures(t *testing.T) { t.Fatalf("WriteSignature() = %v", err) } } + +func TestWriteAttestations(t *testing.T) { + rw := remote.Write + t.Cleanup(func() { + remoteWrite = rw + }) + i, err := random.Image(300 /* byteSize */, 7 /* layers */) + if err != nil { + t.Fatalf("random.Image() = %v", err) + } + si := signed.Image(i) + + want := 6 // Add 6 attestations + for i := 0; i < want; i++ { + sig, err := static.NewAttestation([]byte(fmt.Sprintf("%d", i))) + if err != nil { + t.Fatalf("static.NewSignature() = %v", err) + } + si, err = mutate.AttachAttestationToImage(si, sig) + if err != nil { + t.Fatalf("SignEntity() = %v", err) + } + } + + ref := name.MustParseReference("gcr.io/bistroless/static:nonroot") + + remoteWrite = func(ref name.Reference, img v1.Image, options ...remote.Option) error { + l, err := img.Layers() + if err != nil { + return err + } + + if got := len(l); got != want { + t.Errorf("got %d layers, wanted %d", got, want) + } + + return nil + } + if err := WriteAttestations(ref.Context(), si); err != nil { + t.Fatalf("WriteAttestations() = %v", err) + } +} diff --git a/pkg/oci/signed/image.go b/pkg/oci/signed/image.go index 2c474114636..7d6e3a377aa 100644 --- a/pkg/oci/signed/image.go +++ b/pkg/oci/signed/image.go @@ -39,3 +39,8 @@ var _ oci.SignedImage = (*image)(nil) func (*image) Signatures() (oci.Signatures, error) { return empty.Signatures(), nil } + +// Attestations implements oci.SignedImage +func (*image) Attestations() (oci.Signatures, error) { + return empty.Signatures(), nil +} diff --git a/pkg/oci/signed/image_test.go b/pkg/oci/signed/image_test.go index 523cbf426f0..bd338361ed3 100644 --- a/pkg/oci/signed/image_test.go +++ b/pkg/oci/signed/image_test.go @@ -39,4 +39,15 @@ func TestImage(t *testing.T) { } else if got, want := len(sl), 0; got != want { t.Errorf("len(Get()) = %d, wanted %d", got, want) } + + atts, err := si.Attestations() + if err != nil { + t.Fatalf("Attestations() = %v", err) + } + + if al, err := atts.Get(); err != nil { + t.Errorf("Get() = %v", err) + } else if got, want := len(al), 0; got != want { + t.Errorf("len(Get()) = %d, wanted %d", got, want) + } } diff --git a/pkg/oci/signed/index.go b/pkg/oci/signed/index.go index 1f5ec194475..c4362c90fdd 100644 --- a/pkg/oci/signed/index.go +++ b/pkg/oci/signed/index.go @@ -60,3 +60,8 @@ func (ii *index) SignedImageIndex(h v1.Hash) (oci.SignedImageIndex, error) { func (*index) Signatures() (oci.Signatures, error) { return empty.Signatures(), nil } + +// Attestations implements oci.SignedImageIndex +func (*index) Attestations() (oci.Signatures, error) { + return empty.Signatures(), nil +} diff --git a/pkg/oci/signed/index_test.go b/pkg/oci/signed/index_test.go index 36e3cc52f9c..207592e0e0d 100644 --- a/pkg/oci/signed/index_test.go +++ b/pkg/oci/signed/index_test.go @@ -86,5 +86,16 @@ func TestImageIndex(t *testing.T) { } else if got, want := len(sl), 0; got != want { t.Errorf("len(Get()) = %d, wanted %d", got, want) } + + atts, err := se.Attestations() + if err != nil { + t.Fatalf("Attestations() = %v", err) + } + + if al, err := atts.Get(); err != nil { + t.Errorf("Get() = %v", err) + } else if got, want := len(al), 0; got != want { + t.Errorf("len(Get()) = %d, wanted %d", got, want) + } } }