From 7ec91a449a605cb4c26a54d7559dd50f764c8307 Mon Sep 17 00:00:00 2001 From: priyawadhwa Date: Mon, 22 Nov 2021 23:23:42 +0000 Subject: [PATCH] Add `cosign save` and `cosign load` commands (#1094) These commands allow users to store an image with associated signatures to disk (cosign save) and then take them from disk and load them into a remote registry (cosign load). Currently this only works with signatures, support for attestations and attachments still needs to be added. Right now, the image index is only used as an implementation details. Future work should allow saving and loading the entire image index, including the `index.json`. Signed-off-by: Priya Wadhwa --- cmd/cosign/cli/commands.go | 2 ++ cmd/cosign/cli/load.go | 59 ++++++++++++++++++++++++++++++++++ cmd/cosign/cli/options/load.go | 34 ++++++++++++++++++++ cmd/cosign/cli/options/save.go | 34 ++++++++++++++++++++ cmd/cosign/cli/save.go | 57 ++++++++++++++++++++++++++++++++ doc/cosign.md | 2 ++ doc/cosign_load.md | 37 +++++++++++++++++++++ doc/cosign_save.md | 37 +++++++++++++++++++++ pkg/oci/remote/write.go | 31 ++++++++++++++++++ test/e2e_test.go | 28 ++++++++++++++++ 10 files changed, 321 insertions(+) create mode 100644 cmd/cosign/cli/load.go create mode 100644 cmd/cosign/cli/options/load.go create mode 100644 cmd/cosign/cli/options/save.go create mode 100644 cmd/cosign/cli/save.go create mode 100644 doc/cosign_load.md create mode 100644 doc/cosign_save.md diff --git a/cmd/cosign/cli/commands.go b/cmd/cosign/cli/commands.go index 75019f6062b..c2acb6d361c 100644 --- a/cmd/cosign/cli/commands.go +++ b/cmd/cosign/cli/commands.go @@ -68,11 +68,13 @@ func New() *cobra.Command { cmd.AddCommand(Generate()) cmd.AddCommand(GenerateKeyPair()) cmd.AddCommand(Initialize()) + cmd.AddCommand(Load()) cmd.AddCommand(Manifest()) cmd.AddCommand(PIVTool()) cmd.AddCommand(PKCS11Tool()) cmd.AddCommand(Policy()) cmd.AddCommand(PublicKey()) + cmd.AddCommand(Save()) cmd.AddCommand(Sign()) cmd.AddCommand(SignBlob()) cmd.AddCommand(Upload()) diff --git a/cmd/cosign/cli/load.go b/cmd/cosign/cli/load.go new file mode 100644 index 00000000000..1a2a6db7117 --- /dev/null +++ b/cmd/cosign/cli/load.go @@ -0,0 +1,59 @@ +// +// 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 cli + +import ( + "context" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/pkg/errors" + "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/pkg/oci/layout" + "github.com/sigstore/cosign/pkg/oci/remote" + "github.com/spf13/cobra" +) + +func Load() *cobra.Command { + o := &options.LoadOptions{} + + cmd := &cobra.Command{ + Use: "load", + Short: "Load a signed image on disk to a remote registry", + Long: "Load a signed image on disk to a remote registry", + Example: ` cosign load --dir `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return LoadCmd(cmd.Context(), *o, args[0]) + }, + } + + o.AddFlags(cmd) + return cmd +} + +func LoadCmd(ctx context.Context, opts options.LoadOptions, imageRef string) error { + ref, err := name.ParseReference(imageRef) + if err != nil { + return errors.Wrapf(err, "parsing image name %s", imageRef) + } + + // get the signed image from disk + sii, err := layout.SignedImageIndex(opts.Directory) + if err != nil { + return errors.Wrap(err, "signed image index") + } + return remote.WriteSignedImageIndexImages(ref, sii) +} diff --git a/cmd/cosign/cli/options/load.go b/cmd/cosign/cli/options/load.go new file mode 100644 index 00000000000..4a6e9e9db94 --- /dev/null +++ b/cmd/cosign/cli/options/load.go @@ -0,0 +1,34 @@ +// +// 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 options + +import ( + "github.com/spf13/cobra" +) + +// LoadOptions is the top level wrapper for the load command. +type LoadOptions struct { + Directory string +} + +var _ Interface = (*LoadOptions)(nil) + +// AddFlags implements Interface +func (o *LoadOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Directory, "dir", "", + "path to directory where the signed image is stored on disk") + _ = cmd.MarkFlagRequired("dir") +} diff --git a/cmd/cosign/cli/options/save.go b/cmd/cosign/cli/options/save.go new file mode 100644 index 00000000000..c44556b2d2c --- /dev/null +++ b/cmd/cosign/cli/options/save.go @@ -0,0 +1,34 @@ +// +// 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 options + +import ( + "github.com/spf13/cobra" +) + +// SaveOptions is the top level wrapper for the load command. +type SaveOptions struct { + Directory string +} + +var _ Interface = (*SaveOptions)(nil) + +// AddFlags implements Interface +func (o *SaveOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Directory, "dir", "", + "path to dir where the signed image should be stored on disk") + _ = cmd.MarkFlagRequired("dir") +} diff --git a/cmd/cosign/cli/save.go b/cmd/cosign/cli/save.go new file mode 100644 index 00000000000..f3d1f1ed075 --- /dev/null +++ b/cmd/cosign/cli/save.go @@ -0,0 +1,57 @@ +// +// 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 cli + +import ( + "context" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/pkg/errors" + "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/pkg/oci/layout" + ociremote "github.com/sigstore/cosign/pkg/oci/remote" + "github.com/spf13/cobra" +) + +func Save() *cobra.Command { + o := &options.SaveOptions{} + + cmd := &cobra.Command{ + Use: "save", + Short: "Save the container image and associated signatures to disk at the specified directory.", + Long: "Save the container image and associated signatures to disk at the specified directory.", + Example: ` cosign save --dir `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return SaveCmd(cmd.Context(), *o, args[0]) + }, + } + + o.AddFlags(cmd) + return cmd +} + +func SaveCmd(ctx context.Context, opts options.SaveOptions, imageRef string) error { + ref, err := name.ParseReference(imageRef) + if err != nil { + return errors.Wrapf(err, "parsing image name %s", imageRef) + } + si, err := ociremote.SignedImage(ref) + if err != nil { + return errors.Wrap(err, "getting signed image") + } + return layout.WriteSignedImage(opts.Directory, si) +} diff --git a/doc/cosign.md b/doc/cosign.md index 305b90562f4..9e1453f463a 100644 --- a/doc/cosign.md +++ b/doc/cosign.md @@ -23,11 +23,13 @@ * [cosign generate](cosign_generate.md) - Generates (unsigned) signature payloads from the supplied container image. * [cosign generate-key-pair](cosign_generate-key-pair.md) - Generates a key-pair. * [cosign initialize](cosign_initialize.md) - Initializes SigStore root to retrieve trusted certificate and key targets for verification. +* [cosign load](cosign_load.md) - Load a signed image on disk to a remote registry * [cosign manifest](cosign_manifest.md) - Provides utilities for discovering images in and performing operations on Kubernetes manifests * [cosign piv-tool](cosign_piv-tool.md) - Provides utilities for managing a hardware token * [cosign pkcs11-tool](cosign_pkcs11-tool.md) - Provides utilities for retrieving information from a PKCS11 token. * [cosign policy](cosign_policy.md) - subcommand to manage a keyless policy. * [cosign public-key](cosign_public-key.md) - Gets a public key from the key-pair. +* [cosign save](cosign_save.md) - Save the container image and associated signatures to disk at the specified directory. * [cosign sign](cosign_sign.md) - Sign the supplied container image. * [cosign sign-blob](cosign_sign-blob.md) - Sign the supplied blob, outputting the base64-encoded signature to stdout. * [cosign triangulate](cosign_triangulate.md) - Outputs the located cosign image reference. This is the location cosign stores the specified artifact type. diff --git a/doc/cosign_load.md b/doc/cosign_load.md new file mode 100644 index 00000000000..29fb15059f2 --- /dev/null +++ b/doc/cosign_load.md @@ -0,0 +1,37 @@ +## cosign load + +Load a signed image on disk to a remote registry + +### Synopsis + +Load a signed image on disk to a remote registry + +``` +cosign load [flags] +``` + +### Examples + +``` + cosign load --dir +``` + +### Options + +``` + --dir string path to directory where the signed image is stored on disk + -h, --help help for load +``` + +### Options inherited from parent commands + +``` + --azure-container-registry-config string Path to the file containing Azure container registry configuration information. + --output-file string log output to a file + -d, --verbose log debug output +``` + +### SEE ALSO + +* [cosign](cosign.md) - + diff --git a/doc/cosign_save.md b/doc/cosign_save.md new file mode 100644 index 00000000000..47912c8e798 --- /dev/null +++ b/doc/cosign_save.md @@ -0,0 +1,37 @@ +## cosign save + +Save the container image and associated signatures to disk at the specified directory. + +### Synopsis + +Save the container image and associated signatures to disk at the specified directory. + +``` +cosign save [flags] +``` + +### Examples + +``` + cosign save --dir +``` + +### Options + +``` + --dir string path to dir where the signed image should be stored on disk + -h, --help help for save +``` + +### Options inherited from parent commands + +``` + --azure-container-registry-config string Path to the file containing Azure container registry configuration information. + --output-file string log output to a file + -d, --verbose log debug output +``` + +### SEE ALSO + +* [cosign](cosign.md) - + diff --git a/pkg/oci/remote/write.go b/pkg/oci/remote/write.go index 3d6719077f9..f1f60a04a87 100644 --- a/pkg/oci/remote/write.go +++ b/pkg/oci/remote/write.go @@ -17,9 +17,40 @@ package remote import ( "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/oci" ) +// WriteSignedImageIndexImages writes the images within the image index +// This includes the signed image and associated signatures in the image index +// TODO (priyawadhwa@): write the `index.json` itself to the repo as well +// TODO (priyawadhwa@): write the attestations +func WriteSignedImageIndexImages(ref name.Reference, sii oci.SignedImageIndex, opts ...Option) error { + repo := ref.Context() + o := makeOptions(repo, opts...) + + // write the image + si, err := sii.SignedImage(v1.Hash{}) + if err != nil { + return err + } + if err := remoteWrite(ref, si, o.ROpt...); err != nil { + return err + } + + // write the signatures + sigs, err := sii.Signatures() + if err != nil { + return err + } + sigsTag, err := SignatureTag(ref, opts...) + if err != nil { + return errors.Wrap(err, "sigs tag") + } + return remoteWrite(sigsTag, sigs, o.ROpt...) +} + // WriteSignature publishes the signatures attached to the given entity // into the provided repository. func WriteSignatures(repo name.Repository, se oci.SignedEntity, opts ...Option) error { diff --git a/test/e2e_test.go b/test/e2e_test.go index 171f84b5742..174201b4733 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -611,6 +611,34 @@ func TestUploadBlob(t *testing.T) { } } +func TestSaveLoad(t *testing.T) { + repo, stop := reg(t) + defer stop() + keysDir := t.TempDir() + + imgName := path.Join(repo, "save-load") + + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + _, privKeyPath, pubKeyPath := keypair(t, keysDir) + + ctx := context.Background() + // Now sign the image and verify it + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, ""), t) + must(verify(pubKeyPath, imgName, true, nil, ""), t) + + // save the image to a temp dir + imageDir := t.TempDir() + must(cli.SaveCmd(ctx, options.SaveOptions{Directory: imageDir}, imgName), t) + + // load the image from the temp dir into a new image and verify the new image + imgName2 := path.Join(repo, "save-load-2") + must(cli.LoadCmd(ctx, options.LoadOptions{Directory: imageDir}, imgName2), t) + must(verify(pubKeyPath, imgName2, true, nil, ""), t) +} + func TestAttachSBOM(t *testing.T) { repo, stop := reg(t) defer stop()