Remove encryption code from containerd core
We are separating out the encryption code and have designed a few new interfaces and APIs for processing content streams. This keep the core clean of encryption code but enables not only encryption but support of multiple content types ( custom media types ). Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
This commit is contained in:
@@ -131,22 +131,6 @@ var (
|
||||
Usage: "add a device to a 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
|
||||
|
||||
@@ -1,455 +0,0 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/images"
|
||||
imgenc "github.com/containerd/containerd/images/encryption"
|
||||
"github.com/containerd/containerd/pkg/encryption"
|
||||
encconfig "github.com/containerd/containerd/pkg/encryption/config"
|
||||
encutils "github.com/containerd/containerd/pkg/encryption/utils"
|
||||
"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 {
|
||||
|
||||
idx := strings.Index(recipient, ":")
|
||||
if idx < 0 {
|
||||
return nil, nil, nil, errors.New("Invalid recipient format")
|
||||
}
|
||||
|
||||
protocol := recipient[:idx]
|
||||
value := recipient[idx+1:]
|
||||
|
||||
switch protocol {
|
||||
case "pgp":
|
||||
gpgRecipients = append(gpgRecipients, []byte(value))
|
||||
case "jwe":
|
||||
tmp, err := ioutil.ReadFile(value)
|
||||
if err != nil {
|
||||
return nil, nil, nil, errors.Wrap(err, "Unable to read file")
|
||||
}
|
||||
if !encutils.IsPublicKey(tmp) {
|
||||
return nil, nil, nil, errors.New("File provided is not a public key")
|
||||
}
|
||||
pubkeys = append(pubkeys, tmp)
|
||||
|
||||
case "pkcs7":
|
||||
tmp, err := ioutil.ReadFile(value)
|
||||
if err != nil {
|
||||
return nil, nil, nil, errors.Wrap(err, "Unable to read file")
|
||||
}
|
||||
if !encutils.IsCertificate(tmp) {
|
||||
return nil, nil, nil, errors.New("File provided is not an x509 cert")
|
||||
}
|
||||
x509s = append(x509s, tmp)
|
||||
|
||||
default:
|
||||
return nil, nil, nil, errors.New("Provided protocol not recognized")
|
||||
}
|
||||
}
|
||||
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) (gpgPrivKeys [][]byte, gpgPrivKeysPwds [][]byte, err error) {
|
||||
gpgClient, err := createGPGClient(context)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var gpgVault encryption.GPGVault
|
||||
if len(gpgSecretKeyRingFiles) > 0 {
|
||||
gpgVault = encryption.NewGPGVault()
|
||||
err = gpgVault.AddSecretKeyRingDataArray(gpgSecretKeyRingFiles)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
return encryption.GPGGetPrivateKey(descs, gpgClient, gpgVault, mustFindKey)
|
||||
}
|
||||
|
||||
func createLayerFilter(client *containerd.Client, ctx gocontext.Context, desc ocispec.Descriptor, layers []int32, platformList []ocispec.Platform) (imgenc.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
|
||||
)
|
||||
|
||||
ctx, done, err := client.WithLease(ctx)
|
||||
if err != nil {
|
||||
return images.Image{}, err
|
||||
}
|
||||
defer done(ctx)
|
||||
|
||||
if encrypt {
|
||||
newSpec, modified, err = imgenc.EncryptImage(ctx, client.ContentStore(), image.Target, cc, lf)
|
||||
} else {
|
||||
newSpec, modified, err = imgenc.DecryptImage(ctx, client.ContentStore(), 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
|
||||
}
|
||||
|
||||
// CreateDecryptCryptoConfig creates the CryptoConfig object that contains the necessary
|
||||
// information to perform decryption from command line options and possibly
|
||||
// LayerInfos describing the image and helping us to query for the PGP decryption keys
|
||||
func CreateDecryptCryptoConfig(context *cli.Context, descs []ocispec.Descriptor) (encconfig.CryptoConfig, error) {
|
||||
ccs := []encconfig.CryptoConfig{}
|
||||
|
||||
// x509 cert is needed for PKCS7 decryption
|
||||
_, _, x509s, err := processRecipientKeys(context.StringSlice("dec-recipient"))
|
||||
if err != nil {
|
||||
return encconfig.CryptoConfig{}, err
|
||||
}
|
||||
|
||||
gpgSecretKeyRingFiles, gpgSecretKeyPasswords, privKeys, privKeysPasswords, err := processPrivateKeyFiles(context.StringSlice("key"))
|
||||
if err != nil {
|
||||
return encconfig.CryptoConfig{}, 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
|
||||
gpgPrivKeys, gpgPrivKeyPasswords, err := getGPGPrivateKeys(context, gpgSecretKeyRingFiles, descs, true)
|
||||
if err != nil {
|
||||
return encconfig.CryptoConfig{}, err
|
||||
}
|
||||
|
||||
gpgCc, err := encconfig.DecryptWithGpgPrivKeys(gpgPrivKeys, gpgPrivKeyPasswords)
|
||||
if err != nil {
|
||||
return encconfig.CryptoConfig{}, err
|
||||
}
|
||||
ccs = append(ccs, gpgCc)
|
||||
|
||||
} else if len(gpgSecretKeyRingFiles) > 0 {
|
||||
gpgCc, err := encconfig.DecryptWithGpgPrivKeys(gpgSecretKeyRingFiles, gpgSecretKeyPasswords)
|
||||
if err != nil {
|
||||
return encconfig.CryptoConfig{}, err
|
||||
}
|
||||
ccs = append(ccs, gpgCc)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
x509sCc, err := encconfig.DecryptWithX509s(x509s)
|
||||
if err != nil {
|
||||
return encconfig.CryptoConfig{}, err
|
||||
}
|
||||
ccs = append(ccs, x509sCc)
|
||||
|
||||
privKeysCc, err := encconfig.DecryptWithPrivKeys(privKeys, privKeysPasswords)
|
||||
if err != nil {
|
||||
return encconfig.CryptoConfig{}, err
|
||||
}
|
||||
ccs = append(ccs, privKeysCc)
|
||||
|
||||
return encconfig.CombineCryptoConfigs(ccs), 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
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
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"
|
||||
imgenc "github.com/containerd/containerd/images/encryption"
|
||||
"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 := imgenc.HasEncryptedLayer(ctx, descs)
|
||||
if !isEncrypted {
|
||||
fmt.Printf("Nothing to decrypted.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
cc, err := CreateDecryptCryptoConfig(context, descs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = decryptImage(client, ctx, local, newName, &cc, layers32, context.StringSlice("platform"))
|
||||
|
||||
return err
|
||||
},
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
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/pkg/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.
|
||||
|
||||
Recipients are declared with the protocol prefix as follows:
|
||||
- pgp:<email-address>
|
||||
- jwe:<public-key-file-path>
|
||||
- pkcs7:<x509-file-path>
|
||||
`,
|
||||
Flags: append(append(commands.RegistryFlags, cli.StringSliceFlag{
|
||||
Name: "recipient",
|
||||
Usage: "Recipient of the image is the person who can decrypt it in the form specified above (i.e. jwe:/path/to/key)",
|
||||
}, 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"))
|
||||
|
||||
gpgRecipients, pubKeys, x509s, err := processRecipientKeys(recipients)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, descs, err := getImageLayerInfos(client, ctx, local, layers32, context.StringSlice("platform"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encryptCcs := []encconfig.CryptoConfig{}
|
||||
_, err = createGPGClient(context)
|
||||
gpgInstalled := err == nil
|
||||
|
||||
if len(gpgRecipients) > 0 && gpgInstalled {
|
||||
gpgClient, err := createGPGClient(context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gpgPubRingFile, err := gpgClient.ReadGPGPubRingFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gpgCc, err := encconfig.EncryptWithGpg(gpgRecipients, gpgPubRingFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encryptCcs = append(encryptCcs, gpgCc)
|
||||
|
||||
}
|
||||
|
||||
// Create Encryption Crypto Config
|
||||
pkcs7Cc, err := encconfig.EncryptWithPkcs7(x509s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encryptCcs = append(encryptCcs, pkcs7Cc)
|
||||
|
||||
jweCc, err := encconfig.EncryptWithJwe(pubKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encryptCcs = append(encryptCcs, jweCc)
|
||||
|
||||
cc := encconfig.CombineCryptoConfigs(encryptCcs)
|
||||
|
||||
// Create Decryption CryptoConfig for use in adding recipients to
|
||||
// existing image if decryptable.
|
||||
decryptCc, err := CreateDecryptCryptoConfig(context, descs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cc.EncryptConfig.AttachDecryptConfig(decryptCc.DecryptConfig)
|
||||
|
||||
_, err = encryptImage(client, ctx, local, newName, &cc, layers32, context.StringSlice("platform"))
|
||||
|
||||
return err
|
||||
},
|
||||
}
|
||||
@@ -48,9 +48,6 @@ var Command = cli.Command{
|
||||
removeCommand,
|
||||
tagCommand,
|
||||
setLabelsCommand,
|
||||
encryptCommand,
|
||||
decryptCommand,
|
||||
layerinfoCommand,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
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/pkg/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
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user