diff --git a/pkg/oci/remote/layer.go b/pkg/oci/internal/signature/layer.go similarity index 89% rename from pkg/oci/remote/layer.go rename to pkg/oci/internal/signature/layer.go index 02614a480e0..9091cc57ecd 100644 --- a/pkg/oci/remote/layer.go +++ b/pkg/oci/internal/signature/layer.go @@ -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" @@ -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 diff --git a/pkg/oci/remote/layer_test.go b/pkg/oci/internal/signature/layer_test.go similarity index 98% rename from pkg/oci/remote/layer_test.go rename to pkg/oci/internal/signature/layer_test.go index 5eca7971253..43f4eef17f7 100644 --- a/pkg/oci/remote/layer_test.go +++ b/pkg/oci/internal/signature/layer_test.go @@ -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" @@ -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 { diff --git a/pkg/oci/layout/index.go b/pkg/oci/layout/index.go new file mode 100644 index 00000000000..d71482548ae --- /dev/null +++ b/pkg/oci/layout/index.go @@ -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 +} diff --git a/pkg/oci/layout/signatures.go b/pkg/oci/layout/signatures.go new file mode 100644 index 00000000000..c95388082b5 --- /dev/null +++ b/pkg/oci/layout/signatures.go @@ -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 +} diff --git a/pkg/oci/layout/write.go b/pkg/oci/layout/write.go new file mode 100644 index 00000000000..c9d56308cc6 --- /dev/null +++ b/pkg/oci/layout/write.go @@ -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"}, + )) +} diff --git a/pkg/oci/layout/write_test.go b/pkg/oci/layout/write_test.go new file mode 100644 index 00000000000..e0e913148e7 --- /dev/null +++ b/pkg/oci/layout/write_test.go @@ -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) + } +} diff --git a/pkg/oci/remote/remote.go b/pkg/oci/remote/remote.go index ab4c63b77e8..9644ecf0d78 100644 --- a/pkg/oci/remote/remote.go +++ b/pkg/oci/remote/remote.go @@ -26,13 +26,6 @@ import ( "github.com/sigstore/cosign/pkg/oci" ) -const ( - sigkey = "dev.cosignproject.cosign/signature" - certkey = "dev.sigstore.cosign/certificate" - chainkey = "dev.sigstore.cosign/chain" - BundleKey = "dev.sigstore.cosign/bundle" -) - // These enable mocking for unit testing without faking an entire registry. var ( remoteImage = remote.Image diff --git a/pkg/oci/remote/remote_test.go b/pkg/oci/remote/remote_test.go index 17e4fb1c527..885a5e1b8ec 100644 --- a/pkg/oci/remote/remote_test.go +++ b/pkg/oci/remote/remote_test.go @@ -16,7 +16,6 @@ package remote import ( - "encoding/base64" "testing" "github.com/google/go-containerregistry/pkg/name" @@ -25,14 +24,6 @@ import ( "github.com/pkg/errors" ) -func mustDecode(s string) []byte { - b, err := base64.StdEncoding.DecodeString(s) - if err != nil { - panic(err.Error()) - } - return b -} - func TestTagMethods(t *testing.T) { rg := remoteGet defer func() { diff --git a/pkg/oci/remote/signatures.go b/pkg/oci/remote/signatures.go index a17064ad7a2..9f535a8d9ff 100644 --- a/pkg/oci/remote/signatures.go +++ b/pkg/oci/remote/signatures.go @@ -24,6 +24,7 @@ import ( "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/oci" "github.com/sigstore/cosign/pkg/oci/empty" + "github.com/sigstore/cosign/pkg/oci/internal/signature" ) // Signatures fetches the signatures image represented by the named reference. @@ -63,10 +64,7 @@ func (s *sigs) Get() ([]oci.Signature, error) { if err != nil { return nil, err } - signatures = append(signatures, &sigLayer{ - Layer: layer, - desc: desc, - }) + signatures = append(signatures, signature.New(layer, desc)) } return signatures, nil }