Crypto library movement and changes to content helper interfaces
Signed-off-by: Derek McGowan <derek@mcgstyle.net>
This commit is contained in:
parent
bf8804c743
commit
dde436e65b
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -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"
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
244
pkg/encryption/encryption.go
Normal file
244
pkg/encryption/encryption.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package encryption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/errdefs"
|
||||||
|
"github.com/containerd/containerd/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
|
||||||
|
}
|
@ -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"
|
||||||
)
|
)
|
@ -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"
|
||||||
)
|
)
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
@ -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
|
@ -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"
|
@ -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{
|
@ -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"
|
||||||
)
|
)
|
@ -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
|
Loading…
Reference in New Issue
Block a user