Skip to content

Commit

Permalink
Feature: Allow cosign to sign digests before they are uploaded. (#2959)
Browse files Browse the repository at this point in the history
* Feature: Allow cosign to sign/attest digests before they are uploaded.

:gift: This feature allows `cosign` to sign and attest a digest that doesn't exist yet, to support scenarios such as signing an image prior to pushing it.

This adapts the ideas from the two prior approaches, which were closed by stale-bot.

Fixes: #1905

/kind feature

Signed-off-by: Matt Moore <[email protected]>

* Apply suggestions from code review

Co-authored-by: Jon Johnson <[email protected]>
Signed-off-by: Matt Moore <[email protected]>

---------

Signed-off-by: Matt Moore <[email protected]>
Signed-off-by: Matt Moore <[email protected]>
Co-authored-by: Jon Johnson <[email protected]>
  • Loading branch information
mattmoor and jonjohnsonjr authored Jun 2, 2023
1 parent 83c08ce commit f9ee498
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 16 deletions.
7 changes: 3 additions & 4 deletions cmd/cosign/cli/attest/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,9 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
return err
}

se, err := ociremote.SignedEntity(digest, ociremoteOpts...)
if err != nil {
return err
}
// We don't actually need to access the remote entity to attach things to it
// so we use a placeholder here.
se := ociremote.SignedUnknown(digest)

signOpts := []mutate.SignOption{
mutate.WithDupeDetector(dd),
Expand Down
4 changes: 3 additions & 1 deletion cmd/cosign/cli/sign/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,9 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO

if digest, ok := ref.(name.Digest); ok && !signOpts.Recursive {
se, err := ociremote.SignedEntity(ref, opts...)
if err != nil {
if err == ociremote.ErrEntityNotFound {
se = ociremote.SignedUnknown(digest)
} else if err != nil {
return fmt.Errorf("accessing image: %w", err)
}
err = signDigest(ctx, digest, staticPayload, ko, signOpts, annotations, dd, sv, se)
Expand Down
111 changes: 108 additions & 3 deletions pkg/oci/mutate/mutate.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func AttachSignatureToEntity(se oci.SignedEntity, sig oci.Signature, opts ...Sig
case oci.SignedImageIndex:
return AttachSignatureToImageIndex(obj, sig, opts...)
default:
return nil, fmt.Errorf("unsupported type: %T", se)
return AttachSignatureToUnknown(obj, sig, opts...)
}
}

Expand All @@ -147,7 +147,7 @@ func AttachAttestationToEntity(se oci.SignedEntity, att oci.Signature, opts ...S
case oci.SignedImageIndex:
return AttachAttestationToImageIndex(obj, att, opts...)
default:
return nil, fmt.Errorf("unsupported type: %T", se)
return AttachAttestationToUnknown(obj, att, opts...)
}
}

Expand All @@ -159,7 +159,7 @@ func AttachFileToEntity(se oci.SignedEntity, name string, f oci.File, opts ...Si
case oci.SignedImageIndex:
return AttachFileToImageIndex(obj, name, f, opts...)
default:
return nil, fmt.Errorf("unsupported type: %T", se)
return AttachFileToUnknown(obj, name, f, opts...)
}
}

Expand Down Expand Up @@ -348,3 +348,108 @@ func (sii *signedImageIndex) Attachment(attName string) (oci.File, error) {
}
return nil, fmt.Errorf("attachment %q not found", attName)
}

// AttachSignatureToUnknown attaches the provided signature to the provided image.
func AttachSignatureToUnknown(se oci.SignedEntity, sig oci.Signature, opts ...SignOption) (oci.SignedEntity, error) {
return &signedUnknown{
SignedEntity: se,
sig: sig,
attachments: make(map[string]oci.File),
so: makeSignOpts(opts...),
}, nil
}

// AttachAttestationToUnknown attaches the provided attestation to the provided image.
func AttachAttestationToUnknown(se oci.SignedEntity, att oci.Signature, opts ...SignOption) (oci.SignedEntity, error) {
return &signedUnknown{
SignedEntity: se,
att: att,
attachments: make(map[string]oci.File),
so: makeSignOpts(opts...),
}, nil
}

// AttachFileToUnknown attaches the provided file to the provided image.
func AttachFileToUnknown(se oci.SignedEntity, name string, f oci.File, opts ...SignOption) (oci.SignedEntity, error) {
return &signedUnknown{
SignedEntity: se,
attachments: map[string]oci.File{
name: f,
},
so: makeSignOpts(opts...),
}, nil
}

type signedUnknown struct {
oci.SignedEntity
sig oci.Signature
att oci.Signature
so *signOpts
attachments map[string]oci.File
}

type digestable interface {
Digest() (v1.Hash, error)
}

// Digest is generally implemented by oci.SignedEntity implementations.
func (si *signedUnknown) Digest() (v1.Hash, error) {
d, ok := si.SignedEntity.(digestable)
if !ok {
return v1.Hash{}, fmt.Errorf("underlying signed entity not digestable: %T", si.SignedEntity)
}
return d.Digest()
}

// Signatures implements oci.SignedEntity
func (si *signedUnknown) Signatures() (oci.Signatures, error) {
base, err := si.SignedEntity.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 {
return nil, err
} else if existing != nil {
// Just return base if the signature is redundant
return base, nil
}
}
return AppendSignatures(base, si.sig)
}

// Attestations implements oci.SignedEntity
func (si *signedUnknown) Attestations() (oci.Signatures, error) {
base, err := si.SignedEntity.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
}
}
if si.so.ro != nil {
replace, err := si.so.ro.Replace(base, si.att)
if err != nil {
return nil, err
}
return ReplaceSignatures(replace)
}
return AppendSignatures(base, si.att)
}

// Attachment implements oci.SignedEntity
func (si *signedUnknown) Attachment(attName string) (oci.File, error) {
if f, ok := si.attachments[attName]; ok {
return f, nil
}
return nil, fmt.Errorf("attachment %q not found", attName)
}
27 changes: 20 additions & 7 deletions pkg/oci/mutate/mutate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,21 @@ func TestSignEntity(t *testing.T) {
}
sii := signed.ImageIndex(ii)

// Create an explicitly unknown implementation of oci.SignedEntity, which we
// feed through the table tests below.
want := make([]byte, 300)
rand.Read(want)
orig, err := static.NewFile(want)
if err != nil {
t.Fatalf("static.NewFile() = %v", err)
}
sunk, err := AttachFileToUnknown(sii, "sbom", orig)
if err != nil {
t.Fatalf("AttachFileToUnknown() = %v", err)
}

t.Run("attach SBOMs", func(t *testing.T) {
for _, se := range []oci.SignedEntity{si, sii} {
for _, se := range []oci.SignedEntity{si, sii, sunk} {
want := make([]byte, 300)
rand.Read(want)

Expand Down Expand Up @@ -197,7 +210,7 @@ func TestSignEntity(t *testing.T) {
})

t.Run("without duplicate detector (signature)", func(t *testing.T) {
for _, se := range []oci.SignedEntity{si, sii} {
for _, se := range []oci.SignedEntity{si, sii, sunk} {
orig, err := static.NewSignature(nil, "")
if err != nil {
t.Fatalf("static.NewSignature() = %v", err)
Expand Down Expand Up @@ -232,7 +245,7 @@ func TestSignEntity(t *testing.T) {
})

t.Run("without duplicate detector (attestation)", func(t *testing.T) {
for _, se := range []oci.SignedEntity{si, sii} {
for _, se := range []oci.SignedEntity{si, sii, sunk} {
orig, err := static.NewAttestation([]byte("payload"))
if err != nil {
t.Fatalf("static.NewAttestation() = %v", err)
Expand Down Expand Up @@ -267,7 +280,7 @@ func TestSignEntity(t *testing.T) {
})

t.Run("with duplicate detector (signature)", func(t *testing.T) {
for _, se := range []oci.SignedEntity{si, sii} {
for _, se := range []oci.SignedEntity{si, sii, sunk} {
orig, err := static.NewSignature(nil, "")
if err != nil {
t.Fatalf("static.NewSignature() = %v", err)
Expand Down Expand Up @@ -306,7 +319,7 @@ func TestSignEntity(t *testing.T) {
})

t.Run("with duplicate detector (attestation)", func(t *testing.T) {
for _, se := range []oci.SignedEntity{si, sii} {
for _, se := range []oci.SignedEntity{si, sii, sunk} {
orig, err := static.NewAttestation([]byte("blah"))
if err != nil {
t.Fatalf("static.NewAttestation() = %v", err)
Expand Down Expand Up @@ -345,7 +358,7 @@ func TestSignEntity(t *testing.T) {
})

t.Run("with erroring duplicate detector (signature)", func(t *testing.T) {
for _, se := range []oci.SignedEntity{si, sii} {
for _, se := range []oci.SignedEntity{si, sii, sunk} {
orig, err := static.NewSignature(nil, "")
if err != nil {
t.Fatalf("static.NewSignature() = %v", err)
Expand Down Expand Up @@ -379,7 +392,7 @@ func TestSignEntity(t *testing.T) {
})

t.Run("with erroring duplicate detector (attestation)", func(t *testing.T) {
for _, se := range []oci.SignedEntity{si, sii} {
for _, se := range []oci.SignedEntity{si, sii, sunk} {
orig, err := static.NewAttestation([]byte("blah"))
if err != nil {
t.Fatalf("static.NewAttestation() = %v", err)
Expand Down
6 changes: 5 additions & 1 deletion pkg/oci/remote/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ var (
remoteIndex = remote.Index
remoteGet = remote.Get
remoteWrite = remote.Write

// ErrEntityNotFound is the error that SignedEntity returns when the
// provided ref does not exist.
ErrEntityNotFound = errors.New("entity not found in registry")
)

// SignedEntity provides access to a remote reference, and its signatures.
Expand All @@ -46,7 +50,7 @@ func SignedEntity(ref name.Reference, options ...Option) (oci.SignedEntity, erro
got, err := remoteGet(ref, o.ROpt...)
var te *transport.Error
if errors.As(err, &te) && te.StatusCode == http.StatusNotFound {
return nil, errors.New("entity not found in registry")
return nil, ErrEntityNotFound
} else if err != nil {
return nil, err
}
Expand Down
60 changes: 60 additions & 0 deletions pkg/oci/remote/unknown.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// Copyright 2023 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 remote

import (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/sigstore/cosign/v2/pkg/oci"
)

// SignedUnknown provides access to signed metadata without directly accessing
// the underlying entity. This can be used to access signature metadata for
// digests that have not been published (yet).
func SignedUnknown(digest name.Digest, options ...Option) oci.SignedEntity {
o := makeOptions(digest.Context(), options...)
return &unknown{
digest: digest,
opt: o,
}
}

type unknown struct {
digest name.Digest
opt *options
}

var _ oci.SignedEntity = (*unknown)(nil)

// Digest implements digestable
func (i *unknown) Digest() (v1.Hash, error) {
return v1.NewHash(i.digest.DigestStr())
}

// Signatures implements oci.SignedEntity
func (i *unknown) Signatures() (oci.Signatures, error) {
return signatures(i, i.opt)
}

// Attestations implements oci.SignedEntity
func (i *unknown) Attestations() (oci.Signatures, error) {
return attestations(i, i.opt)
}

// Attachment implements oci.SignedEntity
func (i *unknown) Attachment(name string) (oci.File, error) {
return attachment(i, name, i.opt)
}
Loading

0 comments on commit f9ee498

Please sign in to comment.