diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index 369c6ddd00..ee0dacfff5 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -571,9 +571,18 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, createFlags.StringVar(&cf.PasswdEntry, passwdEntryName, "", "Entry to write to /etc/passwd") _ = cmd.RegisterFlagCompletionFunc(passwdEntryName, completion.AutocompleteNone) + decryptionKeysFlagName := "decryption-key" + createFlags.StringSliceVar( + &cf.DecryptionKeys, + decryptionKeysFlagName, []string{}, + "Key needed to decrypt the image (e.g. /path/to/key.pem)", + ) + _ = cmd.RegisterFlagCompletionFunc(decryptionKeysFlagName, completion.AutocompleteNone) + if registry.IsRemote() { _ = createFlags.MarkHidden("env-host") _ = createFlags.MarkHidden("http-proxy") + _ = createFlags.MarkHidden(decryptionKeysFlagName) } else { createFlags.StringVar( &cf.SignaturePolicy, diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index 228e34fd1c..a73b3975be 100644 --- a/cmd/podman/containers/create.go +++ b/cmd/podman/containers/create.go @@ -334,15 +334,21 @@ func PullImage(imageName string, cliVals *entities.ContainerCreateOptions) (stri skipTLSVerify = types.NewOptionalBool(!cliVals.TLSVerify.Value()) } + decConfig, err := util.DecryptConfig(cliVals.DecryptionKeys) + if err != nil { + return "unable to obtain decryption config", err + } + pullReport, pullErr := registry.ImageEngine().Pull(registry.GetContext(), imageName, entities.ImagePullOptions{ - Authfile: cliVals.Authfile, - Quiet: cliVals.Quiet, - Arch: cliVals.Arch, - OS: cliVals.OS, - Variant: cliVals.Variant, - SignaturePolicy: cliVals.SignaturePolicy, - PullPolicy: pullPolicy, - SkipTLSVerify: skipTLSVerify, + Authfile: cliVals.Authfile, + Quiet: cliVals.Quiet, + Arch: cliVals.Arch, + OS: cliVals.OS, + Variant: cliVals.Variant, + SignaturePolicy: cliVals.SignaturePolicy, + PullPolicy: pullPolicy, + SkipTLSVerify: skipTLSVerify, + OciDecryptConfig: decConfig, }) if pullErr != nil { return "", pullErr diff --git a/cmd/podman/images/pull.go b/cmd/podman/images/pull.go index fe9d1e9b6e..2dd1919d2a 100644 --- a/cmd/podman/images/pull.go +++ b/cmd/podman/images/pull.go @@ -23,6 +23,7 @@ type pullOptionsWrapper struct { entities.ImagePullOptions TLSVerifyCLI bool // CLI only CredentialsCLI string + DecryptionKeys []string } var ( @@ -107,6 +108,13 @@ func pullFlags(cmd *cobra.Command) { flags.StringVar(&pullOptions.Authfile, authfileFlagName, auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") _ = cmd.RegisterFlagCompletionFunc(authfileFlagName, completion.AutocompleteDefault) + decryptionKeysFlagName := "decryption-key" + flags.StringSliceVar(&pullOptions.DecryptionKeys, decryptionKeysFlagName, nil, "Key needed to decrypt the image (e.g. /path/to/key.pem)") + _ = cmd.RegisterFlagCompletionFunc(decryptionKeysFlagName, completion.AutocompleteDefault) + + if registry.IsRemote() { + _ = flags.MarkHidden(decryptionKeysFlagName) + } if !registry.IsRemote() { certDirFlagName := "cert-dir" flags.StringVar(&pullOptions.CertDir, certDirFlagName, "", "`Pathname` of a directory containing TLS certificates and keys") @@ -156,6 +164,12 @@ func imagePull(cmd *cobra.Command, args []string) error { pullOptions.Password = creds.Password } + decConfig, err := util.DecryptConfig(pullOptions.DecryptionKeys) + if err != nil { + return fmt.Errorf("unable to obtain decryption config: %w", err) + } + pullOptions.OciDecryptConfig = decConfig + if !pullOptions.Quiet { pullOptions.Writer = os.Stderr } diff --git a/cmd/podman/images/push.go b/cmd/podman/images/push.go index fa60860db8..6d614cf4f9 100644 --- a/cmd/podman/images/push.go +++ b/cmd/podman/images/push.go @@ -1,6 +1,7 @@ package images import ( + "fmt" "os" "github.com/containers/common/pkg/auth" @@ -20,6 +21,8 @@ type pushOptionsWrapper struct { TLSVerifyCLI bool // CLI only CredentialsCLI string SignPassphraseFileCLI string + EncryptionKeys []string + EncryptLayers []int } var ( @@ -121,6 +124,14 @@ func pushFlags(cmd *cobra.Command) { flags.StringVar(&pushOptions.CompressionFormat, compressionFormat, "", "compression format to use") _ = cmd.RegisterFlagCompletionFunc(compressionFormat, common.AutocompleteCompressionFormat) + encryptionKeysFlagName := "encryption-key" + flags.StringSliceVar(&pushOptions.EncryptionKeys, encryptionKeysFlagName, nil, "Key with the encryption protocol to use to encrypt the image (e.g. jwe:/path/to/key.pem)") + _ = cmd.RegisterFlagCompletionFunc(encryptionKeysFlagName, completion.AutocompleteDefault) + + encryptLayersFlagName := "encrypt-layer" + flags.IntSliceVar(&pushOptions.EncryptLayers, encryptLayersFlagName, nil, "Layers to encrypt, 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified") + _ = cmd.RegisterFlagCompletionFunc(encryptLayersFlagName, completion.AutocompleteDefault) + if registry.IsRemote() { _ = flags.MarkHidden("cert-dir") _ = flags.MarkHidden("compress") @@ -129,6 +140,8 @@ func pushFlags(cmd *cobra.Command) { _ = flags.MarkHidden(signByFlagName) _ = flags.MarkHidden(signBySigstorePrivateKeyFlagName) _ = flags.MarkHidden(signPassphraseFileFlagName) + _ = flags.MarkHidden(encryptionKeysFlagName) + _ = flags.MarkHidden(encryptLayersFlagName) } if !registry.IsRemote() { flags.StringVar(&pushOptions.SignaturePolicy, "signature-policy", "", "Path to a signature-policy file") @@ -172,6 +185,13 @@ func imagePush(cmd *cobra.Command, args []string) error { return err } + encConfig, encLayers, err := util.EncryptConfig(pushOptions.EncryptionKeys, pushOptions.EncryptLayers) + if err != nil { + return fmt.Errorf("unable to obtain encryption config: %w", err) + } + pushOptions.OciEncryptConfig = encConfig + pushOptions.OciEncryptLayers = encLayers + // Let's do all the remaining Yoga in the API to prevent us from scattering // logic across (too) many parts of the code. return registry.ImageEngine().Push(registry.GetContext(), source, destination, pushOptions.ImagePushOptions) diff --git a/docs/source/markdown/options/decryption-key.md b/docs/source/markdown/options/decryption-key.md new file mode 100644 index 0000000000..0a9c1b93ab --- /dev/null +++ b/docs/source/markdown/options/decryption-key.md @@ -0,0 +1,7 @@ +####> This option file is used in: +####> podman create, pull, run +####> If you edit this file, make sure your changes +####> are applicable to all of those. +#### **--decryption-key**=*key[:passphrase]* + +The [key[:passphrase]] to be used for decryption of images. Key can point to keys and/or certificates. Decryption will be tried with all keys. If the key is protected by a passphrase, it is required to be passed in the argument and omitted otherwise. diff --git a/docs/source/markdown/podman-create.1.md.in b/docs/source/markdown/podman-create.1.md.in index e6928fea03..55970a8189 100644 --- a/docs/source/markdown/podman-create.1.md.in +++ b/docs/source/markdown/podman-create.1.md.in @@ -114,6 +114,8 @@ and specified with a _tag_. @@option cpuset-mems +@@option decryption-key + @@option device Note: if the user only has access rights via a group, accessing the device diff --git a/docs/source/markdown/podman-pull.1.md.in b/docs/source/markdown/podman-pull.1.md.in index 2add9d703d..43a89244f3 100644 --- a/docs/source/markdown/podman-pull.1.md.in +++ b/docs/source/markdown/podman-pull.1.md.in @@ -57,6 +57,8 @@ All tagged images in the repository will be pulled. @@option creds +@@option decryption-key + @@option disable-content-trust #### **--help**, **-h** diff --git a/docs/source/markdown/podman-push.1.md.in b/docs/source/markdown/podman-push.1.md.in index b5f78d0eda..b50ff65531 100644 --- a/docs/source/markdown/podman-push.1.md.in +++ b/docs/source/markdown/podman-push.1.md.in @@ -64,6 +64,14 @@ Note: This flag can only be set when using the **dir** transport @@option disable-content-trust +#### **--encrypt-layer**=*layer(s)* + +Layer(s) to encrypt: 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified. + +#### **--encryption-key**=*key* + +The [protocol:keyfile] specifies the encryption protocol, which can be JWE (RFC7516), PGP (RFC4880), and PKCS7 (RFC2315) and the key material required for image encryption. For instance, jwe:/path/to/key.pem or pgp:admin@example.com or pkcs7:/path/to/x509-file. + #### **--format**, **-f**=*format* Manifest Type (oci, v2s2, or v2s1) to use when pushing an image. diff --git a/docs/source/markdown/podman-run.1.md.in b/docs/source/markdown/podman-run.1.md.in index 3d4415dbc9..61c831822d 100644 --- a/docs/source/markdown/podman-run.1.md.in +++ b/docs/source/markdown/podman-run.1.md.in @@ -131,6 +131,8 @@ and specified with a _tag_. @@option cpuset-mems +@@option decryption-key + #### **--detach**, **-d** Detached mode: run the container in the background and print the new container ID. The default is *false*. diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index b1eb3b0057..e1ee70cbfd 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -8,6 +8,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/types" + encconfig "github.com/containers/ocicrypt/config" "github.com/containers/podman/v4/pkg/inspect" "github.com/containers/podman/v4/pkg/trust" "github.com/docker/docker/api/types/container" @@ -158,6 +159,9 @@ type ImagePullOptions struct { PullPolicy config.PullPolicy // Writer is used to display copy information including progress bars. Writer io.Writer + // OciDecryptConfig contains the config that can be used to decrypt an image if it is + // encrypted if non-nil. If nil, it does not attempt to decrypt an image. + OciDecryptConfig *encconfig.DecryptConfig } // ImagePullReport is the response from pulling one or more images. @@ -227,6 +231,15 @@ type ImagePushOptions struct { CompressionFormat string // Writer is used to display copy information including progress bars. Writer io.Writer + // OciEncryptConfig when non-nil indicates that an image should be encrypted. + // The encryption options is derived from the construction of EncryptConfig object. + OciEncryptConfig *encconfig.EncryptConfig + // OciEncryptLayers represents the list of layers to encrypt. + // If nil, don't encrypt any layers. + // If non-nil and len==0, denotes encrypt all layers. + // integers in the slice represent 0-indexed layer indices, with support for negative + // indexing. i.e. 0 is the first layer, -1 is the last (top-most) layer. + OciEncryptLayers *[]int } // ImagePushReport is the response from pushing an image. diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index a059cd7b5b..b02bbc86ca 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -290,6 +290,7 @@ type ContainerCreateOptions struct { ChrootDirs []string IsInfra bool IsClone bool + DecryptionKeys []string Net *NetOptions `json:"net,omitempty"` diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 1775ec24c1..44f0e01d23 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -236,6 +236,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entiti pullOptions.SignaturePolicyPath = options.SignaturePolicy pullOptions.InsecureSkipTLSVerify = options.SkipTLSVerify pullOptions.Writer = options.Writer + pullOptions.OciDecryptConfig = options.OciDecryptConfig if !options.Quiet && pullOptions.Writer == nil { pullOptions.Writer = os.Stderr @@ -309,6 +310,8 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri pushOptions.SignSigstorePrivateKeyPassphrase = options.SignSigstorePrivateKeyPassphrase pushOptions.InsecureSkipTLSVerify = options.SkipTLSVerify pushOptions.Writer = options.Writer + pushOptions.OciEncryptConfig = options.OciEncryptConfig + pushOptions.OciEncryptLayers = options.OciEncryptLayers compressionFormat := options.CompressionFormat if compressionFormat == "" { diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 9ae1ff9598..f48c85f6d7 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -105,6 +105,10 @@ func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOption } func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities.ImagePullOptions) (*entities.ImagePullReport, error) { + if opts.OciDecryptConfig != nil { + return nil, fmt.Errorf("decryption is not supported for remote clients") + } + options := new(images.PullOptions) options.WithAllTags(opts.AllTags).WithAuthfile(opts.Authfile).WithArch(opts.Arch).WithOS(opts.OS) options.WithVariant(opts.Variant).WithPassword(opts.Password) @@ -240,6 +244,10 @@ func (ir *ImageEngine) Import(ctx context.Context, opts entities.ImageImportOpti } func (ir *ImageEngine) Push(ctx context.Context, source string, destination string, opts entities.ImagePushOptions) error { + if opts.OciEncryptConfig != nil { + return fmt.Errorf("encryption is not supported for remote clients") + } + options := new(images.PushOptions) options.WithAll(opts.All).WithCompress(opts.Compress).WithUsername(opts.Username).WithPassword(opts.Password).WithAuthfile(opts.Authfile).WithFormat(opts.Format).WithRemoveSignatures(opts.RemoveSignatures).WithQuiet(opts.Quiet).WithCompressionFormat(opts.CompressionFormat).WithProgressWriter(opts.Writer) diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 51c1854112..d1ee6f9df5 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -20,6 +20,8 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/common/pkg/util" "github.com/containers/image/v5/types" + encconfig "github.com/containers/ocicrypt/config" + enchelpers "github.com/containers/ocicrypt/helpers" "github.com/containers/podman/v4/pkg/errorhandling" "github.com/containers/podman/v4/pkg/namespaces" "github.com/containers/podman/v4/pkg/rootless" @@ -756,3 +758,37 @@ func SizeOfPath(path string) (uint64, error) { }) return size, err } + +// EncryptConfig translates encryptionKeys into a EncriptionsConfig structure +func EncryptConfig(encryptionKeys []string, encryptLayers []int) (*encconfig.EncryptConfig, *[]int, error) { + var encLayers *[]int + var encConfig *encconfig.EncryptConfig + + if len(encryptionKeys) > 0 { + // encryption + encLayers = &encryptLayers + ecc, err := enchelpers.CreateCryptoConfig(encryptionKeys, []string{}) + if err != nil { + return nil, nil, fmt.Errorf("invalid encryption keys: %w", err) + } + cc := encconfig.CombineCryptoConfigs([]encconfig.CryptoConfig{ecc}) + encConfig = cc.EncryptConfig + } + return encConfig, encLayers, nil +} + +// DecryptConfig translates decryptionKeys into a DescriptionConfig structure +func DecryptConfig(decryptionKeys []string) (*encconfig.DecryptConfig, error) { + var decryptConfig *encconfig.DecryptConfig + if len(decryptionKeys) > 0 { + // decryption + dcc, err := enchelpers.CreateCryptoConfig([]string{}, decryptionKeys) + if err != nil { + return nil, fmt.Errorf("invalid decryption keys: %w", err) + } + cc := encconfig.CombineCryptoConfigs([]encconfig.CryptoConfig{dcc}) + decryptConfig = cc.DecryptConfig + } + + return decryptConfig, nil +} diff --git a/test/e2e/pull_test.go b/test/e2e/pull_test.go index ba717f393a..5a5a939aea 100644 --- a/test/e2e/pull_test.go +++ b/test/e2e/pull_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "runtime" + "github.com/containers/podman/v4/pkg/rootless" . "github.com/containers/podman/v4/test/utils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -559,4 +560,89 @@ var _ = Describe("Podman pull", func() { Expect(session).Should(Exit(0)) Expect(session.ErrorToString()).To(BeEmpty()) }) + + Describe("podman pull and decrypt", func() { + + decryptionTestHelper := func(imgPath string) *PodmanSessionIntegration { + bitSize := 1024 + keyFileName := filepath.Join(podmanTest.TempDir, "key") + publicKeyFileName, privateKeyFileName, err := WriteRSAKeyPair(keyFileName, bitSize) + Expect(err).To(BeNil()) + + wrongKeyFileName := filepath.Join(podmanTest.TempDir, "wrong_key") + _, wrongPrivateKeyFileName, err := WriteRSAKeyPair(wrongKeyFileName, bitSize) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"push", "--encryption-key", "jwe:" + publicKeyFileName, "--tls-verify=false", "--remove-signatures", ALPINE, imgPath}) + session.WaitWithDefaultTimeout() + + session = podmanTest.Podman([]string{"rmi", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // Pulling encrypted image without key should fail + session = podmanTest.Podman([]string{"pull", imgPath}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(125)) + + // Pulling encrypted image with wrong key should fail + session = podmanTest.Podman([]string{"pull", "--decryption-key", wrongPrivateKeyFileName, imgPath}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(125)) + + // Pulling encrypted image with correct key should pass + session = podmanTest.Podman([]string{"pull", "--decryption-key", privateKeyFileName, imgPath}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + session = podmanTest.Podman([]string{"images"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + return session + } + + It("From oci", func() { + SkipIfRemote("Remote pull neither supports oci transport, nor decryption") + + podmanTest.AddImageToRWStore(ALPINE) + + bbdir := filepath.Join(podmanTest.TempDir, "busybox-oci") + imgPath := fmt.Sprintf("oci:%s", bbdir) + + session := decryptionTestHelper(imgPath) + + Expect(session.LineInOutputContainsTag(filepath.Join("localhost", bbdir), "latest")).To(BeTrue()) + }) + + It("From local registry", func() { + SkipIfRemote("Remote pull does not support decryption") + + if podmanTest.Host.Arch == "ppc64le" { + Skip("No registry image for ppc64le") + } + + podmanTest.AddImageToRWStore(ALPINE) + + if rootless.IsRootless() { + err := podmanTest.RestoreArtifact(REGISTRY_IMAGE) + Expect(err).ToNot(HaveOccurred()) + } + lock := GetPortLock("5000") + defer lock.Unlock() + session := podmanTest.Podman([]string{"run", "-d", "--name", "registry", "-p", "5000:5000", REGISTRY_IMAGE, "/entrypoint.sh", "/etc/docker/registry/config.yml"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + if !WaitContainerReady(podmanTest, "registry", "listening on", 20, 1) { + Skip("Cannot start docker registry.") + } + + imgPath := "localhost:5000/my-alpine" + + session = decryptionTestHelper(imgPath) + + Expect(session.LineInOutputContainsTag(imgPath, "latest")).To(BeTrue()) + }) + }) + }) diff --git a/test/e2e/push_test.go b/test/e2e/push_test.go index 5af47678fc..f0d5376d13 100644 --- a/test/e2e/push_test.go +++ b/test/e2e/push_test.go @@ -127,6 +127,17 @@ var _ = Describe("Podman push", func() { Expect(output).To(ContainSubstring("Writing manifest to image destination")) Expect(output).To(ContainSubstring("Storing signatures")) + bitSize := 1024 + keyFileName := filepath.Join(podmanTest.TempDir, "key") + publicKeyFileName, _, err := WriteRSAKeyPair(keyFileName, bitSize) + Expect(err).To(BeNil()) + + if !IsRemote() { // Remote does not support --encryption-key + push = podmanTest.Podman([]string{"push", "--encryption-key", "jwe:" + publicKeyFileName, "--tls-verify=false", "--remove-signatures", ALPINE, "localhost:5000/my-alpine"}) + push.WaitWithDefaultTimeout() + Expect(push).Should(Exit(0)) + } + if !IsRemote() { // Remote does not support --digestfile // Test --digestfile option push2 := podmanTest.Podman([]string{"push", "--tls-verify=false", "--digestfile=/tmp/digestfile.txt", "--remove-signatures", ALPINE, "localhost:5000/my-alpine"}) @@ -261,6 +272,25 @@ var _ = Describe("Podman push", func() { Expect(push).Should(Exit(0)) }) + It("podman push and encrypt to oci", func() { + SkipIfRemote("Remote push neither supports oci transport, nor encryption") + + bbdir := filepath.Join(podmanTest.TempDir, "busybox-oci") + + bitSize := 1024 + keyFileName := filepath.Join(podmanTest.TempDir, "key") + publicKeyFileName, _, err := WriteRSAKeyPair(keyFileName, bitSize) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"push", "--encryption-key", "jwe:" + publicKeyFileName, ALPINE, fmt.Sprintf("oci:%s", bbdir)}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"rmi", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + }) + It("podman push to docker-archive", func() { SkipIfRemote("Remote push does not support docker-archive transport") tarfn := filepath.Join(podmanTest.TempDir, "alp.tar") diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index fb02cb4106..d1e2d08f95 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -1996,4 +1996,46 @@ WORKDIR /madethis`, BB) Expect(output).To(ContainSubstring("noexec")) Expect(output).To(ContainSubstring("nodev")) }) + + It("podman run and decrypt from local registry", func() { + SkipIfRemote("Remote run does not support decryption") + + if podmanTest.Host.Arch == "ppc64le" { + Skip("No registry image for ppc64le") + } + + podmanTest.AddImageToRWStore(ALPINE) + + if rootless.IsRootless() { + err := podmanTest.RestoreArtifact(REGISTRY_IMAGE) + Expect(err).ToNot(HaveOccurred()) + } + lock := GetPortLock("5000") + defer lock.Unlock() + session := podmanTest.Podman([]string{"run", "-d", "--name", "registry", "-p", "5000:5000", REGISTRY_IMAGE, "/entrypoint.sh", "/etc/docker/registry/config.yml"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + if !WaitContainerReady(podmanTest, "registry", "listening on", 20, 1) { + Skip("Cannot start docker registry.") + } + + bitSize := 1024 + keyFileName := filepath.Join(podmanTest.TempDir, "key") + publicKeyFileName, privateKeyFileName, err := WriteRSAKeyPair(keyFileName, bitSize) + Expect(err).To(BeNil()) + + imgPath := "localhost:5000/my-alpine" + session = podmanTest.Podman([]string{"push", "--encryption-key", "jwe:" + publicKeyFileName, "--tls-verify=false", "--remove-signatures", ALPINE, imgPath}) + session.WaitWithDefaultTimeout() + + session = podmanTest.Podman([]string{"rmi", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"run", "--decryption-key", privateKeyFileName, imgPath}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.ErrorToString()).To(ContainSubstring("Trying to pull")) + }) }) diff --git a/test/utils/common_function_test.go b/test/utils/common_function_test.go index 724b2deb25..9f0292c0bb 100644 --- a/test/utils/common_function_test.go +++ b/test/utils/common_function_test.go @@ -151,4 +151,20 @@ var _ = Describe("Common functions test", func() { Entry("Docker not in cgroup file", "/tmp/cgroup.test", false, true, false), ) + It("Test WriteRSAKeyPair", func() { + fileName := "/tmp/test_key" + bitSize := 1024 + + publicKeyFileName, privateKeyFileName, err := WriteRSAKeyPair(fileName, bitSize) + Expect(err).To(BeNil(), "Failed to write RSA key pair to files.") + + read, err := os.Open(publicKeyFileName) + Expect(err).To(BeNil(), "Cannot find the public key file after we write it.") + defer read.Close() + + read, err = os.Open(privateKeyFileName) + Expect(err).To(BeNil(), "Cannot find the private key file after we write it.") + defer read.Close() + }) + }) diff --git a/test/utils/utils.go b/test/utils/utils.go index 19b67dfa72..aa0cb1a809 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -11,6 +11,11 @@ import ( "strings" "time" + crypto_rand "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "github.com/sirupsen/logrus" "github.com/containers/storage/pkg/parsers/kernel" @@ -523,3 +528,76 @@ func RandomString(n int) string { } return string(b) } + +// Encode *rsa.PublicKey and store it in a file. +// Adds appropriate extension to the fileName, and returns the complete fileName of +// the file storing the public key. +func savePublicKey(fileName string, publicKey *rsa.PublicKey) (string, error) { + // Encode public key to PKIX, ASN.1 DER form + pubBytes, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return "", err + } + + pubPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: pubBytes, + }, + ) + + // Write public key to file + publicKeyFileName := fileName + ".rsa.pub" + if err := os.WriteFile(publicKeyFileName, pubPEM, 0600); err != nil { + return "", err + } + + return publicKeyFileName, nil +} + +// Encode *rsa.PrivateKey and store it in a file. +// Adds appropriate extension to the fileName, and returns the complete fileName of +// the file storing the private key. +func savePrivateKey(fileName string, privateKey *rsa.PrivateKey) (string, error) { + // Encode private key to PKCS#1, ASN.1 DER form + privBytes := x509.MarshalPKCS1PrivateKey(privateKey) + keyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privBytes, + }, + ) + + // Write private key to file + privateKeyFileName := fileName + ".rsa" + if err := os.WriteFile(privateKeyFileName, keyPEM, 0600); err != nil { + return "", err + } + + return privateKeyFileName, nil +} + +// Generate RSA key pair of specified bit size and write them to files. +// Adds appropriate extension to the fileName, and returns the complete fileName of +// the files storing the public and private key respectively. +func WriteRSAKeyPair(fileName string, bitSize int) (string, string, error) { + // Generate RSA key + privateKey, err := rsa.GenerateKey(crypto_rand.Reader, bitSize) + if err != nil { + return "", "", err + } + + publicKey := privateKey.Public().(*rsa.PublicKey) + + publicKeyFileName, err := savePublicKey(fileName, publicKey) + if err != nil { + return "", "", err + } + + privateKeyFileName, err := savePrivateKey(fileName, privateKey) + if err != nil { + return "", "", err + } + + return publicKeyFileName, privateKeyFileName, nil +}