Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Allow cosign to sign digests before they are uploaded. #2959

Merged
merged 2 commits into from
Jun 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about s/Unknown/Digest/ everywhere? Unknown doesn't feel quite right to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry that Digest isn't clear enough, since it is effectively what SignedEntity is (especially after my PR to add Digest to oci.SignedEntity).

That said, I don't feel particularly strongly about the naming here :bikeshed:


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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure we always want to do this on 404?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think yes, but do you have an alternative in mind?

} 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