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

Add layout package for writing and loading signatures from disk #1040

Merged
merged 8 commits into from
Nov 19, 2021
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
16 changes: 15 additions & 1 deletion pkg/oci/remote/layer.go → pkg/oci/internal/signature/layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package remote
package signature

import (
"crypto/x509"
Expand All @@ -28,11 +28,25 @@ import (
"github.com/sigstore/sigstore/pkg/cryptoutils"
)

const (
sigkey = "dev.cosignproject.cosign/signature"
certkey = "dev.sigstore.cosign/certificate"
chainkey = "dev.sigstore.cosign/chain"
BundleKey = "dev.sigstore.cosign/bundle"
)

type sigLayer struct {
v1.Layer
desc v1.Descriptor
}

func New(l v1.Layer, desc v1.Descriptor) oci.Signature {
return &sigLayer{
Layer: l,
desc: desc,
}
}

var _ oci.Signature = (*sigLayer)(nil)

// Annotations implements oci.Signature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package remote
package signature

import (
"bytes"
"encoding/base64"
"fmt"
"testing"

Expand All @@ -28,6 +29,14 @@ import (
"github.com/sigstore/cosign/pkg/oci"
)

func mustDecode(s string) []byte {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
panic(err.Error())
}
return b
}

func TestSignature(t *testing.T) {
layer, err := random.Layer(300 /* byteSize */, types.DockerLayer)
if err != nil {
Expand Down
117 changes: 117 additions & 0 deletions pkg/oci/layout/index.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//
// Copyright 2021 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 layout

import (
"errors"
"fmt"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/sigstore/cosign/pkg/oci"
"github.com/sigstore/cosign/pkg/oci/signed"
)

const (
imageAnnotation = "dev.cosignproject.cosign/image"
sigsAnnotation = "dev.cosignproject.cosign/sigs"
Comment on lines +29 to +30
Copy link
Member

Choose a reason for hiding this comment

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

@dlorenc What's standard here? should this be dev.sigstore.cosign? Are we documenting these anywhere?

Copy link
Member

Choose a reason for hiding this comment

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

It probably should be dev.sigstore but I apparently never switched them over from the first version, so dev.cosignproject is currently everywhere. We should open a separate issue to track/document/change all of these.

)

// SignedImageIndex provides access to a local index reference, and its signatures.
func SignedImageIndex(path string) (oci.SignedImageIndex, error) {
p, err := layout.FromPath(path)
if err != nil {
return nil, err
}
ii, err := p.ImageIndex()
if err != nil {
return nil, err
}
return &index{
v1Index: ii,
}, nil
}

// We alias ImageIndex so that we can inline it without the type
// name colliding with the name of a method it had to implement.
type v1Index v1.ImageIndex

type index struct {
v1Index
}

var _ oci.SignedImageIndex = (*index)(nil)

// Signatures implements oci.SignedImageIndex
func (i *index) Signatures() (oci.Signatures, error) {
sigsImage, err := i.imageByAnnotation(sigsAnnotation)
if err != nil {
return nil, err
}
return &sigs{sigsImage}, nil
}

// Attestations implements oci.SignedImageIndex
func (i *index) Attestations() (oci.Signatures, error) {
return nil, fmt.Errorf("not yet implemented")
}

// Attestations implements oci.SignedImage
func (i *index) Attachment(name string) (oci.File, error) {
return nil, fmt.Errorf("not yet implemented")
}

// SignedImage implements oci.SignedImageIndex
// if an empty hash is passed in, return the original image that was signed
func (i *index) SignedImage(h v1.Hash) (oci.SignedImage, error) {
var img v1.Image
var err error
if h.String() == ":" {
img, err = i.imageByAnnotation(imageAnnotation)
} else {
img, err = i.Image(h)
}
if err != nil {
return nil, err
}
return signed.Image(img), nil
}

// imageByAnnotation searches through all manifests in the index.json
// and returns the image that has the matching annotation
func (i *index) imageByAnnotation(annotation string) (v1.Image, error) {
manifest, err := i.IndexManifest()
if err != nil {
return nil, err
}
for _, m := range manifest.Manifests {
if _, ok := m.Annotations[annotation]; ok {
return i.Image(m.Digest)
}
}
return nil, errors.New("unable to find image")
}

// SignedImageIndex implements oci.SignedImageIndex
func (i *index) SignedImageIndex(h v1.Hash) (oci.SignedImageIndex, error) {
ii, err := i.ImageIndex(h)
if err != nil {
return nil, err
}
return &index{
v1Index: ii,
}, nil
}
45 changes: 45 additions & 0 deletions pkg/oci/layout/signatures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// Copyright 2021 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 layout

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

type sigs struct {
v1.Image
}

var _ oci.Signatures = (*sigs)(nil)

// Get implements oci.Signatures
func (s *sigs) Get() ([]oci.Signature, error) {
manifest, err := s.Image.Manifest()
if err != nil {
return nil, err
}
signatures := make([]oci.Signature, 0, len(manifest.Layers))
for _, desc := range manifest.Layers {
l, err := s.Image.LayerByDigest(desc.Digest)
if err != nil {
return nil, err
}
signatures = append(signatures, signature.New(l, desc))
}
return signatures, nil
}
53 changes: 53 additions & 0 deletions pkg/oci/layout/write.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// Copyright 2021 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 layout

import (
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/pkg/errors"
"github.com/sigstore/cosign/pkg/oci"
)

// WriteSignedImage writes the image and all related signatures, attestations and attachments
func WriteSignedImage(path string, si oci.SignedImage) error {
// First, write an empty index
layoutPath, err := layout.Write(path, empty.Index)
if err != nil {
return err
}
// write the image
if err := appendImage(layoutPath, si, imageAnnotation); err != nil {
return errors.Wrap(err, "appending signed image")
}
// write the signatures
sigs, err := si.Signatures()
if err != nil {
return errors.Wrap(err, "getting signatures")
}
if err := appendImage(layoutPath, sigs, sigsAnnotation); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

If the resulting index were pushed, how would it run?

cc @jonjohnsonjr

Copy link
Contributor

Choose a reason for hiding this comment

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

Generally clients take the first entry that matches their criteria (platform).

This might be a reasonable knob to expose when pushing to a registry? By default, just push all the entries in index.json, but optionally include that index.json as a top-level index for everything related to the artifact. I don't know how this is intended to be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

for cosign load I was planning on pushing everything in index.json for now and not including the index.json itself so this effectively is the same as running cosign copy -- i think it would be cool to optionally include pushing index.json as a feature for later (although running into some errors with it atm that would need to be addressed)

return errors.Wrap(err, "appending signatures")
}
// TODO (priyawadhwa@) write attestations and attachments
return nil
}

func appendImage(path layout.Path, img v1.Image, annotation string) error {
return path.AppendImage(img, layout.WithAnnotations(
map[string]string{annotation: "true"},
))
}
114 changes: 114 additions & 0 deletions pkg/oci/layout/write_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//
// Copyright 2021 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 layout

import (
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/sigstore/cosign/pkg/oci"
"github.com/sigstore/cosign/pkg/oci/mutate"
"github.com/sigstore/cosign/pkg/oci/signed"
"github.com/sigstore/cosign/pkg/oci/static"
)

func TestReadWrite(t *testing.T) {
// write random signed image to disk
si := randomSignedImage(t)
tmp := t.TempDir()
if err := WriteSignedImage(tmp, si); err != nil {
t.Fatal(err)
}

// read the image and make sure the signatures exist
imageIndex, err := SignedImageIndex(tmp)
if err != nil {
t.Fatal(err)
}
gotSignedImage, err := imageIndex.SignedImage(v1.Hash{})
if err != nil {
t.Fatal(err)
}
// compare the image we read with the one we wrote
compareDigests(t, si, gotSignedImage)

// make sure signatures are correct
sigImage, err := imageIndex.Signatures()
if err != nil {
t.Fatal(err)
}
sigs, err := sigImage.Get()
if err != nil {
t.Fatal(err)
}
want := 6
if len(sigs) != want {
t.Fatal("didn't get the expected number of signatures")
}
// make sure the annotation is correct
for i, sig := range sigs {
annotations, err := sig.Annotations()
if err != nil {
t.Fatal(err)
}
val, ok := annotations["layer"]
if !ok {
t.Fatal("expected annotation doesn't exist on signature")
}
if val != fmt.Sprintf("%d", i) {
t.Fatal("expected annotation isn't correct")
}
}
}

func randomSignedImage(t *testing.T) oci.SignedImage {
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 signatures
for i := 0; i < want; i++ {
annotationOption := static.WithAnnotations(map[string]string{"layer": fmt.Sprintf("%d", i)})
sig, err := static.NewSignature(nil, fmt.Sprintf("%d", i), annotationOption)
if err != nil {
t.Fatalf("static.NewSignature() = %v", err)
}
si, err = mutate.AttachSignatureToImage(si, sig)
if err != nil {
t.Fatalf("SignEntity() = %v", err)
}
}
return si
}

func compareDigests(t *testing.T, img1 oci.SignedImage, img2 oci.SignedImage) {
d1, err := img1.Digest()
if err != nil {
t.Fatal(err)
}
d2, err := img2.Digest()
if err != nil {
t.Fatal(err)
}
if d := cmp.Diff(d1, d2); d != "" {
t.Fatalf("digests are different: %s", d)
}
}
Loading