-
Notifications
You must be signed in to change notification settings - Fork 545
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
Changes from all commits
96d2154
9a634cb
e104ca4
f5a015d
daf3f43
d1b6d07
f8ffe71
add535a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
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 | ||
} |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the resulting index were pushed, how would it run? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for |
||
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"}, | ||
)) | ||
} |
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) | ||
} | ||
} |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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.