-
Notifications
You must be signed in to change notification settings - Fork 547
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add layout package for writing and loading signatures from disk (#1040)
* 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
authored
Nov 19, 2021
1 parent
9cf8c3f
commit f8f0f6d
Showing
9 changed files
with
356 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"}, | ||
)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.