From fbd0fccf89f994a90fbc8d63e9c90942acdbc201 Mon Sep 17 00:00:00 2001 From: Brent Baude Date: Sat, 9 May 2020 09:09:35 -0500 Subject: v2podman image sign this is a straight port to add the podman image sign command. no improvements or refactoring done Signed-off-by: Brent Baude --- cmd/podman/images/sign.go | 55 +++++++++++++ pkg/domain/entities/engine_image.go | 1 + pkg/domain/entities/images.go | 10 +++ pkg/domain/infra/abi/images.go | 154 ++++++++++++++++++++++++++++++++++++ pkg/domain/infra/tunnel/images.go | 4 + 5 files changed, 224 insertions(+) create mode 100644 cmd/podman/images/sign.go diff --git a/cmd/podman/images/sign.go b/cmd/podman/images/sign.go new file mode 100644 index 000000000..bd9cf2ea7 --- /dev/null +++ b/cmd/podman/images/sign.go @@ -0,0 +1,55 @@ +package images + +import ( + "os" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + signDescription = "Create a signature file that can be used later to verify the image." + signCommand = &cobra.Command{ + Use: "sign [flags] IMAGE [IMAGE...]", + Short: "Sign an image", + Long: signDescription, + RunE: sign, + Args: cobra.MinimumNArgs(1), + Example: `podman image sign --sign-by mykey imageID + podman image sign --sign-by mykey --directory ./mykeydir imageID`, + } +) + +var ( + signOptions entities.SignOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: signCommand, + Parent: imageCmd, + }) + flags := signCommand.Flags() + flags.StringVarP(&signOptions.Directory, "directory", "d", "", "Define an alternate directory to store signatures") + flags.StringVar(&signOptions.SignBy, "sign-by", "", "Name of the signing key") + flags.StringVar(&signOptions.CertDir, "cert-dir", "", "`Pathname` of a directory containing TLS certificates and keys") +} + +func sign(cmd *cobra.Command, args []string) error { + if signOptions.SignBy == "" { + return errors.Errorf("please provide an identity") + } + + var sigStoreDir string + if len(signOptions.Directory) > 0 { + sigStoreDir = signOptions.Directory + if _, err := os.Stat(sigStoreDir); err != nil { + return errors.Wrapf(err, "invalid directory %s", sigStoreDir) + } + } + _, err := registry.ImageEngine().Sign(registry.Context(), args, signOptions) + return err +} diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index ffa71abd6..7d7099838 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -34,4 +34,5 @@ type ImageEngine interface { ManifestAnnotate(ctx context.Context, names []string, opts ManifestAnnotateOptions) (string, error) ManifestRemove(ctx context.Context, names []string) (string, error) ManifestPush(ctx context.Context, names []string, manifestPushOpts ManifestPushOptions) error + Sign(ctx context.Context, names []string, options SignOptions) (*SignReport, error) } diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index e116a90b9..cce3001eb 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -309,3 +309,13 @@ type SetTrustOptions struct { PubKeysFile []string Type string } + +// SignOptions describes input options for the CLI signing +type SignOptions struct { + Directory string + SignBy string + CertDir string +} + +// SignReport describes the result of signing +type SignReport struct{} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 7ab5131f0..6e774df8e 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -4,14 +4,22 @@ import ( "context" "fmt" "io" + "io/ioutil" + "net/url" "os" + "path/filepath" + "strconv" "strings" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker" dockerarchive "github.com/containers/image/v5/docker/archive" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/signature" + "github.com/containers/image/v5/transports" "github.com/containers/image/v5/transports/alltransports" "github.com/containers/image/v5/types" "github.com/containers/libpod/libpod/define" @@ -19,6 +27,7 @@ import ( libpodImage "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/domain/entities" domainUtils "github.com/containers/libpod/pkg/domain/utils" + "github.com/containers/libpod/pkg/trust" "github.com/containers/libpod/pkg/util" "github.com/containers/storage" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -26,6 +35,9 @@ import ( "github.com/sirupsen/logrus" ) +// SignatureStoreDir defines default directory to store signatures +const SignatureStoreDir = "/var/lib/containers/sigstore" + func (ir *ImageEngine) Exists(_ context.Context, nameOrId string) (*entities.BoolReport, error) { _, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrId) if err != nil && errors.Cause(err) != define.ErrNoSuchImage { @@ -549,3 +561,145 @@ func (ir *ImageEngine) Shutdown(_ context.Context) { _ = ir.Libpod.Shutdown(false) }) } + +func (ir *ImageEngine) Sign(ctx context.Context, names []string, options entities.SignOptions) (*entities.SignReport, error) { + dockerRegistryOptions := image.DockerRegistryOptions{ + DockerCertPath: options.CertDir, + } + + mech, err := signature.NewGPGSigningMechanism() + if err != nil { + return nil, errors.Wrap(err, "error initializing GPG") + } + defer mech.Close() + if err := mech.SupportsSigning(); err != nil { + return nil, errors.Wrap(err, "signing is not supported") + } + sc := ir.Libpod.SystemContext() + sc.DockerCertPath = options.CertDir + + systemRegistriesDirPath := trust.RegistriesDirPath(sc) + registryConfigs, err := trust.LoadAndMergeConfig(systemRegistriesDirPath) + if err != nil { + return nil, errors.Wrapf(err, "error reading registry configuration") + } + + for _, signimage := range names { + var sigStoreDir string + srcRef, err := alltransports.ParseImageName(signimage) + if err != nil { + return nil, errors.Wrapf(err, "error parsing image name") + } + rawSource, err := srcRef.NewImageSource(ctx, sc) + if err != nil { + return nil, errors.Wrapf(err, "error getting image source") + } + err = rawSource.Close() + if err != nil { + logrus.Errorf("unable to close new image source %q", err) + } + getManifest, _, err := rawSource.GetManifest(ctx, nil) + if err != nil { + return nil, errors.Wrapf(err, "error getting getManifest") + } + dockerReference := rawSource.Reference().DockerReference() + if dockerReference == nil { + return nil, errors.Errorf("cannot determine canonical Docker reference for destination %s", transports.ImageName(rawSource.Reference())) + } + + // create the signstore file + rtc, err := ir.Libpod.GetConfig() + if err != nil { + return nil, err + } + newImage, err := ir.Libpod.ImageRuntime().New(ctx, signimage, rtc.Engine.SignaturePolicyPath, "", os.Stderr, &dockerRegistryOptions, image.SigningOptions{SignBy: options.SignBy}, nil, util.PullImageMissing) + if err != nil { + return nil, errors.Wrapf(err, "error pulling image %s", signimage) + } + if sigStoreDir == "" { + if rootless.IsRootless() { + sigStoreDir = filepath.Join(filepath.Dir(ir.Libpod.StorageConfig().GraphRoot), "sigstore") + } else { + registryInfo := trust.HaveMatchRegistry(rawSource.Reference().DockerReference().String(), registryConfigs) + if registryInfo != nil { + if sigStoreDir = registryInfo.SigStoreStaging; sigStoreDir == "" { + sigStoreDir = registryInfo.SigStore + + } + } + } + } + sigStoreDir, err = isValidSigStoreDir(sigStoreDir) + if err != nil { + return nil, errors.Wrapf(err, "invalid signature storage %s", sigStoreDir) + } + repos, err := newImage.RepoDigests() + if err != nil { + return nil, errors.Wrapf(err, "error calculating repo digests for %s", signimage) + } + if len(repos) == 0 { + logrus.Errorf("no repodigests associated with the image %s", signimage) + continue + } + + // create signature + newSig, err := signature.SignDockerManifest(getManifest, dockerReference.String(), mech, options.SignBy) + if err != nil { + return nil, errors.Wrapf(err, "error creating new signature") + } + + trimmedDigest := strings.TrimPrefix(repos[0], strings.Split(repos[0], "/")[0]) + sigStoreDir = filepath.Join(sigStoreDir, strings.Replace(trimmedDigest, ":", "=", 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(filepath.Join(sigStoreDir, sigFilename), newSig, 0644) + if err != nil { + logrus.Errorf("error storing signature for %s", rawSource.Reference().DockerReference().String()) + continue + } + } + return nil, 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/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 00893194c..788752fd8 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -264,3 +264,7 @@ func (ir *ImageEngine) Tree(ctx context.Context, nameOrId string, opts entities. // Shutdown Libpod engine func (ir *ImageEngine) Shutdown(_ context.Context) { } + +func (ir *ImageEngine) Sign(ctx context.Context, names []string, options entities.SignOptions) (*entities.SignReport, error) { + return nil, errors.New("not implemented yet") +} -- cgit v1.2.3-54-g00ecf