diff --git a/libpod/image/image.go b/libpod/image/image.go index 0900944eb5..62c6a20884 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -177,7 +177,7 @@ func (ir *Runtime) New(ctx context.Context, name, signaturePolicyPath, authfile // SaveImages stores one more images in a multi-image archive. // Note that only `docker-archive` supports storing multiple // image. -func (ir *Runtime) SaveImages(ctx context.Context, namesOrIDs []string, format string, outputFile string, quiet bool) (finalErr error) { +func (ir *Runtime) SaveImages(ctx context.Context, namesOrIDs []string, format string, outputFile string, quiet, removeSignatures bool) (finalErr error) { if format != DockerArchive { return errors.Errorf("multi-image archives are only supported in in the %q format", DockerArchive) } @@ -264,7 +264,7 @@ func (ir *Runtime) SaveImages(ctx context.Context, namesOrIDs []string, format s } img := imageMap[id] - copyOptions := getCopyOptions(sys, writer, nil, nil, SigningOptions{}, "", img.tags) + copyOptions := getCopyOptions(sys, writer, nil, nil, SigningOptions{RemoveSignatures: removeSignatures}, "", img.tags) copyOptions.DestinationCtx.SystemRegistriesConfPath = registries.SystemRegistriesConfPath() // For copying, we need a source reference that we can create @@ -1584,7 +1584,7 @@ func (i *Image) Comment(ctx context.Context, manifestType string) (string, error } // Save writes a container image to the filesystem -func (i *Image) Save(ctx context.Context, source, format, output string, moreTags []string, quiet, compress bool) error { +func (i *Image) Save(ctx context.Context, source, format, output string, moreTags []string, quiet, compress, removeSignatures bool) error { var ( writer io.Writer destRef types.ImageReference @@ -1636,7 +1636,7 @@ func (i *Image) Save(ctx context.Context, source, format, output string, moreTag return err } } - if err := i.PushImageToReference(ctx, destRef, manifestType, "", "", "", writer, compress, SigningOptions{}, &DockerRegistryOptions{}, additionaltags); err != nil { + if err := i.PushImageToReference(ctx, destRef, manifestType, "", "", "", writer, compress, SigningOptions{RemoveSignatures: removeSignatures}, &DockerRegistryOptions{}, additionaltags); err != nil { return errors.Wrapf(err, "unable to save %q", source) } i.newImageEvent(events.Save) diff --git a/pkg/api/handlers/compat/images.go b/pkg/api/handlers/compat/images.go index 3431823bd5..d177b2335d 100644 --- a/pkg/api/handlers/compat/images.go +++ b/pkg/api/handlers/compat/images.go @@ -60,7 +60,7 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile")) return } - if err := newImage.Save(r.Context(), name, "docker-archive", tmpfile.Name(), []string{}, false, false); err != nil { + if err := newImage.Save(r.Context(), name, "docker-archive", tmpfile.Name(), []string{}, false, false, true); err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to save image")) return } @@ -429,7 +429,7 @@ func ExportImages(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile")) return } - if err := runtime.ImageRuntime().SaveImages(r.Context(), images, "docker-archive", tmpfile.Name(), false); err != nil { + if err := runtime.ImageRuntime().SaveImages(r.Context(), images, "docker-archive", tmpfile.Name(), false, true); err != nil { utils.InternalServerError(w, err) return } diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 3fb5d23c8a..598a46abe6 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -206,7 +206,7 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { utils.Error(w, "unknown format", http.StatusInternalServerError, errors.Errorf("unknown format %q", query.Format)) return } - if err := newImage.Save(r.Context(), name, query.Format, output, []string{}, false, query.Compress); err != nil { + if err := newImage.Save(r.Context(), name, query.Format, output, []string{}, false, query.Compress, true); err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err) return } @@ -284,6 +284,7 @@ func ExportImages(w http.ResponseWriter, r *http.Request) { Format: query.Format, MultiImageArchive: true, Output: output, + RemoveSignatures: true, } imageEngine := abi.ImageEngine{Libpod: runtime} diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 982fa0cc05..101542a980 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -300,6 +300,8 @@ type ImageSaveOptions struct { MultiImageArchive bool // Output - write image to the specified path. Output string + // Do not save the signature from the source image + RemoveSignatures bool // Quiet - suppress output when copying images Quiet bool } diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index f9d733c634..25335cf111 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -482,13 +482,13 @@ func (ir *ImageEngine) Import(ctx context.Context, opts entities.ImageImportOpti func (ir *ImageEngine) Save(ctx context.Context, nameOrID string, tags []string, options entities.ImageSaveOptions) error { if options.MultiImageArchive { nameOrIDs := append([]string{nameOrID}, tags...) - return ir.Libpod.ImageRuntime().SaveImages(ctx, nameOrIDs, options.Format, options.Output, options.Quiet) + return ir.Libpod.ImageRuntime().SaveImages(ctx, nameOrIDs, options.Format, options.Output, options.Quiet, true) } newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrID) if err != nil { return err } - return newImage.Save(ctx, nameOrID, options.Format, options.Output, tags, options.Quiet, options.Compress) + return newImage.Save(ctx, nameOrID, options.Format, options.Output, tags, options.Quiet, options.Compress, true) } func (ir *ImageEngine) Diff(_ context.Context, nameOrID string, _ entities.DiffOptions) (*entities.DiffReport, error) { diff --git a/pkg/varlinkapi/images.go b/pkg/varlinkapi/images.go index ef310d5909..af6c43fec7 100644 --- a/pkg/varlinkapi/images.go +++ b/pkg/varlinkapi/images.go @@ -843,7 +843,7 @@ func (i *VarlinkAPI) ImageSave(call iopodman.VarlinkCall, options iopodman.Image saveOutput := bytes.NewBuffer([]byte{}) c := make(chan error) go func() { - err := newImage.Save(getContext(), options.Name, options.Format, output, options.MoreTags, options.Quiet, options.Compress) + err := newImage.Save(getContext(), options.Name, options.Format, output, options.MoreTags, options.Quiet, options.Compress, true) c <- err close(c) }() diff --git a/test/e2e/save_test.go b/test/e2e/save_test.go index 1f1258be38..79fc4d7373 100644 --- a/test/e2e/save_test.go +++ b/test/e2e/save_test.go @@ -1,8 +1,12 @@ package integration import ( + "io/ioutil" "os" + "os/exec" "path/filepath" + "strconv" + "strings" "github.com/containers/podman/v2/pkg/rootless" . "github.com/containers/podman/v2/test/utils" @@ -116,6 +120,71 @@ var _ = Describe("Podman save", func() { Expect(save).To(ExitWithError()) }) + It("podman save remove signature", func() { + SkipIfRootless("FIXME: Need get in rootless push sign") + if podmanTest.Host.Arch == "ppc64le" { + Skip("No registry image for ppc64le") + } + tempGNUPGHOME := filepath.Join(podmanTest.TempDir, "tmpGPG") + err := os.Mkdir(tempGNUPGHOME, os.ModePerm) + Expect(err).To(BeNil()) + origGNUPGHOME := os.Getenv("GNUPGHOME") + err = os.Setenv("GNUPGHOME", tempGNUPGHOME) + Expect(err).To(BeNil()) + defer os.Setenv("GNUPGHOME", origGNUPGHOME) + + port := 5000 + session := podmanTest.Podman([]string{"run", "-d", "--name", "registry", "-p", strings.Join([]string{strconv.Itoa(port), strconv.Itoa(port)}, ":"), "docker.io/registry:2.6"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + if !WaitContainerReady(podmanTest, "registry", "listening on", 20, 1) { + Skip("Cannot start docker registry.") + } + + cmd := exec.Command("gpg", "--import", "sign/secret-key.asc") + err = cmd.Run() + Expect(err).To(BeNil()) + + cmd = exec.Command("cp", "/etc/containers/registries.d/default.yaml", "default.yaml") + if err = cmd.Run(); err != nil { + Skip("no signature store to verify") + } + defer func() { + cmd = exec.Command("cp", "default.yaml", "/etc/containers/registries.d/default.yaml") + cmd.Run() + }() + + cmd = exec.Command("cp", "sign/key.gpg", "/tmp/key.gpg") + Expect(cmd.Run()).To(BeNil()) + sigstore := ` +default-docker: + sigstore: file:///var/lib/containers/sigstore + sigstore-staging: file:///var/lib/containers/sigstore +` + Expect(ioutil.WriteFile("/etc/containers/registries.d/default.yaml", []byte(sigstore), 0755)).To(BeNil()) + + session = podmanTest.Podman([]string{"tag", ALPINE, "localhost:5000/alpine"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"push", "--tls-verify=false", "--sign-by", "foo@bar.com", "localhost:5000/alpine"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"rmi", ALPINE, "localhost:5000/alpine"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"pull", "--tls-verify=false", "--signature-policy=sign/policy.json", "localhost:5000/alpine"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + outfile := filepath.Join(podmanTest.TempDir, "temp.tar") + save := podmanTest.Podman([]string{"save", "remove-signatures=true", "-o", outfile, "localhost:5000/alpine"}) + save.WaitWithDefaultTimeout() + Expect(save).To(ExitWithError()) + }) + It("podman save image with digest reference", func() { // pull a digest reference session := podmanTest.PodmanNoCache([]string{"pull", ALPINELISTDIGEST}) diff --git a/test/e2e/sign/key.gpg b/test/e2e/sign/key.gpg new file mode 100644 index 0000000000..32968fc04b --- /dev/null +++ b/test/e2e/sign/key.gpg @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBF8kNqwBCAC0x3Kog+WlDNwcR6rWIP8Gj2T6LrQ2/3knSyAWzTgC/OBB6Oh0 +KAokXLjy8J3diG3EaSltE7erGG/bZCz8jYvMiwDJScON4zzidotqjoY80E+NeRDg +CC0gqvqmh0ftJIjYNBHzSxqrGRQwzwZU+u6ezlE8+0dvsHcHY+MRnxXJQrdM07EP +Prp85kKckChDlJ1tyGUB/YHieFQmOW5+TERA7ZqQOAQ12Vviv6V4kNfEJJq3MS2c +csZpO323tcHt3oebqsZCIElhX7uVw6GAeCw1tm4NZXs4g1yIC21Of/hzPeC18F72 +splCgKaAOiE9w/nMGLNEYy2NzgEclZLs2Y7jABEBAAG0FGZvb2JhciA8Zm9vQGJh +ci5jb20+iQFUBBMBCAA+FiEERyT4ac7LLibByeabqaoHAy6P2bIFAl8kNqwCGwMF +CQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQqaoHAy6P2bKtuggAgv54 +/F8wgi+uMrtFr8rqNtZMDyXRxfXaXUy5uGNfqHD83yqxweEqxiA8lmFkRHixPWtg +Z2MniFXMVc9kVmg8GNIIuzewXrPqtXztvuURQo9phK68v8fXEqqT6K25wtq8TiQZ +0J3mQIJPPTMe3pCCOyR6+W3iMtQp2AmitxKbzLP3J3GG2i0rG5S147A2rPnzTeMY +hds819+JE7jNMD7FkV+TcQlOVl4wyOQhNEJcjb6rA6EUe5+s85pIFTBSyPMJpJ03 +Y0dLdcSGpKdncGTK2X9+hS96G1+FP/t8hRIDblqUHtBRXe3Ozz6zSqpqu1DbAQSM +bIrLYxXfnZEN+ro0dLkBDQRfJDasAQgAncvLLZUHZkJWDPka3ocysJ7+/lmrXyAj +T3D4r7UM4oaLBOMKjvaKSDw1uW5qYmTxnnsqFDI0O5+XJxD1/0qEf6l2oUpnILdx +Vruf28FuvymbsyhDgs+MBoHz0jLWWPHUW2oWLIqcvaF0BePQ1GS6UoZlmZejsLww +cSpbaAHJng7An/iLuqOBr5EdUA5XMXqmdMFDrjh0uZezImJ2Eacu/hshBdu3IY49 +J5XP18GWrSdUnP27cv3tOii9j5Lfl8QAvCN89vkALIU3eZtnMlWZqLgl5o6COVFm +zpyx+iHOoCznQBt0aGoSNmE/dAqWIQS/xCSFqMHI6kNd9N0oR0rEHwARAQABiQE8 +BBgBCAAmFiEERyT4ac7LLibByeabqaoHAy6P2bIFAl8kNqwCGwwFCQPCZwAACgkQ +qaoHAy6P2bJfjQgAje6YR+p1QaNlTN9l4t2kGzy9RhkfYMrTgI2fEqbS9bFJUy3Y +3mH+vj/r2gN/kaN8LHH4K1d7fAohBsFqSI0flzHHIx2rfti9zAlbXcAErbnG+f0f +k0AaqU7KelU35vjPfNe6Vn7ky6G9CC6jW04NkLZDNFA2GusdYf1aM0LWew5t4WZa +quLVFhL36q9eHaogO/fcPR/quvQefHokk+b541ytwMN9l/g43rTbCvAjrUDHwipb +Gbw91Wg2XjbecRiCXDKWds2M149BpxUzY5xHFtD5t5WSEE/SkkryGTMmTxS3tuQZ +9PdtCPGrNDO6Ts/amORF04Tf+YMJgfv3IWxMeQ== +=y0uZ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/test/e2e/sign/policy.json b/test/e2e/sign/policy.json new file mode 100644 index 0000000000..ab01137bfe --- /dev/null +++ b/test/e2e/sign/policy.json @@ -0,0 +1,18 @@ +{ + "default": [ + { + "type": "insecureAcceptAnything" + } + ], + "transports": { + "docker": { + "localhost:5000": [ + { + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "/tmp/key.gpg" + } + ] + } + } +}