From bce22dc6210cfdbf045bb14cdd12e0b8f409c6c9 Mon Sep 17 00:00:00 2001 From: Qi Wang Date: Wed, 12 Dec 2018 15:48:21 -0500 Subject: [WIP]Support podman image sign Generate a signature claim for an image using user keyring (--sign-by). The signature file will be stored in simple json format under the default or the given directory (--directory or yaml file in /etc/containers/registries.d/). Signed-off-by: Qi Wang --- cmd/podman/image.go | 1 + cmd/podman/sign.go | 194 ++++++++++++++++++++++++++++++++++++++++++++ docs/podman-image-sign.1.md | 52 ++++++++++++ docs/podman-image.1.md | 3 +- 4 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 cmd/podman/sign.go create mode 100644 docs/podman-image-sign.1.md diff --git a/cmd/podman/image.go b/cmd/podman/image.go index e978b9cf5..557fc1056 100644 --- a/cmd/podman/image.go +++ b/cmd/podman/image.go @@ -20,6 +20,7 @@ var ( saveCommand, tagCommand, trustCommand, + signCommand, } imageDescription = "Manage images" diff --git a/cmd/podman/sign.go b/cmd/podman/sign.go new file mode 100644 index 000000000..790b6031d --- /dev/null +++ b/cmd/podman/sign.go @@ -0,0 +1,194 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/url" + "os" + "strconv" + "strings" + + "github.com/containers/image/signature" + "github.com/containers/image/transports" + "github.com/containers/image/transports/alltransports" + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/trust" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +var ( + signFlags = []cli.Flag{ + cli.StringFlag{ + Name: "sign-by", + Usage: "Name of the signing key", + }, + cli.StringFlag{ + Name: "directory, d", + Usage: "Define an alternate directory to store signatures", + }, + } + + signDescription = "Create a signature file that can be used later to verify the image" + signCommand = cli.Command{ + Name: "sign", + Usage: "Sign an image", + Description: signDescription, + Flags: sortFlags(signFlags), + Action: signCmd, + ArgsUsage: "IMAGE-NAME [IMAGE-NAME ...]", + OnUsageError: usageErrorHandler, + } +) + +// SignatureStoreDir defines default directory to store signatures +const SignatureStoreDir = "/var/lib/containers/sigstore" + +func signCmd(c *cli.Context) error { + args := c.Args() + if len(args) < 1 { + return errors.Errorf("at least one image name must be specified") + } + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not create runtime") + } + defer runtime.Shutdown(false) + + signby := c.String("sign-by") + if signby == "" { + return errors.Errorf("You must provide an identity") + } + + var sigStoreDir string + if c.IsSet("directory") { + sigStoreDir = c.String("directory") + if _, err := os.Stat(sigStoreDir); err != nil { + return errors.Wrapf(err, "invalid directory %s", sigStoreDir) + } + } + + mech, err := signature.NewGPGSigningMechanism() + if err != nil { + return errors.Wrap(err, "Error initializing GPG") + } + defer mech.Close() + if err := mech.SupportsSigning(); err != nil { + return errors.Wrap(err, "Signing is not supported") + } + + systemRegistriesDirPath := trust.RegistriesDirPath(runtime.SystemContext()) + registryConfigs, err := trust.LoadAndMergeConfig(systemRegistriesDirPath) + if err != nil { + return errors.Wrapf(err, "error reading registry configuration") + } + + for _, signimage := range args { + srcRef, err := alltransports.ParseImageName(signimage) + if err != nil { + return errors.Wrapf(err, "error parsing image name") + } + rawSource, err := srcRef.NewImageSource(getContext(), runtime.SystemContext()) + if err != nil { + return errors.Wrapf(err, "error getting image source") + } + manifest, _, err := rawSource.GetManifest(getContext(), nil) + if err != nil { + return errors.Wrapf(err, "error getting manifest") + } + dockerReference := rawSource.Reference().DockerReference() + if dockerReference == nil { + return errors.Errorf("Cannot determine canonical Docker reference for destination %s", transports.ImageName(rawSource.Reference())) + } + + // create the signstore file + newImage, err := runtime.ImageRuntime().New(getContext(), signimage, runtime.GetConfig().SignaturePolicyPath, "", os.Stderr, nil, image.SigningOptions{SignBy: signby}, false) + if err != nil { + return errors.Wrapf(err, "error pulling image %s", signimage) + } + + registryInfo := trust.HaveMatchRegistry(rawSource.Reference().DockerReference().String(), registryConfigs) + if registryInfo != nil { + if sigStoreDir == "" { + sigStoreDir = registryInfo.SigStoreStaging + if sigStoreDir == "" { + sigStoreDir = registryInfo.SigStore + } + } + sigStoreDir, err = isValidSigStoreDir(sigStoreDir) + if err != nil { + return errors.Wrapf(err, "invalid signature storage %s", sigStoreDir) + } + } + if sigStoreDir == "" { + sigStoreDir = SignatureStoreDir + } + + repos := newImage.RepoDigests() + if len(repos) == 0 { + logrus.Errorf("no repodigests associated with the image %s", signimage) + continue + } + + // create signature + newSig, err := signature.SignDockerManifest(manifest, dockerReference.String(), mech, signby) + if err != nil { + return errors.Wrapf(err, "error creating new signature") + } + + sigStoreDir = fmt.Sprintf("%s/%s", sigStoreDir, strings.Replace(repos[0][strings.Index(repos[0], "/")+1:len(repos[0])], ":", "=", 1)) + if err := os.MkdirAll(sigStoreDir, 0751); err != nil { + // The directory is allowed to exist + if !os.IsExist(err) { + logrus.Errorf("error creating directory %s: %s", sigStoreDir, err) + continue + } + } + sigFilename, err := getSigFilename(sigStoreDir) + if err != nil { + logrus.Errorf("error creating sigstore file: %v", err) + continue + } + err = ioutil.WriteFile(sigStoreDir+"/"+sigFilename, newSig, 0644) + if err != nil { + logrus.Errorf("error storing signature for %s", rawSource.Reference().DockerReference().String()) + continue + } + } + return nil +} + +func getSigFilename(sigStoreDirPath string) (string, error) { + sigFileSuffix := 1 + sigFiles, err := ioutil.ReadDir(sigStoreDirPath) + if err != nil { + return "", err + } + sigFilenames := make(map[string]bool) + for _, file := range sigFiles { + sigFilenames[file.Name()] = true + } + for { + sigFilename := "signature-" + strconv.Itoa(sigFileSuffix) + if _, exists := sigFilenames[sigFilename]; !exists { + return sigFilename, nil + } + sigFileSuffix++ + } +} + +func isValidSigStoreDir(sigStoreDir string) (string, error) { + writeURIs := map[string]bool{"file": true} + url, err := url.Parse(sigStoreDir) + if err != nil { + return sigStoreDir, errors.Wrapf(err, "invalid directory %s", sigStoreDir) + } + _, exists := writeURIs[url.Scheme] + if !exists { + return sigStoreDir, errors.Errorf("Writing to %s is not supported. Use a supported scheme", sigStoreDir) + } + sigStoreDir = url.Path + return sigStoreDir, nil +} diff --git a/docs/podman-image-sign.1.md b/docs/podman-image-sign.1.md new file mode 100644 index 000000000..c4f3c6676 --- /dev/null +++ b/docs/podman-image-sign.1.md @@ -0,0 +1,52 @@ +% podman-image-sign(1) + +# NAME +podman-image-sign- Create a signature for an image + +# SYNOPSIS +**podman image sign** +[**-h**|**--help**] +[**-d**, **--directory**] +[**--sign-by**] +[ IMAGE... ] + +# DESCRIPTION +**podmain image sign** will create a local signature for one or more local images that have +been pulled from a registry. The signature will be written to a directory +derived from the registry configuration files in /etc/containers/registries.d. By default, the signature will be written into /var/lib/containers/sigstore directory. + +# OPTIONS +**-h** **--help** + Print usage statement. + +**-d** **--directory** + Store the signatures in the specified directory. Default: /var/lib/containers/sigstore + +**--sign-by** + Override the default identity of the signature. + +# EXAMPLES +Sign the busybox image with the identify of foo@bar.com with a user's keyring and save the signature in /tmp/signatures/. + + sudo podman image sign --sign-by foo@bar.com -d /tmp/signatures transport://privateregistry.example.com/foobar + +# RELATED CONFIGURATION + +The write (and read) location for signatures is defined in YAML-based +configuration files in /etc/containers/registries.d/. When you sign +an image, podman will use those configuration files to determine +where to write the signature based on the the name of the originating +registry or a default storage value unless overriden with the -d +option. For example, consider the following configuration file. + +docker: + privateregistry.example.com: + sigstore: file:///var/lib/containers/sigstore + +When signing an image preceeded with the registry name 'privateregistry.example.com', +the signature will be written into subdirectories of +/var/lib/containers/sigstore/privateregistry.example.com. The use of 'sigstore' also means +the signature will be 'read' from that same location on a pull-related function. + +# HISTORY +November 2018, Originally compiled by Qi Wang (qiwan at redhat dot com) diff --git a/docs/podman-image.1.md b/docs/podman-image.1.md index 19893dfda..5a0c4e5f9 100644 --- a/docs/podman-image.1.md +++ b/docs/podman-image.1.md @@ -27,7 +27,8 @@ The image command allows you to manage images | rm | [podman-rm(1)](podman-rmi.1.md) | Removes one or more locally stored images. | | save | [podman-save(1)](podman-save.1.md) | Save an image to docker-archive or oci. | | tag | [podman-tag(1)](podman-tag.1.md) | Add an additional name to a local image. | -| trust | [podman-image-trust(1)](podman-image-trust.1.md) | Manage container image trust policy. +| trust | [podman-image-trust(1)](podman-image-trust.1.md) | Manage container image trust policy. | +| sign | [podman-image-sign(1)](podman-image-sign.1.md) | Sign an image. | ## SEE ALSO podman -- cgit v1.2.3-54-g00ecf