Skip to content

Commit

Permalink
Add layout package for writing and loading signatures from disk (#1040)
Browse files Browse the repository at this point in the history
* Add layout package for writing and loading signatures from disk

Signed-off-by: Priya Wadhwa <priyawadhwa@google.com>

* Create siglayer package, which is used by  and

Signed-off-by: Priya Wadhwa <priyawadhwa@google.com>

* Address code review comments

Signed-off-by: Priya Wadhwa <priyawadhwa@google.com>

* Get signatures by layer rather than using partialg

Signed-off-by: Priya Wadhwa <priyawadhwa@google.com>

* Add unit test for reading and writing from disk

Signed-off-by: Priya Wadhwa <priyawadhwa@google.com>

* Use ImageIndex to store image related signatures etc

Signed-off-by: Priya Wadhwa <priyawadhwa@google.com>

* Code review comments, also support for passing in an empty hash to SignedImage

Signed-off-by: Priya Wadhwa <priyawadhwa@google.com>
  • Loading branch information
priyawadhwa committed Nov 19, 2021
1 parent 9cf8c3f commit f8f0f6d
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 22 deletions.
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"
)

// 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 {
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)
}
}

0 comments on commit f8f0f6d

Please sign in to comment.