Crypto library movement and changes to content helper interfaces

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
This commit is contained in:
Derek McGowan 2019-07-16 14:36:42 -07:00 committed by Brandon Lum
parent bf8804c743
commit dde436e65b
29 changed files with 713 additions and 710 deletions

View File

@ -28,10 +28,11 @@ import (
"github.com/containerd/containerd" "github.com/containerd/containerd"
"github.com/containerd/containerd/images" "github.com/containerd/containerd/images"
"github.com/containerd/containerd/images/encryption" imgenc "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/leases"
"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" "github.com/containerd/containerd/platforms"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@ -209,7 +210,7 @@ func getGPGPrivateKeys(context *cli.Context, gpgSecretKeyRingFiles [][]byte, des
return encryption.GPGGetPrivateKey(descs, gpgClient, gpgVault, mustFindKey, dcparameters) 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) { 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) alldescs, err := images.GetImageLayerDescriptors(ctx, client.ContentStore(), desc)
if err != nil { if err != nil {
return nil, err return nil, err
@ -261,9 +262,9 @@ func cryptImage(client *containerd.Client, ctx gocontext.Context, name, newName
defer ls.Delete(ctx, l, leases.SynchronousDelete) defer ls.Delete(ctx, l, leases.SynchronousDelete)
if encrypt { if encrypt {
newSpec, modified, err = images.EncryptImage(ctx, client.ContentStore(), ls, l, image.Target, cc, lf) newSpec, modified, err = imgenc.EncryptImage(ctx, client.ContentStore(), ls, l, image.Target, cc, lf)
} else { } else {
newSpec, modified, err = images.DecryptImage(ctx, client.ContentStore(), ls, l, image.Target, cc, lf) newSpec, modified, err = imgenc.DecryptImage(ctx, client.ContentStore(), ls, l, image.Target, cc, lf)
} }
if err != nil { if err != nil {
return image, err return image, err

View File

@ -20,8 +20,8 @@ import (
"fmt" "fmt"
"github.com/containerd/containerd/cmd/ctr/commands" "github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/images" imgenc "github.com/containerd/containerd/images/encryption"
encconfig "github.com/containerd/containerd/images/encryption/config" encconfig "github.com/containerd/containerd/pkg/encryption/config"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -77,7 +77,7 @@ var decryptCommand = cli.Command{
return err return err
} }
isEncrypted := images.HasEncryptedLayer(ctx, descs) isEncrypted := imgenc.HasEncryptedLayer(ctx, descs)
if !isEncrypted { if !isEncrypted {
fmt.Printf("Nothing to decrypted.\n") fmt.Printf("Nothing to decrypted.\n")
return nil return nil

View File

@ -20,7 +20,7 @@ import (
"fmt" "fmt"
"github.com/containerd/containerd/cmd/ctr/commands" "github.com/containerd/containerd/cmd/ctr/commands"
encconfig "github.com/containerd/containerd/images/encryption/config" encconfig "github.com/containerd/containerd/pkg/encryption/config"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli" "github.com/urfave/cli"
) )

View File

@ -24,7 +24,7 @@ import (
"text/tabwriter" "text/tabwriter"
"github.com/containerd/containerd/cmd/ctr/commands" "github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/images/encryption" "github.com/containerd/containerd/pkg/encryption"
"github.com/containerd/containerd/platforms" "github.com/containerd/containerd/platforms"
"github.com/pkg/errors" "github.com/pkg/errors"

View File

@ -87,46 +87,6 @@ func WriteBlob(ctx context.Context, cs Ingester, ref string, r io.Reader, desc o
return Copy(ctx, cw, r, desc.Size, desc.Digest, opts...) 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 // OpenWriter opens a new writer for the given reference, retrying if the writer
// is locked until the reference is available or returns an error. // is locked until the reference is available or returns an error.
func OpenWriter(ctx context.Context, cs Ingester, opts ...WriterOpt) (Writer, error) { func OpenWriter(ctx context.Context, cs Ingester, opts ...WriterOpt) (Writer, error) {
@ -209,6 +169,28 @@ func CopyReaderAt(cw Writer, ra ReaderAt, n int64) error {
return err return err
} }
// CopyReader copies to a writer from a given reader, returning
// the number of bytes copied.
// Note: if the writer has a non-zero offset, the total number
// of bytes read may be greater than those copied if the reader
// is not an io.Seeker.
// This copy does not commit the writer.
func CopyReader(cw Writer, r io.Reader) (int64, error) {
ws, err := cw.Status()
if err != nil {
return 0, errors.Wrap(err, "failed to get status")
}
if ws.Offset > 0 {
r, err = seekReader(r, ws.Offset, 0)
if err != nil {
return 0, errors.Wrapf(err, "unable to resume write to %v", ws.Ref)
}
}
return copyWithBuffer(cw, r)
}
// seekReader attempts to seek the reader to the given offset, either by // seekReader attempts to seek the reader to the given offset, either by
// resolving `io.Seeker`, by detecting `io.ReaderAt`, or discarding // resolving `io.Seeker`, by detecting `io.ReaderAt`, or discarding
// up to the given offset. // up to the given offset.

View File

@ -25,9 +25,10 @@ import (
"github.com/containerd/containerd/content" "github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images" "github.com/containerd/containerd/images"
encconfig "github.com/containerd/containerd/images/encryption/config" imgenc "github.com/containerd/containerd/images/encryption"
"github.com/containerd/containerd/images/encryption/utils"
"github.com/containerd/containerd/leases" "github.com/containerd/containerd/leases"
encconfig "github.com/containerd/containerd/pkg/encryption/config"
"github.com/containerd/containerd/pkg/encryption/utils"
"github.com/containerd/containerd/platforms" "github.com/containerd/containerd/platforms"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
) )
@ -142,7 +143,7 @@ func TestImageEncryption(t *testing.T) {
defer ls.Delete(ctx, l, leases.SynchronousDelete) defer ls.Delete(ctx, l, leases.SynchronousDelete)
// Perform encryption of image // Perform encryption of image
encSpec, modified, err := images.EncryptImage(ctx, client.ContentStore(), ls, l, image.Target, cc, lf) encSpec, modified, err := imgenc.EncryptImage(ctx, client.ContentStore(), ls, l, image.Target, cc, lf)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -180,7 +181,7 @@ func TestImageEncryption(t *testing.T) {
} }
defer ls.Delete(ctx, l, leases.SynchronousDelete) defer ls.Delete(ctx, l, leases.SynchronousDelete)
decSpec, modified, err := images.DecryptImage(ctx, client.ContentStore(), ls, l, encSpec, cc, lf) decSpec, modified, err := imgenc.DecryptImage(ctx, client.ContentStore(), ls, l, encSpec, cc, lf)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

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

View File

@ -17,228 +17,461 @@
package encryption package encryption
import ( import (
"encoding/base64" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"strings" "math/rand"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/pkg/encryption"
encconfig "github.com/containerd/containerd/pkg/encryption/config"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images/encryption/blockcipher" "github.com/containerd/containerd/leases"
"github.com/containerd/containerd/images/encryption/config" "github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/images/encryption/keywrap" digest "github.com/opencontainers/go-digest"
"github.com/containerd/containerd/images/encryption/keywrap/jwe" specs "github.com/opencontainers/image-spec/specs-go"
"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" "github.com/pkg/errors"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
) )
func init() { type cryptoOp int
keyWrappers = make(map[string]keywrap.KeyWrapper)
keyWrapperAnnotations = make(map[string]string) const (
RegisterKeyWrapper("pgp", pgp.NewKeyWrapper()) cryptoOpEncrypt cryptoOp = iota
RegisterKeyWrapper("jwe", jwe.NewKeyWrapper()) cryptoOpDecrypt = iota
RegisterKeyWrapper("pkcs7", pkcs7.NewKeyWrapper()) cryptoOpUnwrapOnly = iota
)
// LayerFilter allows to select Layers by certain criteria
type LayerFilter func(desc ocispec.Descriptor) bool
// IsEncryptedDiff returns true if mediaType is a known encrypted media type.
func IsEncryptedDiff(ctx context.Context, mediaType string) bool {
switch mediaType {
case images.MediaTypeDockerSchema2LayerGzipEnc, images.MediaTypeDockerSchema2LayerEnc:
return true
}
return false
} }
var keyWrappers map[string]keywrap.KeyWrapper // HasEncryptedLayer returns true if any LayerInfo indicates that the layer is encrypted
var keyWrapperAnnotations map[string]string func HasEncryptedLayer(ctx context.Context, layerInfos []ocispec.Descriptor) bool {
for i := 0; i < len(layerInfos); i++ {
// RegisterKeyWrapper allows to register key wrappers by their encryption scheme if IsEncryptedDiff(ctx, layerInfos[i].MediaType) {
func RegisterKeyWrapper(scheme string, iface keywrap.KeyWrapper) { return true
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 return false
} }
// EncryptLayer encrypts the layer by running one encryptor after the other // encryptLayer encrypts the layer using the CryptoConfig and creates a new OCI Descriptor.
func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, desc ocispec.Descriptor) (io.Reader, map[string]string, error) { // 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 ( var (
encLayerReader io.Reader size int64
err error d digest.Digest
optsData []byte err error
) )
if ec == nil { encLayerReader, annotations, err := encryption.EncryptLayer(cc.EncryptConfig, encryption.ReaderFromReaderAt(dataReader), desc)
return nil, nil, errors.Wrapf(errdefs.ErrInvalidArgument, "EncryptConfig must not be nil") if err != nil {
return ocispec.Descriptor{}, nil, err
} }
for annotationsID := range keyWrapperAnnotations { // were data touched ?
annotation := desc.Annotations[annotationsID] if encLayerReader != nil {
if annotation != "" { size = 0
optsData, err = decryptLayerKeyOptsData(&ec.DecryptConfig, desc) d = ""
if err != nil { } else {
return nil, nil, err size = desc.Size
} d = desc.Digest
// already encrypted!
}
} }
newAnnotations := make(map[string]string) newDesc := ocispec.Descriptor{
Digest: d,
Size: size,
Platform: desc.Platform,
Annotations: annotations,
}
for annotationsID, scheme := range keyWrapperAnnotations { switch desc.MediaType {
b64Annotations := desc.Annotations[annotationsID] case images.MediaTypeDockerSchema2LayerGzip:
if b64Annotations == "" && optsData == nil { newDesc.MediaType = images.MediaTypeDockerSchema2LayerGzipEnc
encLayerReader, optsData, err = commonEncryptLayer(encOrPlainLayerReader, desc.Digest, blockcipher.AESSIVCMAC512) case images.MediaTypeDockerSchema2Layer:
if err != nil { newDesc.MediaType = images.MediaTypeDockerSchema2LayerEnc
return nil, nil, err case images.MediaTypeDockerSchema2LayerGzipEnc:
} newDesc.MediaType = images.MediaTypeDockerSchema2LayerGzipEnc
} case images.MediaTypeDockerSchema2LayerEnc:
keywrapper := GetKeyWrapper(scheme) newDesc.MediaType = images.MediaTypeDockerSchema2LayerEnc
b64Annotations, err = preWrapKeys(keywrapper, ec, b64Annotations, optsData)
if err != nil { // TODO: Mediatypes to be added in ocispec
return nil, nil, err case ocispec.MediaTypeImageLayerGzip:
} newDesc.MediaType = images.MediaTypeDockerSchema2LayerGzipEnc
if b64Annotations != "" { case ocispec.MediaTypeImageLayer:
newAnnotations[annotationsID] = b64Annotations newDesc.MediaType = images.MediaTypeDockerSchema2LayerEnc
}
default:
return ocispec.Descriptor{}, nil, errors.Errorf("Encryption: unsupporter layer MediaType: %s\n", desc.MediaType)
} }
if len(newAnnotations) == 0 {
err = errors.Errorf("no encryptor found to handle encryption") return newDesc, encLayerReader, nil
}
// 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 // decryptLayer decrypts the layer using the CryptoConfig and creates a new OCI Descriptor.
// annotation data // The caller is expected to store the returned plain data and OCI Descriptor
func preWrapKeys(keywrapper keywrap.KeyWrapper, ec *config.EncryptConfig, b64Annotations string, optsData []byte) (string, error) { func decryptLayer(cc *encconfig.CryptoConfig, dataReader content.ReaderAt, desc ocispec.Descriptor, unwrapOnly bool) (ocispec.Descriptor, io.Reader, error) {
newAnnotation, err := keywrapper.WrapKeys(ec, optsData) resultReader, d, err := encryption.DecryptLayer(cc.DecryptConfig, encryption.ReaderFromReaderAt(dataReader), desc, unwrapOnly)
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 { if err != nil || unwrapOnly {
return nil, "", err return ocispec.Descriptor{}, nil, err
} }
return commonDecryptLayer(encLayerReader, optsData) newDesc := ocispec.Descriptor{
Digest: d,
Size: 0,
Platform: desc.Platform,
}
switch desc.MediaType {
case images.MediaTypeDockerSchema2LayerGzipEnc:
newDesc.MediaType = images.MediaTypeDockerSchema2LayerGzip
case images.MediaTypeDockerSchema2LayerEnc:
newDesc.MediaType = images.MediaTypeDockerSchema2Layer
default:
return ocispec.Descriptor{}, nil, errors.Errorf("Decryption: unsupporter layer MediaType: %s\n", desc.MediaType)
}
return newDesc, resultReader, nil
} }
func decryptLayerKeyOptsData(dc *config.DecryptConfig, desc ocispec.Descriptor) ([]byte, error) { // cryptLayer handles the changes due to encryption or decryption of a layer
privKeyGiven := false 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) {
for annotationsID, scheme := range keyWrapperAnnotations { var (
b64Annotation := desc.Annotations[annotationsID] resultReader io.Reader
if b64Annotation != "" { newDesc ocispec.Descriptor
keywrapper := GetKeyWrapper(scheme) )
if len(keywrapper.GetPrivateKeys(dc.Parameters)) == 0 { dataReader, err := cs.ReaderAt(ctx, desc)
continue 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",
} }
privKeyGiven = true
optsData, err := preUnwrapKey(keywrapper, dc, b64Annotation) }
// 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 = ingestReader(ctx, cs, ref, resultReader)
if err != nil { if err != nil {
// try next keywrap.KeyWrapper return ocispec.Descriptor{}, err
continue
} }
if optsData == nil { }
// try next keywrap.KeyWrapper }
continue return newDesc, err
}
func ingestReader(ctx context.Context, cs content.Ingester, ref string, r io.Reader) (digest.Digest, int64, error) {
cw, err := content.OpenWriter(ctx, cs, content.WithRef(ref))
if err != nil {
return "", 0, errors.Wrap(err, "failed to open writer")
}
defer cw.Close()
if _, err := content.CopyReader(cw, r); err != nil {
return "", 0, errors.Wrap(err, "copy failed")
}
st, err := cw.Status()
if err != nil {
return "", 0, errors.Wrap(err, "failed to get state")
}
if err := cw.Commit(ctx, st.Offset, ""); err != nil {
if !errdefs.IsAlreadyExists(err) {
return "", 0, errors.Wrapf(err, "failed commit on ref %q", ref)
}
}
return cw.Digest(), st.Offset, nil
}
// 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 := images.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 images.MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig:
config = child
case images.MediaTypeDockerSchema2LayerGzip, images.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)
} }
return optsData, nil case images.MediaTypeDockerSchema2LayerGzipEnc, images.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 images.MediaTypeDockerSchema2LayerForeign, images.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 !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 if modified && len(newLayers) > 0 {
// of the given keywrapper with it and returns the result in case the Unwrap functions newManifest := ocispec.Manifest{
// does not return an error. If all attempts fail, an error is returned. Versioned: specs.Versioned{
func preUnwrapKey(keywrapper keywrap.KeyWrapper, dc *config.DecryptConfig, b64Annotations string) ([]byte, error) { SchemaVersion: 2,
if b64Annotations == "" { },
return nil, nil Config: config,
} Layers: newLayers,
for _, b64Annotation := range strings.Split(b64Annotations, ",") { }
annotation, err := base64.StdEncoding.DecodeString(b64Annotation)
mb, err := json.MarshalIndent(newManifest, "", " ")
if err != nil { if err != nil {
return nil, errors.New("could not base64 decode the annotation") return ocispec.Descriptor{}, false, errors.Wrap(err, "failed to marshal image")
} }
optsData, err := keywrapper.UnwrapKey(dc, annotation)
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 { if err != nil {
continue return ocispec.Descriptor{}, false, errors.Wrap(err, "failed to marshal index")
} }
return optsData, nil
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 nil, errors.New("no suitable key found for decrypting layer key")
return desc, false, nil
} }
// commonEncryptLayer is a function to encrypt the plain layer using a new random // cryptImage is the dispatcher to encrypt/decrypt an image; it accepts either an OCI descriptor
// symmetric key and return the LayerBlockCipherHandler's JSON in string form for // representing a manifest list or a single manifest
// later use during decryption 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) {
func commonEncryptLayer(plainLayerReader io.Reader, d digest.Digest, typ blockcipher.LayerCipherType) (io.Reader, []byte, error) { if cc == nil {
lbch, err := blockcipher.NewLayerBlockCipherHandler() return ocispec.Descriptor{}, false, errors.Wrapf(errdefs.ErrInvalidArgument, "CryptoConfig must not be nil")
if err != nil {
return nil, nil, err
} }
switch desc.MediaType {
encLayerReader, opts, err := lbch.Encrypt(plainLayerReader, typ) case ocispec.MediaTypeImageIndex, images.MediaTypeDockerSchema2ManifestList:
if err != nil { return cryptManifestList(ctx, cs, ls, l, desc, cc, lf, cryptoOp)
return nil, nil, err case ocispec.MediaTypeImageManifest, images.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)
} }
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 // EncryptImage encrypts an image; it accepts either an OCI descriptor representing a manifest list or a single manifest
// by passing along the optsData 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) {
func commonDecryptLayer(encLayerReader io.Reader, optsData []byte) (io.Reader, digest.Digest, error) { return cryptImage(ctx, cs, ls, l, desc, cc, lf, cryptoOpEncrypt)
opts := blockcipher.LayerBlockCipherOptions{} }
err := json.Unmarshal(optsData, &opts)
if err != nil { // DecryptImage decrypts an image; it accepts either an OCI descriptor representing a manifest list or a single manifest
return nil, "", errors.Wrapf(err, "could not JSON unmarshal optsData") 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)
}
lbch, err := blockcipher.NewLayerBlockCipherHandler()
if err != nil { // CheckAuthorization checks whether a user has the right keys to be allowed to access an image (every layer)
return nil, "", err // 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 {
plainLayerReader, opts, err := lbch.Decrypt(encLayerReader, opts) cc := encconfig.CryptoConfig{
if err != nil { DecryptConfig: dc,
return nil, "", err }
} lf := func(desc ocispec.Descriptor) bool {
return true
return plainLayerReader, opts.Digest, nil }
// We shouldn't need to create any objects in CheckAuthorization, so no lease required.
_, _, err := cryptImage(ctx, cs, nil, leases.Lease{}, desc, &cc, lf, cryptoOpUnwrapOnly)
if err != nil {
return errors.Wrapf(err, "you are not authorized to use this image")
}
return nil
} }

View File

@ -64,9 +64,6 @@ type DeleteOptions struct {
// DeleteOpt allows configuring a delete operation // DeleteOpt allows configuring a delete operation
type DeleteOpt func(context.Context, *DeleteOptions) error 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 // SynchronousDelete is used to indicate that an image deletion and removal of
// the image resources should occur synchronously before returning a result. // the image resources should occur synchronously before returning a result.
func SynchronousDelete() DeleteOpt { func SynchronousDelete() DeleteOpt {
@ -89,14 +86,6 @@ type Store interface {
Delete(ctx context.Context, name string, opts ...DeleteOpt) error 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, // 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 // which are untrue in a lot of cases. More refactoring must be done here to
// make this work in all cases. // make this work in all cases.
@ -408,7 +397,7 @@ func RootFS(ctx context.Context, provider content.Provider, configDesc ocispec.D
func IsCompressedDiff(ctx context.Context, mediaType string) (bool, error) { func IsCompressedDiff(ctx context.Context, mediaType string) (bool, error) {
switch mediaType { switch mediaType {
case ocispec.MediaTypeImageLayer, MediaTypeDockerSchema2Layer: case ocispec.MediaTypeImageLayer, MediaTypeDockerSchema2Layer:
case ocispec.MediaTypeImageLayerGzip, MediaTypeDockerSchema2LayerGzip, MediaTypeDockerSchema2LayerGzipEnc: case ocispec.MediaTypeImageLayerGzip, MediaTypeDockerSchema2LayerGzip:
return true, nil return true, nil
default: default:
// Still apply all generic media types *.tar[.+]gzip and *.tar // Still apply all generic media types *.tar[.+]gzip and *.tar
@ -447,13 +436,17 @@ func GetImageLayerDescriptors(ctx context.Context, cs content.Store, desc ocispe
for _, child := range children { for _, child := range children {
var tmp []ocispec.Descriptor var tmp []ocispec.Descriptor
if isDescriptorALayer(child) { switch child.MediaType {
case MediaTypeDockerSchema2LayerGzip, MediaTypeDockerSchema2Layer,
ocispec.MediaTypeImageLayerGzip, ocispec.MediaTypeImageLayer,
MediaTypeDockerSchema2LayerGzipEnc, MediaTypeDockerSchema2LayerEnc:
tdesc := child tdesc := child
tdesc.Platform = platform tdesc.Platform = platform
tmp = append(tmp, tdesc) tmp = append(tmp, tdesc)
} else { default:
tmp, err = GetImageLayerDescriptors(ctx, cs, child) tmp, err = GetImageLayerDescriptors(ctx, cs, child)
} }
if err != nil { if err != nil {
return []ocispec.Descriptor{}, err return []ocispec.Descriptor{}, err
} }

View File

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

View File

@ -21,7 +21,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/containerd/containerd/images/encryption/config" "github.com/containerd/containerd/pkg/encryption/config"
digest "github.com/opencontainers/go-digest" digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
) )

View File

@ -19,9 +19,9 @@ package jwe
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"github.com/containerd/containerd/images/encryption/config" "github.com/containerd/containerd/pkg/encryption/config"
"github.com/containerd/containerd/images/encryption/keywrap" "github.com/containerd/containerd/pkg/encryption/keywrap"
"github.com/containerd/containerd/images/encryption/utils" "github.com/containerd/containerd/pkg/encryption/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
jose "gopkg.in/square/go-jose.v2" jose "gopkg.in/square/go-jose.v2"
) )

View File

@ -20,8 +20,8 @@ import (
"crypto/elliptic" "crypto/elliptic"
"testing" "testing"
"github.com/containerd/containerd/images/encryption/config" "github.com/containerd/containerd/pkg/encryption/config"
"github.com/containerd/containerd/images/encryption/utils" "github.com/containerd/containerd/pkg/encryption/utils"
jose "gopkg.in/square/go-jose.v2" jose "gopkg.in/square/go-jose.v2"
) )

View File

@ -17,7 +17,7 @@
package keywrap package keywrap
import ( import (
"github.com/containerd/containerd/images/encryption/config" "github.com/containerd/containerd/pkg/encryption/config"
) )
// KeyWrapper is the interface used for wrapping keys using // KeyWrapper is the interface used for wrapping keys using

View File

@ -29,8 +29,8 @@ import (
"strings" "strings"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images/encryption/config" "github.com/containerd/containerd/pkg/encryption/config"
"github.com/containerd/containerd/images/encryption/keywrap" "github.com/containerd/containerd/pkg/encryption/keywrap"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet" "golang.org/x/crypto/openpgp/packet"

View File

@ -19,7 +19,7 @@ package pgp
import ( import (
"testing" "testing"
"github.com/containerd/containerd/images/encryption/config" "github.com/containerd/containerd/pkg/encryption/config"
) )
var validGpgCcs = []*config.CryptoConfig{ var validGpgCcs = []*config.CryptoConfig{

View File

@ -20,9 +20,9 @@ import (
"crypto" "crypto"
"crypto/x509" "crypto/x509"
"github.com/containerd/containerd/images/encryption/config" "github.com/containerd/containerd/pkg/encryption/config"
"github.com/containerd/containerd/images/encryption/keywrap" "github.com/containerd/containerd/pkg/encryption/keywrap"
"github.com/containerd/containerd/images/encryption/utils" "github.com/containerd/containerd/pkg/encryption/utils"
"github.com/fullsailor/pkcs7" "github.com/fullsailor/pkcs7"
"github.com/pkg/errors" "github.com/pkg/errors"
) )

View File

@ -20,8 +20,8 @@ import (
"crypto/x509" "crypto/x509"
"testing" "testing"
"github.com/containerd/containerd/images/encryption/config" "github.com/containerd/containerd/pkg/encryption/config"
"github.com/containerd/containerd/images/encryption/utils" "github.com/containerd/containerd/pkg/encryption/utils"
) )
var oneEmpty []byte var oneEmpty []byte