Skip to content

Commit

Permalink
Add cosign save and cosign load commands (#1094)
Browse files Browse the repository at this point in the history
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 <priyawadhwa@google.com>
  • Loading branch information
priyawadhwa committed Nov 22, 2021
1 parent e1141af commit 7ec91a4
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 0 deletions.
2 changes: 2 additions & 0 deletions cmd/cosign/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
59 changes: 59 additions & 0 deletions cmd/cosign/cli/load.go
Original file line number Diff line number Diff line change
@@ -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 <path to directory> <IMAGE>`,
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)
}
34 changes: 34 additions & 0 deletions cmd/cosign/cli/options/load.go
Original file line number Diff line number Diff line change
@@ -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")
}
34 changes: 34 additions & 0 deletions cmd/cosign/cli/options/save.go
Original file line number Diff line number Diff line change
@@ -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")
}
57 changes: 57 additions & 0 deletions cmd/cosign/cli/save.go
Original file line number Diff line number Diff line change
@@ -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 <path to directory> <IMAGE>`,
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)
}
2 changes: 2 additions & 0 deletions doc/cosign.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions doc/cosign_load.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions doc/cosign_save.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions pkg/oci/remote/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
28 changes: 28 additions & 0 deletions test/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 7ec91a4

Please sign in to comment.