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:
parent
30c3443947
commit
bf8804c743
@ -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
|
||||
|
419
cmd/ctr/commands/images/crypt_utils.go
Normal file
419
cmd/ctr/commands/images/crypt_utils.go
Normal 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
|
||||
}
|
100
cmd/ctr/commands/images/decrypt.go
Normal file
100
cmd/ctr/commands/images/decrypt.go
Normal 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
|
||||
},
|
||||
}
|
148
cmd/ctr/commands/images/encrypt.go
Normal file
148
cmd/ctr/commands/images/encrypt.go
Normal 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
|
||||
},
|
||||
}
|
@ -48,6 +48,9 @@ var Command = cli.Command{
|
||||
removeCommand,
|
||||
tagCommand,
|
||||
setLabelsCommand,
|
||||
encryptCommand,
|
||||
decryptCommand,
|
||||
layerinfoCommand,
|
||||
},
|
||||
}
|
||||
|
||||
|
118
cmd/ctr/commands/images/layerinfo.go
Normal file
118
cmd/ctr/commands/images/layerinfo.go
Normal 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
27
cmd/ctr/commands/utils.go
Normal 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
|
||||
}
|
@ -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
120
docs/encryption.md
Normal 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
215
image_enc_test.go
Normal 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
451
images/enc_utils.go
Normal 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
|
||||
}
|
114
images/encryption/blockcipher/blockcipher.go
Normal file
114
images/encryption/blockcipher/blockcipher.go
Normal 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
|
||||
}
|
194
images/encryption/blockcipher/blockcipher_siv.go
Normal file
194
images/encryption/blockcipher/blockcipher_siv.go
Normal 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
|
||||
}
|
163
images/encryption/blockcipher/blockcipher_siv_test.go
Normal file
163
images/encryption/blockcipher/blockcipher_siv_test.go
Normal 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")
|
||||
}
|
||||
}
|
115
images/encryption/blockcipher/blockcipher_test.go
Normal file
115
images/encryption/blockcipher/blockcipher_test.go
Normal 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")
|
||||
}
|
||||
}
|
40
images/encryption/config/config.go
Normal file
40
images/encryption/config/config.go
Normal 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
|
||||
}
|
244
images/encryption/encryption.go
Normal file
244
images/encryption/encryption.go
Normal 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
|
||||
}
|
137
images/encryption/encryption_test.go
Normal file
137
images/encryption/encryption_test.go
Normal 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
453
images/encryption/gpg.go
Normal 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)
|
||||
}
|
100
images/encryption/gpgvault.go
Normal file
100
images/encryption/gpgvault.go
Normal 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
|
||||
}
|
132
images/encryption/keywrap/jwe/keywrapper_jwe.go
Normal file
132
images/encryption/keywrap/jwe/keywrapper_jwe.go
Normal 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
|
||||
}
|
345
images/encryption/keywrap/jwe/keywrapper_jwe_test.go
Normal file
345
images/encryption/keywrap/jwe/keywrapper_jwe_test.go
Normal 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")
|
||||
}
|
||||
}
|
40
images/encryption/keywrap/keywrap.go
Normal file
40
images/encryption/keywrap/keywrap.go
Normal 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)
|
||||
}
|
270
images/encryption/keywrap/pgp/keywrapper_gpg.go
Normal file
270
images/encryption/keywrap/pgp/keywrapper_gpg.go
Normal 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
|
||||
}
|
190
images/encryption/keywrap/pgp/keywrapper_gpg_test.go
Normal file
190
images/encryption/keywrap/pgp/keywrapper_gpg_test.go
Normal 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")
|
||||
}
|
||||
}
|
87
images/encryption/keywrap/pgp/testingkeys_test.go
Normal file
87
images/encryption/keywrap/pgp/testingkeys_test.go
Normal 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(`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
|
||||
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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`))
|
||||
)
|
132
images/encryption/keywrap/pkcs7/keywrapper_pkcs7.go
Normal file
132
images/encryption/keywrap/pkcs7/keywrapper_pkcs7.go
Normal 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
|
||||
}
|
254
images/encryption/keywrap/pkcs7/keywrapper_pkcs7_test.go
Normal file
254
images/encryption/keywrap/pkcs7/keywrapper_pkcs7_test.go
Normal 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")
|
||||
}
|
||||
}
|
40
images/encryption/reader.go
Normal file
40
images/encryption/reader.go
Normal 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
|
||||
}
|
166
images/encryption/utils/testing.go
Normal file
166
images/encryption/utils/testing.go
Normal 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
|
||||
}
|
221
images/encryption/utils/utils.go
Normal file
221
images/encryption/utils/utils.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user