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/images"
|
||||
"github.com/containerd/containerd/images/encryption"
|
||||
encconfig "github.com/containerd/containerd/images/encryption/config"
|
||||
encutils "github.com/containerd/containerd/images/encryption/utils"
|
||||
imgenc "github.com/containerd/containerd/images/encryption"
|
||||
"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"
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -261,9 +262,9 @@ func cryptImage(client *containerd.Client, ctx gocontext.Context, name, newName
|
||||
defer ls.Delete(ctx, l, leases.SynchronousDelete)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return image, err
|
||||
|
@ -20,8 +20,8 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/cmd/ctr/commands"
|
||||
"github.com/containerd/containerd/images"
|
||||
encconfig "github.com/containerd/containerd/images/encryption/config"
|
||||
imgenc "github.com/containerd/containerd/images/encryption"
|
||||
encconfig "github.com/containerd/containerd/pkg/encryption/config"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -77,7 +77,7 @@ var decryptCommand = cli.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
isEncrypted := images.HasEncryptedLayer(ctx, descs)
|
||||
isEncrypted := imgenc.HasEncryptedLayer(ctx, descs)
|
||||
if !isEncrypted {
|
||||
fmt.Printf("Nothing to decrypted.\n")
|
||||
return nil
|
||||
|
@ -20,7 +20,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"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/urfave/cli"
|
||||
)
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
"text/tabwriter"
|
||||
|
||||
"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/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...)
|
||||
}
|
||||
|
||||
// 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
|
||||
// is locked until the reference is available or returns an 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
|
||||
}
|
||||
|
||||
// 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
|
||||
// resolving `io.Seeker`, by detecting `io.ReaderAt`, or discarding
|
||||
// up to the given offset.
|
||||
|
@ -25,9 +25,10 @@ import (
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/images"
|
||||
encconfig "github.com/containerd/containerd/images/encryption/config"
|
||||
"github.com/containerd/containerd/images/encryption/utils"
|
||||
imgenc "github.com/containerd/containerd/images/encryption"
|
||||
"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"
|
||||
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)
|
||||
|
||||
// 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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -180,7 +181,7 @@ func TestImageEncryption(t *testing.T) {
|
||||
}
|
||||
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 {
|
||||
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
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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/images/encryption/blockcipher"
|
||||
"github.com/containerd/containerd/images/encryption/config"
|
||||
"github.com/containerd/containerd/images/encryption/keywrap"
|
||||
"github.com/containerd/containerd/images/encryption/keywrap/jwe"
|
||||
"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/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"
|
||||
)
|
||||
|
||||
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())
|
||||
type cryptoOp int
|
||||
|
||||
const (
|
||||
cryptoOpEncrypt cryptoOp = iota
|
||||
cryptoOpDecrypt = iota
|
||||
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
|
||||
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
|
||||
// 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 wrappedKeysMap
|
||||
return false
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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 (
|
||||
encLayerReader io.Reader
|
||||
err error
|
||||
optsData []byte
|
||||
size int64
|
||||
d digest.Digest
|
||||
err error
|
||||
)
|
||||
|
||||
if ec == nil {
|
||||
return nil, nil, errors.Wrapf(errdefs.ErrInvalidArgument, "EncryptConfig must not be nil")
|
||||
encLayerReader, annotations, err := encryption.EncryptLayer(cc.EncryptConfig, encryption.ReaderFromReaderAt(dataReader), desc)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
|
||||
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!
|
||||
}
|
||||
// were data touched ?
|
||||
if encLayerReader != nil {
|
||||
size = 0
|
||||
d = ""
|
||||
} else {
|
||||
size = desc.Size
|
||||
d = desc.Digest
|
||||
}
|
||||
|
||||
newAnnotations := make(map[string]string)
|
||||
newDesc := ocispec.Descriptor{
|
||||
Digest: d,
|
||||
Size: size,
|
||||
Platform: desc.Platform,
|
||||
Annotations: annotations,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
switch desc.MediaType {
|
||||
case images.MediaTypeDockerSchema2LayerGzip:
|
||||
newDesc.MediaType = images.MediaTypeDockerSchema2LayerGzipEnc
|
||||
case images.MediaTypeDockerSchema2Layer:
|
||||
newDesc.MediaType = images.MediaTypeDockerSchema2LayerEnc
|
||||
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)
|
||||
}
|
||||
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
|
||||
|
||||
return newDesc, encLayerReader, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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 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) {
|
||||
privKeyGiven := false
|
||||
for annotationsID, scheme := range keyWrapperAnnotations {
|
||||
b64Annotation := desc.Annotations[annotationsID]
|
||||
if b64Annotation != "" {
|
||||
keywrapper := GetKeyWrapper(scheme)
|
||||
// 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
|
||||
)
|
||||
|
||||
if len(keywrapper.GetPrivateKeys(dc.Parameters)) == 0 {
|
||||
continue
|
||||
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",
|
||||
}
|
||||
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 {
|
||||
// try next keywrap.KeyWrapper
|
||||
continue
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
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
|
||||
// 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 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 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 {
|
||||
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
|
||||
// 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
|
||||
// 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")
|
||||
}
|
||||
|
||||
encLayerReader, opts, err := lbch.Encrypt(plainLayerReader, typ)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
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)
|
||||
}
|
||||
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
|
||||
// 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
|
||||
}
|
||||
|
@ -64,9 +64,6 @@ type DeleteOptions struct {
|
||||
// DeleteOpt allows configuring a delete operation
|
||||
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
|
||||
// the image resources should occur synchronously before returning a result.
|
||||
func SynchronousDelete() DeleteOpt {
|
||||
@ -89,14 +86,6 @@ type Store interface {
|
||||
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,
|
||||
// which are untrue in a lot of cases. More refactoring must be done here to
|
||||
// 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) {
|
||||
switch mediaType {
|
||||
case ocispec.MediaTypeImageLayer, MediaTypeDockerSchema2Layer:
|
||||
case ocispec.MediaTypeImageLayerGzip, MediaTypeDockerSchema2LayerGzip, MediaTypeDockerSchema2LayerGzipEnc:
|
||||
case ocispec.MediaTypeImageLayerGzip, MediaTypeDockerSchema2LayerGzip:
|
||||
return true, nil
|
||||
default:
|
||||
// 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 {
|
||||
var tmp []ocispec.Descriptor
|
||||
|
||||
if isDescriptorALayer(child) {
|
||||
switch child.MediaType {
|
||||
case MediaTypeDockerSchema2LayerGzip, MediaTypeDockerSchema2Layer,
|
||||
ocispec.MediaTypeImageLayerGzip, ocispec.MediaTypeImageLayer,
|
||||
MediaTypeDockerSchema2LayerGzipEnc, MediaTypeDockerSchema2LayerEnc:
|
||||
tdesc := child
|
||||
tdesc.Platform = platform
|
||||
tmp = append(tmp, tdesc)
|
||||
} else {
|
||||
default:
|
||||
tmp, err = GetImageLayerDescriptors(ctx, cs, child)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
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"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/images/encryption/config"
|
||||
"github.com/containerd/containerd/pkg/encryption/config"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
@ -19,9 +19,9 @@ package jwe
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
|
||||
"github.com/containerd/containerd/images/encryption/config"
|
||||
"github.com/containerd/containerd/images/encryption/keywrap"
|
||||
"github.com/containerd/containerd/images/encryption/utils"
|
||||
"github.com/containerd/containerd/pkg/encryption/config"
|
||||
"github.com/containerd/containerd/pkg/encryption/keywrap"
|
||||
"github.com/containerd/containerd/pkg/encryption/utils"
|
||||
"github.com/pkg/errors"
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
)
|
@ -20,8 +20,8 @@ import (
|
||||
"crypto/elliptic"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/images/encryption/config"
|
||||
"github.com/containerd/containerd/images/encryption/utils"
|
||||
"github.com/containerd/containerd/pkg/encryption/config"
|
||||
"github.com/containerd/containerd/pkg/encryption/utils"
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
@ -17,7 +17,7 @@
|
||||
package keywrap
|
||||
|
||||
import (
|
||||
"github.com/containerd/containerd/images/encryption/config"
|
||||
"github.com/containerd/containerd/pkg/encryption/config"
|
||||
)
|
||||
|
||||
// KeyWrapper is the interface used for wrapping keys using
|
@ -29,8 +29,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/images/encryption/config"
|
||||
"github.com/containerd/containerd/images/encryption/keywrap"
|
||||
"github.com/containerd/containerd/pkg/encryption/config"
|
||||
"github.com/containerd/containerd/pkg/encryption/keywrap"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/openpgp"
|
||||
"golang.org/x/crypto/openpgp/packet"
|
@ -19,7 +19,7 @@ package pgp
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/images/encryption/config"
|
||||
"github.com/containerd/containerd/pkg/encryption/config"
|
||||
)
|
||||
|
||||
var validGpgCcs = []*config.CryptoConfig{
|
@ -20,9 +20,9 @@ import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/containerd/containerd/images/encryption/config"
|
||||
"github.com/containerd/containerd/images/encryption/keywrap"
|
||||
"github.com/containerd/containerd/images/encryption/utils"
|
||||
"github.com/containerd/containerd/pkg/encryption/config"
|
||||
"github.com/containerd/containerd/pkg/encryption/keywrap"
|
||||
"github.com/containerd/containerd/pkg/encryption/utils"
|
||||
"github.com/fullsailor/pkcs7"
|
||||
"github.com/pkg/errors"
|
||||
)
|
@ -20,8 +20,8 @@ import (
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/images/encryption/config"
|
||||
"github.com/containerd/containerd/images/encryption/utils"
|
||||
"github.com/containerd/containerd/pkg/encryption/config"
|
||||
"github.com/containerd/containerd/pkg/encryption/utils"
|
||||
)
|
||||
|
||||
var oneEmpty []byte
|
Loading…
Reference in New Issue
Block a user