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:

committed by
Brandon Lum

parent
30c3443947
commit
bf8804c743
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
|
||||
}
|
Reference in New Issue
Block a user