From 96d215421c0a7b4f5747dcd386a16b61edd63e42 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Thu, 11 Nov 2021 19:59:33 +0000 Subject: [PATCH 1/7] Add layout package for writing and loading signatures from disk Signed-off-by: Priya Wadhwa --- pkg/oci/layout/image.go | 107 ++++++++++++++++++++++++++++++++++++ pkg/oci/layout/layer.go | 116 ++++++++++++++++++++++++++++++++++++++++ pkg/oci/layout/write.go | 74 +++++++++++++++++++++++++ 3 files changed, 297 insertions(+) create mode 100644 pkg/oci/layout/image.go create mode 100644 pkg/oci/layout/layer.go create mode 100644 pkg/oci/layout/write.go diff --git a/pkg/oci/layout/image.go b/pkg/oci/layout/image.go new file mode 100644 index 00000000000..989907b4fa7 --- /dev/null +++ b/pkg/oci/layout/image.go @@ -0,0 +1,107 @@ +// +// 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" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/sigstore/cosign/pkg/oci" +) + +// SignedImage provides access to a remote image reference, and its signatures. +func SignedImage(path string) (oci.SignedImage, error) { + p, err := layout.FromPath(imagePath(path)) + if err != nil { + return nil, err + } + img, err := p.Image(v1.Hash{}) + if err != nil { + return nil, err + } + + return &image{ + Image: img, + path: path, + }, nil +} + +type image struct { + path string + v1.Image +} + +var _ oci.SignedImage = (*image)(nil) + +type sigs struct { + v1.Image +} + +var _ oci.Signatures = (*sigs)(nil) + +// Get implements oci.Signatures +func (s *sigs) Get() ([]oci.Signature, error) { + layers, err := s.Image.Layers() + if err != nil { + return nil, err + } + var signatures []oci.Signature + for _, l := range layers { + d, err := partial.Descriptor(l) + if err != nil { + return nil, err + } + if d == nil { + continue + } + // convert descriptor to oci.Signature + signatures = append(signatures, &sigLayer{ + Layer: l, + img: s, + desc: *d, + }) + } + return signatures, nil +} + +// Signatures implements oci.SignedImage +func (i *image) Signatures() (oci.Signatures, error) { + sigPath, err := layout.FromPath(signaturesPath(i.path)) + if err != nil { + return nil, err + } + img, err := sigPath.Image(v1.Hash{}) + if err != nil { + return nil, err + } + return &sigs{ + Image: img, + }, nil +} + +// Attestations implements oci.SignedImage +// TODO (priyawadhwa@) +func (i *image) Attestations() (oci.Signatures, error) { + return nil, fmt.Errorf("not yet implemented") +} + +// Attestations implements oci.SignedImage +// TODO (priyawadhwa@) +func (i *image) Attachment(name string) (oci.File, error) { + return nil, fmt.Errorf("not yet implemented") +} diff --git a/pkg/oci/layout/layer.go b/pkg/oci/layout/layer.go new file mode 100644 index 00000000000..a5df9dbb2db --- /dev/null +++ b/pkg/oci/layout/layer.go @@ -0,0 +1,116 @@ +// +// 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 ( + "crypto/x509" + "encoding/json" + "fmt" + "io" + "strings" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/pkg/errors" + "github.com/sigstore/cosign/pkg/oci" + "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 + img *sigs + desc v1.Descriptor +} + +var _ oci.Signature = (*sigLayer)(nil) + +// Annotations implements oci.Signature +func (s *sigLayer) Annotations() (map[string]string, error) { + return s.desc.Annotations, nil +} + +// Payload implements oci.Signature +func (s *sigLayer) Payload() ([]byte, error) { + l, err := s.img.LayerByDigest(s.desc.Digest) + if err != nil { + return nil, err + } + + // Compressed is a misnomer here, we just want the raw bytes from the registry. + r, err := l.Compressed() + if err != nil { + return nil, err + } + payload, err := io.ReadAll(r) + if err != nil { + return nil, err + } + return payload, nil +} + +// Base64Signature implements oci.Signature +func (s *sigLayer) Base64Signature() (string, error) { + b64sig, ok := s.desc.Annotations[sigkey] + if !ok { + return "", fmt.Errorf("signature layer %s is missing %q annotation", s.desc.Digest, sigkey) + } + return b64sig, nil +} + +// Cert implements oci.Signature +func (s *sigLayer) Cert() (*x509.Certificate, error) { + certPEM := s.desc.Annotations[certkey] + if certPEM == "" { + return nil, nil + } + certs, err := cryptoutils.LoadCertificatesFromPEM(strings.NewReader(certPEM)) + if err != nil { + return nil, err + } + return certs[0], nil +} + +// Chain implements oci.Signature +func (s *sigLayer) Chain() ([]*x509.Certificate, error) { + chainPEM := s.desc.Annotations[chainkey] + if chainPEM == "" { + return nil, nil + } + certs, err := cryptoutils.LoadCertificatesFromPEM(strings.NewReader(chainPEM)) + if err != nil { + return nil, err + } + return certs, nil +} + +// Bundle implements oci.Signature +func (s *sigLayer) Bundle() (*oci.Bundle, error) { + bundle := s.desc.Annotations[BundleKey] + if bundle == "" { + return nil, nil + } + var b oci.Bundle + if err := json.Unmarshal([]byte(bundle), &b); err != nil { + return nil, errors.Wrap(err, "unmarshaling bundle") + } + return &b, nil +} diff --git a/pkg/oci/layout/write.go b/pkg/oci/layout/write.go new file mode 100644 index 00000000000..13f1c98af10 --- /dev/null +++ b/pkg/oci/layout/write.go @@ -0,0 +1,74 @@ +// +// 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 ( + "path/filepath" + + ociremote "github.com/sigstore/cosign/pkg/oci/remote" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/pkg/errors" +) + +func WriteSignedImage(path string, ref name.Reference) error { + // First, write the image + if err := write(path, imagePath, ref); err != nil { + return errors.Wrap(err, "writing image") + } + // Then, write the signatures + sigRef, err := ociremote.SignatureTag(ref) + if err != nil { + return err + } + if err := write(path, signaturesPath, sigRef); err != nil { + return errors.Wrap(err, "writing signatures") + } + // TODO (priyawadhwa@) write attestations and attachments + return nil +} + +func imagePath(path string) string { + return filepath.Join(path, "image") +} + +func signaturesPath(path string) string { + return filepath.Join(path, "signatures") +} + +type pathFunc func(string) string + +func write(path string, pf pathFunc, ref name.Reference) error { + p := pf(path) + // write empty image + layoutPath, err := layout.Write(p, empty.Index) + if err != nil { + return err + } + // get the image + img, err := remote.Image(ref) + if err != nil { + return err + } + // write image to disk + if err := layoutPath.AppendImage(img); err != nil { + return err + } + return nil +} From 9a634cb12a7828c5207feba27c28b1ddac85d80f Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Fri, 12 Nov 2021 15:54:53 +0000 Subject: [PATCH 2/7] Create siglayer package, which is used by and Signed-off-by: Priya Wadhwa --- pkg/oci/layout/image.go | 8 +- pkg/oci/layout/write.go | 25 ++-- pkg/oci/remote/layer.go | 109 ------------------ pkg/oci/remote/remote.go | 7 -- pkg/oci/remote/remote_test.go | 16 --- pkg/oci/remote/signatures.go | 7 +- .../{layout/layer.go => siglayer/siglayer.go} | 12 +- .../siglayer_test.go} | 43 ++++++- 8 files changed, 65 insertions(+), 162 deletions(-) delete mode 100644 pkg/oci/remote/layer.go rename pkg/oci/{layout/layer.go => siglayer/siglayer.go} (94%) rename pkg/oci/{remote/layer_test.go => siglayer/siglayer_test.go} (93%) diff --git a/pkg/oci/layout/image.go b/pkg/oci/layout/image.go index 989907b4fa7..ae95644e098 100644 --- a/pkg/oci/layout/image.go +++ b/pkg/oci/layout/image.go @@ -22,6 +22,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/partial" "github.com/sigstore/cosign/pkg/oci" + "github.com/sigstore/cosign/pkg/oci/siglayer" ) // SignedImage provides access to a remote image reference, and its signatures. @@ -69,12 +70,7 @@ func (s *sigs) Get() ([]oci.Signature, error) { if d == nil { continue } - // convert descriptor to oci.Signature - signatures = append(signatures, &sigLayer{ - Layer: l, - img: s, - desc: *d, - }) + signatures = append(signatures, siglayer.New(l, s, *d)) } return signatures, nil } diff --git a/pkg/oci/layout/write.go b/pkg/oci/layout/write.go index 13f1c98af10..7ebd0707ce7 100644 --- a/pkg/oci/layout/write.go +++ b/pkg/oci/layout/write.go @@ -18,26 +18,24 @@ package layout import ( "path/filepath" - ociremote "github.com/sigstore/cosign/pkg/oci/remote" - - "github.com/google/go-containerregistry/pkg/name" + 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/google/go-containerregistry/pkg/v1/remote" "github.com/pkg/errors" + "github.com/sigstore/cosign/pkg/oci" ) -func WriteSignedImage(path string, ref name.Reference) error { +// WriteSignedImage writes the image and all related signatures, attestations and attachments +func WriteSignedImage(path string, si oci.SignedImage) error { // First, write the image - if err := write(path, imagePath, ref); err != nil { + if err := write(path, imagePath, si); err != nil { return errors.Wrap(err, "writing image") } - // Then, write the signatures - sigRef, err := ociremote.SignatureTag(ref) + sigs, err := si.Signatures() if err != nil { - return err + return errors.Wrap(err, "getting signatures") } - if err := write(path, signaturesPath, sigRef); err != nil { + if err := write(path, signaturesPath, sigs); err != nil { return errors.Wrap(err, "writing signatures") } // TODO (priyawadhwa@) write attestations and attachments @@ -54,18 +52,13 @@ func signaturesPath(path string) string { type pathFunc func(string) string -func write(path string, pf pathFunc, ref name.Reference) error { +func write(path string, pf pathFunc, img v1.Image) error { p := pf(path) // write empty image layoutPath, err := layout.Write(p, empty.Index) if err != nil { return err } - // get the image - img, err := remote.Image(ref) - if err != nil { - return err - } // write image to disk if err := layoutPath.AppendImage(img); err != nil { return err diff --git a/pkg/oci/remote/layer.go b/pkg/oci/remote/layer.go deleted file mode 100644 index e5974269d2f..00000000000 --- a/pkg/oci/remote/layer.go +++ /dev/null @@ -1,109 +0,0 @@ -// -// 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 remote - -import ( - "crypto/x509" - "encoding/json" - "fmt" - "io" - "strings" - - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/pkg/errors" - "github.com/sigstore/cosign/pkg/oci" - "github.com/sigstore/sigstore/pkg/cryptoutils" -) - -type sigLayer struct { - v1.Layer - img *sigs - desc v1.Descriptor -} - -var _ oci.Signature = (*sigLayer)(nil) - -// Annotations implements oci.Signature -func (s *sigLayer) Annotations() (map[string]string, error) { - return s.desc.Annotations, nil -} - -// Payload implements oci.Signature -func (s *sigLayer) Payload() ([]byte, error) { - l, err := s.img.LayerByDigest(s.desc.Digest) - if err != nil { - return nil, err - } - - // Compressed is a misnomer here, we just want the raw bytes from the registry. - r, err := l.Compressed() - if err != nil { - return nil, err - } - payload, err := io.ReadAll(r) - if err != nil { - return nil, err - } - return payload, nil -} - -// Base64Signature implements oci.Signature -func (s *sigLayer) Base64Signature() (string, error) { - b64sig, ok := s.desc.Annotations[sigkey] - if !ok { - return "", fmt.Errorf("signature layer %s is missing %q annotation", s.desc.Digest, sigkey) - } - return b64sig, nil -} - -// Cert implements oci.Signature -func (s *sigLayer) Cert() (*x509.Certificate, error) { - certPEM := s.desc.Annotations[certkey] - if certPEM == "" { - return nil, nil - } - certs, err := cryptoutils.LoadCertificatesFromPEM(strings.NewReader(certPEM)) - if err != nil { - return nil, err - } - return certs[0], nil -} - -// Chain implements oci.Signature -func (s *sigLayer) Chain() ([]*x509.Certificate, error) { - chainPEM := s.desc.Annotations[chainkey] - if chainPEM == "" { - return nil, nil - } - certs, err := cryptoutils.LoadCertificatesFromPEM(strings.NewReader(chainPEM)) - if err != nil { - return nil, err - } - return certs, nil -} - -// Bundle implements oci.Signature -func (s *sigLayer) Bundle() (*oci.Bundle, error) { - bundle := s.desc.Annotations[BundleKey] - if bundle == "" { - return nil, nil - } - var b oci.Bundle - if err := json.Unmarshal([]byte(bundle), &b); err != nil { - return nil, errors.Wrap(err, "unmarshaling bundle") - } - return &b, nil -} 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 b070d8c518b..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,21 +24,6 @@ import ( "github.com/pkg/errors" ) -func must(img v1.Image, err error) v1.Image { - if err != nil { - panic(err.Error()) - } - return img -} - -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 e2b7000f46c..da4c1d990e8 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/siglayer" ) // Signatures fetches the signatures image represented by the named reference. @@ -63,11 +64,7 @@ func (s *sigs) Get() ([]oci.Signature, error) { if err != nil { return nil, err } - signatures = append(signatures, &sigLayer{ - Layer: layer, - img: s, - desc: desc, - }) + signatures = append(signatures, siglayer.New(layer, s, desc)) } return signatures, nil } diff --git a/pkg/oci/layout/layer.go b/pkg/oci/siglayer/siglayer.go similarity index 94% rename from pkg/oci/layout/layer.go rename to pkg/oci/siglayer/siglayer.go index a5df9dbb2db..28a0e127fa1 100644 --- a/pkg/oci/layout/layer.go +++ b/pkg/oci/siglayer/siglayer.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package layout +package siglayer import ( "crypto/x509" @@ -37,10 +37,18 @@ const ( type sigLayer struct { v1.Layer - img *sigs + img oci.Signatures desc v1.Descriptor } +func New(l v1.Layer, img oci.Signatures, desc v1.Descriptor) *sigLayer { + return &sigLayer{ + Layer: l, + img: img, + desc: desc, + } +} + var _ oci.Signature = (*sigLayer)(nil) // Annotations implements oci.Signature diff --git a/pkg/oci/remote/layer_test.go b/pkg/oci/siglayer/siglayer_test.go similarity index 93% rename from pkg/oci/remote/layer_test.go rename to pkg/oci/siglayer/siglayer_test.go index d4523a8aea4..70cf527fe4d 100644 --- a/pkg/oci/remote/layer_test.go +++ b/pkg/oci/siglayer/siglayer_test.go @@ -13,10 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package remote +package siglayer import ( "bytes" + "encoding/base64" "fmt" "testing" @@ -30,6 +31,46 @@ import ( ociempty "github.com/sigstore/cosign/pkg/oci/empty" ) +type sigs struct { + v1.Image +} + +// Get implements oci.Signatures +func (s *sigs) Get() ([]oci.Signature, error) { + m, err := s.Manifest() + if err != nil { + return nil, err + } + signatures := make([]oci.Signature, 0, len(m.Layers)) + for _, desc := range m.Layers { + layer, err := s.Image.LayerByDigest(desc.Digest) + if err != nil { + return nil, err + } + signatures = append(signatures, &sigLayer{ + Layer: layer, + img: s, + desc: desc, + }) + } + return signatures, nil +} + +func must(img v1.Image, err error) v1.Image { + if err != nil { + panic(err.Error()) + } + return img +} + +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 { From e104ca460c59688004cfa7f785f9be9a7f1c6079 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Fri, 12 Nov 2021 18:42:11 +0000 Subject: [PATCH 3/7] Address code review comments Signed-off-by: Priya Wadhwa --- .../{siglayer/siglayer.go => internal/signature/layer.go} | 4 ++-- .../siglayer_test.go => internal/signature/layer_test.go} | 2 +- pkg/oci/layout/image.go | 8 +++++--- pkg/oci/layout/write.go | 7 ++----- pkg/oci/remote/signatures.go | 4 ++-- 5 files changed, 12 insertions(+), 13 deletions(-) rename pkg/oci/{siglayer/siglayer.go => internal/signature/layer.go} (96%) rename pkg/oci/{siglayer/siglayer_test.go => internal/signature/layer_test.go} (99%) diff --git a/pkg/oci/siglayer/siglayer.go b/pkg/oci/internal/signature/layer.go similarity index 96% rename from pkg/oci/siglayer/siglayer.go rename to pkg/oci/internal/signature/layer.go index 28a0e127fa1..051ef309b89 100644 --- a/pkg/oci/siglayer/siglayer.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 siglayer +package signature import ( "crypto/x509" @@ -41,7 +41,7 @@ type sigLayer struct { desc v1.Descriptor } -func New(l v1.Layer, img oci.Signatures, desc v1.Descriptor) *sigLayer { +func New(l v1.Layer, img oci.Signatures, desc v1.Descriptor) oci.Signature { return &sigLayer{ Layer: l, img: img, diff --git a/pkg/oci/siglayer/siglayer_test.go b/pkg/oci/internal/signature/layer_test.go similarity index 99% rename from pkg/oci/siglayer/siglayer_test.go rename to pkg/oci/internal/signature/layer_test.go index 70cf527fe4d..e4c5d1d1417 100644 --- a/pkg/oci/siglayer/siglayer_test.go +++ b/pkg/oci/internal/signature/layer_test.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package siglayer +package signature import ( "bytes" diff --git a/pkg/oci/layout/image.go b/pkg/oci/layout/image.go index ae95644e098..0cfdc6e6a68 100644 --- a/pkg/oci/layout/image.go +++ b/pkg/oci/layout/image.go @@ -22,7 +22,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/partial" "github.com/sigstore/cosign/pkg/oci" - "github.com/sigstore/cosign/pkg/oci/siglayer" + "github.com/sigstore/cosign/pkg/oci/internal/signature" ) // SignedImage provides access to a remote image reference, and its signatures. @@ -31,6 +31,7 @@ func SignedImage(path string) (oci.SignedImage, error) { if err != nil { return nil, err } + // there should only be one image in the index, so we can pass in empty hash to get it img, err := p.Image(v1.Hash{}) if err != nil { return nil, err @@ -61,7 +62,7 @@ func (s *sigs) Get() ([]oci.Signature, error) { if err != nil { return nil, err } - var signatures []oci.Signature + signatures := make([]oci.Signature, 0, len(layers)) for _, l := range layers { d, err := partial.Descriptor(l) if err != nil { @@ -70,7 +71,7 @@ func (s *sigs) Get() ([]oci.Signature, error) { if d == nil { continue } - signatures = append(signatures, siglayer.New(l, s, *d)) + signatures = append(signatures, signature.New(l, s, *d)) } return signatures, nil } @@ -81,6 +82,7 @@ func (i *image) Signatures() (oci.Signatures, error) { if err != nil { return nil, err } + // there should only be one image in the index, so we can pass in empty hash to get it img, err := sigPath.Image(v1.Hash{}) if err != nil { return nil, err diff --git a/pkg/oci/layout/write.go b/pkg/oci/layout/write.go index 7ebd0707ce7..932ff2c43d5 100644 --- a/pkg/oci/layout/write.go +++ b/pkg/oci/layout/write.go @@ -47,7 +47,7 @@ func imagePath(path string) string { } func signaturesPath(path string) string { - return filepath.Join(path, "signatures") + return filepath.Join(path, "sigs") } type pathFunc func(string) string @@ -60,8 +60,5 @@ func write(path string, pf pathFunc, img v1.Image) error { return err } // write image to disk - if err := layoutPath.AppendImage(img); err != nil { - return err - } - return nil + return layoutPath.AppendImage(img) } diff --git a/pkg/oci/remote/signatures.go b/pkg/oci/remote/signatures.go index da4c1d990e8..250b3cbd08c 100644 --- a/pkg/oci/remote/signatures.go +++ b/pkg/oci/remote/signatures.go @@ -24,7 +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/siglayer" + "github.com/sigstore/cosign/pkg/oci/internal/signature" ) // Signatures fetches the signatures image represented by the named reference. @@ -64,7 +64,7 @@ func (s *sigs) Get() ([]oci.Signature, error) { if err != nil { return nil, err } - signatures = append(signatures, siglayer.New(layer, s, desc)) + signatures = append(signatures, signature.New(layer, s, desc)) } return signatures, nil } From daf3f4391e9c8df460f504a643c91df3c1b61ac9 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Mon, 15 Nov 2021 17:16:59 +0000 Subject: [PATCH 4/7] Get signatures by layer rather than using partialg Signed-off-by: Priya Wadhwa --- pkg/oci/layout/image.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pkg/oci/layout/image.go b/pkg/oci/layout/image.go index d5ca28897b4..dff0394ef70 100644 --- a/pkg/oci/layout/image.go +++ b/pkg/oci/layout/image.go @@ -20,7 +20,6 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/layout" - "github.com/google/go-containerregistry/pkg/v1/partial" "github.com/sigstore/cosign/pkg/oci" "github.com/sigstore/cosign/pkg/oci/internal/signature" ) @@ -58,20 +57,17 @@ var _ oci.Signatures = (*sigs)(nil) // Get implements oci.Signatures func (s *sigs) Get() ([]oci.Signature, error) { - layers, err := s.Image.Layers() + manifest, err := s.Image.Manifest() if err != nil { return nil, err } - signatures := make([]oci.Signature, 0, len(layers)) - for _, l := range layers { - d, err := partial.Descriptor(l) + 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 } - if d == nil { - continue - } - signatures = append(signatures, signature.New(l, *d)) + signatures = append(signatures, signature.New(l, desc)) } return signatures, nil } From d1b6d0774a9b1d669bedf63632647d0d901a2415 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Mon, 15 Nov 2021 17:29:03 +0000 Subject: [PATCH 5/7] Add unit test for reading and writing from disk Signed-off-by: Priya Wadhwa --- pkg/oci/layout/write_test.go | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 pkg/oci/layout/write_test.go diff --git a/pkg/oci/layout/write_test.go b/pkg/oci/layout/write_test.go new file mode 100644 index 00000000000..fa47b518aa0 --- /dev/null +++ b/pkg/oci/layout/write_test.go @@ -0,0 +1,83 @@ +// +// 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-containerregistry/pkg/v1/random" + "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 + 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) + } + } + + tmp := t.TempDir() + if err := WriteSignedImage(tmp, si); err != nil { + t.Fatal(err) + } + // read the image and make sure the signatures exist + image, err := SignedImage(tmp) + if err != nil { + t.Fatal(err) + } + sigImage, err := image.Signatures() + if err != nil { + t.Fatal(err) + } + sigs, err := sigImage.Get() + if err != nil { + t.Fatal(err) + } + 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") + } + } +} From f8ffe71e33d2f7864ce3acae01da6efe5f9cf74b Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Tue, 16 Nov 2021 15:19:38 +0000 Subject: [PATCH 6/7] Use ImageIndex to store image related signatures etc Signed-off-by: Priya Wadhwa --- pkg/oci/layout/image.go | 101 ------------------------------- pkg/oci/layout/index.go | 113 +++++++++++++++++++++++++++++++++++ pkg/oci/layout/signatures.go | 45 ++++++++++++++ pkg/oci/layout/write.go | 41 +++++-------- pkg/oci/layout/write_test.go | 4 +- 5 files changed, 175 insertions(+), 129 deletions(-) delete mode 100644 pkg/oci/layout/image.go create mode 100644 pkg/oci/layout/index.go create mode 100644 pkg/oci/layout/signatures.go diff --git a/pkg/oci/layout/image.go b/pkg/oci/layout/image.go deleted file mode 100644 index dff0394ef70..00000000000 --- a/pkg/oci/layout/image.go +++ /dev/null @@ -1,101 +0,0 @@ -// -// 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" - - 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/internal/signature" -) - -// SignedImage provides access to a remote image reference, and its signatures. -func SignedImage(path string) (oci.SignedImage, error) { - p, err := layout.FromPath(imagePath(path)) - if err != nil { - return nil, err - } - // there should only be one image in the index, so we can pass in empty hash to get it - img, err := p.Image(v1.Hash{}) - if err != nil { - return nil, err - } - - return &image{ - Image: img, - path: path, - }, nil -} - -type image struct { - path string - v1.Image -} - -var _ oci.SignedImage = (*image)(nil) - -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 -} - -// Signatures implements oci.SignedImage -func (i *image) Signatures() (oci.Signatures, error) { - sigPath, err := layout.FromPath(signaturesPath(i.path)) - if err != nil { - return nil, err - } - // there should only be one image in the index, so we can pass in empty hash to get it - img, err := sigPath.Image(v1.Hash{}) - if err != nil { - return nil, err - } - return &sigs{ - Image: img, - }, nil -} - -// Attestations implements oci.SignedImage -// TODO (priyawadhwa@) -func (i *image) Attestations() (oci.Signatures, error) { - return nil, fmt.Errorf("not yet implemented") -} - -// Attestations implements oci.SignedImage -// TODO (priyawadhwa@) -func (i *image) Attachment(name string) (oci.File, error) { - return nil, fmt.Errorf("not yet implemented") -} diff --git a/pkg/oci/layout/index.go b/pkg/oci/layout/index.go new file mode 100644 index 00000000000..55714b6f1c9 --- /dev/null +++ b/pkg/oci/layout/index.go @@ -0,0 +1,113 @@ +// +// 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" + + 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 +func (i *index) SignedImage(h v1.Hash) (oci.SignedImage, error) { + 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 { + img, err := i.Image(m.Digest) + if err != nil { + return nil, err + } + return img, nil + } + } + return nil, fmt.Errorf("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 index 932ff2c43d5..c9d56308cc6 100644 --- a/pkg/oci/layout/write.go +++ b/pkg/oci/layout/write.go @@ -16,8 +16,6 @@ package layout import ( - "path/filepath" - v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/layout" @@ -27,38 +25,29 @@ import ( // WriteSignedImage writes the image and all related signatures, attestations and attachments func WriteSignedImage(path string, si oci.SignedImage) error { - // First, write the image - if err := write(path, imagePath, si); err != nil { - return errors.Wrap(err, "writing image") + // 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 := write(path, signaturesPath, sigs); err != nil { - return errors.Wrap(err, "writing signatures") + if err := appendImage(layoutPath, sigs, sigsAnnotation); err != nil { + return errors.Wrap(err, "appending signatures") } // TODO (priyawadhwa@) write attestations and attachments return nil } -func imagePath(path string) string { - return filepath.Join(path, "image") -} - -func signaturesPath(path string) string { - return filepath.Join(path, "sigs") -} - -type pathFunc func(string) string - -func write(path string, pf pathFunc, img v1.Image) error { - p := pf(path) - // write empty image - layoutPath, err := layout.Write(p, empty.Index) - if err != nil { - return err - } - // write image to disk - return layoutPath.AppendImage(img) +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 index fa47b518aa0..e3f4ae934c1 100644 --- a/pkg/oci/layout/write_test.go +++ b/pkg/oci/layout/write_test.go @@ -51,11 +51,11 @@ func TestReadWrite(t *testing.T) { t.Fatal(err) } // read the image and make sure the signatures exist - image, err := SignedImage(tmp) + imageIndex, err := SignedImageIndex(tmp) if err != nil { t.Fatal(err) } - sigImage, err := image.Signatures() + sigImage, err := imageIndex.Signatures() if err != nil { t.Fatal(err) } From add535a0d22e5c32eb1c381257537d2e00eaa2fd Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Wed, 17 Nov 2021 13:12:43 +0000 Subject: [PATCH 7/7] Code review comments, also support for passing in an empty hash to SignedImage Signed-off-by: Priya Wadhwa --- pkg/oci/layout/index.go | 18 ++++++---- pkg/oci/layout/write_test.go | 69 ++++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/pkg/oci/layout/index.go b/pkg/oci/layout/index.go index 55714b6f1c9..d71482548ae 100644 --- a/pkg/oci/layout/index.go +++ b/pkg/oci/layout/index.go @@ -16,6 +16,7 @@ package layout import ( + "errors" "fmt" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -74,8 +75,15 @@ func (i *index) Attachment(name string) (oci.File, error) { } // 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) { - img, err := i.Image(h) + 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 } @@ -91,14 +99,10 @@ func (i *index) imageByAnnotation(annotation string) (v1.Image, error) { } for _, m := range manifest.Manifests { if _, ok := m.Annotations[annotation]; ok { - img, err := i.Image(m.Digest) - if err != nil { - return nil, err - } - return img, nil + return i.Image(m.Digest) } } - return nil, fmt.Errorf("unable to find image") + return nil, errors.New("unable to find image") } // SignedImageIndex implements oci.SignedImageIndex diff --git a/pkg/oci/layout/write_test.go b/pkg/oci/layout/write_test.go index e3f4ae934c1..e0e913148e7 100644 --- a/pkg/oci/layout/write_test.go +++ b/pkg/oci/layout/write_test.go @@ -19,7 +19,10 @@ 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" @@ -27,34 +30,25 @@ import ( func TestReadWrite(t *testing.T) { // write random signed image to disk - 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) - } - } - + 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) @@ -63,6 +57,7 @@ func TestReadWrite(t *testing.T) { if err != nil { t.Fatal(err) } + want := 6 if len(sigs) != want { t.Fatal("didn't get the expected number of signatures") } @@ -81,3 +76,39 @@ func TestReadWrite(t *testing.T) { } } } + +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) + } +}