From fbfe8c760f6fca0e41667240c62634320b275b44 Mon Sep 17 00:00:00 2001 From: Brandon Lum Date: Tue, 13 Aug 2019 16:17:45 -0400 Subject: [PATCH] Added encryption support in copy.Image Signed-off-by: Brandon Lum --- copy/copy.go | 197 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 186 insertions(+), 11 deletions(-) diff --git a/copy/copy.go b/copy/copy.go index 671252d57a..a29d536347 100644 --- a/copy/copy.go +++ b/copy/copy.go @@ -17,6 +17,10 @@ import ( "github.com/containers/image/image" "github.com/containers/image/manifest" "github.com/containers/image/pkg/blobinfocache" + "github.com/containers/ocicrypt" + encconfig "github.com/containers/ocicrypt/config" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/containers/image/pkg/compression" "github.com/containers/image/signature" "github.com/containers/image/transports" @@ -102,6 +106,8 @@ type copier struct { progress chan types.ProgressProperties blobInfoCache types.BlobInfoCache copyInParallel bool + decryptConfig *encconfig.DecryptConfig + encryptConfig *encconfig.EncryptConfig } // imageCopier tracks state specific to a single image (possibly an item of a manifest list) @@ -112,6 +118,10 @@ type imageCopier struct { diffIDsAreNeeded bool canModifyManifest bool canSubstituteBlobs bool + checkAuthorization bool + decryptConfig *encconfig.DecryptConfig + encryptConfig *encconfig.EncryptConfig + encryptLayers *[]int } // Options allows supplying non-default configuration modifying the behavior of CopyImage. @@ -125,6 +135,13 @@ type Options struct { Progress chan types.ProgressProperties // Reported to when ProgressInterval has arrived for a single artifact+offset. // manifest MIME type of image set by user. "" is default and means use the autodetection to the the manifest MIME type ForceManifestMIMEType string + CheckAuthorization bool + // If non-nil indicates that image should be encrypted. + EncryptConfig *encconfig.EncryptConfig + // EncryptLayers represents the list of layers to encrypt. + // If nil, don't encrypt any layers + // If non-nil and len==0, denotes encrypt all layers. + EncryptLayers *[]int } // Image copies image from srcRef to destRef, using policyContext to validate @@ -246,6 +263,10 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli return nil, errors.Wrapf(err, "Error initializing image from source %s", transports.ImageName(c.rawSource.Reference())) } + if err = src.SupportsEncryption(ctx); err != nil && options.EncryptLayers != nil { + return nil, errors.Wrap(err, "Encryption requested but not supported by source image type") + } + // If the destination is a digested reference, make a note of that, determine what digest value we're // expecting, and check that the source manifest matches it. destIsDigestedReference := false @@ -288,12 +309,29 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli } } + // Set up encryption structs + var ( + //ec *encconfig.EncryptConfig + dc *encconfig.DecryptConfig + ) + if options.SourceCtx.CryptoConfig != nil { + //ec = options.SourceCtx.CryptoConfig.EncryptConfig + dc = options.SourceCtx.CryptoConfig.DecryptConfig + } + + encConfig := options.EncryptConfig + encLayers := options.EncryptLayers + ic := imageCopier{ c: c, manifestUpdates: &types.ManifestUpdateOptions{InformationOnly: types.ManifestUpdateInformation{Destination: c.dest}}, src: src, // diffIDsAreNeeded is computed later - canModifyManifest: len(sigs) == 0 && !destIsDigestedReference, + canModifyManifest: len(sigs) == 0 && !destIsDigestedReference, + checkAuthorization: options.CheckAuthorization, + decryptConfig: dc, + encryptConfig: encConfig, + encryptLayers: encLayers, } // Ensure _this_ copy sees exactly the intended data when either processing a signed image or signing it. // This may be too conservative, but for now, better safe than sorry, _especially_ on the SignBy path: @@ -476,7 +514,7 @@ func (ic *imageCopier) copyLayers(ctx context.Context) error { } data := make([]copyLayerData, numLayers) - copyLayerHelper := func(index int, srcLayer types.BlobInfo, pool *mpb.Progress) { + copyLayerHelper := func(index int, srcLayer types.BlobInfo, toEncrypt bool, pool *mpb.Progress) { defer copySemaphore.Release(1) defer copyGroup.Done() cld := copyLayerData{} @@ -491,18 +529,32 @@ func (ic *imageCopier) copyLayers(ctx context.Context) error { logrus.Debugf("Skipping foreign layer %q copy to %s", cld.destInfo.Digest, ic.c.dest.Reference().Transport().Name()) } } else { - cld.destInfo, cld.diffID, cld.err = ic.copyLayer(ctx, srcLayer, pool) + cld.destInfo, cld.diffID, cld.err = ic.copyLayer(ctx, srcLayer, toEncrypt, pool) } data[index] = cld } + // Create layer Encryption map + encLayerBitmap := map[int]bool{} + var encryptAll bool + encryptLayers := ic.encryptLayers != nil + if ic.encryptLayers != nil { + encryptAll = len(*ic.encryptLayers) == 0 + totalLayers := len(srcInfos) + for _, l := range *ic.encryptLayers { + // if layer is negative, it is reverse indexed. + encLayerBitmap[(totalLayers+l)%totalLayers] = true + } + } + func() { // A scope for defer progressPool, progressCleanup := ic.c.newProgressPool(ctx) defer progressCleanup() for i, srcLayer := range srcInfos { copySemaphore.Acquire(ctx, 1) - go copyLayerHelper(i, srcLayer, progressPool) + toEncrypt := encryptLayers && (encryptAll || encLayerBitmap[i]) + go copyLayerHelper(i, srcLayer, toEncrypt, progressPool) } // Wait for all layers to be copied @@ -650,7 +702,7 @@ func (c *copier) copyConfig(ctx context.Context, src types.Image) error { progressPool, progressCleanup := c.newProgressPool(ctx) defer progressCleanup() bar := c.createProgressBar(progressPool, srcInfo, "config", "done") - destInfo, err := c.copyBlobFromStream(ctx, bytes.NewReader(configBlob), srcInfo, nil, false, true, bar) + destInfo, err := c.copyBlobFromStream(ctx, bytes.NewReader(configBlob), srcInfo, nil, false, true, false, bar) if err != nil { return types.BlobInfo{}, err } @@ -676,7 +728,32 @@ type diffIDResult struct { // copyLayer copies a layer with srcInfo (with known Digest and possibly known Size) in src to dest, perhaps compressing it if canCompress, // and returns a complete blobInfo of the copied layer, and a value for LayerDiffIDs if diffIDIsNeeded -func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, pool *mpb.Progress) (types.BlobInfo, digest.Digest, error) { +func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, toEncrypt bool, pool *mpb.Progress) (types.BlobInfo, digest.Digest, error) { + // This is used to authenticate the encrypted images that are already + // present in the image cached. For every layer we will try to unwrap + // the symmetric key with the provided private keys. If we fail, we will + // not allow the image to be provisioned. + if ic.checkAuthorization { + if srcInfo.MediaType == manifest.DockerV2Schema2LayerGzipEncMediaType || + srcInfo.MediaType == manifest.DockerV2Schema2LayerEncMediaType { + + if ic.decryptConfig == nil { + return types.BlobInfo{}, "", errors.New("Necessary DecryptParameters not present") + } + + dc := ic.decryptConfig + + newDesc := ocispec.Descriptor{ + Annotations: srcInfo.Annotations, + } + + _, _, err := ocicrypt.DecryptLayer(dc, nil, newDesc, true) + if err != nil { + return types.BlobInfo{}, "", errors.Wrapf(err, "Image authentication failed for the digest %+v", srcInfo.Digest) + } + } + } + cachedDiffID := ic.c.blobInfoCache.UncompressedDigest(srcInfo.Digest) // May be "" diffIDIsNeeded := ic.diffIDsAreNeeded && cachedDiffID == "" @@ -703,7 +780,7 @@ func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, po bar := ic.c.createProgressBar(pool, srcInfo, "blob", "done") - blobInfo, diffIDChan, err := ic.copyLayerFromStream(ctx, srcStream, types.BlobInfo{Digest: srcInfo.Digest, Size: srcBlobSize}, diffIDIsNeeded, bar) + blobInfo, diffIDChan, err := ic.copyLayerFromStream(ctx, srcStream, types.BlobInfo{Digest: srcInfo.Digest, Size: srcBlobSize, MediaType: srcInfo.MediaType, Annotations: srcInfo.Annotations}, diffIDIsNeeded, toEncrypt, bar) if err != nil { return types.BlobInfo{}, "", err } @@ -734,7 +811,7 @@ func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, po // perhaps compressing the stream if canCompress, // and returns a complete blobInfo of the copied blob and perhaps a <-chan diffIDResult if diffIDIsNeeded, to be read by the caller. func (ic *imageCopier) copyLayerFromStream(ctx context.Context, srcStream io.Reader, srcInfo types.BlobInfo, - diffIDIsNeeded bool, bar *mpb.Bar) (types.BlobInfo, <-chan diffIDResult, error) { + diffIDIsNeeded bool, toEncrypt bool, bar *mpb.Bar) (types.BlobInfo, <-chan diffIDResult, error) { var getDiffIDRecorder func(compression.DecompressorFunc) io.Writer // = nil var diffIDChan chan diffIDResult @@ -758,7 +835,10 @@ func (ic *imageCopier) copyLayerFromStream(ctx context.Context, srcStream io.Rea return pipeWriter } } - blobInfo, err := ic.c.copyBlobFromStream(ctx, srcStream, srcInfo, getDiffIDRecorder, ic.canModifyManifest, false, bar) // Sets err to nil on success + ic.c.decryptConfig = ic.decryptConfig + ic.c.encryptConfig = ic.encryptConfig + + blobInfo, err := ic.c.copyBlobFromStream(ctx, srcStream, srcInfo, getDiffIDRecorder, ic.canModifyManifest, false, toEncrypt, bar) // Sets err to nil on success return blobInfo, diffIDChan, err // We need the defer … pipeWriter.CloseWithError() to happen HERE so that the caller can block on reading from diffIDChan } @@ -795,7 +875,7 @@ func computeDiffID(stream io.Reader, decompressor compression.DecompressorFunc) // and returns a complete blobInfo of the copied blob. func (c *copier) copyBlobFromStream(ctx context.Context, srcStream io.Reader, srcInfo types.BlobInfo, getOriginalLayerCopyWriter func(decompressor compression.DecompressorFunc) io.Writer, - canModifyBlob bool, isConfig bool, bar *mpb.Bar) (types.BlobInfo, error) { + canModifyBlob bool, isConfig bool, toEncrypt bool, bar *mpb.Bar) (types.BlobInfo, error) { // The copying happens through a pipeline of connected io.Readers. // === Input: srcStream @@ -805,7 +885,42 @@ func (c *copier) copyBlobFromStream(ctx context.Context, srcStream io.Reader, sr // Note that for this check we don't use the stronger "validationSucceeded" indicator, because // dest.PutBlob may detect that the layer already exists, in which case we don't // read stream to the end, and validation does not happen. - digestingReader, err := newDigestingReader(srcStream, srcInfo.Digest, false) + + var decrypted bool + var err error + if srcInfo.MediaType == manifest.DockerV2Schema2LayerGzipEncMediaType || + srcInfo.MediaType == manifest.DockerV2Schema2LayerEncMediaType { + + if c.decryptConfig == nil { + return types.BlobInfo{}, errors.New("Necessary DecryptParameters not present") + } + + dc := c.decryptConfig + + newDesc := ocispec.Descriptor{ + Annotations: srcInfo.Annotations, + } + + var d digest.Digest + srcStream, d, err = ocicrypt.DecryptLayer(dc, srcStream, newDesc, false) + if err != nil { + return types.BlobInfo{}, errors.Wrapf(err, "Error decrypting layer %s", srcInfo.Digest) + } + + srcInfo.Digest = d + srcInfo.Size = -1 + switch srcInfo.MediaType { + case manifest.DockerV2Schema2LayerGzipEncMediaType: + srcInfo.MediaType = ocispec.MediaTypeImageLayerGzip + case manifest.DockerV2Schema2LayerEncMediaType: + srcInfo.MediaType = ocispec.MediaTypeImageLayer + } + decrypted = true + } + + skipDigestValidation := srcInfo.Digest == "" + + digestingReader, err := newDigestingReader(srcStream, srcInfo.Digest, skipDigestValidation) if err != nil { return types.BlobInfo{}, errors.Wrapf(err, "Error preparing to verify blob %s", srcInfo.Digest) } @@ -859,6 +974,49 @@ func (c *copier) copyBlobFromStream(ctx context.Context, srcStream io.Reader, sr compressionOperation = types.PreserveOriginal inputInfo = srcInfo } + if decrypted { + inputInfo.MediaType = srcInfo.MediaType + } + + // Perform image encryption for valid mediatypes if encryptConfig provided + var ( + encryptMediaType string + encrypted bool + finalizer ocicrypt.EncryptLayerFinalizer + ) + if toEncrypt { + switch srcInfo.MediaType { + case manifest.DockerV2Schema2LayerMediaType, ocispec.MediaTypeImageLayerGzip: + encryptMediaType = manifest.DockerV2Schema2LayerGzipEncMediaType + case ocispec.MediaTypeImageLayer: + encryptMediaType = manifest.DockerV2Schema2LayerEncMediaType + } + + if encryptMediaType != "" && c.encryptConfig != nil { + var annotations map[string]string + if !decrypted { + annotations = srcInfo.Annotations + } + desc := ocispec.Descriptor{ + MediaType: srcInfo.MediaType, + Digest: srcInfo.Digest, + Size: srcInfo.Size, + Annotations: annotations, + } + + s, fin, err := ocicrypt.EncryptLayer(c.encryptConfig, destStream, desc) + if err != nil { + return types.BlobInfo{}, errors.Wrapf(err, "Image encryption failed for the digest %+v", srcInfo.Digest) + } + + destStream = s + finalizer = fin + inputInfo.Digest = "" + inputInfo.Size = -1 + inputInfo.MediaType = encryptMediaType + encrypted = true + } + } // === Report progress using the c.progress channel, if required. if c.progress != nil && c.progressInterval > 0 { @@ -877,6 +1035,23 @@ func (c *copier) copyBlobFromStream(ctx context.Context, srcStream io.Reader, sr return types.BlobInfo{}, errors.Wrap(err, "Error writing blob") } + if decrypted { + uploadedInfo.MediaType = srcInfo.MediaType + } + if encrypted { + encryptAnnotations, err := finalizer() + if err != nil { + return types.BlobInfo{}, errors.Wrap(err, "Unable to finalize encryption") + } + uploadedInfo.MediaType = encryptMediaType + if uploadedInfo.Annotations == nil { + uploadedInfo.Annotations = map[string]string{} + } + for k, v := range encryptAnnotations { + uploadedInfo.Annotations[k] = v + } + } + // This is fairly horrible: the writer from getOriginalLayerCopyWriter wants to consumer // all of the input (to compute DiffIDs), even if dest.PutBlob does not need it. // So, read everything from originalLayerReader, which will cause the rest to be