Implemented image encryption/decryption libraries and ctr commands

Signed-off-by: Stefan Berger <stefanb@linux.ibm.com>
Signed-off-by: Brandon Lum <lumjjb@gmail.com>
This commit is contained in:
Stefan Berger 2019-02-11 12:30:50 -05:00 committed by Brandon Lum
parent 30c3443947
commit bf8804c743
34 changed files with 5157 additions and 2 deletions

View File

@ -127,6 +127,22 @@ var (
Usage: "memory limit (in bytes) for the container",
},
}
// ImageDecryptionFlags are cli flags needed when decrypting an image
ImageDecryptionFlags = []cli.Flag{
cli.StringFlag{
Name: "gpg-homedir",
Usage: "The GPG homedir to use; by default gpg uses ~/.gnupg",
}, cli.StringFlag{
Name: "gpg-version",
Usage: "The GPG version (\"v1\" or \"v2\"), default will make an educated guess",
}, cli.StringSliceFlag{
Name: "key",
Usage: "A secret key's filename and an optional password separated by colon; this option may be provided multiple times",
}, cli.StringSliceFlag{
Name: "dec-recipient",
Usage: "Recipient of the image; used only for PKCS7 and must be an x509 certificate",
},
}
)
// ObjectWithLabelArgs returns the first arg and a LabelArgs object

View File

@ -0,0 +1,419 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package images
import (
gocontext "context"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"time"
"github.com/containerd/containerd"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/images/encryption"
encconfig "github.com/containerd/containerd/images/encryption/config"
encutils "github.com/containerd/containerd/images/encryption/utils"
"github.com/containerd/containerd/leases"
"github.com/containerd/containerd/platforms"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
// LayerInfo holds information about an image layer
type LayerInfo struct {
// The Number of this layer in the sequence; starting at 0
Index uint32
Descriptor ocispec.Descriptor
}
// isUserSelectedLayer checks whether a layer is user-selected given its number
// A layer can be described with its (positive) index number or its negative number.
// The latter is counted relative to the topmost one (-1), the former relative to
// the bottommost one (0).
func isUserSelectedLayer(layerIndex, layersTotal int32, layers []int32) bool {
if len(layers) == 0 {
// convenience for the user; none given means 'all'
return true
}
negNumber := layerIndex - layersTotal
for _, l := range layers {
if l == negNumber || l == layerIndex {
return true
}
}
return false
}
// isUserSelectedPlatform determines whether the platform matches one in
// the array of user-provided platforms
func isUserSelectedPlatform(platform *ocispec.Platform, platformList []ocispec.Platform) bool {
if len(platformList) == 0 {
// convenience for the user; none given means 'all'
return true
}
matcher := platforms.NewMatcher(*platform)
for _, platform := range platformList {
if matcher.Match(platform) {
return true
}
}
return false
}
// processRecipientKeys sorts the array of recipients by type. Recipients may be either
// x509 certificates, public keys, or PGP public keys identified by email address or name
func processRecipientKeys(recipients []string) ([][]byte, [][]byte, [][]byte, error) {
var (
gpgRecipients [][]byte
pubkeys [][]byte
x509s [][]byte
)
for _, recipient := range recipients {
tmp, err := ioutil.ReadFile(recipient)
if err != nil {
gpgRecipients = append(gpgRecipients, []byte(recipient))
continue
}
if encutils.IsCertificate(tmp) {
x509s = append(x509s, tmp)
} else if encutils.IsPublicKey(tmp) {
pubkeys = append(pubkeys, tmp)
} else {
gpgRecipients = append(gpgRecipients, []byte(recipient))
}
}
return gpgRecipients, pubkeys, x509s, nil
}
// Process a password that may be in any of the following formats:
// - file=<passwordfile>
// - pass=<password>
// - fd=<filedescriptor>
// - <password>
func processPwdString(pwdString string) ([]byte, error) {
if strings.HasPrefix(pwdString, "file=") {
return ioutil.ReadFile(pwdString[5:])
} else if strings.HasPrefix(pwdString, "pass=") {
return []byte(pwdString[5:]), nil
} else if strings.HasPrefix(pwdString, "fd=") {
fdStr := pwdString[3:]
fd, err := strconv.Atoi(fdStr)
if err != nil {
return nil, errors.Wrapf(err, "could not parse file descriptor %s", fdStr)
}
f := os.NewFile(uintptr(fd), "pwdfile")
if f == nil {
return nil, fmt.Errorf("%s is not a valid file descriptor", fdStr)
}
defer f.Close()
pwd := make([]byte, 64)
n, err := f.Read(pwd)
if err != nil {
return nil, errors.Wrapf(err, "could not read from file descriptor")
}
return pwd[:n], nil
}
return []byte(pwdString), nil
}
// processPrivateKeyFiles sorts the different types of private key files; private key files may either be
// private keys or GPG private key ring files. The private key files may include the password for the
// private key and take any of the following forms:
// - <filename>
// - <filename>:file=<passwordfile>
// - <filename>:pass=<password>
// - <filename>:fd=<filedescriptor>
// - <filename>:<password>
func processPrivateKeyFiles(keyFilesAndPwds []string) ([][]byte, [][]byte, [][]byte, [][]byte, error) {
var (
gpgSecretKeyRingFiles [][]byte
gpgSecretKeyPasswords [][]byte
privkeys [][]byte
privkeysPasswords [][]byte
err error
)
// keys needed for decryption in case of adding a recipient
for _, keyfileAndPwd := range keyFilesAndPwds {
var password []byte
parts := strings.Split(keyfileAndPwd, ":")
if len(parts) == 2 {
password, err = processPwdString(parts[1])
if err != nil {
return nil, nil, nil, nil, err
}
}
keyfile := parts[0]
tmp, err := ioutil.ReadFile(keyfile)
if err != nil {
return nil, nil, nil, nil, err
}
isPrivKey, err := encutils.IsPrivateKey(tmp, password)
if encutils.IsPasswordError(err) {
return nil, nil, nil, nil, err
}
if isPrivKey {
privkeys = append(privkeys, tmp)
privkeysPasswords = append(privkeysPasswords, password)
} else if encutils.IsGPGPrivateKeyRing(tmp) {
gpgSecretKeyRingFiles = append(gpgSecretKeyRingFiles, tmp)
gpgSecretKeyPasswords = append(gpgSecretKeyPasswords, password)
} else {
return nil, nil, nil, nil, fmt.Errorf("unidentified private key in file %s (password=%s)", keyfile, string(password))
}
}
return gpgSecretKeyRingFiles, gpgSecretKeyPasswords, privkeys, privkeysPasswords, nil
}
func createGPGClient(context *cli.Context) (encryption.GPGClient, error) {
return encryption.NewGPGClient(context.String("gpg-version"), context.String("gpg-homedir"))
}
func getGPGPrivateKeys(context *cli.Context, gpgSecretKeyRingFiles [][]byte, descs []ocispec.Descriptor, mustFindKey bool, dcparameters map[string][][]byte) error {
gpgClient, err := createGPGClient(context)
if err != nil {
return err
}
var gpgVault encryption.GPGVault
if len(gpgSecretKeyRingFiles) > 0 {
gpgVault = encryption.NewGPGVault()
err = gpgVault.AddSecretKeyRingDataArray(gpgSecretKeyRingFiles)
if err != nil {
return err
}
}
return encryption.GPGGetPrivateKey(descs, gpgClient, gpgVault, mustFindKey, dcparameters)
}
func createLayerFilter(client *containerd.Client, ctx gocontext.Context, desc ocispec.Descriptor, layers []int32, platformList []ocispec.Platform) (images.LayerFilter, error) {
alldescs, err := images.GetImageLayerDescriptors(ctx, client.ContentStore(), desc)
if err != nil {
return nil, err
}
_, descs := filterLayerDescriptors(alldescs, layers, platformList)
lf := func(d ocispec.Descriptor) bool {
for _, desc := range descs {
if desc.Digest.String() == d.Digest.String() {
return true
}
}
return false
}
return lf, nil
}
// cryptImage encrypts or decrypts an image with the given name and stores it either under the newName
// or updates the existing one
func cryptImage(client *containerd.Client, ctx gocontext.Context, name, newName string, cc *encconfig.CryptoConfig, layers []int32, platformList []string, encrypt bool) (images.Image, error) {
s := client.ImageService()
image, err := s.Get(ctx, name)
if err != nil {
return images.Image{}, err
}
pl, err := parsePlatformArray(platformList)
if err != nil {
return images.Image{}, err
}
lf, err := createLayerFilter(client, ctx, image.Target, layers, pl)
if err != nil {
return images.Image{}, err
}
var (
modified bool
newSpec ocispec.Descriptor
)
ls := client.LeasesService()
l, err := ls.Create(ctx, leases.WithRandomID(), leases.WithExpiration(5*time.Minute))
if err != nil {
return images.Image{}, err
}
defer ls.Delete(ctx, l, leases.SynchronousDelete)
if encrypt {
newSpec, modified, err = images.EncryptImage(ctx, client.ContentStore(), ls, l, image.Target, cc, lf)
} else {
newSpec, modified, err = images.DecryptImage(ctx, client.ContentStore(), ls, l, image.Target, cc, lf)
}
if err != nil {
return image, err
}
if !modified {
return image, nil
}
image.Target = newSpec
// if newName is either empty or equal to the existing name, it's an update
if newName == "" || strings.Compare(image.Name, newName) == 0 {
// first Delete the existing and then Create a new one
// We have to do it this way since we have a newSpec!
err = s.Delete(ctx, image.Name)
if err != nil {
return images.Image{}, err
}
newName = image.Name
}
image.Name = newName
return s.Create(ctx, image)
}
func encryptImage(client *containerd.Client, ctx gocontext.Context, name, newName string, cc *encconfig.CryptoConfig, layers []int32, platformList []string) (images.Image, error) {
return cryptImage(client, ctx, name, newName, cc, layers, platformList, true)
}
func decryptImage(client *containerd.Client, ctx gocontext.Context, name, newName string, cc *encconfig.CryptoConfig, layers []int32, platformList []string) (images.Image, error) {
return cryptImage(client, ctx, name, newName, cc, layers, platformList, false)
}
func getImageLayerInfos(client *containerd.Client, ctx gocontext.Context, name string, layers []int32, platformList []string) ([]LayerInfo, []ocispec.Descriptor, error) {
s := client.ImageService()
image, err := s.Get(ctx, name)
if err != nil {
return nil, nil, err
}
pl, err := parsePlatformArray(platformList)
if err != nil {
return nil, nil, err
}
alldescs, err := images.GetImageLayerDescriptors(ctx, client.ContentStore(), image.Target)
if err != nil {
return nil, nil, err
}
lis, descs := filterLayerDescriptors(alldescs, layers, pl)
return lis, descs, nil
}
func countLayers(descs []ocispec.Descriptor, platform *ocispec.Platform) int32 {
c := int32(0)
for _, desc := range descs {
if desc.Platform == platform {
c = c + 1
}
}
return c
}
func filterLayerDescriptors(alldescs []ocispec.Descriptor, layers []int32, pl []ocispec.Platform) ([]LayerInfo, []ocispec.Descriptor) {
var (
layerInfos []LayerInfo
descs []ocispec.Descriptor
curplat *ocispec.Platform
layerIndex int32
layersTotal int32
)
for _, desc := range alldescs {
if curplat != desc.Platform {
curplat = desc.Platform
layerIndex = 0
layersTotal = countLayers(alldescs, desc.Platform)
} else {
layerIndex = layerIndex + 1
}
if isUserSelectedLayer(layerIndex, layersTotal, layers) && isUserSelectedPlatform(curplat, pl) {
li := LayerInfo{
Index: uint32(layerIndex),
Descriptor: desc,
}
descs = append(descs, desc)
layerInfos = append(layerInfos, li)
}
}
return layerInfos, descs
}
// CreateDcParameters creates the decryption parameter map from command line options and possibly
// LayerInfos describing the image and helping us to query for the PGP decryption keys
func CreateDcParameters(context *cli.Context, descs []ocispec.Descriptor) (map[string][][]byte, error) {
dcparameters := make(map[string][][]byte)
// x509 cert is needed for PKCS7 decryption
_, _, x509s, err := processRecipientKeys(context.StringSlice("dec-recipient"))
if err != nil {
return nil, err
}
gpgSecretKeyRingFiles, gpgSecretKeyPasswords, privKeys, privKeysPasswords, err := processPrivateKeyFiles(context.StringSlice("key"))
if err != nil {
return nil, err
}
_, err = createGPGClient(context)
gpgInstalled := err == nil
if gpgInstalled {
if len(gpgSecretKeyRingFiles) == 0 && len(privKeys) == 0 && descs != nil {
// Get pgp private keys from keyring only if no private key was passed
err = getGPGPrivateKeys(context, gpgSecretKeyRingFiles, descs, true, dcparameters)
if err != nil {
return nil, err
}
} else {
if len(gpgSecretKeyRingFiles) == 0 {
dcparameters["gpg-client"] = [][]byte{[]byte("1")}
dcparameters["gpg-client-version"] = [][]byte{[]byte(context.String("gpg-version"))}
dcparameters["gpg-client-homedir"] = [][]byte{[]byte(context.String("gpg-homedir"))}
} else {
dcparameters["gpg-privatekeys"] = gpgSecretKeyRingFiles
dcparameters["gpg-privatekeys-passwords"] = gpgSecretKeyPasswords
}
}
}
dcparameters["privkeys"] = privKeys
dcparameters["privkeys-passwords"] = privKeysPasswords
dcparameters["x509s"] = x509s
return dcparameters, nil
}
// parsePlatformArray parses an array of specifiers and converts them into an array of specs.Platform
func parsePlatformArray(specifiers []string) ([]ocispec.Platform, error) {
var speclist []ocispec.Platform
for _, specifier := range specifiers {
spec, err := platforms.Parse(specifier)
if err != nil {
return []ocispec.Platform{}, err
}
speclist = append(speclist, spec)
}
return speclist, nil
}

View File

@ -0,0 +1,100 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package images
import (
"fmt"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/images"
encconfig "github.com/containerd/containerd/images/encryption/config"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
var decryptCommand = cli.Command{
Name: "decrypt",
Usage: "decrypt an image locally",
ArgsUsage: "[flags] <local> <new name>",
Description: `Decrypt an image locally.
Decrypt an image using private keys.
The user has contol over which layers to decrypt and for which platform.
If no payers or platforms are specified, all layers for all platforms are
decrypted.
Private keys in PEM format may be encrypted and the password may be passed
along in any of the following formats:
- <filename>:<password>
- <filename>:pass=<password>
- <filename>:fd=<file descriptor>
- <filename>:filename=<password file>
`,
Flags: append(append(commands.RegistryFlags, cli.IntSliceFlag{
Name: "layer",
Usage: "The layer to decrypt; this must be either the layer number or a negative number starting with -1 for topmost layer",
}, cli.StringSliceFlag{
Name: "platform",
Usage: "For which platform to decrypt; by default decryption is done for all platforms",
},
), commands.ImageDecryptionFlags...),
Action: func(context *cli.Context) error {
local := context.Args().First()
if local == "" {
return errors.New("please provide the name of an image to decrypt")
}
newName := context.Args().Get(1)
if newName != "" {
fmt.Printf("Decrypting %s to %s\n", local, newName)
} else {
fmt.Printf("Decrypting %s and replacing it with the decrypted image\n", local)
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
layers32 := commands.IntToInt32Array(context.IntSlice("layer"))
_, descs, err := getImageLayerInfos(client, ctx, local, layers32, context.StringSlice("platform"))
if err != nil {
return err
}
isEncrypted := images.HasEncryptedLayer(ctx, descs)
if !isEncrypted {
fmt.Printf("Nothing to decrypted.\n")
return nil
}
dcparameters, err := CreateDcParameters(context, descs)
if err != nil {
return err
}
cc := &encconfig.CryptoConfig{
DecryptConfig: &encconfig.DecryptConfig{
Parameters: dcparameters,
},
}
_, err = decryptImage(client, ctx, local, newName, cc, layers32, context.StringSlice("platform"))
return err
},
}

View File

@ -0,0 +1,148 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package images
import (
"fmt"
"github.com/containerd/containerd/cmd/ctr/commands"
encconfig "github.com/containerd/containerd/images/encryption/config"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
var encryptCommand = cli.Command{
Name: "encrypt",
Usage: "encrypt an image locally",
ArgsUsage: "[flags] <local> <new name>",
Description: `Encrypt an image locally.
Encrypt an image using public keys managed by GPG.
The user must provide recpients who will be able to decrypt the image using
their GPG-managed private key. For this the user's GPG keyring must hold the public
keys of the recipients.
The user has control over the individual layers and the platforms they are
associated with and can encrypt them separately. If no layers or platforms are
specified, all layers for all platforms will be encrypted.
This tool also allows management of the recipients of the image through changes
to the list of recipients.
Once the image has been encrypted it may be pushed to a registry.
`,
Flags: append(append(commands.RegistryFlags, cli.StringSliceFlag{
Name: "recipient",
Usage: "Recipient of the image is the person who can decrypt it",
}, cli.IntSliceFlag{
Name: "layer",
Usage: "The layer to encrypt; this must be either the layer number or a negative number starting with -1 for topmost layer",
}, cli.StringSliceFlag{
Name: "platform",
Usage: "For which platform to encrypt; by default encrytion is done for all platforms",
}), commands.ImageDecryptionFlags...),
Action: func(context *cli.Context) error {
local := context.Args().First()
if local == "" {
return errors.New("please provide the name of an image to encrypt")
}
newName := context.Args().Get(1)
if newName != "" {
fmt.Printf("Encrypting %s to %s\n", local, newName)
} else {
fmt.Printf("Encrypting %s and replacing it with the encrypted image\n", local)
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
recipients := context.StringSlice("recipient")
if len(recipients) == 0 {
return errors.New("no recipients given -- nothing to do")
}
layers32 := commands.IntToInt32Array(context.IntSlice("layer"))
gpgSecretKeyRingFiles, _, privKeys, privKeysPasswords, err := processPrivateKeyFiles(context.StringSlice("key"))
if err != nil {
return err
}
gpgRecipients, pubKeys, x509s, err := processRecipientKeys(recipients)
if err != nil {
return err
}
_, _, decX509s, err := processRecipientKeys(context.StringSlice("dec-recipient"))
if err != nil {
return err
}
dcparameters := make(map[string][][]byte)
parameters := make(map[string][][]byte)
parameters["pubkeys"] = pubKeys
parameters["x509s"] = x509s
_, descs, err := getImageLayerInfos(client, ctx, local, layers32, context.StringSlice("platform"))
if err != nil {
return err
}
_, err = createGPGClient(context)
gpgInstalled := err == nil
if len(privKeys) == 0 && gpgInstalled {
// Get pgp private keys from keyring only if no private key was passed
err = getGPGPrivateKeys(context, gpgSecretKeyRingFiles, descs, true, dcparameters)
if err != nil {
return err
}
}
if len(gpgRecipients) > 0 && gpgInstalled {
parameters["gpg-recipients"] = gpgRecipients
gpgClient, err := createGPGClient(context)
if err != nil {
return err
}
gpgPubRingFile, err := gpgClient.ReadGPGPubRingFile()
if err != nil {
return err
}
parameters["gpg-pubkeyringfile"] = [][]byte{gpgPubRingFile}
}
dcparameters["privkeys"] = privKeys
dcparameters["privkeys-passwords"] = privKeysPasswords
dcparameters["x509s"] = decX509s
cc := &encconfig.CryptoConfig{
EncryptConfig: &encconfig.EncryptConfig{
Parameters: parameters,
DecryptConfig: encconfig.DecryptConfig{
Parameters: dcparameters,
},
},
}
_, err = encryptImage(client, ctx, local, newName, cc, layers32, context.StringSlice("platform"))
return err
},
}

View File

@ -48,6 +48,9 @@ var Command = cli.Command{
removeCommand,
tagCommand,
setLabelsCommand,
encryptCommand,
decryptCommand,
layerinfoCommand,
},
}

View File

@ -0,0 +1,118 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package images
import (
"fmt"
"os"
"sort"
"strings"
"text/tabwriter"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/images/encryption"
"github.com/containerd/containerd/platforms"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
var layerinfoCommand = cli.Command{
Name: "layerinfo",
Usage: "get information about an image's layers",
ArgsUsage: "[flags] <local>",
Description: `Get encryption information about the layers of an image.
Get information about the layers of an image and display with which
encryption technology the individual layers are encrypted with.
The user has control over the individual layers and the platforms they are
associated with and can retrieve information for them separately. If no
layers or platforms are specified, infomration for all layers and all
platforms will be retrieved.
`,
Flags: append(commands.RegistryFlags, cli.IntSliceFlag{
Name: "layer",
Usage: "The layer to get info for; this must be either the layer number or a negative number starting with -1 for topmost layer",
}, cli.StringSliceFlag{
Name: "platform",
Usage: "For which platform to get the layer info; by default info for all platforms is retrieved",
}, cli.StringFlag{
Name: "gpg-homedir",
Usage: "The GPG homedir to use; by default gpg uses ~/.gnupg",
}, cli.StringFlag{
Name: "gpg-version",
Usage: "The GPG version (\"v1\" or \"v2\"), default will make an educated guess",
}, cli.BoolFlag{
Name: "n",
Usage: "Do not resolve PGP key IDs to email addresses",
}),
Action: func(context *cli.Context) error {
local := context.Args().First()
if local == "" {
return errors.New("please provide the name of an image to decrypt")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
layers32 := commands.IntToInt32Array(context.IntSlice("layer"))
LayerInfos, _, err := getImageLayerInfos(client, ctx, local, layers32, context.StringSlice("platform"))
if err != nil {
return err
}
if len(LayerInfos) == 0 {
return nil
}
var gpgClient encryption.GPGClient
if !context.Bool("n") {
// create a GPG client to resolve keyIds to names
gpgClient, _ = createGPGClient(context)
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.AlignRight)
fmt.Fprintf(w, "#\tDIGEST\tPLATFORM\tSIZE\tENCRYPTION\tRECIPIENTS\t\n")
for _, layer := range LayerInfos {
var recipients []string
var schemes []string
for scheme, wrappedKeys := range encryption.GetWrappedKeysMap(layer.Descriptor) {
schemes = append(schemes, scheme)
keywrapper := encryption.GetKeyWrapper(scheme)
if keywrapper != nil {
addRecipients, err := keywrapper.GetRecipients(wrappedKeys)
if err != nil {
return err
}
if scheme == "pgp" && gpgClient != nil {
addRecipients = gpgClient.ResolveRecipients(addRecipients)
}
recipients = append(recipients, addRecipients...)
} else {
recipients = append(recipients, fmt.Sprintf("No %s KeyWrapper", scheme))
}
}
sort.Strings(schemes)
sort.Strings(recipients)
fmt.Fprintf(w, "%d\t%s\t%s\t%d\t%s\t%s\t\n", layer.Index, layer.Descriptor.Digest.String(), platforms.Format(*layer.Descriptor.Platform), layer.Descriptor.Size, strings.Join(schemes, ","), strings.Join(recipients, ", "))
}
w.Flush()
return nil
},
}

27
cmd/ctr/commands/utils.go Normal file
View File

@ -0,0 +1,27 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package commands
// IntToInt32Array converts an array of int's to int32's
func IntToInt32Array(in []int) []int32 {
var ret []int32
for _, v := range in {
ret = append(ret, int32(v))
}
return ret
}

View File

@ -87,6 +87,46 @@ func WriteBlob(ctx context.Context, cs Ingester, ref string, r io.Reader, desc o
return Copy(ctx, cw, r, desc.Size, desc.Digest, opts...)
}
// WriteBlobBlind writes data without expected digest into the content store. If
// expected already exists, the method returns immediately and the reader will
// not be consumed.
//
// This is useful when the digest and size are NOT known beforehand.
//
// Copy is buffered, so no need to wrap reader in buffered io.
func WriteBlobBlind(ctx context.Context, cs Ingester, ref string, r io.Reader, opts ...Opt) (digest.Digest, int64, error) {
cw, err := OpenWriter(ctx, cs, WithRef(ref))
if err != nil {
return "", 0, errors.Wrap(err, "failed to open writer")
}
defer cw.Close()
ws, err := cw.Status()
if err != nil {
return "", 0, errors.Wrap(err, "failed to get status")
}
if ws.Offset > 0 {
// not needed
return "", 0, errors.New("ws.Offset > 0 is not supported")
}
size, err := copyWithBuffer(cw, r)
if err != nil {
return "", 0, errors.Wrap(err, "failed to copy")
}
digest := cw.Digest()
if err := cw.Commit(ctx, size, digest); err != nil {
if !errdefs.IsAlreadyExists(err) {
return "", 0, errors.Wrapf(err, "failed commit block")
}
}
return digest, size, err
}
// OpenWriter opens a new writer for the given reference, retrying if the writer
// is locked until the reference is available or returns an error.
func OpenWriter(ctx context.Context, cs Ingester, opts ...WriterOpt) (Writer, error) {

120
docs/encryption.md Normal file
View File

@ -0,0 +1,120 @@
# containerd image encryption
The containerd encryption feature allows the encryption and decryption of a container image.
It is based on a [proposal for an OCI specification](https://github.com/opencontainers/image-spec/issues/747) to define the structure of an encrypted image.
The encryption is specified on the layer, allowing the encryption of specific layers within an image. For example, given an image with an ubuntu, node and some custom code on top, only the custom code (top-most layer) can be encrypted to still benefit from layer deduplication of insensitive data. The key sharing is done via wrapped keys in the metadata.
Therefore, an encrypted image data intended for multiple recipients can also be deduplicated.
All this is done via the addition of additional layer mediatype `+enc`.
More details can be viewed in the [design doc](https://docs.google.com/document/d/146Eaj7_r1B0Q_2KylVHbXhxcuogsnlSbqjwGTORB8iw).
The two main usage points are in the creation of an encrypted image, and the decryption of the image upon usage.
As most of the integration points would be transparent or consumed by other containerd runtime components or other utilities like buildkit, we have created several `ctr` commands to better illustrate the usage of the encryption feature.
# Example End User Usage
We have added 3 commands in the [`ctr`](https://github.com/containerd/containerd/tree/master/cmd/ctr) client under the image module. They are:
- `ctr image encrypt`
- `ctr image decrypt`
- `ctr image layerinfo`
## Encrypt
The following command performs an encryption of the image `docker.io/library/alpine:latest` to an encrypted image with the tag `docker.io/library/alpine:enc`.
The encryption is done for two recipients with the public key of `/tmp/tmp.AGrSDkaSad/mypubkey.pem` (jwe) and `/tmp/tmp.AGrSDkaSad/clientcert.pem` (pkcs7).
The option `--layer -1` specifies the layer filter for encryption, -1 indicating the top-most layer should be encrypted.
```
$ ctr images encrypt \
--recipient /tmp/tmp.AGrSDkaSad/mypubkey.pem \
--recipient /tmp/tmp.AGrSDkaSad/clientcert.pem \
--layer -1 \
docker.io/library/alpine:latest docker.io/library/alpine:enc
Encrypting docker.io/library/alpine:latest to docker.io/library/alpine:enc
```
## Layerinfo
The layerinfo command provides information about the encryption status of an image. In the following command, we use it to inspect the encryption metadata.
```
$ ctr images layerinfo docker.io/library/alpine:enc
# DIGEST PLATFORM SIZE ENCRYPTION RECIPIENTS
0 sha256:3427d6934e7749d556be6881a17265c9817abc6447df80a09c8eecc465c5bfb3 linux/amd64 2206947
0 sha256:d9a094b6b49fc760501d44ae96f19284e86db0a51b979756ca8a0df4a2746c79 linux/arm/v6 2146469
1 sha256:ef87d8b3048d8f1f7af7605328f63aab078a1433116dc15738989551184d7a87 linux/arm/v6 191 jwe,pkcs7 [jwe], [pkcs7]
0 sha256:4b0872dff46806a4037c5f158d1d17d5252c9e1f421b7c61445f1a64f6a853a8 linux/arm64/v8 2099778
1 sha256:fe022206e6848082f9c1d6e69974157af70ad56bf8698d89e1641d4598bf8ce9 linux/arm64/v8 192 jwe,pkcs7 [jwe], [pkcs7]
0 sha256:d1fceb26d4a2dc1f30d05fd0f9567edb5997d504f044ad6486aecc3d5aaa9b4e linux/386 2271476
1 sha256:383a3d4c6789667dbfb6b3742492c4a925315e750f99a5d664ff72f2bb0ae659 linux/386 191 jwe,pkcs7 [jwe], [pkcs7]
0 sha256:8aea19b10fd75004ab8fd2d02df719c06528ad3539e686a2d26c933d53f25675 linux/ppc64le 2195242
1 sha256:965a60ab5513a2eee33f4d960b63ee347215eb31d06a4ed61f6d90d209462d76 linux/ppc64le 193 jwe,pkcs7 [jwe], [pkcs7]
0 sha256:783541963cb4e52173193fe947bb7a7f7e5a6657a4cbbb6b8b077bbee7255605 linux/s390x 2307762
1 sha256:69d3260b3f5430ade9a3ee0f1b71a32a8e4ef268552beeae29930a8795dc54bf linux/s390x 192 jwe,pkcs7 [jwe], [pkcs7]
```
## Decrypt
The following command performs an decryption of the encrypted image `docker.io/library/alpine:enc` to the image tag `docker.io/library/alpine:dec`.
The decryption is done by passing in the private key that corresponds to at least one of the recipients of the encrypted image.
```
$ ctr images decrypt \
--key /tmp/tmp.AGrSDkaSad/mykey2.pem \
docker.io/library/alpine:enc docker.io/library/alpine:dec
Decrypting docker.io/library/alpine:enc to docker.io/library/alpine:dec
```
# Other Consumers
Other consumers of the encryption routine can include the [containerd diff plugin](https://github.com/containerd/containerd/tree/master/services/diff) to have the decryption be performed in the pull path, as well as the docker CLI or other build tools that wish to provide an encryption option.
The current draft of exposed interfaces we believe will be used by consumers are as follows:
```
/* Functions */
// EncryptImage encrypts an image; it accepts either an OCI descriptor representing a manifest list or a single manifest
func EncryptImage(ctx context.Context, cs content.Store, ls leases.Manager, l leases.Lease, desc ocispec.Descriptor, cc *encconfig.CryptoConfig, lf *LayerFilter) (ocispec.Descriptor, bool, error)
// DecryptImage decrypts an image; it accepts either an OCI descriptor representing a manifest list or a single manifest
func DecryptImage(ctx context.Context, cs content.Store, ls leases.Manager, l leases.Lease, desc ocispec.Descriptor, cc *encconfig.CryptoConfig, lf *LayerFilter) (ocispec.Descriptor, bool, error)
// CheckAuthorization checks whether a user has the right keys to be allowed to access an image (every layer)
// It takes decrypting of the layers only as far as decrypting the asymmetrically encrypted data
// The decryption is only done for the current platform
func CheckAuthorization(ctx context.Context, cs content.Store, desc ocispec.Descriptor, dc *encconfig.DecryptConfig) error
// GetImageLayerDescriptors gets the image layer Descriptors of an image; the array contains
// a list of Descriptors belonging to one platform followed by lists of other platforms
func GetImageLayerDescriptors(ctx context.Context, cs content.Store, desc ocispec.Descriptor) ([]ocispec.Descriptor, error)
/* Cryptography Configuration Datastructures */
// CryptoConfig is a common wrapper for EncryptConfig and DecrypConfig that can
// be passed through functions that share much code for encryption and decryption
type CryptoConfig struct {
Ec *EncryptConfig
Dc *DecryptConfig
}
// EncryptConfig is the container image PGP encryption configuration holding
// the identifiers of those that will be able to decrypt the container and
// the PGP public keyring file data that contains their public keys.
type EncryptConfig struct {
// map holding 'gpg-recipients', 'gpg-pubkeyringfile', 'pubkeys', 'x509s'
Parameters map[string][][]byte
// for adding recipients on an already encrypted image we need the
// symmetric keys for the layers so we can wrap them with the recpient's
// public key
Operation int32 // currently only OperationAddRecipients is supported, if at all
Dc DecryptConfig
}
// DecryptConfig wraps the Parameters map that holds the decryption key
type DecryptConfig struct {
// map holding 'privkeys', 'x509s', 'gpg-privatekeys'
Parameters map[string][][]byte
}
```

215
image_enc_test.go Normal file
View File

@ -0,0 +1,215 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package containerd
import (
"context"
"runtime"
"testing"
"time"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
encconfig "github.com/containerd/containerd/images/encryption/config"
"github.com/containerd/containerd/images/encryption/utils"
"github.com/containerd/containerd/leases"
"github.com/containerd/containerd/platforms"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func setupBusyboxImage(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip()
}
const imageName = "docker.io/library/busybox:latest"
ctx, cancel := testContext(t)
defer cancel()
client, err := newClient(t, address)
if err != nil {
t.Fatal(err)
}
defer client.Close()
// Cleanup
err = client.ImageService().Delete(ctx, imageName)
if err != nil && !errdefs.IsNotFound(err) {
t.Fatal(err)
}
// By default pull does not unpack an image
image, err := client.Pull(ctx, imageName, WithPlatform("linux/amd64"))
if err != nil {
t.Fatal(err)
}
err = image.Unpack(ctx, DefaultSnapshotter)
if err != nil {
t.Fatal(err)
}
}
func TestImageEncryption(t *testing.T) {
setupBusyboxImage(t)
publicKey, privateKey, err := utils.CreateRSATestKey(2048, nil, true)
if err != nil {
t.Fatal(err)
}
const imageName = "docker.io/library/busybox:latest"
const encImageName = "docker.io/library/busybox:enc"
ctx, cancel := testContext(t)
defer cancel()
client, err := newClient(t, address)
if err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.ImageService()
ls := client.LeasesService()
image, err := s.Get(ctx, imageName)
if err != nil {
t.Fatal(err)
}
pl, err := platforms.Parse("linux/amd64")
if err != nil {
t.Fatal(err)
}
matcher := platforms.NewMatcher(pl)
alldescs, err := images.GetImageLayerDescriptors(ctx, client.ContentStore(), image.Target)
if err != nil {
t.Fatal(err)
}
var descs []ocispec.Descriptor
for _, desc := range alldescs {
if matcher.Match(*desc.Platform) {
descs = append(descs, desc)
}
}
lf := func(d ocispec.Descriptor) bool {
for _, desc := range descs {
if desc.Digest.String() == d.Digest.String() {
return true
}
}
return false
}
dcparameters := make(map[string][][]byte)
parameters := make(map[string][][]byte)
parameters["pubkeys"] = [][]byte{publicKey}
dcparameters["privkeys"] = [][]byte{privateKey}
dcparameters["privkeys-passwords"] = [][]byte{{}}
cc := &encconfig.CryptoConfig{
EncryptConfig: &encconfig.EncryptConfig{
Parameters: parameters,
DecryptConfig: encconfig.DecryptConfig{
Parameters: dcparameters,
},
},
}
l, err := ls.Create(ctx, leases.WithRandomID(), leases.WithExpiration(5*time.Minute))
if err != nil {
t.Fatal("Unable to create lease for encryption")
}
defer ls.Delete(ctx, l, leases.SynchronousDelete)
// Perform encryption of image
encSpec, modified, err := images.EncryptImage(ctx, client.ContentStore(), ls, l, image.Target, cc, lf)
if err != nil {
t.Fatal(err)
}
if !modified || image.Target.Digest == encSpec.Digest {
t.Fatal("Encryption did not modify the spec")
}
if !hasEncryption(ctx, client.ContentStore(), encSpec) {
t.Fatal("Encrypted image does not have encrypted layers")
}
image.Name = encImageName
image.Target = encSpec
if _, err := s.Create(ctx, image); err != nil {
t.Fatalf("Unable to create image: %v", err)
}
// Force deletion of lease early to check for proper referencing
ls.Delete(ctx, l, leases.SynchronousDelete)
cc = &encconfig.CryptoConfig{
DecryptConfig: &encconfig.DecryptConfig{
Parameters: dcparameters,
},
}
// Perform decryption of image
defer client.ImageService().Delete(ctx, imageName, images.SynchronousDelete())
defer client.ImageService().Delete(ctx, encImageName, images.SynchronousDelete())
lf = func(desc ocispec.Descriptor) bool {
return true
}
l, err = ls.Create(ctx, leases.WithRandomID(), leases.WithExpiration(5*time.Minute))
if err != nil {
t.Fatal("Unable to create lease for decryption")
}
defer ls.Delete(ctx, l, leases.SynchronousDelete)
decSpec, modified, err := images.DecryptImage(ctx, client.ContentStore(), ls, l, encSpec, cc, lf)
if err != nil {
t.Fatal(err)
}
if !modified || encSpec.Digest == decSpec.Digest {
t.Fatal("Decryption did not modify the spec")
}
if hasEncryption(ctx, client.ContentStore(), decSpec) {
t.Fatal("Decrypted image has encrypted layers")
}
}
func hasEncryption(ctx context.Context, provider content.Provider, spec ocispec.Descriptor) bool {
switch spec.MediaType {
case images.MediaTypeDockerSchema2LayerEnc, images.MediaTypeDockerSchema2LayerGzipEnc:
return true
default:
// pass
}
cspecs, err := images.Children(ctx, provider, spec)
if err != nil {
return false
}
for _, v := range cspecs {
if hasEncryption(ctx, provider, v) {
return true
}
}
return false
}

451
images/enc_utils.go Normal file
View File

@ -0,0 +1,451 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package images
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"math/rand"
"github.com/containerd/containerd/images/encryption"
encconfig "github.com/containerd/containerd/images/encryption/config"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/leases"
"github.com/containerd/containerd/platforms"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
"github.com/pkg/errors"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// IsEncryptedDiff returns true if mediaType is a known encrypted media type.
func IsEncryptedDiff(ctx context.Context, mediaType string) bool {
switch mediaType {
case MediaTypeDockerSchema2LayerGzipEnc, MediaTypeDockerSchema2LayerEnc:
return true
}
return false
}
// HasEncryptedLayer returns true if any LayerInfo indicates that the layer is encrypted
func HasEncryptedLayer(ctx context.Context, layerInfos []ocispec.Descriptor) bool {
for i := 0; i < len(layerInfos); i++ {
if IsEncryptedDiff(ctx, layerInfos[i].MediaType) {
return true
}
}
return false
}
// encryptLayer encrypts the layer using the CryptoConfig and creates a new OCI Descriptor.
// A call to this function may also only manipulate the wrapped keys list.
// The caller is expected to store the returned encrypted data and OCI Descriptor
func encryptLayer(cc *encconfig.CryptoConfig, dataReader content.ReaderAt, desc ocispec.Descriptor) (ocispec.Descriptor, io.Reader, error) {
var (
size int64
d digest.Digest
err error
)
encLayerReader, annotations, err := encryption.EncryptLayer(cc.EncryptConfig, encryption.ReaderFromReaderAt(dataReader), desc)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
// were data touched ?
if encLayerReader != nil {
size = 0
d = ""
} else {
size = desc.Size
d = desc.Digest
}
newDesc := ocispec.Descriptor{
Digest: d,
Size: size,
Platform: desc.Platform,
Annotations: annotations,
}
switch desc.MediaType {
case MediaTypeDockerSchema2LayerGzip:
newDesc.MediaType = MediaTypeDockerSchema2LayerGzipEnc
case MediaTypeDockerSchema2Layer:
newDesc.MediaType = MediaTypeDockerSchema2LayerEnc
case MediaTypeDockerSchema2LayerGzipEnc:
newDesc.MediaType = MediaTypeDockerSchema2LayerGzipEnc
case MediaTypeDockerSchema2LayerEnc:
newDesc.MediaType = MediaTypeDockerSchema2LayerEnc
// TODO: Mediatypes to be added in ocispec
case ocispec.MediaTypeImageLayerGzip:
newDesc.MediaType = MediaTypeDockerSchema2LayerGzipEnc
case ocispec.MediaTypeImageLayer:
newDesc.MediaType = MediaTypeDockerSchema2LayerEnc
default:
return ocispec.Descriptor{}, nil, errors.Errorf("Encryption: unsupporter layer MediaType: %s\n", desc.MediaType)
}
return newDesc, encLayerReader, nil
}
// decryptLayer decrypts the layer using the CryptoConfig and creates a new OCI Descriptor.
// The caller is expected to store the returned plain data and OCI Descriptor
func decryptLayer(cc *encconfig.CryptoConfig, dataReader content.ReaderAt, desc ocispec.Descriptor, unwrapOnly bool) (ocispec.Descriptor, io.Reader, error) {
resultReader, d, err := encryption.DecryptLayer(cc.DecryptConfig, encryption.ReaderFromReaderAt(dataReader), desc, unwrapOnly)
if err != nil || unwrapOnly {
return ocispec.Descriptor{}, nil, err
}
newDesc := ocispec.Descriptor{
Digest: d,
Size: 0,
Platform: desc.Platform,
}
switch desc.MediaType {
case MediaTypeDockerSchema2LayerGzipEnc:
newDesc.MediaType = MediaTypeDockerSchema2LayerGzip
case MediaTypeDockerSchema2LayerEnc:
newDesc.MediaType = MediaTypeDockerSchema2Layer
default:
return ocispec.Descriptor{}, nil, errors.Errorf("Decryption: unsupporter layer MediaType: %s\n", desc.MediaType)
}
return newDesc, resultReader, nil
}
// cryptLayer handles the changes due to encryption or decryption of a layer
func cryptLayer(ctx context.Context, cs content.Store, ls leases.Manager, l leases.Lease, desc ocispec.Descriptor, cc *encconfig.CryptoConfig, cryptoOp cryptoOp) (ocispec.Descriptor, error) {
var (
resultReader io.Reader
newDesc ocispec.Descriptor
)
dataReader, err := cs.ReaderAt(ctx, desc)
if err != nil {
return ocispec.Descriptor{}, err
}
defer dataReader.Close()
if cryptoOp == cryptoOpEncrypt {
newDesc, resultReader, err = encryptLayer(cc, dataReader, desc)
} else {
newDesc, resultReader, err = decryptLayer(cc, dataReader, desc, cryptoOp == cryptoOpUnwrapOnly)
}
if err != nil || cryptoOp == cryptoOpUnwrapOnly {
return ocispec.Descriptor{}, err
}
// some operations, such as changing recipients, may not touch the layer at all
if resultReader != nil {
if ls == nil {
return ocispec.Descriptor{}, errors.New("Unexpected write to object without lease")
}
var rsrc leases.Resource
var ref string
// If we have the digest, write blob with checks
haveDigest := newDesc.Digest.String() != ""
if haveDigest {
ref = fmt.Sprintf("layer-%s", newDesc.Digest.String())
rsrc = leases.Resource{
ID: newDesc.Digest.String(),
Type: "content",
}
} else {
ref = fmt.Sprintf("blob-%d-%d", rand.Int(), rand.Int())
rsrc = leases.Resource{
ID: ref,
Type: "ingests",
}
}
// Add resource to lease and write blob
if err := ls.AddResource(ctx, l, rsrc); err != nil {
return ocispec.Descriptor{}, errors.Wrap(err, "Unable to add resource to lease")
}
if haveDigest {
if err := content.WriteBlob(ctx, cs, ref, resultReader, newDesc); err != nil {
return ocispec.Descriptor{}, errors.Wrap(err, "failed to write config")
}
} else {
newDesc.Digest, newDesc.Size, err = content.WriteBlobBlind(ctx, cs, ref, resultReader)
if err != nil {
return ocispec.Descriptor{}, err
}
}
}
return newDesc, err
}
// isDecriptorALayer determines whether the given Descriptor describes an image layer
func isDescriptorALayer(desc ocispec.Descriptor) bool {
switch desc.MediaType {
case MediaTypeDockerSchema2LayerGzip, MediaTypeDockerSchema2Layer,
ocispec.MediaTypeImageLayerGzip, ocispec.MediaTypeImageLayer,
MediaTypeDockerSchema2LayerGzipEnc, MediaTypeDockerSchema2LayerEnc:
return true
}
return false
}
// Encrypt or decrypt all the Children of a given descriptor
func cryptChildren(ctx context.Context, cs content.Store, ls leases.Manager, l leases.Lease, desc ocispec.Descriptor, cc *encconfig.CryptoConfig, lf LayerFilter, cryptoOp cryptoOp, thisPlatform *ocispec.Platform) (ocispec.Descriptor, bool, error) {
children, err := Children(ctx, cs, desc)
if err != nil {
if errdefs.IsNotFound(err) {
return desc, false, nil
}
return ocispec.Descriptor{}, false, err
}
var newLayers []ocispec.Descriptor
var config ocispec.Descriptor
modified := false
for _, child := range children {
// we only encrypt child layers and have to update their parents if encryption happened
switch child.MediaType {
case MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig:
config = child
case MediaTypeDockerSchema2LayerGzip, MediaTypeDockerSchema2Layer,
ocispec.MediaTypeImageLayerGzip, ocispec.MediaTypeImageLayer:
if cryptoOp == cryptoOpEncrypt && lf(child) {
nl, err := cryptLayer(ctx, cs, ls, l, child, cc, cryptoOp)
if err != nil {
return ocispec.Descriptor{}, false, err
}
modified = true
newLayers = append(newLayers, nl)
} else {
newLayers = append(newLayers, child)
}
case MediaTypeDockerSchema2LayerGzipEnc, MediaTypeDockerSchema2LayerEnc:
// this one can be decrypted but also its recipients list changed
if lf(child) {
nl, err := cryptLayer(ctx, cs, ls, l, child, cc, cryptoOp)
if err != nil || cryptoOp == cryptoOpUnwrapOnly {
return ocispec.Descriptor{}, false, err
}
modified = true
newLayers = append(newLayers, nl)
} else {
newLayers = append(newLayers, child)
}
case MediaTypeDockerSchema2LayerForeign, MediaTypeDockerSchema2LayerForeignGzip:
// never encrypt/decrypt
newLayers = append(newLayers, child)
default:
return ocispec.Descriptor{}, false, errors.Errorf("bad/unhandled MediaType %s in encryptChildren\n", child.MediaType)
}
}
if modified && len(newLayers) > 0 {
newManifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
Config: config,
Layers: newLayers,
}
mb, err := json.MarshalIndent(newManifest, "", " ")
if err != nil {
return ocispec.Descriptor{}, false, errors.Wrap(err, "failed to marshal image")
}
newDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Size: int64(len(mb)),
Digest: digest.Canonical.FromBytes(mb),
Platform: desc.Platform,
}
labels := map[string]string{}
labels["containerd.io/gc.ref.content.0"] = newManifest.Config.Digest.String()
for i, ch := range newManifest.Layers {
labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = ch.Digest.String()
}
ref := fmt.Sprintf("manifest-%s", newDesc.Digest.String())
if ls == nil {
return ocispec.Descriptor{}, false, errors.New("Unexpected write to object without lease")
}
rsrc := leases.Resource{
ID: desc.Digest.String(),
Type: "content",
}
if err := ls.AddResource(ctx, l, rsrc); err != nil {
return ocispec.Descriptor{}, false, errors.Wrap(err, "Unable to add resource to lease")
}
if err := content.WriteBlob(ctx, cs, ref, bytes.NewReader(mb), newDesc, content.WithLabels(labels)); err != nil {
return ocispec.Descriptor{}, false, errors.Wrap(err, "failed to write config")
}
return newDesc, true, nil
}
return desc, modified, nil
}
// cryptManifest encrypts or decrypts the children of a top level manifest
func cryptManifest(ctx context.Context, cs content.Store, ls leases.Manager, l leases.Lease, desc ocispec.Descriptor, cc *encconfig.CryptoConfig, lf LayerFilter, cryptoOp cryptoOp) (ocispec.Descriptor, bool, error) {
p, err := content.ReadBlob(ctx, cs, desc)
if err != nil {
return ocispec.Descriptor{}, false, err
}
var manifest ocispec.Manifest
if err := json.Unmarshal(p, &manifest); err != nil {
return ocispec.Descriptor{}, false, err
}
platform := platforms.DefaultSpec()
newDesc, modified, err := cryptChildren(ctx, cs, ls, l, desc, cc, lf, cryptoOp, &platform)
if err != nil || cryptoOp == cryptoOpUnwrapOnly {
return ocispec.Descriptor{}, false, err
}
return newDesc, modified, nil
}
// cryptManifestList encrypts or decrypts the children of a top level manifest list
func cryptManifestList(ctx context.Context, cs content.Store, ls leases.Manager, l leases.Lease, desc ocispec.Descriptor, cc *encconfig.CryptoConfig, lf LayerFilter, cryptoOp cryptoOp) (ocispec.Descriptor, bool, error) {
// read the index; if any layer is encrypted and any manifests change we will need to rewrite it
b, err := content.ReadBlob(ctx, cs, desc)
if err != nil {
return ocispec.Descriptor{}, false, err
}
var index ocispec.Index
if err := json.Unmarshal(b, &index); err != nil {
return ocispec.Descriptor{}, false, err
}
var newManifests []ocispec.Descriptor
modified := false
for _, manifest := range index.Manifests {
newManifest, m, err := cryptChildren(ctx, cs, ls, l, manifest, cc, lf, cryptoOp, manifest.Platform)
if err != nil || cryptoOp == cryptoOpUnwrapOnly {
return ocispec.Descriptor{}, false, err
}
if m {
modified = true
}
newManifests = append(newManifests, newManifest)
}
if modified {
// we need to update the index
newIndex := ocispec.Index{
Versioned: index.Versioned,
Manifests: newManifests,
}
mb, err := json.MarshalIndent(newIndex, "", " ")
if err != nil {
return ocispec.Descriptor{}, false, errors.Wrap(err, "failed to marshal index")
}
newDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Size: int64(len(mb)),
Digest: digest.Canonical.FromBytes(mb),
}
labels := map[string]string{}
for i, m := range newIndex.Manifests {
labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = m.Digest.String()
}
ref := fmt.Sprintf("index-%s", newDesc.Digest.String())
if ls == nil {
return ocispec.Descriptor{}, false, errors.New("Unexpected write to object without lease")
}
rsrc := leases.Resource{
ID: desc.Digest.String(),
Type: "content",
}
if err := ls.AddResource(ctx, l, rsrc); err != nil {
return ocispec.Descriptor{}, false, errors.Wrap(err, "Unable to add resource to lease")
}
if err = content.WriteBlob(ctx, cs, ref, bytes.NewReader(mb), newDesc, content.WithLabels(labels)); err != nil {
return ocispec.Descriptor{}, false, errors.Wrap(err, "failed to write index")
}
return newDesc, true, nil
}
return desc, false, nil
}
// cryptImage is the dispatcher to encrypt/decrypt an image; it accepts either an OCI descriptor
// representing a manifest list or a single manifest
func cryptImage(ctx context.Context, cs content.Store, ls leases.Manager, l leases.Lease, desc ocispec.Descriptor, cc *encconfig.CryptoConfig, lf LayerFilter, cryptoOp cryptoOp) (ocispec.Descriptor, bool, error) {
if cc == nil {
return ocispec.Descriptor{}, false, errors.Wrapf(errdefs.ErrInvalidArgument, "CryptoConfig must not be nil")
}
switch desc.MediaType {
case ocispec.MediaTypeImageIndex, MediaTypeDockerSchema2ManifestList:
return cryptManifestList(ctx, cs, ls, l, desc, cc, lf, cryptoOp)
case ocispec.MediaTypeImageManifest, MediaTypeDockerSchema2Manifest:
return cryptManifest(ctx, cs, ls, l, desc, cc, lf, cryptoOp)
default:
return ocispec.Descriptor{}, false, errors.Errorf("CryptImage: Unhandled media type: %s", desc.MediaType)
}
}
// EncryptImage encrypts an image; it accepts either an OCI descriptor representing a manifest list or a single manifest
func EncryptImage(ctx context.Context, cs content.Store, ls leases.Manager, l leases.Lease, desc ocispec.Descriptor, cc *encconfig.CryptoConfig, lf LayerFilter) (ocispec.Descriptor, bool, error) {
return cryptImage(ctx, cs, ls, l, desc, cc, lf, cryptoOpEncrypt)
}
// DecryptImage decrypts an image; it accepts either an OCI descriptor representing a manifest list or a single manifest
func DecryptImage(ctx context.Context, cs content.Store, ls leases.Manager, l leases.Lease, desc ocispec.Descriptor, cc *encconfig.CryptoConfig, lf LayerFilter) (ocispec.Descriptor, bool, error) {
return cryptImage(ctx, cs, ls, l, desc, cc, lf, cryptoOpDecrypt)
}
// CheckAuthorization checks whether a user has the right keys to be allowed to access an image (every layer)
// It takes decrypting of the layers only as far as decrypting the asymmetrically encrypted data
// The decryption is only done for the current platform
func CheckAuthorization(ctx context.Context, cs content.Store, desc ocispec.Descriptor, dc *encconfig.DecryptConfig) error {
cc := encconfig.CryptoConfig{
DecryptConfig: dc,
}
lf := func(desc ocispec.Descriptor) bool {
return true
}
// We shouldn't need to create any objects in CheckAuthorization, so no lease required.
_, _, err := cryptImage(ctx, cs, nil, leases.Lease{}, desc, &cc, lf, cryptoOpUnwrapOnly)
if err != nil {
return errors.Wrapf(err, "you are not authorized to use this image")
}
return nil
}

View File

@ -0,0 +1,114 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package blockcipher
import (
"io"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
// LayerCipherType is the ciphertype as specified in the layer metadata
type LayerCipherType string
// TODO: Should be obtained from OCI spec once included
const (
AESSIVCMAC256 LayerCipherType = "AEAD_AES_SIV_CMAC_STREAM_256"
AESSIVCMAC512 LayerCipherType = "AEAD_AES_SIV_CMAC_STREAM_512"
CipherTypeOpt string = "type"
)
// LayerBlockCipherOptions includes the information required to encrypt/decrypt
// an image
type LayerBlockCipherOptions struct {
// SymmetricKey represents the symmetric key used for encryption/decryption
// This field should be populated by Encrypt/Decrypt calls
SymmetricKey []byte `json:"symkey"`
// Digest is the digest of the original data for verification.
// This is NOT populated by Encrypt/Decrypt calls
Digest digest.Digest `json:"digest"`
// CipherOptions contains the cipher metadata used for encryption/decryption
// This field should be populated by Encrypt/Decrypt calls
CipherOptions map[string][]byte `json:"cipheroptions"`
}
// LayerBlockCipher returns a provider for encrypt/decrypt functionality
// for handling the layer data for a specific algorithm
type LayerBlockCipher interface {
// GenerateKey creates a symmetric key
GenerateKey() []byte
// Encrypt takes in layer data and returns the ciphertext and relevant LayerBlockCipherOptions
Encrypt(layerDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, LayerBlockCipherOptions, error)
// Decrypt takes in layer ciphertext data and returns the plaintext and relevant LayerBlockCipherOptions
Decrypt(layerDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, LayerBlockCipherOptions, error)
}
// LayerBlockCipherHandler is the handler for encrypt/decrypt for layers
type LayerBlockCipherHandler struct {
cipherMap map[LayerCipherType]LayerBlockCipher
}
// Encrypt is the handler for the layer decryption routine
func (h *LayerBlockCipherHandler) Encrypt(plainDataReader io.Reader, typ LayerCipherType) (io.Reader, LayerBlockCipherOptions, error) {
if c, ok := h.cipherMap[typ]; ok {
opt := LayerBlockCipherOptions{
SymmetricKey: c.GenerateKey(),
}
encDataReader, newopt, err := c.Encrypt(plainDataReader, opt)
if err == nil {
newopt.CipherOptions[CipherTypeOpt] = []byte(typ)
}
return encDataReader, newopt, err
}
return nil, LayerBlockCipherOptions{}, errors.Errorf("unsupported cipher type: %s", typ)
}
// Decrypt is the handler for the layer decryption routine
func (h *LayerBlockCipherHandler) Decrypt(encDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, LayerBlockCipherOptions, error) {
typ, ok := opt.CipherOptions[CipherTypeOpt]
if !ok {
return nil, LayerBlockCipherOptions{}, errors.New("no cipher type provided")
}
if c, ok := h.cipherMap[LayerCipherType(typ)]; ok {
return c.Decrypt(encDataReader, opt)
}
return nil, LayerBlockCipherOptions{}, errors.Errorf("unsupported cipher type: %s", typ)
}
// NewLayerBlockCipherHandler returns a new default handler
func NewLayerBlockCipherHandler() (*LayerBlockCipherHandler, error) {
h := LayerBlockCipherHandler{
cipherMap: map[LayerCipherType]LayerBlockCipher{},
}
var err error
h.cipherMap[AESSIVCMAC256], err = NewAESSIVLayerBlockCipher(256)
if err != nil {
return nil, errors.Wrap(err, "unable to set up Cipher AES-SIV-CMAC-256")
}
h.cipherMap[AESSIVCMAC512], err = NewAESSIVLayerBlockCipher(512)
if err != nil {
return nil, errors.Wrap(err, "unable to set up Cipher AES-SIV-CMAC-512")
}
return &h, nil
}

View File

@ -0,0 +1,194 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package blockcipher
import (
"crypto/rand"
"fmt"
"io"
miscreant "github.com/miscreant/miscreant-go"
"github.com/pkg/errors"
)
// AESSIVLayerBlockCipher implements the AES SIV block cipher
type AESSIVLayerBlockCipher struct {
keylen int // in bytes
reader io.Reader
encryptor *miscreant.StreamEncryptor
decryptor *miscreant.StreamDecryptor
err error // error that occurred during operation
eof bool // hit EOF in the input data
toread int // how many bytes to read in one chunk
inbuffer []byte // input buffer with data from reader
inoffset int64 // offset where to read from next
outbuffer []byte // output buffer to return to user
outoffset int // offset in output buffer
outsize int64 // output size
}
type aessivcryptor struct {
bc *AESSIVLayerBlockCipher
outputReader io.Reader
}
// NewAESSIVLayerBlockCipher returns a new AES SIV block cipher of 256 or 512 bits
func NewAESSIVLayerBlockCipher(bits int) (LayerBlockCipher, error) {
if bits != 256 && bits != 512 {
return nil, errors.New("AES SIV bit count not supported")
}
return &AESSIVLayerBlockCipher{keylen: bits / 8}, nil
}
func (r *aessivcryptor) Read(p []byte) (int, error) {
if r.bc.err != nil {
return 0, r.bc.err
}
for {
// return data if we have any
if r.bc.outbuffer != nil && r.bc.outoffset < len(r.bc.outbuffer) {
n := copy(p, r.bc.outbuffer[r.bc.outoffset:])
r.bc.outoffset += n
return n, nil
}
// no data and hit eof before?
if r.bc.eof {
return 0, io.EOF
}
// read new data; we expect to get r.bc.toread number of bytes
// for anything less we assume it's EOF
numbytes := 0
for numbytes < r.bc.toread {
var n int
n, r.bc.err = r.bc.reader.Read(r.bc.inbuffer[numbytes:r.bc.toread])
numbytes += n
if r.bc.err != nil {
if r.bc.err == io.EOF {
r.bc.eof = true
r.bc.err = nil
break
} else {
return 0, r.bc.err
}
}
if n == 0 {
break
}
}
if numbytes < r.bc.toread {
r.bc.eof = true
}
r.bc.inoffset += int64(numbytes)
// transform the data
if r.bc.encryptor != nil {
r.bc.outbuffer = r.bc.encryptor.Seal(nil, r.bc.inbuffer[:numbytes], []byte(""), r.bc.eof)
} else {
r.bc.outbuffer, r.bc.err = r.bc.decryptor.Open(nil, r.bc.inbuffer[:numbytes], []byte(""), r.bc.eof)
if r.bc.err != nil {
return 0, r.bc.err
}
}
// let reader start from beginning of buffer
r.bc.outoffset = 0
r.bc.outsize += int64(len(r.bc.outbuffer))
}
}
// init initializes an instance
func (bc *AESSIVLayerBlockCipher) init(encrypt bool, reader io.Reader, opt LayerBlockCipherOptions) (LayerBlockCipherOptions, error) {
var (
err error
se miscreant.StreamEncryptor
)
bc.reader = reader
key := opt.SymmetricKey
if len(key) != bc.keylen {
return LayerBlockCipherOptions{}, fmt.Errorf("invalid key length of %d bytes; need %d bytes", len(key), bc.keylen)
}
nonce := opt.CipherOptions["nonce"]
if len(nonce) == 0 {
nonce = make([]byte, se.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return LayerBlockCipherOptions{}, errors.Wrap(err, "unable to generate random nonce")
}
}
bc.inbuffer = make([]byte, 1024*1024)
bc.toread = len(bc.inbuffer)
bc.inoffset = 0
bc.outbuffer = nil
bc.outoffset = 0
bc.eof = false
bc.err = nil
bc.outsize = 0
if encrypt {
bc.encryptor, err = miscreant.NewStreamEncryptor("AES-SIV", key, nonce)
if err != nil {
return LayerBlockCipherOptions{}, errors.Wrap(err, "unable to create AES-SIV stream encryptor")
}
bc.toread -= bc.encryptor.Overhead()
bc.decryptor = nil
} else {
bc.decryptor, err = miscreant.NewStreamDecryptor("AES-SIV", key, nonce)
if err != nil {
return LayerBlockCipherOptions{}, errors.Wrap(err, "unable to create AES-SIV stream decryptor")
}
bc.encryptor = nil
}
lbco := LayerBlockCipherOptions{
SymmetricKey: key,
CipherOptions: map[string][]byte{
"nonce": nonce,
},
}
return lbco, nil
}
// GenerateKey creates a synmmetric key
func (bc *AESSIVLayerBlockCipher) GenerateKey() []byte {
return miscreant.GenerateKey(bc.keylen)
}
// Encrypt takes in layer data and returns the ciphertext and relevant LayerBlockCipherOptions
func (bc *AESSIVLayerBlockCipher) Encrypt(plainDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, LayerBlockCipherOptions, error) {
lbco, err := bc.init(true, plainDataReader, opt)
if err != nil {
return nil, LayerBlockCipherOptions{}, err
}
return &aessivcryptor{bc, nil}, lbco, nil
}
// Decrypt takes in layer ciphertext data and returns the plaintext and relevant LayerBlockCipherOptions
func (bc *AESSIVLayerBlockCipher) Decrypt(encDataReader io.Reader, opt LayerBlockCipherOptions) (io.Reader, LayerBlockCipherOptions, error) {
lbco, err := bc.init(false, encDataReader, opt)
if err != nil {
return nil, LayerBlockCipherOptions{}, err
}
return &aessivcryptor{bc, nil}, lbco, nil
}

View File

@ -0,0 +1,163 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package blockcipher
import (
"bytes"
_ "crypto/sha256"
"io"
"testing"
)
func TestBlockCipherAesSivCreateValid(t *testing.T) {
_, err := NewAESSIVLayerBlockCipher(256)
if err != nil {
t.Fatal(err)
}
_, err = NewAESSIVLayerBlockCipher(512)
if err != nil {
t.Fatal(err)
}
}
func TestBlockCipherAesSivCreateInvalid(t *testing.T) {
_, err := NewAESSIVLayerBlockCipher(8)
if err == nil {
t.Fatal("Test should have failed due to invalid cipher size")
}
_, err = NewAESSIVLayerBlockCipher(255)
if err == nil {
t.Fatal("Test should have failed due to invalid cipher size")
}
}
func TestBlockCipherAesSivEncryption(t *testing.T) {
var (
symKey = []byte("01234567890123456789012345678912")
opt = LayerBlockCipherOptions{
SymmetricKey: symKey,
}
layerData = []byte("this is some data")
)
bc, err := NewAESSIVLayerBlockCipher(256)
if err != nil {
t.Fatal(err)
}
layerDataReader := bytes.NewReader(layerData)
ciphertextReader, lbco, err := bc.Encrypt(layerDataReader, opt)
if err != nil {
t.Fatal(err)
}
// Use a different instantiated object to indicate an invocation at a diff time
bc2, err := NewAESSIVLayerBlockCipher(256)
if err != nil {
t.Fatal(err)
}
ciphertext := make([]byte, 1024)
encsize, err := ciphertextReader.Read(ciphertext)
if err != nil {
t.Fatal(err)
}
ciphertextTestReader := bytes.NewReader(ciphertext[:encsize])
plaintextReader, _, err := bc2.Decrypt(ciphertextTestReader, lbco)
if err != nil {
t.Fatal(err)
}
plaintext := make([]byte, 1024)
size, err := plaintextReader.Read(plaintext)
if err != nil && err != io.EOF {
t.Fatal(err)
}
if string(plaintext[:size]) != string(layerData) {
t.Fatalf("expected %q, got %q", layerData, plaintext[:size])
}
}
func TestBlockCipherAesSivEncryptionInvalidKey(t *testing.T) {
var (
symKey = []byte("01234567890123456789012345678912")
opt = LayerBlockCipherOptions{
SymmetricKey: symKey,
}
layerData = []byte("this is some data")
)
bc, err := NewAESSIVLayerBlockCipher(256)
if err != nil {
t.Fatal(err)
}
layerDataReader := bytes.NewReader(layerData)
ciphertextReader, lbco, err := bc.Encrypt(layerDataReader, opt)
if err != nil {
t.Fatal(err)
}
// Use a different instantiated object to indicate an invokation at a diff time
bc2, err := NewAESSIVLayerBlockCipher(256)
if err != nil {
t.Fatal(err)
}
lbco.SymmetricKey = []byte("aaa34567890123456789012345678912")
ciphertext := make([]byte, 1024)
encsize, err := ciphertextReader.Read(ciphertext)
if err != nil {
t.Fatal(err)
}
ciphertextTestReader := bytes.NewReader(ciphertext[:encsize])
plaintextReader, _, err := bc2.Decrypt(ciphertextTestReader, lbco)
if err != nil {
t.Fatal(err)
}
plaintext := make([]byte, 1024)
_, err = plaintextReader.Read(plaintext)
if err == nil {
t.Fatal("Read() should have failed due to wrong key")
}
}
func TestBlockCipherAesSivEncryptionInvalidKeyLength(t *testing.T) {
var (
symKey = []byte("012345")
opt = LayerBlockCipherOptions{
SymmetricKey: symKey,
}
layerData = []byte("this is some data")
)
bc, err := NewAESSIVLayerBlockCipher(256)
if err != nil {
t.Fatal(err)
}
layerDataReader := bytes.NewReader(layerData)
_, _, err = bc.Encrypt(layerDataReader, opt)
if err == nil {
t.Fatal("Test should have failed due to invalid key length")
}
}

View File

@ -0,0 +1,115 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package blockcipher
import (
"bytes"
"io"
"testing"
)
func TestBlockCipherHandlerCreate(t *testing.T) {
_, err := NewLayerBlockCipherHandler()
if err != nil {
t.Fatal(err)
}
}
func TestBlockCipherEncryption(t *testing.T) {
var (
layerData = []byte("this is some data")
)
h, err := NewLayerBlockCipherHandler()
if err != nil {
t.Fatal(err)
}
layerDataReader := bytes.NewReader(layerData)
ciphertextReader, lbco, err := h.Encrypt(layerDataReader, AESSIVCMAC256)
if err != nil {
t.Fatal(err)
}
ciphertext := make([]byte, 1024)
encsize, err := ciphertextReader.Read(ciphertext)
if err != nil && err != io.EOF {
t.Fatal("Reading the ciphertext should not have failed")
}
ciphertextTestReader := bytes.NewReader(ciphertext[:encsize])
// Use a different instantiated object to indicate an invokation at a diff time
plaintextReader, _, err := h.Decrypt(ciphertextTestReader, lbco)
if err != nil {
t.Fatal(err)
}
plaintext := make([]byte, 1024)
decsize, err := plaintextReader.Read(plaintext)
if err != nil && err != io.EOF {
t.Fatal("Read the plaintext should not have failed")
}
if string(plaintext[:decsize]) != string(layerData) {
t.Fatal("Decrypted data is incorrect")
}
}
func TestBlockCipherEncryptionInvalidKey(t *testing.T) {
var (
layerData = []byte("this is some data")
)
h, err := NewLayerBlockCipherHandler()
if err != nil {
t.Fatal(err)
}
layerDataReader := bytes.NewReader(layerData)
ciphertextReader, lbco, err := h.Encrypt(layerDataReader, AESSIVCMAC512)
if err != nil {
t.Fatal(err)
}
// Use a different instantiated object to indicate an invokation at a diff time
bc2, err := NewAESSIVLayerBlockCipher(512)
if err != nil {
t.Fatal(err)
}
lbco.SymmetricKey = []byte("aaa3456789012345678901234567890123456789012345678901234567890123")
ciphertext := make([]byte, 1024)
encsize, err := ciphertextReader.Read(ciphertext)
if err != nil {
t.Fatal(err)
}
ciphertextTestReader := bytes.NewReader(ciphertext[:encsize])
plaintextReader, _, err := bc2.Decrypt(ciphertextTestReader, lbco)
if err != nil {
t.Fatal(err)
}
plaintext := make([]byte, 1024)
_, err = plaintextReader.Read(plaintext)
if err == nil {
t.Fatal("Read() should have failed due to wrong key")
}
}

View File

@ -0,0 +1,40 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
// EncryptConfig is the container image PGP encryption configuration holding
// the identifiers of those that will be able to decrypt the container and
// the PGP public keyring file data that contains their public keys.
type EncryptConfig struct {
// map holding 'gpg-recipients', 'gpg-pubkeyringfile', 'pubkeys', 'x509s'
Parameters map[string][][]byte
DecryptConfig DecryptConfig
}
// DecryptConfig wraps the Parameters map that holds the decryption key
type DecryptConfig struct {
// map holding 'privkeys', 'x509s', 'gpg-privatekeys'
Parameters map[string][][]byte
}
// CryptoConfig is a common wrapper for EncryptConfig and DecrypConfig that can
// be passed through functions that share much code for encryption and decryption
type CryptoConfig struct {
EncryptConfig *EncryptConfig
DecryptConfig *DecryptConfig
}

View File

@ -0,0 +1,244 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package encryption
import (
"encoding/base64"
"encoding/json"
"io"
"strings"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images/encryption/blockcipher"
"github.com/containerd/containerd/images/encryption/config"
"github.com/containerd/containerd/images/encryption/keywrap"
"github.com/containerd/containerd/images/encryption/keywrap/jwe"
"github.com/containerd/containerd/images/encryption/keywrap/pgp"
"github.com/containerd/containerd/images/encryption/keywrap/pkcs7"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
func init() {
keyWrappers = make(map[string]keywrap.KeyWrapper)
keyWrapperAnnotations = make(map[string]string)
RegisterKeyWrapper("pgp", pgp.NewKeyWrapper())
RegisterKeyWrapper("jwe", jwe.NewKeyWrapper())
RegisterKeyWrapper("pkcs7", pkcs7.NewKeyWrapper())
}
var keyWrappers map[string]keywrap.KeyWrapper
var keyWrapperAnnotations map[string]string
// RegisterKeyWrapper allows to register key wrappers by their encryption scheme
func RegisterKeyWrapper(scheme string, iface keywrap.KeyWrapper) {
keyWrappers[scheme] = iface
keyWrapperAnnotations[iface.GetAnnotationID()] = scheme
}
// GetKeyWrapper looks up the encryptor interface given an encryption scheme (gpg, jwe)
func GetKeyWrapper(scheme string) keywrap.KeyWrapper {
return keyWrappers[scheme]
}
// GetWrappedKeysMap returns a map of wrappedKeys as values in a
// map with the encryption scheme(s) as the key(s)
func GetWrappedKeysMap(desc ocispec.Descriptor) map[string]string {
wrappedKeysMap := make(map[string]string)
for annotationsID, scheme := range keyWrapperAnnotations {
if annotation, ok := desc.Annotations[annotationsID]; ok {
wrappedKeysMap[scheme] = annotation
}
}
return wrappedKeysMap
}
// EncryptLayer encrypts the layer by running one encryptor after the other
func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, desc ocispec.Descriptor) (io.Reader, map[string]string, error) {
var (
encLayerReader io.Reader
err error
optsData []byte
)
if ec == nil {
return nil, nil, errors.Wrapf(errdefs.ErrInvalidArgument, "EncryptConfig must not be nil")
}
for annotationsID := range keyWrapperAnnotations {
annotation := desc.Annotations[annotationsID]
if annotation != "" {
optsData, err = decryptLayerKeyOptsData(&ec.DecryptConfig, desc)
if err != nil {
return nil, nil, err
}
// already encrypted!
}
}
newAnnotations := make(map[string]string)
for annotationsID, scheme := range keyWrapperAnnotations {
b64Annotations := desc.Annotations[annotationsID]
if b64Annotations == "" && optsData == nil {
encLayerReader, optsData, err = commonEncryptLayer(encOrPlainLayerReader, desc.Digest, blockcipher.AESSIVCMAC512)
if err != nil {
return nil, nil, err
}
}
keywrapper := GetKeyWrapper(scheme)
b64Annotations, err = preWrapKeys(keywrapper, ec, b64Annotations, optsData)
if err != nil {
return nil, nil, err
}
if b64Annotations != "" {
newAnnotations[annotationsID] = b64Annotations
}
}
if len(newAnnotations) == 0 {
err = errors.Errorf("no encryptor found to handle encryption")
}
// if nothing was encrypted, we just return encLayer = nil
return encLayerReader, newAnnotations, err
}
// preWrapKeys calls WrapKeys and handles the base64 encoding and concatenation of the
// annotation data
func preWrapKeys(keywrapper keywrap.KeyWrapper, ec *config.EncryptConfig, b64Annotations string, optsData []byte) (string, error) {
newAnnotation, err := keywrapper.WrapKeys(ec, optsData)
if err != nil || len(newAnnotation) == 0 {
return b64Annotations, err
}
b64newAnnotation := base64.StdEncoding.EncodeToString(newAnnotation)
if b64Annotations == "" {
return b64newAnnotation, nil
}
return b64Annotations + "," + b64newAnnotation, nil
}
// DecryptLayer decrypts a layer trying one keywrap.KeyWrapper after the other to see whether it
// can apply the provided private key
// If unwrapOnly is set we will only try to decrypt the layer encryption key and return
func DecryptLayer(dc *config.DecryptConfig, encLayerReader io.Reader, desc ocispec.Descriptor, unwrapOnly bool) (io.Reader, digest.Digest, error) {
if dc == nil {
return nil, "", errors.Wrapf(errdefs.ErrInvalidArgument, "DecryptConfig must not be nil")
}
optsData, err := decryptLayerKeyOptsData(dc, desc)
if err != nil || unwrapOnly {
return nil, "", err
}
return commonDecryptLayer(encLayerReader, optsData)
}
func decryptLayerKeyOptsData(dc *config.DecryptConfig, desc ocispec.Descriptor) ([]byte, error) {
privKeyGiven := false
for annotationsID, scheme := range keyWrapperAnnotations {
b64Annotation := desc.Annotations[annotationsID]
if b64Annotation != "" {
keywrapper := GetKeyWrapper(scheme)
if len(keywrapper.GetPrivateKeys(dc.Parameters)) == 0 {
continue
}
privKeyGiven = true
optsData, err := preUnwrapKey(keywrapper, dc, b64Annotation)
if err != nil {
// try next keywrap.KeyWrapper
continue
}
if optsData == nil {
// try next keywrap.KeyWrapper
continue
}
return optsData, nil
}
}
if !privKeyGiven {
return nil, errors.New("missing private key needed for decryption")
}
return nil, errors.Errorf("no suitable key unwrapper found or none of the private keys could be used for decryption")
}
// preUnwrapKey decodes the comma separated base64 strings and calls the Unwrap function
// of the given keywrapper with it and returns the result in case the Unwrap functions
// does not return an error. If all attempts fail, an error is returned.
func preUnwrapKey(keywrapper keywrap.KeyWrapper, dc *config.DecryptConfig, b64Annotations string) ([]byte, error) {
if b64Annotations == "" {
return nil, nil
}
for _, b64Annotation := range strings.Split(b64Annotations, ",") {
annotation, err := base64.StdEncoding.DecodeString(b64Annotation)
if err != nil {
return nil, errors.New("could not base64 decode the annotation")
}
optsData, err := keywrapper.UnwrapKey(dc, annotation)
if err != nil {
continue
}
return optsData, nil
}
return nil, errors.New("no suitable key found for decrypting layer key")
}
// commonEncryptLayer is a function to encrypt the plain layer using a new random
// symmetric key and return the LayerBlockCipherHandler's JSON in string form for
// later use during decryption
func commonEncryptLayer(plainLayerReader io.Reader, d digest.Digest, typ blockcipher.LayerCipherType) (io.Reader, []byte, error) {
lbch, err := blockcipher.NewLayerBlockCipherHandler()
if err != nil {
return nil, nil, err
}
encLayerReader, opts, err := lbch.Encrypt(plainLayerReader, typ)
if err != nil {
return nil, nil, err
}
opts.Digest = d
optsData, err := json.Marshal(opts)
if err != nil {
return nil, nil, errors.Wrapf(err, "could not JSON marshal opts")
}
return encLayerReader, optsData, err
}
// commonDecryptLayer decrypts an encrypted layer previously encrypted with commonEncryptLayer
// by passing along the optsData
func commonDecryptLayer(encLayerReader io.Reader, optsData []byte) (io.Reader, digest.Digest, error) {
opts := blockcipher.LayerBlockCipherOptions{}
err := json.Unmarshal(optsData, &opts)
if err != nil {
return nil, "", errors.Wrapf(err, "could not JSON unmarshal optsData")
}
lbch, err := blockcipher.NewLayerBlockCipherHandler()
if err != nil {
return nil, "", err
}
plainLayerReader, opts, err := lbch.Decrypt(encLayerReader, opts)
if err != nil {
return nil, "", err
}
return plainLayerReader, opts.Digest, nil
}

View File

@ -0,0 +1,137 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package encryption
import (
"bytes"
"reflect"
"testing"
"github.com/containerd/containerd/images/encryption/config"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
var (
privateKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAnnExCL+jpTAgZzgEBwmR88ltusMBdsd4ZDO4M5NohTq0T6TP
vJN99ud8ZAY8fVpN63TKD2enWy6imkSE3Cdp+yFgOntsg8WgdPF+8VQaNpn/g8LC
rpWXpJuJIGzCSW5SlUt0OkvyeDvQFrKCqdI63H4cxXY5ly2HlTHJ1+YzEBSLONJY
xsJt6/7cL7mJ7CR/9NZouft2xeNQto9JSqWUNCwUGdZOS1pMVkka/2tFmq8psciF
YvCl6msX6haZ7IDq4/GfeteL8gp6t+hgyWmbJ5/h0hNEvAz4DVb5RnKYVLwWM/e9
oQTw9WgKCqUZKe0+DKmuKYMH2g77oTvDtP8NvQIDAQABAoIBAC8tUQZj2ZxEGkHh
wgE+bkECxzOHARaXClf7tmtVBxg0hJ/6WQizehxcjQNTgAtrKixj2A6CNKjH2A7L
PCw5aCsooviG66bI36AykDPXcP61GAnpogJN9JtE3K3U9Hzc5qYhk3gQSSBX3vwD
Jzjdqj0hJ/v72eYT3n0kGA+7MZUlsObpOouPAZMo72Bcvg2s20FLnKQCiGfH8zWv
wJAnO5BhinwTPhi+01Xj9LePk/2bs3hEzH1/bA3DVmmaWp5H8RuaGuvQ6eX4EXir
3xq9BjjYIK21dmD2R1S0jjez3/d2P7gENKGVItcakURWIn7IS0bYr8P2xIhnxQQV
OTYgWRkCgYEAyboK1GDr+5KtCoAQE1e1Ntbi+x3R2sIjX8lGzLumd5ybqSMGH8V9
6efRo7onuP5V66bDwxSeSFomOEF0rQZj3dpoEXkF95h8G65899okXMURsqjb6+wm
xyFKZAJojJXsR076ES3tas/TgPVD/ZfcBYTU8Ssvfsi3uzeUrbuVL58CgYEAyRHq
/1zsPDf3B7E8EsRA/YZJTfjzDlqXatTX5dnoUKBQH9nZut4qGWBwWPj2DKJwlGQr
m12RIbM5uGvUe3csddzClp0zInDhvD/K3XlUthUfrYX209xaeOD6d4+7wd56SNEo
AzhSobgmrITEAy8QA1u546ID+gFOQnzG17HelSMCgYEAsdmoep4I7//dOAi4I5WM
WxERhTxBLJFFBso59X7rwUD9rB0I5TIFVRfhKGyTYPI7ZkvdBD1FX5y7XZW3/GRJ
3+sTHXSJ4kU6Bl3MJ+jXbkMA23csjc/iUGX1ZD8LVgdIDYZ/ym2niCg63NNgYlBk
1yjJZOciNLFZ62GRX6qmWRkCgYAYS7j4mFLXR+/qlwjqP5qWx9YtvMopztqDByr7
VCRVMbncz2cWxGeT32pT5eldR3eRBrWaNWknCFAOL8FiFdlieIVuy5n1LGyqYY7y
yglpYw4L2qcjnHm2J4E8VzrZxzdBezx5fyHE9sp9iCFjPRmTPk8s6VPPrr61G/yu
7Yg2vwKBgAsJFi6zjqfUacMxB+Bb4Ehz7bqRAoeHZCH9MU2lGimjTUC322uQdu2H
ZkYkHwYVP/RngrN7bhYwPahoDThKy+fIGJAuMhPXg6HVTSkcQSJJ/VeIB/DE4AVj
8heezMN183u5gJvwaEj84fJvUEo/QdvG3NSjQptEGsXYSsE56wHu
-----END RSA PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnnExCL+jpTAgZzgEBwmR
88ltusMBdsd4ZDO4M5NohTq0T6TPvJN99ud8ZAY8fVpN63TKD2enWy6imkSE3Cdp
+yFgOntsg8WgdPF+8VQaNpn/g8LCrpWXpJuJIGzCSW5SlUt0OkvyeDvQFrKCqdI6
3H4cxXY5ly2HlTHJ1+YzEBSLONJYxsJt6/7cL7mJ7CR/9NZouft2xeNQto9JSqWU
NCwUGdZOS1pMVkka/2tFmq8psciFYvCl6msX6haZ7IDq4/GfeteL8gp6t+hgyWmb
J5/h0hNEvAz4DVb5RnKYVLwWM/e9oQTw9WgKCqUZKe0+DKmuKYMH2g77oTvDtP8N
vQIDAQAB
-----END PUBLIC KEY-----`)
)
var (
ec *config.EncryptConfig
dc *config.DecryptConfig
)
func init() {
// TODO: Create various EncryptConfigs for testing purposes
dcparameters := make(map[string][][]byte)
parameters := make(map[string][][]byte)
parameters["pubkeys"] = [][]byte{publicKey}
dcparameters["privkeys"] = [][]byte{privateKey}
dcparameters["privkeys-passwords"] = [][]byte{{}}
ec = &config.EncryptConfig{
Parameters: parameters,
DecryptConfig: config.DecryptConfig{
Parameters: dcparameters,
},
}
dc = &config.DecryptConfig{
Parameters: dcparameters,
}
}
func TestEncryptLayer(t *testing.T) {
data := []byte("This is some text!")
desc := ocispec.Descriptor{
Digest: digest.FromBytes(data),
Size: int64(len(data)),
}
dataReader := bytes.NewReader(data)
encLayerReader, annotations, err := EncryptLayer(ec, dataReader, desc)
if err != nil {
t.Fatal(err)
}
if len(annotations) == 0 {
t.Fatal("No keys created for annotations")
}
newDesc := ocispec.Descriptor{
Annotations: annotations,
}
encLayer := make([]byte, 1024)
encsize, err := encLayerReader.Read(encLayer)
if err != nil {
t.Fatal(err)
}
encLayerReaderAt := bytes.NewReader(encLayer[:encsize])
decLayerReader, _, err := DecryptLayer(dc, encLayerReaderAt, newDesc, false)
if err != nil {
t.Fatal(err)
}
decLayer := make([]byte, 1024)
decsize, err := decLayerReader.Read(decLayer)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(decLayer[:decsize], data) {
t.Fatalf("Expected %v, got %v", data, decLayer)
}
}

453
images/encryption/gpg.go Normal file
View File

@ -0,0 +1,453 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package encryption
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/containerd/containerd/errdefs"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh/terminal"
)
// GPGVersion enum representing the GPG client version to use.
type GPGVersion int
const (
// GPGv2 signifies gpgv2+
GPGv2 GPGVersion = iota
// GPGv1 signifies gpgv1+
GPGv1
// GPGVersionUndetermined signifies gpg client version undetermined
GPGVersionUndetermined
)
// GPGClient defines an interface for wrapping the gpg command line tools
type GPGClient interface {
// ReadGPGPubRingFile gets the byte sequence of the gpg public keyring
ReadGPGPubRingFile() ([]byte, error)
// GetGPGPrivateKey gets the private key bytes of a keyid given a passphrase
GetGPGPrivateKey(keyid uint64, passphrase string) ([]byte, error)
// GetSecretKeyDetails gets the details of a secret key
GetSecretKeyDetails(keyid uint64) ([]byte, bool, error)
// GetKeyDetails gets the details of a public key
GetKeyDetails(keyid uint64) ([]byte, bool, error)
// ResolveRecipients resolves PGP key ids to user names
ResolveRecipients([]string) []string
}
// gpgClient contains generic gpg client information
type gpgClient struct {
gpgHomeDir string
}
// gpgv2Client is a gpg2 client
type gpgv2Client struct {
gpgClient
}
// gpgv1Client is a gpg client
type gpgv1Client struct {
gpgClient
}
// GuessGPGVersion guesses the version of gpg. Defaults to gpg2 if exists, if
// not defaults to regular gpg.
func GuessGPGVersion() GPGVersion {
if err := exec.Command("gpg2", "--version").Run(); err == nil {
return GPGv2
} else if err := exec.Command("gpg", "--version").Run(); err == nil {
return GPGv1
} else {
return GPGVersionUndetermined
}
}
// NewGPGClient creates a new GPGClient object representing the given version
// and using the given home directory
func NewGPGClient(gpgVersion, gpgHomeDir string) (GPGClient, error) {
v := new(GPGVersion)
switch gpgVersion {
case "v1":
*v = GPGv1
case "v2":
*v = GPGv2
default:
v = nil
}
return newGPGClient(v, gpgHomeDir)
}
func newGPGClient(version *GPGVersion, homedir string) (GPGClient, error) {
var gpgVersion GPGVersion
if version != nil {
gpgVersion = *version
} else {
gpgVersion = GuessGPGVersion()
}
switch gpgVersion {
case GPGv1:
return &gpgv1Client{
gpgClient: gpgClient{gpgHomeDir: homedir},
}, nil
case GPGv2:
return &gpgv2Client{
gpgClient: gpgClient{gpgHomeDir: homedir},
}, nil
case GPGVersionUndetermined:
return nil, fmt.Errorf("unable to determine GPG version")
default:
return nil, fmt.Errorf("unhandled case: NewGPGClient")
}
}
// GetGPGPrivateKey gets the bytes of a specified keyid, supplying a passphrase
func (gc *gpgv2Client) GetGPGPrivateKey(keyid uint64, passphrase string) ([]byte, error) {
var args []string
if gc.gpgHomeDir != "" {
args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
}
args = append(args, []string{"--pinentry-mode", "loopback", "--batch", "--passphrase", passphrase, "--export-secret-key", fmt.Sprintf("0x%x", keyid)}...)
cmd := exec.Command("gpg2", args...)
return runGPGGetOutput(cmd)
}
// ReadGPGPubRingFile reads the GPG public key ring file
func (gc *gpgv2Client) ReadGPGPubRingFile() ([]byte, error) {
var args []string
if gc.gpgHomeDir != "" {
args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
}
args = append(args, []string{"--batch", "--export"}...)
cmd := exec.Command("gpg2", args...)
return runGPGGetOutput(cmd)
}
func (gc *gpgv2Client) getKeyDetails(option string, keyid uint64) ([]byte, bool, error) {
var args []string
if gc.gpgHomeDir != "" {
args = append([]string{"--homedir", gc.gpgHomeDir})
}
args = append(args, option, fmt.Sprintf("0x%x", keyid))
cmd := exec.Command("gpg2", args...)
keydata, err := runGPGGetOutput(cmd)
return keydata, err == nil, err
}
// GetSecretKeyDetails retrives the secret key details of key with keyid.
// returns a byte array of the details and a bool if the key exists
func (gc *gpgv2Client) GetSecretKeyDetails(keyid uint64) ([]byte, bool, error) {
return gc.getKeyDetails("-K", keyid)
}
// GetKeyDetails retrives the public key details of key with keyid.
// returns a byte array of the details and a bool if the key exists
func (gc *gpgv2Client) GetKeyDetails(keyid uint64) ([]byte, bool, error) {
return gc.getKeyDetails("-k", keyid)
}
// ResolveRecipients converts PGP keyids to email addresses, if possible
func (gc *gpgv2Client) ResolveRecipients(recipients []string) []string {
return resolveRecipients(gc, recipients)
}
// GetGPGPrivateKey gets the bytes of a specified keyid, supplying a passphrase
func (gc *gpgv1Client) GetGPGPrivateKey(keyid uint64, _ string) ([]byte, error) {
var args []string
if gc.gpgHomeDir != "" {
args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
}
args = append(args, []string{"--batch", "--export-secret-key", fmt.Sprintf("0x%x", keyid)}...)
cmd := exec.Command("gpg", args...)
return runGPGGetOutput(cmd)
}
// ReadGPGPubRingFile reads the GPG public key ring file
func (gc *gpgv1Client) ReadGPGPubRingFile() ([]byte, error) {
var args []string
if gc.gpgHomeDir != "" {
args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
}
args = append(args, []string{"--batch", "--export"}...)
cmd := exec.Command("gpg", args...)
return runGPGGetOutput(cmd)
}
func (gc *gpgv1Client) getKeyDetails(option string, keyid uint64) ([]byte, bool, error) {
var args []string
if gc.gpgHomeDir != "" {
args = append([]string{"--homedir", gc.gpgHomeDir})
}
args = append(args, option, fmt.Sprintf("0x%x", keyid))
cmd := exec.Command("gpg", args...)
keydata, err := runGPGGetOutput(cmd)
return keydata, err == nil, err
}
// GetSecretKeyDetails retrives the secret key details of key with keyid.
// returns a byte array of the details and a bool if the key exists
func (gc *gpgv1Client) GetSecretKeyDetails(keyid uint64) ([]byte, bool, error) {
return gc.getKeyDetails("-K", keyid)
}
// GetKeyDetails retrives the public key details of key with keyid.
// returns a byte array of the details and a bool if the key exists
func (gc *gpgv1Client) GetKeyDetails(keyid uint64) ([]byte, bool, error) {
return gc.getKeyDetails("-k", keyid)
}
// ResolveRecipients converts PGP keyids to email addresses, if possible
func (gc *gpgv1Client) ResolveRecipients(recipients []string) []string {
return resolveRecipients(gc, recipients)
}
// runGPGGetOutput runs the GPG commandline and returns stdout as byte array
// and any stderr in the error
func runGPGGetOutput(cmd *exec.Cmd) ([]byte, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
return nil, err
}
stdoutstr, err2 := ioutil.ReadAll(stdout)
stderrstr, _ := ioutil.ReadAll(stderr)
if err := cmd.Wait(); err != nil {
return nil, fmt.Errorf("error from %s: %s", cmd.Path, string(stderrstr))
}
return stdoutstr, err2
}
// resolveRecipients walks the list of recipients and attempts to convert
// all keyIds to email addresses; if something goes wrong during the
// conversion of a recipient, the original string is returned for that
// recpient
func resolveRecipients(gc GPGClient, recipients []string) []string {
var result []string
for _, recipient := range recipients {
keyID, err := strconv.ParseUint(recipient, 0, 64)
if err != nil {
result = append(result, recipient)
} else {
details, found, _ := gc.GetKeyDetails(keyID)
if !found {
result = append(result, recipient)
} else {
email := extractEmailFromDetails(details)
if email == "" {
result = append(result, recipient)
} else {
result = append(result, email)
}
}
}
}
return result
}
var emailPattern = regexp.MustCompile(`uid\s+\[.*\]\s.*\s<(?P<email>.+)>`)
func extractEmailFromDetails(details []byte) string {
loc := emailPattern.FindSubmatchIndex(details)
if len(loc) == 0 {
return ""
}
return string(emailPattern.Expand(nil, []byte("$email"), details, loc))
}
// uint64ToStringArray converts an array of uint64's to an array of strings
// by applying a format string to each uint64
func uint64ToStringArray(format string, in []uint64) []string {
var ret []string
for _, v := range in {
ret = append(ret, fmt.Sprintf(format, v))
}
return ret
}
// GPGGetPrivateKey walks the list of layerInfos and tries to decrypt the
// wrapped symmetric keys. For this it determines whether a private key is
// in the GPGVault or on this system and prompts for the passwords for those
// that are available. If we do not find a private key on the system for
// getting to the symmetric key of a layer then an error is generated.
func GPGGetPrivateKey(descs []ocispec.Descriptor, gpgClient GPGClient, gpgVault GPGVault, mustFindKey bool, dcparameters map[string][][]byte) error {
// PrivateKeyData describes a private key
type PrivateKeyData struct {
KeyData []byte
KeyDataPassword []byte
}
var pkd PrivateKeyData
keyIDPasswordMap := make(map[uint64]PrivateKeyData)
for _, desc := range descs {
for scheme, b64pgpPackets := range GetWrappedKeysMap(desc) {
if scheme != "pgp" {
continue
}
keywrapper := GetKeyWrapper(scheme)
if keywrapper == nil {
return errors.Errorf("could not get KeyWrapper for %s\n", scheme)
}
keyIds, err := keywrapper.GetKeyIdsFromPacket(b64pgpPackets)
if err != nil {
return err
}
found := false
for _, keyid := range keyIds {
// do we have this key? -- first check the vault
if gpgVault != nil {
_, keydata := gpgVault.GetGPGPrivateKey(keyid)
if len(keydata) > 0 {
pkd = PrivateKeyData{
KeyData: keydata,
KeyDataPassword: nil, // password not supported in this case
}
keyIDPasswordMap[keyid] = pkd
found = true
break
}
} else if gpgClient != nil {
// check the local system's gpg installation
keyinfo, haveKey, _ := gpgClient.GetSecretKeyDetails(keyid)
// this may fail if the key is not here; we ignore the error
if !haveKey {
// key not on this system
continue
}
_, found = keyIDPasswordMap[keyid]
if !found {
fmt.Printf("Passphrase required for Key id 0x%x: \n%v", keyid, string(keyinfo))
fmt.Printf("Enter passphrase for key with Id 0x%x: ", keyid)
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
fmt.Printf("\n")
if err != nil {
return err
}
keydata, err := gpgClient.GetGPGPrivateKey(keyid, string(password))
if err != nil {
return err
}
pkd = PrivateKeyData{
KeyData: keydata,
KeyDataPassword: password,
}
keyIDPasswordMap[keyid] = pkd
found = true
}
break
} else {
return errors.Wrapf(errdefs.ErrInvalidArgument, "no GPGVault or GPGClient passed.")
}
}
if !found && len(b64pgpPackets) > 0 && mustFindKey {
ids := uint64ToStringArray("0x%x", keyIds)
return errors.Wrapf(errdefs.ErrNotFound, "missing key for decryption of layer %x of %s. Need one of the following keys: %s", desc.Digest, desc.Platform, strings.Join(ids, ", "))
}
}
}
var (
privKeys [][]byte
privKeysPwd [][]byte
)
for _, pkd := range keyIDPasswordMap {
privKeys = append(privKeys, pkd.KeyData)
privKeysPwd = append(privKeysPwd, pkd.KeyDataPassword)
}
dcparameters["gpg-privatekeys"] = privKeys
dcparameters["gpg-privatekeys-passwords"] = privKeysPwd
return nil
}
// GPGSetupPrivateKeys uses the gpg specific parameters in the dcparameters map
// to get the private keys needed for decryption the give layers
func GPGSetupPrivateKeys(dcparameters map[string][][]byte, descs []ocispec.Descriptor) error {
/* we have to find a GPG key until we also get other private keys passed */
var (
gpgVault GPGVault
gpgClient GPGClient
gpgVersion string
gpgHomeDir string
err error
)
gpgPrivateKeys := dcparameters["gpg-privatekeys"]
if len(gpgPrivateKeys) > 0 {
gpgVault = NewGPGVault()
gpgVault.AddSecretKeyRingDataArray(gpgPrivateKeys)
}
haveGPGClient := dcparameters["gpg-client"]
if len(haveGPGClient) > 0 {
item := dcparameters["gpg-client-version"]
if len(item) == 1 {
gpgVersion = string(item[0])
}
item = dcparameters["gpg-client-homedir"]
if len(item) == 1 {
gpgHomeDir = string(item[0])
}
gpgClient, err = NewGPGClient(gpgVersion, gpgHomeDir)
if err != nil {
return err
}
}
return GPGGetPrivateKey(descs, gpgClient, gpgVault, false, dcparameters)
}

View File

@ -0,0 +1,100 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package encryption
import (
"bytes"
"io/ioutil"
"github.com/pkg/errors"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet"
)
// GPGVault defines an interface for wrapping multiple secret key rings
type GPGVault interface {
// AddSecretKeyRingData adds a secret keyring via its raw byte array
AddSecretKeyRingData(gpgSecretKeyRingData []byte) error
// AddSecretKeyRingDataArray adds secret keyring via its raw byte arrays
AddSecretKeyRingDataArray(gpgSecretKeyRingDataArray [][]byte) error
// AddSecretKeyRingFiles adds secret keyrings given their filenames
AddSecretKeyRingFiles(filenames []string) error
// GetGPGPrivateKey gets the private key bytes of a keyid given a passphrase
GetGPGPrivateKey(keyid uint64) ([]openpgp.Key, []byte)
}
// gpgVault wraps an array of gpgSecretKeyRing
type gpgVault struct {
entityLists []openpgp.EntityList
keyDataList [][]byte // the raw data original passed in
}
// NewGPGVault creates an empty GPGVault
func NewGPGVault() GPGVault {
return &gpgVault{}
}
// AddSecretKeyRingData adds a secret keyring's to the gpgVault; the raw byte
// array read from the file must be passed and will be parsed by this function
func (g *gpgVault) AddSecretKeyRingData(gpgSecretKeyRingData []byte) error {
// read the private keys
r := bytes.NewReader(gpgSecretKeyRingData)
entityList, err := openpgp.ReadKeyRing(r)
if err != nil {
return errors.Wrapf(err, "could not read keyring")
}
g.entityLists = append(g.entityLists, entityList)
g.keyDataList = append(g.keyDataList, gpgSecretKeyRingData)
return nil
}
// AddSecretKeyRingDataArray adds secret keyrings to the gpgVault; the raw byte
// arrays read from files must be passed
func (g *gpgVault) AddSecretKeyRingDataArray(gpgSecretKeyRingDataArray [][]byte) error {
for _, gpgSecretKeyRingData := range gpgSecretKeyRingDataArray {
if err := g.AddSecretKeyRingData(gpgSecretKeyRingData); err != nil {
return err
}
}
return nil
}
// AddSecretKeyRingFiles adds the secret key rings given their filenames
func (g *gpgVault) AddSecretKeyRingFiles(filenames []string) error {
for _, filename := range filenames {
gpgSecretKeyRingData, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
err = g.AddSecretKeyRingData(gpgSecretKeyRingData)
if err != nil {
return err
}
}
return nil
}
// GetGPGPrivateKey gets the bytes of a specified keyid, supplying a passphrase
func (g *gpgVault) GetGPGPrivateKey(keyid uint64) ([]openpgp.Key, []byte) {
for i, el := range g.entityLists {
decKeys := el.KeysByIdUsage(keyid, packet.KeyFlagEncryptCommunications)
if len(decKeys) > 0 {
return decKeys, g.keyDataList[i]
}
}
return nil, nil
}

View File

@ -0,0 +1,132 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package jwe
import (
"crypto/ecdsa"
"github.com/containerd/containerd/images/encryption/config"
"github.com/containerd/containerd/images/encryption/keywrap"
"github.com/containerd/containerd/images/encryption/utils"
"github.com/pkg/errors"
jose "gopkg.in/square/go-jose.v2"
)
type jweKeyWrapper struct {
}
func (kw *jweKeyWrapper) GetAnnotationID() string {
return "org.opencontainers.image.enc.keys.jwe"
}
// NewKeyWrapper returns a new key wrapping interface using jwe
func NewKeyWrapper() keywrap.KeyWrapper {
return &jweKeyWrapper{}
}
// WrapKeys wraps the session key for recpients and encrypts the optsData, which
// describe the symmetric key used for encrypting the layer
func (kw *jweKeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) {
var joseRecipients []jose.Recipient
err := addPubKeys(&joseRecipients, ec.Parameters["pubkeys"])
if err != nil {
return nil, err
}
// no recipients is not an error...
if len(joseRecipients) == 0 {
return nil, nil
}
encrypter, err := jose.NewMultiEncrypter(jose.A256GCM, joseRecipients, nil)
if err != nil {
return nil, errors.Wrapf(err, "jose.NewMultiEncrypter failed")
}
jwe, err := encrypter.Encrypt(optsData)
if err != nil {
return nil, errors.Wrapf(err, "JWE Encrypt failed")
}
return []byte(jwe.FullSerialize()), nil
}
func (kw *jweKeyWrapper) UnwrapKey(dc *config.DecryptConfig, jweString []byte) ([]byte, error) {
jwe, err := jose.ParseEncrypted(string(jweString))
if err != nil {
return nil, errors.New("jose.ParseEncrypted failed")
}
privKeys := kw.GetPrivateKeys(dc.Parameters)
if len(privKeys) == 0 {
return nil, errors.New("No private keys found for JWE decryption")
}
privKeysPasswords := kw.getPrivateKeysPasswords(dc.Parameters)
if len(privKeysPasswords) != len(privKeys) {
return nil, errors.New("Private key password array length must be same as that of private keys")
}
for idx, privKey := range privKeys {
key, err := utils.ParsePrivateKey(privKey, privKeysPasswords[idx], "JWE")
if err != nil {
return nil, err
}
_, _, plain, err := jwe.DecryptMulti(key)
if err == nil {
return plain, nil
}
}
return nil, errors.New("JWE: No suitable private key found for decryption")
}
func (kw *jweKeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte {
return dcparameters["privkeys"]
}
func (kw *jweKeyWrapper) getPrivateKeysPasswords(dcparameters map[string][][]byte) [][]byte {
return dcparameters["privkeys-passwords"]
}
func (kw *jweKeyWrapper) GetKeyIdsFromPacket(b64jwes string) ([]uint64, error) {
return nil, nil
}
func (kw *jweKeyWrapper) GetRecipients(b64jwes string) ([]string, error) {
return []string{"[jwe]"}, nil
}
func addPubKeys(joseRecipients *[]jose.Recipient, pubKeys [][]byte) error {
if len(pubKeys) == 0 {
return nil
}
for _, pubKey := range pubKeys {
key, err := utils.ParsePublicKey(pubKey, "JWE")
if err != nil {
return err
}
alg := jose.RSA_OAEP
switch key.(type) {
case *ecdsa.PublicKey:
alg = jose.ECDH_ES_A256KW
}
*joseRecipients = append(*joseRecipients, jose.Recipient{
Algorithm: alg,
Key: key,
})
}
return nil
}

View File

@ -0,0 +1,345 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package jwe
import (
"crypto/elliptic"
"testing"
"github.com/containerd/containerd/images/encryption/config"
"github.com/containerd/containerd/images/encryption/utils"
jose "gopkg.in/square/go-jose.v2"
)
var oneEmpty []byte
func createValidJweCcs() ([]*config.CryptoConfig, error) {
jwePubKeyPem, jwePrivKeyPem, err := utils.CreateRSATestKey(2048, oneEmpty, true)
if err != nil {
return nil, err
}
jwePubKey2Pem, jwePrivKey2Pem, err := utils.CreateRSATestKey(1024, oneEmpty, true)
if err != nil {
return nil, err
}
jwePrivKey3Password := []byte("password")
jwePubKey3Pem, jwePrivKey3PassPem, err := utils.CreateRSATestKey(2048, jwePrivKey3Password, true)
if err != nil {
return nil, err
}
jwePubKeyDer, jwePrivKeyDer, err := utils.CreateRSATestKey(2048, oneEmpty, false)
if err != nil {
return nil, err
}
jweEcPubKeyDer, jweEcPrivKeyDer, err := utils.CreateECDSATestKey(elliptic.P521())
if err != nil {
return nil, err
}
key, err := utils.CreateRSAKey(2048)
if err != nil {
return nil, err
}
jwePrivKeyJwk, err := jose.JSONWebKey{Key: key}.MarshalJSON()
if err != nil {
return nil, err
}
jwePubKeyJwk, err := jose.JSONWebKey{Key: &key.PublicKey}.MarshalJSON()
if err != nil {
return nil, err
}
validJweCcs := []*config.CryptoConfig{
// Key 1
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"pubkeys": {jwePubKeyPem},
},
DecryptConfig: config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePrivKeyPem},
"privkeys-passwords": {oneEmpty},
},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePrivKeyPem},
"privkeys-passwords": {oneEmpty},
},
},
},
// Key 2
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"pubkeys": {jwePubKey2Pem},
},
DecryptConfig: config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePrivKey2Pem},
"privkeys-passwords": {oneEmpty},
},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePrivKey2Pem},
"privkeys-passwords": {oneEmpty},
},
},
},
// Key 1 without enc private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"pubkeys": {jwePubKeyPem},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePrivKeyPem},
"privkeys-passwords": {oneEmpty},
},
},
},
// Key 2 without enc private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"pubkeys": {jwePubKey2Pem},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePrivKey2Pem},
"privkeys-passwords": {oneEmpty},
},
},
},
// Key 3 with enc private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"pubkeys": {jwePubKey3Pem},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePrivKey3PassPem},
"privkeys-passwords": {jwePrivKey3Password},
},
},
},
// Key (DER format)
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"pubkeys": {jwePubKeyDer},
},
DecryptConfig: config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePrivKeyDer},
"privkeys-passwords": {oneEmpty},
},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePrivKeyDer},
"privkeys-passwords": {oneEmpty},
},
},
},
// Key (JWK format)
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"pubkeys": {jwePubKeyJwk},
},
DecryptConfig: config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePrivKeyJwk},
"privkeys-passwords": {oneEmpty},
},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePrivKeyJwk},
"privkeys-passwords": {oneEmpty},
},
},
},
// EC Key (DER format)
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"pubkeys": {jweEcPubKeyDer},
},
DecryptConfig: config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jweEcPrivKeyDer},
"privkeys-passwords": {oneEmpty},
},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jweEcPrivKeyDer},
"privkeys-passwords": {oneEmpty},
},
},
},
}
return validJweCcs, nil
}
func createInvalidJweCcs() ([]*config.CryptoConfig, error) {
jwePubKeyPem, _, err := utils.CreateRSATestKey(2048, oneEmpty, true)
if err != nil {
return nil, err
}
jwePubKey2Pem, _, err := utils.CreateRSATestKey(2048, oneEmpty, true)
if err != nil {
return nil, err
}
invalidJweCcs := []*config.CryptoConfig{
// Client key 1 public with client 2 private decrypt
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"pubkeys": {jwePubKeyPem},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePubKey2Pem},
"privkeys-passwords": {oneEmpty},
},
},
},
// Client key 1 public with no private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"pubkeys": {jwePubKeyPem},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{},
},
},
// Invalid Client key 1 private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"pubkeys": {jwePubKeyPem},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {jwePubKeyPem},
"privkeys-passwords": {oneEmpty},
},
},
},
}
return invalidJweCcs, nil
}
func TestKeyWrapJweSuccess(t *testing.T) {
validJweCcs, err := createValidJweCcs()
if err != nil {
t.Fatal(err)
}
for _, cc := range validJweCcs {
kw := NewKeyWrapper()
data := []byte("This is some secret text")
wk, err := kw.WrapKeys(cc.EncryptConfig, data)
if err != nil {
t.Fatal(err)
}
ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
if err != nil {
t.Fatal(err)
}
if string(data) != string(ud) {
t.Fatal("Strings don't match")
}
}
}
func TestKeyWrapJweInvalid(t *testing.T) {
invalidJweCcs, err := createInvalidJweCcs()
if err != nil {
t.Fatal(err)
}
for _, cc := range invalidJweCcs {
kw := NewKeyWrapper()
data := []byte("This is some secret text")
wk, err := kw.WrapKeys(cc.EncryptConfig, data)
if err != nil {
return
}
ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
if err != nil {
return
}
if string(data) != string(ud) {
return
}
t.Fatal("Successfully wrap for invalid crypto config")
}
}

View File

@ -0,0 +1,40 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package keywrap
import (
"github.com/containerd/containerd/images/encryption/config"
)
// KeyWrapper is the interface used for wrapping keys using
// a specific encryption technology (pgp, jwe)
type KeyWrapper interface {
WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error)
UnwrapKey(dc *config.DecryptConfig, annotation []byte) ([]byte, error)
GetAnnotationID() string
// GetPrivateKeys (optional) gets the array of private keys. It is an optional implementation
// as in some key services, a private key may not be exportable (i.e. HSM)
GetPrivateKeys(dcparameters map[string][][]byte) [][]byte
// GetKeyIdsFromPacket (optional) gets a list of key IDs. This is optional as some encryption
// schemes may not have a notion of key IDs
GetKeyIdsFromPacket(packet string) ([]uint64, error)
// GetRecipients (optional) gets a list of recipients. It is optional due to the validity of
// recipients in a particular encryptiong scheme
GetRecipients(packet string) ([]string, error)
}

View File

@ -0,0 +1,270 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pgp
import (
"bytes"
"crypto"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"net/mail"
"strconv"
"strings"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images/encryption/config"
"github.com/containerd/containerd/images/encryption/keywrap"
"github.com/pkg/errors"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet"
)
type gpgKeyWrapper struct {
}
// NewKeyWrapper returns a new key wrapping interface for pgp
func NewKeyWrapper() keywrap.KeyWrapper {
return &gpgKeyWrapper{}
}
var (
// GPGDefaultEncryptConfig is the default configuration for layer encryption/decryption
GPGDefaultEncryptConfig = &packet.Config{
Rand: rand.Reader,
DefaultHash: crypto.SHA256,
DefaultCipher: packet.CipherAES256,
CompressionConfig: &packet.CompressionConfig{Level: 0}, // No compression
RSABits: 2048,
}
)
func (kw *gpgKeyWrapper) GetAnnotationID() string {
return "org.opencontainers.image.enc.keys.pgp"
}
// WrapKeys wraps the session key for recpients and encrypts the optsData, which
// describe the symmetric key used for encrypting the layer
func (kw *gpgKeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) {
ciphertext := new(bytes.Buffer)
el, err := kw.createEntityList(ec)
if err != nil {
return nil, errors.Wrap(err, "unable to create entity list")
}
if len(el) == 0 {
// nothing to do -- not an error
return nil, nil
}
plaintextWriter, err := openpgp.Encrypt(ciphertext,
el, /*EntityList*/
nil, /* Sign*/
nil, /* FileHint */
GPGDefaultEncryptConfig)
if err != nil {
return nil, err
}
if _, err = plaintextWriter.Write(optsData); err != nil {
return nil, err
} else if err = plaintextWriter.Close(); err != nil {
return nil, err
}
return ciphertext.Bytes(), err
}
// UnwrapKey unwraps the symmetric key with which the layer is encrypted
// This symmetric key is encrypted in the PGP payload.
func (kw *gpgKeyWrapper) UnwrapKey(dc *config.DecryptConfig, pgpPacket []byte) ([]byte, error) {
pgpPrivateKeys, pgpPrivateKeysPwd, err := kw.getKeyParameters(dc.Parameters)
if err != nil {
return nil, err
}
for idx, pgpPrivateKey := range pgpPrivateKeys {
r := bytes.NewBuffer(pgpPrivateKey)
entityList, err := openpgp.ReadKeyRing(r)
if err != nil {
return nil, errors.Wrap(err, "unable to parse private keys")
}
var prompt openpgp.PromptFunction
if len(pgpPrivateKeysPwd) > idx {
responded := false
prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
if responded {
return nil, fmt.Errorf("don't seem to have the right password")
}
responded = true
for _, key := range keys {
if key.PrivateKey != nil {
key.PrivateKey.Decrypt(pgpPrivateKeysPwd[idx])
}
}
return pgpPrivateKeysPwd[idx], nil
}
}
r = bytes.NewBuffer(pgpPacket)
md, err := openpgp.ReadMessage(r, entityList, prompt, GPGDefaultEncryptConfig)
if err != nil {
continue
}
// we get the plain key options back
optsData, err := ioutil.ReadAll(md.UnverifiedBody)
if err != nil {
continue
}
return optsData, nil
}
return nil, errors.New("PGP: No suitable key found to unwrap key")
}
// GetKeyIdsFromWrappedKeys converts the base64 encoded PGPPacket to uint64 keyIds
func (kw *gpgKeyWrapper) GetKeyIdsFromPacket(b64pgpPackets string) ([]uint64, error) {
var keyids []uint64
for _, b64pgpPacket := range strings.Split(b64pgpPackets, ",") {
pgpPacket, err := base64.StdEncoding.DecodeString(b64pgpPacket)
if err != nil {
return nil, errors.Wrapf(err, "could not decode base64 encoded PGP packet")
}
newids, err := kw.getKeyIDs(pgpPacket)
if err != nil {
return nil, err
}
keyids = append(keyids, newids...)
}
return keyids, nil
}
// getKeyIDs parses a PGPPacket and gets the list of recipients' key IDs
func (kw *gpgKeyWrapper) getKeyIDs(pgpPacket []byte) ([]uint64, error) {
var keyids []uint64
kbuf := bytes.NewBuffer(pgpPacket)
packets := packet.NewReader(kbuf)
ParsePackets:
for {
p, err := packets.Next()
if err == io.EOF {
break ParsePackets
}
if err != nil {
return []uint64{}, errors.Wrapf(err, "packets.Next() failed")
}
switch p := p.(type) {
case *packet.EncryptedKey:
keyids = append(keyids, p.KeyId)
case *packet.SymmetricallyEncrypted:
break ParsePackets
}
}
return keyids, nil
}
// GetRecipients converts the wrappedKeys to an array of recipients
func (kw *gpgKeyWrapper) GetRecipients(b64pgpPackets string) ([]string, error) {
keyIds, err := kw.GetKeyIdsFromPacket(b64pgpPackets)
if err != nil {
return nil, err
}
var array []string
for _, keyid := range keyIds {
array = append(array, "0x"+strconv.FormatUint(keyid, 16))
}
return array, nil
}
func (kw *gpgKeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte {
return dcparameters["gpg-privatekeys"]
}
func (kw *gpgKeyWrapper) getKeyParameters(dcparameters map[string][][]byte) ([][]byte, [][]byte, error) {
privKeys := kw.GetPrivateKeys(dcparameters)
if len(privKeys) == 0 {
return nil, nil, errors.New("GPG: Missing private key parameter")
}
return privKeys, dcparameters["gpg-privatekeys-passwords"], nil
}
// createEntityList creates the opengpg EntityList by reading the KeyRing
// first and then filtering out recipients' keys
func (kw *gpgKeyWrapper) createEntityList(ec *config.EncryptConfig) (openpgp.EntityList, error) {
pgpPubringFile := ec.Parameters["gpg-pubkeyringfile"]
if len(pgpPubringFile) == 0 {
return nil, nil
}
r := bytes.NewReader(pgpPubringFile[0])
entityList, err := openpgp.ReadKeyRing(r)
if err != nil {
return nil, err
}
gpgRecipients := ec.Parameters["gpg-recipients"]
if len(gpgRecipients) == 0 {
return nil, nil
}
rSet := make(map[string]int)
for _, r := range gpgRecipients {
rSet[string(r)] = 0
}
var filteredList openpgp.EntityList
for _, entity := range entityList {
for k := range entity.Identities {
addr, err := mail.ParseAddress(k)
if err != nil {
return nil, err
}
for _, r := range gpgRecipients {
recp := string(r)
if strings.Compare(addr.Name, recp) == 0 || strings.Compare(addr.Address, recp) == 0 {
filteredList = append(filteredList, entity)
rSet[recp] = rSet[recp] + 1
}
}
}
}
// make sure we found keys for all the Recipients...
var buffer bytes.Buffer
notFound := false
buffer.WriteString("PGP: No key found for the following recipients: ")
for k, v := range rSet {
if v == 0 {
if notFound {
buffer.WriteString(", ")
}
buffer.WriteString(k)
notFound = true
}
}
if notFound {
return nil, errors.Wrapf(errdefs.ErrNotFound, buffer.String())
}
return filteredList, nil
}

View File

@ -0,0 +1,190 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pgp
import (
"testing"
"github.com/containerd/containerd/images/encryption/config"
)
var validGpgCcs = []*config.CryptoConfig{
// Key 1
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"gpg-pubkeyringfile": {gpgPubKeyRing},
"gpg-recipients": {gpgRecipient1},
},
DecryptConfig: config.DecryptConfig{
Parameters: map[string][][]byte{
"gpg-privatekeys": {gpgPrivKey1},
},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"gpg-privatekeys": {gpgPrivKey1},
},
},
},
// Key 2
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"gpg-pubkeyringfile": {gpgPubKeyRing},
"gpg-recipients": {gpgRecipient2},
},
DecryptConfig: config.DecryptConfig{
Parameters: map[string][][]byte{
"gpg-privatekeys": {gpgPrivKey2},
},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"gpg-privatekeys": {gpgPrivKey2},
},
},
},
// Key 1 without enc private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"gpg-pubkeyringfile": {gpgPubKeyRing},
"gpg-recipients": {gpgRecipient1},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"gpg-privatekeys": {gpgPrivKey1},
},
},
},
// Key 2 without enc private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"gpg-pubkeyringfile": {gpgPubKeyRing},
"gpg-recipients": {gpgRecipient2},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"gpg-privatekeys": {gpgPrivKey2},
},
},
},
}
var invalidGpgCcs = []*config.CryptoConfig{
// Client key 1 public with client 2 private decrypt
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"gpg-pubkeyringfile": {gpgPubKeyRing},
"gpg-recipients": {gpgRecipient1},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"gpg-privatekeys": {gpgPrivKey2},
},
},
},
// Client key 1 public with no private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"gpg-pubkeyringfile": {gpgPubKeyRing},
"gpg-recipients": {gpgRecipient1},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{},
},
},
// Invalid Client key 1 private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"gpg-pubkeyringfile": {gpgPrivKey1},
"gpg-recipients": {gpgRecipient1},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"gpg-privatekeys": {gpgPrivKey1},
},
},
},
}
func TestKeyWrapGpgSuccess(t *testing.T) {
for _, cc := range validGpgCcs {
kw := NewKeyWrapper()
data := []byte("This is some secret text")
wk, err := kw.WrapKeys(cc.EncryptConfig, data)
if err != nil {
t.Fatal(err)
}
ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
if err != nil {
t.Fatal(err)
}
if string(data) != string(ud) {
t.Fatal("Strings don't match")
}
}
}
func TestKeyWrapGpgInvalid(t *testing.T) {
for _, cc := range invalidGpgCcs {
kw := NewKeyWrapper()
data := []byte("This is some secret text")
wk, err := kw.WrapKeys(cc.EncryptConfig, data)
if err != nil {
return
}
ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
if err != nil {
return
}
if string(data) != string(ud) {
return
}
t.Fatal("Successfully wrap for invalid crypto config")
}
}

View File

@ -0,0 +1,87 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pgp
import (
"encoding/base64"
"strings"
)
func b64Dec(str string) string {
str = strings.Replace(str, " ", "", -1)
s, err := base64.StdEncoding.DecodeString(str)
if err != nil {
panic(err)
}
return string(s)
}
var (
gpgPubKeyRing = []byte(b64Dec(`mQENBFulXFgBCADKrLe251CMrFS4Un4sPcFb9TVZxdSuMlf4lhFhMphqQkctMoyjeeGebN8P0R8E
8xeV4iJnIMPWqoWTabvDGkl9HorFrSVeZVj0OD9JoMAIg55KSbT1XUWzDgNiZ4p6PJkORx2uTdfZ
AwhdAAAu4HDzAGHF0YKV31iZbSdAcFMVAxCxc6zAVV7qL+3SLxT5UxB/lAbKX1c4Tn6y7wlKZOGm
WUWsBLQ1aQ/iloFIakUwwa+Yc03WUYEDEXnaQ9tDSyjI3fWcwTVRI29LOkFT7JiIK0FgYkebYex9
Cp+G8QuW6XK7A4ljrhQM5SVfw+XPbbPQG3kbA0YMP86oZ/VPHzq3ABEBAAG0G3Rlc3RrZXkxIDx0
ZXN0a2V5MUBrZXkub3JnPokBVAQTAQgAPhYhBNKhPj7F2BYBPVBwEO/H08vyNX7IBQJbpVxYAhsD
BQkDwmcABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEO/H08vyNX7ILWoH/135x+mCK9MV7YpI
WATHI3TjZ0e5VEzbMU4b4hH8R9TaFo2nbOO3APbfrOU8AnZSPSdgUMlcFJQhDLbP5rs01e+r2EG6
ksny3LNnXv1kfyn9aqC4gQVKVHXnZzd/Tn6H9h6AaZb3TrbgOY2ZBAZKXGPBzpHVKlRv93GiW8h8
VVlaHRJJK/NpLAA3QgcraGgBmp3u8FCGtvzJ5lXvUCbHrCjxHsGt/aj23xfo+wtlGnkg0kfvapQq
U1f69RoodoJTxP86WVeX5/Gm/NebZTgE538nXvJn+jta4Meh3//xf8g2yzhUEUaq0YUf96lYjf6j
Xb3uZhcu2eM37vM4sczE9Aa5AQ0EW6VcWAEIAK04qvvFX9gN8NDmUJaguSuQCwsEYG9H6HATZsJY
UvjwCbsL2HBQU08Yytm9maf0exYSKsoARapr53DGxnE0J4My1PcijE2daIwly0N1uF5IcXEHJqJ+
QPhfArFxd4HRP/R6xpcDfGuoJQ3G3Nl2KuLMVqD2+admltenwf+AjPYDqrsYBJkaLcY/IaHiSAgj
JPEm/T70J5ZxCbGqEPx93dTgdg4y4ybFiFWsHwFt8d2/gK7TlNEGILGAjzfy4zcEg9UKg7LYPacs
Pw6BbaUGOu4bqcKAZM0PP8+P+/9LVvFGE3V3XzKGDE5BxnzzaBpltnOC5t5MozQsy2XdKiQ4LzcA
EQEAAYkBPAQYAQgAJhYhBNKhPj7F2BYBPVBwEO/H08vyNX7IBQJbpVxYAhsMBQkDwmcAAAoJEO/H
08vyNX7IfDgIAK9qxhz/j0NUlbUx7KCqXOA1VSe9eBhWaVn6fenBzghLicr6H0FD1L8nWIuh1qAm
/Ad8aQEKEAQK5OoS/hBteECs1rlt9FHYa4gI8fCnAHzYleqntpKCeHDmYX4KxUuzbCaqOBgpgMog
yU6cx/VgqgAk/YHH4P1L+Gtbmv8sDAEo0l/6lRcZHZu98oQrAQnw5sYOvL8vCFFrAPh8Gvy8+M4P
/dS0tHkvLP1ah8uxj0SWiw5QIiovcAnwlM8aHNxPSkbhhKKZlh4lOWDWedobSwRK4HmSZcGVMflP
XvPlz32v+pJ64G9XUiyM/N9/8iQGWiu5Jb3ZMLB7JoQgoENKTY2ZAQ0EW6VcbgEIAL2RwpOU4Ffp
oxPDHWdjiz4+Qv/3JdEn23DXSeg2CfxgqnQ/SLH4tVkxIDsQgIRJ68Jh4l6L6xCGcb4gFQPJECy7
O6DA3q26bGlq0iEagHIvcM2QKkX1X9YaR0HsqFjQ42fxIP1ItpMJ8fvfyLB3/UhZdzpx3B0VzRt3
MRAfzvRsCm3bvPevmiRbjiJayf075NHWPxyj9S9aesQYc4Ql30IuFQ5nXWX1vmGWyjvM+iBmbZHr
uxQUommI76vi90yqB3ZoXmYOI5K+VEpE98F12qrOqQhEgsUuRm/s4H2eJaHazYRElvWydV+cLXJJ
GnJUUHm0rUz6ikrLcppf7M7QQ1UAEQEAAbQbdGVzdGtleTIgPHRlc3RrZXkyQGtleS5vcmc+iQFU
BBMBCAA+FiEEHAf5pLOwt580eOkqFWZAd+pqZ0kFAlulXG4CGwMFCQPCZwAFCwkIBwIGFQoJCAsC
BBYCAwECHgECF4AACgkQFWZAd+pqZ0mkGAf+NpJgHxKIFjnObqLLpPLiyN0WY1Bbu7RQpg1CqeMf
SNskZuHqz4AjgJXKUW3rtVY3RDx3vatEz0c05dJ0L5PPU8gcUOP2kA7mHlPvTne1R0ZBKXYXpHL1
wEBMKshoInVrL24JHd4w7bSiwokzhVRRW9yUn4kZiVnyJp+swZ5aKQE/QB1CMHEONz0KYe5aOjug
7Zi4b+wps2KSewSezU24i4bLEDZNrKWagsYFLGzJhZDDdbi1DzOSstYUvu8v9GBLEgi7lAK2Eud/
c2w3wS4Gc7LQNmtM4t686cH1xeCT7mQEpqdDT0HWf8YTl+8FtddohF4xtJi4aSxZlrvYtxlNdbkB
DQRbpVxuAQgAu5mM8iT/9495MySWxtTlOILxFAxluehWlJt4Btd/3vSn447bnkWsmGQBU6Fs6OZJ
O3F4SSwYghhUF/+Dtdn7Ua1r/Lt5ClXCLHe9m3akyutWJLocaEfn1/BifbhRNzJnqYIq4gMI4JB+
KPe+eLz8DhhLeRmPu4wPyJbicfICT5Pk+tvaUzxPrOybpd5fTkQc1X0JdiheUJDtnLeUOBLGzr3P
TlTd01+qIYKqUy2UsBi0bD/ajvUWuREfLnZYlyLXMLykz4JfBEABbn/L+ts9cvFqOGsNlP944ZXo
6vjSnJbq1rqnyI/uHOoY0eSF8mtVTs4ALZ9/8+vAsnKMCimJxQARAQABiQE8BBgBCAAmFiEEHAf5
pLOwt580eOkqFWZAd+pqZ0kFAlulXG4CGwwFCQPCZwAACgkQFWZAd+pqZ0lidQgAnnh6h3GontAZ
KJOtvI1Ybitv5LPGPmbGiOjQDJfGVGASDvD9fAmD4U8J5xFnR4/kLYO0OhS9HsSFv2eQTHC/8o2F
djH/1ft6IyvFn6DaLbitfMTkngJrwwo19MrKFiItM50EKbwYP6cRYAaiiqk6m3hhruhptzItc/v/
KiPYYGZulMTiN/b5uxIeKeIEzCaReJaVD+8n0BGI/VmNovgiBLg9XO8qjv4Xz7hiwTMKZAh6VC+y
wOPa32z1+jvKXtqJKIMBTooiHgTs38oWh5pcMZbDwWwJNqjQmeoeopjk1w7H/WD/Qdx6tTlBG0Zd
1ttP1WJTHrzjmCU0STxczxaigQ==`))
gpgRecipient1 = []byte("testkey1@key.org")
gpgRecipient2 = []byte("testkey2@key.org")
gpgPrivKey1 = []byte(b64Dec(`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`))
gpgPrivKey2 = []byte(b64Dec(`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`))
)

View File

@ -0,0 +1,132 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pkcs7
import (
"crypto"
"crypto/x509"
"github.com/containerd/containerd/images/encryption/config"
"github.com/containerd/containerd/images/encryption/keywrap"
"github.com/containerd/containerd/images/encryption/utils"
"github.com/fullsailor/pkcs7"
"github.com/pkg/errors"
)
type pkcs7KeyWrapper struct {
}
// NewKeyWrapper returns a new key wrapping interface using jwe
func NewKeyWrapper() keywrap.KeyWrapper {
return &pkcs7KeyWrapper{}
}
func (kw *pkcs7KeyWrapper) GetAnnotationID() string {
return "org.opencontainers.image.enc.keys.pkcs7"
}
// WrapKeys wraps the session key for recpients and encrypts the optsData, which
// describe the symmetric key used for encrypting the layer
func (kw *pkcs7KeyWrapper) WrapKeys(ec *config.EncryptConfig, optsData []byte) ([]byte, error) {
x509Certs, err := collectX509s(ec.Parameters["x509s"])
if err != nil {
return nil, err
}
// no recipients is not an error...
if len(x509Certs) == 0 {
return nil, nil
}
pkcs7.ContentEncryptionAlgorithm = pkcs7.EncryptionAlgorithmAES128GCM
return pkcs7.Encrypt(optsData, x509Certs)
}
func collectX509s(x509s [][]byte) ([]*x509.Certificate, error) {
if len(x509s) == 0 {
return nil, nil
}
var x509Certs []*x509.Certificate
for _, x509 := range x509s {
x509Cert, err := utils.ParseCertificate(x509, "PKCS7")
if err != nil {
return nil, err
}
x509Certs = append(x509Certs, x509Cert)
}
return x509Certs, nil
}
func (kw *pkcs7KeyWrapper) GetPrivateKeys(dcparameters map[string][][]byte) [][]byte {
return dcparameters["privkeys"]
}
func (kw *pkcs7KeyWrapper) getPrivateKeysPasswords(dcparameters map[string][][]byte) [][]byte {
return dcparameters["privkeys-passwords"]
}
// UnwrapKey unwraps the symmetric key with which the layer is encrypted
// This symmetric key is encrypted in the PKCS7 payload.
func (kw *pkcs7KeyWrapper) UnwrapKey(dc *config.DecryptConfig, pkcs7Packet []byte) ([]byte, error) {
privKeys := kw.GetPrivateKeys(dc.Parameters)
if len(privKeys) == 0 {
return nil, errors.New("no private keys found for PKCS7 decryption")
}
privKeysPasswords := kw.getPrivateKeysPasswords(dc.Parameters)
if len(privKeysPasswords) != len(privKeys) {
return nil, errors.New("private key password array length must be same as that of private keys")
}
x509Certs, err := collectX509s(dc.Parameters["x509s"])
if err != nil {
return nil, err
}
if len(x509Certs) == 0 {
return nil, errors.New("no x509 certificates found needed for PKCS7 decryption")
}
p7, err := pkcs7.Parse(pkcs7Packet)
if err != nil {
return nil, errors.Wrapf(err, "could not parse PKCS7 packet")
}
for idx, privKey := range privKeys {
key, err := utils.ParsePrivateKey(privKey, privKeysPasswords[idx], "PKCS7")
if err != nil {
return nil, err
}
for _, x509Cert := range x509Certs {
optsData, err := p7.Decrypt(x509Cert, crypto.PrivateKey(key))
if err != nil {
continue
}
return optsData, nil
}
}
return nil, errors.New("PKCS7: No suitable private key found for decryption")
}
// GetKeyIdsFromWrappedKeys converts the base64 encoded Packet to uint64 keyIds;
// We cannot do this with pkcs7
func (kw *pkcs7KeyWrapper) GetKeyIdsFromPacket(b64pkcs7Packets string) ([]uint64, error) {
return nil, nil
}
// GetRecipients converts the wrappedKeys to an array of recipients
// We cannot do this with pkcs7
func (kw *pkcs7KeyWrapper) GetRecipients(b64pkcs7Packets string) ([]string, error) {
return []string{"[pkcs7]"}, nil
}

View File

@ -0,0 +1,254 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pkcs7
import (
"crypto/x509"
"testing"
"github.com/containerd/containerd/images/encryption/config"
"github.com/containerd/containerd/images/encryption/utils"
)
var oneEmpty []byte
func createKeys() (*x509.Certificate, []byte, *x509.Certificate, []byte, error) {
caKey, caCert, err := utils.CreateTestCA()
if err != nil {
return nil, nil, nil, nil, err
}
pkcs7ClientPubKey, pkcs7ClientPrivKey, err := utils.CreateRSATestKey(2048, oneEmpty, true)
if err != nil {
return nil, nil, nil, nil, err
}
pkcs7ClientCert, err := utils.CertifyKey(pkcs7ClientPubKey, nil, caKey, caCert)
if err != nil {
return nil, nil, nil, nil, err
}
pkcs7ClientPubKey2, pkcs7ClientPrivKey2, err := utils.CreateRSATestKey(2048, oneEmpty, true)
if err != nil {
return nil, nil, nil, nil, err
}
pkcs7ClientCert2, err := utils.CertifyKey(pkcs7ClientPubKey2, nil, caKey, caCert)
if err != nil {
return nil, nil, nil, nil, err
}
return pkcs7ClientCert, pkcs7ClientPrivKey, pkcs7ClientCert2, pkcs7ClientPrivKey2, nil
}
func createValidPkcs7Ccs() ([]*config.CryptoConfig, error) {
pkcs7ClientCert, pkcs7ClientPrivKey, pkcs7ClientCert2, pkcs7ClientPrivKey2, err := createKeys()
if err != nil {
return nil, err
}
validPkcs7Ccs := []*config.CryptoConfig{
// Client key 1
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"x509s": {pkcs7ClientCert.Raw},
},
DecryptConfig: config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {pkcs7ClientPrivKey},
"privkeys-passwords": {oneEmpty},
"x509s": {pkcs7ClientCert.Raw},
},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {pkcs7ClientPrivKey},
"privkeys-passwords": {oneEmpty},
"x509s": {pkcs7ClientCert.Raw},
},
},
},
// Client key 2
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"x509s": {pkcs7ClientCert2.Raw},
},
DecryptConfig: config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {pkcs7ClientPrivKey2},
"privkeys-passwords": {oneEmpty},
"x509s": {pkcs7ClientCert2.Raw},
},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {pkcs7ClientPrivKey2},
"privkeys-passwords": {oneEmpty},
"x509s": {pkcs7ClientCert2.Raw},
},
},
},
// Client key 1 without enc private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"x509s": {pkcs7ClientCert.Raw},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {pkcs7ClientPrivKey},
"privkeys-passwords": {oneEmpty},
"x509s": {pkcs7ClientCert.Raw},
},
},
},
// Client key 2 without enc private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"x509s": {pkcs7ClientCert2.Raw},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {pkcs7ClientPrivKey2},
"privkeys-passwords": {oneEmpty},
"x509s": {pkcs7ClientCert2.Raw},
},
},
},
}
return validPkcs7Ccs, nil
}
func createInvalidPkcs7Ccs() ([]*config.CryptoConfig, error) {
pkcs7ClientCert, pkcs7ClientPrivKey, pkcs7ClientCert2, pkcs7ClientPrivKey2, err := createKeys()
if err != nil {
return nil, err
}
invalidPkcs7Ccs := []*config.CryptoConfig{
// Client key 1 public with client 2 private decrypt
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"x509s": {pkcs7ClientCert.Raw},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {pkcs7ClientPrivKey2},
"privkeys-passwords": {oneEmpty},
"x509s": {pkcs7ClientCert2.Raw},
},
},
},
// Client key 1 public with no private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"x509s": {pkcs7ClientCert.Raw},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{},
},
},
// Invalid Client key 1 private key
{
EncryptConfig: &config.EncryptConfig{
Parameters: map[string][][]byte{
"x509s": {pkcs7ClientPrivKey},
},
},
DecryptConfig: &config.DecryptConfig{
Parameters: map[string][][]byte{
"privkeys": {pkcs7ClientCert.Raw},
"privkeys-passwords": {oneEmpty},
"x509s": {pkcs7ClientCert.Raw},
},
},
},
}
return invalidPkcs7Ccs, nil
}
func TestKeyWrapPkcs7Success(t *testing.T) {
validPkcs7Ccs, err := createValidPkcs7Ccs()
if err != nil {
t.Fatal(err)
}
for _, cc := range validPkcs7Ccs {
kw := NewKeyWrapper()
data := []byte("This is some secret text")
wk, err := kw.WrapKeys(cc.EncryptConfig, data)
if err != nil {
t.Fatal(err)
}
ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
if err != nil {
t.Fatal(err)
}
if string(data) != string(ud) {
t.Fatal("Strings don't match")
}
}
}
func TestKeyWrapPkcs7Invalid(t *testing.T) {
invalidPkcs7Ccs, err := createInvalidPkcs7Ccs()
if err != nil {
t.Fatal(err)
}
for _, cc := range invalidPkcs7Ccs {
kw := NewKeyWrapper()
data := []byte("This is some secret text")
wk, err := kw.WrapKeys(cc.EncryptConfig, data)
if err != nil {
return
}
ud, err := kw.UnwrapKey(cc.DecryptConfig, wk)
if err != nil {
return
}
if string(data) != string(ud) {
return
}
t.Fatal("Successfully wrap for invalid crypto config")
}
}

View File

@ -0,0 +1,40 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package encryption
import (
"io"
)
type readerAtReader struct {
r io.ReaderAt
off int64
}
// ReaderFromReaderAt takes an io.ReaderAt and returns an io.Reader
func ReaderFromReaderAt(r io.ReaderAt) io.Reader {
return &readerAtReader{
r: r,
off: 0,
}
}
func (rar *readerAtReader) Read(p []byte) (n int, err error) {
n, err = rar.r.ReadAt(p, rar.off)
rar.off += int64(n)
return n, err
}

View File

@ -0,0 +1,166 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"time"
"github.com/pkg/errors"
)
// CreateRSAKey creates an RSA key
func CreateRSAKey(bits int) (*rsa.PrivateKey, error) {
key, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return nil, errors.Wrap(err, "rsa.GenerateKey failed")
}
return key, nil
}
// CreateRSATestKey creates an RSA key of the given size and returns
// the public and private key in PEM or DER format
func CreateRSATestKey(bits int, password []byte, pemencode bool) ([]byte, []byte, error) {
key, err := CreateRSAKey(bits)
if err != nil {
return nil, nil, err
}
pubData, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return nil, nil, errors.Wrap(err, "x509.MarshalPKIXPublicKey failed")
}
privData := x509.MarshalPKCS1PrivateKey(key)
// no more encoding needed for DER
if !pemencode {
return pubData, privData, nil
}
publicKey := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: pubData,
})
var block *pem.Block
typ := "RSA PRIVATE KEY"
if len(password) > 0 {
block, err = x509.EncryptPEMBlock(rand.Reader, typ, privData, password, x509.PEMCipherAES256)
if err != nil {
return nil, nil, errors.Wrap(err, "x509.EncryptPEMBlock failed")
}
} else {
block = &pem.Block{
Type: typ,
Bytes: privData,
}
}
privateKey := pem.EncodeToMemory(block)
return publicKey, privateKey, nil
}
// CreateECDSATestKey creates and elliptic curve key for the given curve and returns
// the public and private key in DER format
func CreateECDSATestKey(curve elliptic.Curve) ([]byte, []byte, error) {
key, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, errors.Wrapf(err, "ecdsa.GenerateKey failed")
}
pubData, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return nil, nil, errors.Wrapf(err, "x509.MarshalPKIXPublicKey failed")
}
privData, err := x509.MarshalECPrivateKey(key)
if err != nil {
return nil, nil, errors.Wrapf(err, "x509.MarshalECPrivateKey failed")
}
return pubData, privData, nil
}
// CreateTestCA creates a root CA for testing
func CreateTestCA() (*rsa.PrivateKey, *x509.Certificate, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, errors.Wrap(err, "rsa.GenerateKey failed")
}
ca := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "test-ca",
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
IsCA: true,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
caCert, err := certifyKey(&key.PublicKey, ca, key, ca)
return key, caCert, err
}
// CertifyKey certifies a public key using the given CA's private key and cert;
// The certificate template for the public key is optional
func CertifyKey(pubbytes []byte, template *x509.Certificate, caKey *rsa.PrivateKey, caCert *x509.Certificate) (*x509.Certificate, error) {
pubKey, err := ParsePublicKey(pubbytes, "CertifyKey")
if err != nil {
return nil, err
}
return certifyKey(pubKey, template, caKey, caCert)
}
func certifyKey(pub interface{}, template *x509.Certificate, caKey *rsa.PrivateKey, caCert *x509.Certificate) (*x509.Certificate, error) {
if template == nil {
template = &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "testkey",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
IsCA: false,
KeyUsage: x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
}
}
certDER, err := x509.CreateCertificate(rand.Reader, template, caCert, pub, caKey)
if err != nil {
return nil, errors.Wrap(err, "x509.CreateCertificate failed")
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, errors.Wrap(err, "x509.ParseCertificate failed")
}
return cert, nil
}

View File

@ -0,0 +1,221 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"strings"
"github.com/containerd/containerd/errdefs"
"github.com/pkg/errors"
"golang.org/x/crypto/openpgp"
json "gopkg.in/square/go-jose.v2"
)
// parseJWKPrivateKey parses the input byte array as a JWK and makes sure it's a private key
func parseJWKPrivateKey(privKey []byte, prefix string) (interface{}, error) {
jwk := json.JSONWebKey{}
err := jwk.UnmarshalJSON(privKey)
if err != nil {
return nil, errors.Wrapf(err, "%s: Could not parse input as JWK", prefix)
}
if jwk.IsPublic() {
return nil, fmt.Errorf("%s: JWK is not a private key", prefix)
}
return &jwk, nil
}
// parseJWKPublicKey parses the input byte array as a JWK
func parseJWKPublicKey(privKey []byte, prefix string) (interface{}, error) {
jwk := json.JSONWebKey{}
err := jwk.UnmarshalJSON(privKey)
if err != nil {
return nil, errors.Wrapf(err, "%s: Could not parse input as JWK", prefix)
}
if !jwk.IsPublic() {
return nil, fmt.Errorf("%s: JWK is not a public key", prefix)
}
return &jwk, nil
}
// IsPasswordError checks whether an error is related to a missing or wrong
// password
func IsPasswordError(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "password") &&
(strings.Contains(msg, "missing") || strings.Contains(msg, "wrong"))
}
// ParsePrivateKey tries to parse a private key in DER format first and
// PEM format after, returning an error if the parsing failed
func ParsePrivateKey(privKey, privKeyPassword []byte, prefix string) (interface{}, error) {
key, err := x509.ParsePKCS8PrivateKey(privKey)
if err != nil {
key, err = x509.ParsePKCS1PrivateKey(privKey)
if err != nil {
key, err = x509.ParseECPrivateKey(privKey)
}
}
if err != nil {
block, _ := pem.Decode(privKey)
if block != nil {
var der []byte
if x509.IsEncryptedPEMBlock(block) {
if privKeyPassword == nil {
return nil, errors.Wrapf(errdefs.ErrInvalidArgument, "%s: Missing password for encrypted private key", prefix)
}
der, err = x509.DecryptPEMBlock(block, privKeyPassword)
if err != nil {
return nil, errors.Wrapf(errdefs.ErrInvalidArgument, "%s: Wrong password: could not decrypt private key", prefix)
}
} else {
der = block.Bytes
}
key, err = x509.ParsePKCS8PrivateKey(der)
if err != nil {
key, err = x509.ParsePKCS1PrivateKey(der)
if err != nil {
return nil, errors.Wrapf(err, "%s: Could not parse private key", prefix)
}
}
} else {
key, err = parseJWKPrivateKey(privKey, prefix)
}
}
return key, err
}
// IsPrivateKey returns true in case the given byte array represents a private key
// It returns an error if for example the password is wrong
func IsPrivateKey(data []byte, password []byte) (bool, error) {
_, err := ParsePrivateKey(data, password, "")
return err == nil, err
}
// ParsePublicKey tries to parse a public key in DER format first and
// PEM format after, returning an error if the parsing failed
func ParsePublicKey(pubKey []byte, prefix string) (interface{}, error) {
key, err := x509.ParsePKIXPublicKey(pubKey)
if err != nil {
block, _ := pem.Decode(pubKey)
if block != nil {
key, err = x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, errors.Wrapf(err, "%s: Could not parse public key", prefix)
}
} else {
key, err = parseJWKPublicKey(pubKey, prefix)
}
}
return key, err
}
// IsPublicKey returns true in case the given byte array represents a public key
func IsPublicKey(data []byte) bool {
_, err := ParsePublicKey(data, "")
return err == nil
}
// ParseCertificate tries to parse a public key in DER format first and
// PEM format after, returning an error if the parsing failed
func ParseCertificate(certBytes []byte, prefix string) (*x509.Certificate, error) {
x509Cert, err := x509.ParseCertificate(certBytes)
if err != nil {
block, _ := pem.Decode(certBytes)
if block == nil {
return nil, fmt.Errorf("%s: Could not PEM decode x509 certificate", prefix)
}
x509Cert, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.Wrapf(err, "%s: Could not parse x509 certificate", prefix)
}
}
return x509Cert, err
}
// IsCertificate returns true in case the given byte array represents an x.509 certificate
func IsCertificate(data []byte) bool {
_, err := ParseCertificate(data, "")
return err == nil
}
// IsGPGPrivateKeyRing returns true in case the given byte array represents a GPG private key ring file
func IsGPGPrivateKeyRing(data []byte) bool {
r := bytes.NewBuffer(data)
_, err := openpgp.ReadKeyRing(r)
return err == nil
}
// SortDecryptionKeys parses a list of comma separated base64 entries and sorts the data into
// a map. Each entry in the list may be either a GPG private key ring, private key, or x.509
// certificate
func SortDecryptionKeys(b64ItemList string) (map[string][][]byte, error) {
dcparameters := make(map[string][][]byte)
for _, b64Item := range strings.Split(b64ItemList, ",") {
var password []byte
b64Data := strings.Split(b64Item, ":")
keyData, err := base64.StdEncoding.DecodeString(b64Data[0])
if err != nil {
return nil, errors.New("Could not base64 decode a passed decryption key")
}
if len(b64Data) == 2 {
password, err = base64.StdEncoding.DecodeString(b64Data[1])
if err != nil {
return nil, errors.New("Could not base64 decode a passed decryption key password")
}
}
var key string
isPrivKey, err := IsPrivateKey(keyData, password)
if IsPasswordError(err) {
return nil, err
}
if isPrivKey {
key = "privkeys"
if _, ok := dcparameters["privkeys-passwords"]; !ok {
dcparameters["privkeys-passwords"] = [][]byte{password}
} else {
dcparameters["privkeys-passwords"] = append(dcparameters["privkeys-passwords"], password)
}
} else if IsCertificate(keyData) {
key = "x509s"
} else if IsGPGPrivateKeyRing(keyData) {
key = "gpg-privatekeys"
}
if key != "" {
values := dcparameters[key]
if values == nil {
dcparameters[key] = [][]byte{keyData}
} else {
dcparameters[key] = append(dcparameters[key], keyData)
}
} else {
return nil, errors.New("Unknown decryption key type")
}
}
return dcparameters, nil
}

View File

@ -64,6 +64,9 @@ type DeleteOptions struct {
// DeleteOpt allows configuring a delete operation
type DeleteOpt func(context.Context, *DeleteOptions) error
// LayerFilter allows to select Layers by certain criteria
type LayerFilter func(desc ocispec.Descriptor) bool
// SynchronousDelete is used to indicate that an image deletion and removal of
// the image resources should occur synchronously before returning a result.
func SynchronousDelete() DeleteOpt {
@ -86,6 +89,14 @@ type Store interface {
Delete(ctx context.Context, name string, opts ...DeleteOpt) error
}
type cryptoOp int
const (
cryptoOpEncrypt cryptoOp = iota
cryptoOpDecrypt = iota
cryptoOpUnwrapOnly = iota
)
// TODO(stevvooe): Many of these functions make strong platform assumptions,
// which are untrue in a lot of cases. More refactoring must be done here to
// make this work in all cases.
@ -359,6 +370,7 @@ func Children(ctx context.Context, provider content.Provider, desc ocispec.Descr
descs = append(descs, index.Manifests...)
case MediaTypeDockerSchema2Layer, MediaTypeDockerSchema2LayerGzip,
MediaTypeDockerSchema2LayerEnc, MediaTypeDockerSchema2LayerGzipEnc,
MediaTypeDockerSchema2LayerForeign, MediaTypeDockerSchema2LayerForeignGzip,
MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig,
ocispec.MediaTypeImageLayer, ocispec.MediaTypeImageLayerGzip,
@ -396,7 +408,7 @@ func RootFS(ctx context.Context, provider content.Provider, configDesc ocispec.D
func IsCompressedDiff(ctx context.Context, mediaType string) (bool, error) {
switch mediaType {
case ocispec.MediaTypeImageLayer, MediaTypeDockerSchema2Layer:
case ocispec.MediaTypeImageLayerGzip, MediaTypeDockerSchema2LayerGzip:
case ocispec.MediaTypeImageLayerGzip, MediaTypeDockerSchema2LayerGzip, MediaTypeDockerSchema2LayerGzipEnc:
return true, nil
default:
// Still apply all generic media types *.tar[.+]gzip and *.tar
@ -408,3 +420,49 @@ func IsCompressedDiff(ctx context.Context, mediaType string) (bool, error) {
}
return false, nil
}
// GetImageLayerDescriptors gets the image layer Descriptors of an image; the array contains
// a list of Descriptors belonging to one platform followed by lists of other platforms
func GetImageLayerDescriptors(ctx context.Context, cs content.Store, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
var lis []ocispec.Descriptor
ds := platforms.DefaultSpec()
platform := &ds
switch desc.MediaType {
case MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex,
MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
children, err := Children(ctx, cs, desc)
if err != nil {
if errdefs.IsNotFound(err) {
return []ocispec.Descriptor{}, nil
}
return []ocispec.Descriptor{}, err
}
if desc.Platform != nil {
platform = desc.Platform
}
for _, child := range children {
var tmp []ocispec.Descriptor
if isDescriptorALayer(child) {
tdesc := child
tdesc.Platform = platform
tmp = append(tmp, tdesc)
} else {
tmp, err = GetImageLayerDescriptors(ctx, cs, child)
}
if err != nil {
return []ocispec.Descriptor{}, err
}
lis = append(lis, tmp...)
}
case MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig:
default:
return nil, errors.Wrapf(errdefs.ErrInvalidArgument, "GetImageLayerInfo: unhandled media type %s", desc.MediaType)
}
return lis, nil
}

View File

@ -22,8 +22,10 @@ package images
// here for clarity.
const (
MediaTypeDockerSchema2Layer = "application/vnd.docker.image.rootfs.diff.tar"
MediaTypeDockerSchema2LayerEnc = "application/vnd.docker.image.rootfs.diff.tar+enc"
MediaTypeDockerSchema2LayerForeign = "application/vnd.docker.image.rootfs.foreign.diff.tar"
MediaTypeDockerSchema2LayerGzip = "application/vnd.docker.image.rootfs.diff.tar.gzip"
MediaTypeDockerSchema2LayerGzipEnc = "application/vnd.docker.image.rootfs.diff.tar.gzip+enc"
MediaTypeDockerSchema2LayerForeignGzip = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
MediaTypeDockerSchema2Config = "application/vnd.docker.container.image.v1+json"
MediaTypeDockerSchema2Manifest = "application/vnd.docker.distribution.manifest.v2+json"

View File

@ -48,7 +48,8 @@ func MakeRefKey(ctx context.Context, desc ocispec.Descriptor) string {
case images.MediaTypeDockerSchema2Layer, images.MediaTypeDockerSchema2LayerGzip,
images.MediaTypeDockerSchema2LayerForeign, images.MediaTypeDockerSchema2LayerForeignGzip,
ocispec.MediaTypeImageLayer, ocispec.MediaTypeImageLayerGzip,
ocispec.MediaTypeImageLayerNonDistributable, ocispec.MediaTypeImageLayerNonDistributableGzip:
ocispec.MediaTypeImageLayerNonDistributable, ocispec.MediaTypeImageLayerNonDistributableGzip,
images.MediaTypeDockerSchema2LayerEnc, images.MediaTypeDockerSchema2LayerGzipEnc:
return "layer-" + desc.Digest.String()
case images.MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig:
return "config-" + desc.Digest.String()