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
d digest.Digest
err error err error
optsData []byte
) )
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:
newDesc.MediaType = images.MediaTypeDockerSchema2LayerEnc
// TODO: Mediatypes to be added in ocispec
case ocispec.MediaTypeImageLayerGzip:
newDesc.MediaType = images.MediaTypeDockerSchema2LayerGzipEnc
case ocispec.MediaTypeImageLayer:
newDesc.MediaType = images.MediaTypeDockerSchema2LayerEnc
default:
return ocispec.Descriptor{}, nil, errors.Errorf("Encryption: unsupporter layer MediaType: %s\n", desc.MediaType)
} }
}
keywrapper := GetKeyWrapper(scheme) return newDesc, encLayerReader, nil
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 // 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
}
privKeyGiven = true
optsData, err := preUnwrapKey(keywrapper, dc, b64Annotation)
if err != nil { if err != nil {
// try next keywrap.KeyWrapper return ocispec.Descriptor{}, err
continue
} }
if optsData == nil { defer dataReader.Close()
// try next keywrap.KeyWrapper
continue if cryptoOp == cryptoOpEncrypt {
newDesc, resultReader, err = encryptLayer(cc, dataReader, desc)
} else {
newDesc, resultReader, err = decryptLayer(cc, dataReader, desc, cryptoOp == cryptoOpUnwrapOnly)
} }
return optsData, nil 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 = ingestReader(ctx, cs, ref, resultReader)
if err != nil {
return ocispec.Descriptor{}, err
} }
} }
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") return newDesc, err
} }
// preUnwrapKey decodes the comma separated base64 strings and calls the Unwrap function func ingestReader(ctx context.Context, cs content.Ingester, ref string, r io.Reader) (digest.Digest, int64, error) {
// of the given keywrapper with it and returns the result in case the Unwrap functions cw, err := content.OpenWriter(ctx, cs, content.WithRef(ref))
// 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 { if err != nil {
return nil, errors.New("could not base64 decode the annotation") return "", 0, errors.Wrap(err, "failed to open writer")
} }
optsData, err := keywrapper.UnwrapKey(dc, annotation) 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 { if err != nil {
continue return "", 0, errors.Wrap(err, "failed to get state")
} }
return optsData, nil
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 nil, errors.New("no suitable key found for decrypting layer key") }
return cw.Digest(), st.Offset, nil
} }
// commonEncryptLayer is a function to encrypt the plain layer using a new random // Encrypt or decrypt all the Children of a given descriptor
// symmetric key and return the LayerBlockCipherHandler's JSON in string form for 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) {
// later use during decryption children, err := images.Children(ctx, cs, desc)
func commonEncryptLayer(plainLayerReader io.Reader, d digest.Digest, typ blockcipher.LayerCipherType) (io.Reader, []byte, error) {
lbch, err := blockcipher.NewLayerBlockCipherHandler()
if err != nil { if err != nil {
return nil, nil, err if errdefs.IsNotFound(err) {
return desc, false, nil
}
return ocispec.Descriptor{}, false, err
} }
encLayerReader, opts, err := lbch.Encrypt(plainLayerReader, typ) var newLayers []ocispec.Descriptor
if err != nil { var config ocispec.Descriptor
return nil, nil, err modified := false
}
opts.Digest = d
optsData, err := json.Marshal(opts) 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 { if err != nil {
return nil, nil, errors.Wrapf(err, "could not JSON marshal opts") return ocispec.Descriptor{}, false, err
}
modified = true
newLayers = append(newLayers, nl)
} else {
newLayers = append(newLayers, child)
}
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)
}
} }
return encLayerReader, optsData, err 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
} }
// commonDecryptLayer decrypts an encrypted layer previously encrypted with commonEncryptLayer // cryptManifest encrypts or decrypts the children of a top level manifest
// by passing along the optsData 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) {
func commonDecryptLayer(encLayerReader io.Reader, optsData []byte) (io.Reader, digest.Digest, error) { p, err := content.ReadBlob(ctx, cs, desc)
opts := blockcipher.LayerBlockCipherOptions{}
err := json.Unmarshal(optsData, &opts)
if err != nil { if err != nil {
return nil, "", errors.Wrapf(err, "could not JSON unmarshal optsData") return ocispec.Descriptor{}, false, err
} }
var manifest ocispec.Manifest
lbch, err := blockcipher.NewLayerBlockCipherHandler() if err := json.Unmarshal(p, &manifest); err != nil {
if err != nil { return ocispec.Descriptor{}, false, err
return nil, "", err
} }
platform := platforms.DefaultSpec()
plainLayerReader, opts, err := lbch.Decrypt(encLayerReader, opts) newDesc, modified, err := cryptChildren(ctx, cs, ls, l, desc, cc, lf, cryptoOp, &platform)
if err != nil { if err != nil || cryptoOp == cryptoOpUnwrapOnly {
return nil, "", err return ocispec.Descriptor{}, false, err
} }
return newDesc, modified, nil
return plainLayerReader, opts.Digest, 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, images.MediaTypeDockerSchema2ManifestList:
return cryptManifestList(ctx, cs, ls, l, desc, cc, lf, cryptoOp)
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)
}
}
// 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

@ -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