From 8d1a4649f280038a4e0850cc4b039239c7087fb0 Mon Sep 17 00:00:00 2001 From: Harshal Patil Date: Thu, 6 Feb 2020 19:48:22 +0530 Subject: [PATCH] Partial image encryption support Signed-off-by: Harshal Patil --- cmd/skopeo/copy.go | 13 +++++++++++- docs/skopeo-copy.1.md | 12 +++++++++-- integration/copy_test.go | 44 ++++++++++++++++++++++++++++++++-------- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 9b5d4125..c8d2eb91 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -28,6 +28,7 @@ type copyOptions struct { format optionalString // Force conversion of the image to a specified format quiet bool // Suppress output information when copying images all bool // Copy all of the images if the source is a list + encryptLayer cli.IntSlice // The list of layers to encrypt encryptionKeys cli.StringSlice // Keys needed to encrypt the image decryptionKeys cli.StringSlice // Keys needed to decrypt the image } @@ -92,6 +93,11 @@ func copyCmd(global *globalOptions) cli.Command { Usage: "*Experimental* key with the encryption protocol to use needed to encrypt the image (e.g. jwe:/path/to/key.pem)", Value: &opts.encryptionKeys, }, + cli.IntSliceFlag{ + Name: "encrypt-layer", + Usage: "*Experimental* the 0-indexed layer indices, with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer)", + Value: &opts.encryptLayer, + }, cli.StringSliceFlag{ Name: "decryption-key", Usage: "*Experimental* key needed to decrypt the image", @@ -180,9 +186,14 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) error { var encConfig *encconfig.EncryptConfig var decConfig *encconfig.DecryptConfig + if len(opts.encryptLayer.Value()) > 0 && len(opts.encryptionKeys.Value()) == 0 { + return fmt.Errorf("--encrypt-layer can only be used with --encryption-key") + } + if len(opts.encryptionKeys.Value()) > 0 { // encryption - encLayers = &[]int{} + p := opts.encryptLayer.Value() + encLayers = &p encryptionKeys := opts.encryptionKeys.Value() ecc, err := enchelpers.CreateCryptoConfig(encryptionKeys, []string{}) if err != nil { diff --git a/docs/skopeo-copy.1.md b/docs/skopeo-copy.1.md index 1d4653ea..036e9d56 100644 --- a/docs/skopeo-copy.1.md +++ b/docs/skopeo-copy.1.md @@ -98,12 +98,12 @@ To copy and sign an image: To encrypt an image: ```sh -skopeo copy docker://docker.io/library/nginx:latest oci:local_nginx:latest +skopeo copy docker://docker.io/library/nginx:1.17.8 oci:local_nginx:1.17.8 openssl genrsa -out private.key 1024 openssl rsa -in private.key -pubout > public.key -skopeo copy --encryption-key jwe:./public.key oci:local_nginx:latest oci:try-encrypt:encrypted +skopeo copy --encryption-key jwe:./public.key oci:local_nginx:1.17.8 oci:try-encrypt:encrypted ``` To decrypt an image: @@ -120,6 +120,14 @@ To decrypt an image that requires more than one key: ```sh skopeo copy --decryption-key ./private1.key --decryption-key ./private2.key --decryption-key ./private3.key oci:try-encrypt:encrypted oci:try-decrypt:decrypted ``` + +Container images can also be partially encrypted by specifying the index of the layer. Layers are 0-indexed indices, with support for negative indexing. i.e. 0 is the first layer, -1 is the last layer. + +Let's say out of 3 layers that the image `docker.io/library/nginx:1.17.8` is made up of, we only want to encrypt the 2nd layer, +```sh +skopeo copy --encryption-key jwe:./public.key --encrypt-layer 1 oci:local_nginx:1.17.8 oci:try-encrypt:encrypted +``` + ## SEE ALSO skopeo(1), podman-login(1), docker-login(1) diff --git a/integration/copy_test.go b/integration/copy_test.go index 707293a5..696a3d74 100644 --- a/integration/copy_test.go +++ b/integration/copy_test.go @@ -553,6 +553,15 @@ func (s *CopySuite) TestCopyEncryption(c *check.C) { defer os.RemoveAll(keysDir) undecryptedImgDir, err := ioutil.TempDir("", "copy-5") defer os.RemoveAll(undecryptedImgDir) + multiLayerImageDir, err := ioutil.TempDir("", "copy-6") + c.Assert(err, check.IsNil) + defer os.RemoveAll(multiLayerImageDir) + partiallyEncryptedImgDir, err := ioutil.TempDir("", "copy-7") + c.Assert(err, check.IsNil) + defer os.RemoveAll(partiallyEncryptedImgDir) + partiallyDecryptedImgDir, err := ioutil.TempDir("", "copy-8") + c.Assert(err, check.IsNil) + defer os.RemoveAll(partiallyDecryptedImgDir) // Create RSA key pair privateKey, err := rsa.GenerateKey(rand.Reader, 4096) @@ -577,7 +586,7 @@ func (s *CopySuite) TestCopyEncryption(c *check.C) { "oci:"+encryptedImgDir+":encrypted", "oci:"+decryptedImgDir+":decrypted") // Copy a standard busybox image locally - assertSkopeoSucceeds(c, "", "copy", "docker://busybox", "oci:"+originalImageDir+":latest") + assertSkopeoSucceeds(c, "", "copy", "docker://busybox:1.31.1", "oci:"+originalImageDir+":latest") // Encrypt the image assertSkopeoSucceeds(c, "", "copy", "--encryption-key", @@ -597,7 +606,7 @@ func (s *CopySuite) TestCopyEncryption(c *check.C) { assertSkopeoSucceeds(c, "", "copy", "oci:"+encryptedImgDir+":encrypted", "oci:"+undecryptedImgDir+":encrypted") // Original busybox image has gzipped layers. But encrypted busybox layers should // not be of gzip type - matchLayerBlobBinaryType(c, undecryptedImgDir+"/blobs/sha256", "application/x-gzip", false) + matchLayerBlobBinaryType(c, undecryptedImgDir+"/blobs/sha256", "application/x-gzip", 0) // Decrypt the image assertSkopeoSucceeds(c, "", "copy", "--decryption-key", keysDir+"/private.key", @@ -605,13 +614,32 @@ func (s *CopySuite) TestCopyEncryption(c *check.C) { // After successful decryption we should find the gzipped layer from the // busybox image - matchLayerBlobBinaryType(c, decryptedImgDir+"/blobs/sha256", "application/x-gzip", true) + matchLayerBlobBinaryType(c, decryptedImgDir+"/blobs/sha256", "application/x-gzip", 1) + + // Copy a standard multi layer nginx image locally + assertSkopeoSucceeds(c, "", "copy", "docker://nginx:1.17.8", "oci:"+multiLayerImageDir+":latest") + + // Partially encrypt the image + assertSkopeoSucceeds(c, "", "copy", "--encryption-key", "jwe:"+keysDir+"/public.key", + "--encrypt-layer", "1", "oci:"+multiLayerImageDir+":latest", "oci:"+partiallyEncryptedImgDir+":encrypted") + + // Since the image is partially encrypted we should find layers that aren't encrypted + matchLayerBlobBinaryType(c, partiallyEncryptedImgDir+"/blobs/sha256", "application/x-gzip", 2) + + // Decrypt the partically encrypted image + assertSkopeoSucceeds(c, "", "copy", "--decryption-key", keysDir+"/private.key", + "oci:"+partiallyEncryptedImgDir+":encrypted", "oci:"+partiallyDecryptedImgDir+":decrypted") + + // After successful decryption we should find the gzipped layers from the nginx image + matchLayerBlobBinaryType(c, partiallyDecryptedImgDir+"/blobs/sha256", "application/x-gzip", 3) + } -func matchLayerBlobBinaryType(c *check.C, ociImageDirPath string, contentType string, shouldMatch bool) { +func matchLayerBlobBinaryType(c *check.C, ociImageDirPath string, contentType string, matchCount int) { files, err := ioutil.ReadDir(ociImageDirPath) c.Assert(err, check.IsNil) - blobFound := false + + foundCount := 0 for _, f := range files { fileContent, err := os.Open(ociImageDirPath + "/" + f.Name()) c.Assert(err, check.IsNil) @@ -619,13 +647,11 @@ func matchLayerBlobBinaryType(c *check.C, ociImageDirPath string, contentType st c.Assert(err, check.IsNil) if layerContentType == contentType { - blobFound = true - break + foundCount = foundCount + 1 } } - c.Assert(blobFound, check.Equals, shouldMatch) - + c.Assert(foundCount, check.Equals, matchCount) } func getFileContentType(out *os.File) (string, error) {