From 52df4c8cef5a6227e7f6c65c8a5b29b4eeb3ef84 Mon Sep 17 00:00:00 2001 From: Stefan Berger Date: Wed, 3 Mar 2021 15:59:04 -0500 Subject: [PATCH] Add computation and verification of previous layers' hashes This patch adds the computation of previous layers accumulated hashes on the encryption side and writes this computed hash into the private options of a layer. The private options will be encrypted then. On the decryption side it also performs the computations and, if the private options contain the previous layers' hash, which may not be the case for older images but will be the case for newer ones, it compares the expected hash against the computed one and errors if they don't match. The previous layers' digest needs to be passed from one layer encrytion step to the next. The sequence must begin with the bottom-most layer using the result of GetInitalPreviousLayersDigest() so that no other layer can be 'slid' underneath the bottom-most one. This patch at least helps fulfill the requirement that previous layers cannot be manipulated assuming the attacker can access the registry but of course not manipulate the decryption code. Signed-off-by: Stefan Berger --- blockcipher/blockcipher.go | 3 ++ encryption.go | 81 ++++++++++++++++++++++++++++---------- encryption_test.go | 14 ++++++- utils/hashes.go | 48 ++++++++++++++++++++++ 4 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 utils/hashes.go diff --git a/blockcipher/blockcipher.go b/blockcipher/blockcipher.go index da403d9..3cb13b9 100644 --- a/blockcipher/blockcipher.go +++ b/blockcipher/blockcipher.go @@ -45,6 +45,9 @@ type PrivateLayerBlockCipherOptions struct { // CipherOptions contains the cipher metadata used for encryption/decryption // This field should be populated by Encrypt/Decrypt calls CipherOptions map[string][]byte `json:"cipheroptions"` + + // PreviousLayersDigest is the accumulated digest of all previous layers + PreviousLayersDigest digest.Digest `json:"previouslayersdigest"` } // PublicLayerBlockCipherOptions includes the information required to encrypt/decrypt diff --git a/encryption.go b/encryption.go index f5142cc..bcab1fc 100644 --- a/encryption.go +++ b/encryption.go @@ -17,25 +17,28 @@ package ocicrypt import ( + "bytes" "encoding/base64" + "encoding/hex" "encoding/json" "fmt" - keyproviderconfig "github.com/containers/ocicrypt/config/keyprovider-config" - "github.com/containers/ocicrypt/keywrap/keyprovider" "io" "strings" "github.com/containers/ocicrypt/blockcipher" "github.com/containers/ocicrypt/config" + keyproviderconfig "github.com/containers/ocicrypt/config/keyprovider-config" "github.com/containers/ocicrypt/keywrap" "github.com/containers/ocicrypt/keywrap/jwe" + "github.com/containers/ocicrypt/keywrap/keyprovider" "github.com/containers/ocicrypt/keywrap/pgp" "github.com/containers/ocicrypt/keywrap/pkcs11" "github.com/containers/ocicrypt/keywrap/pkcs7" + "github.com/containers/ocicrypt/utils" "github.com/opencontainers/go-digest" - log "github.com/sirupsen/logrus" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" ) // EncryptLayerFinalizer is a finalizer run to return the annotations to set for @@ -86,8 +89,20 @@ func GetWrappedKeysMap(desc ocispec.Descriptor) map[string]string { return wrappedKeysMap } +// comparePreviousLayersDigests compares the given digests and returns an error if they do not match +func comparePreviousLayersDigests(previousLayersDigest []byte, expPreviousLayersDigest digest.Digest) error { + digest, err := hex.DecodeString(expPreviousLayersDigest.Encoded()) + if err != nil { + return errors.Wrapf(err, "Hex-decoding expected previous layers hash failed") + } + if !bytes.Equal(digest, previousLayersDigest) { + return errors.Errorf("Previous layer digest '%x' does not match expected one '%x'", previousLayersDigest, digest) + } + return nil +} + // EncryptLayer encrypts the layer by running one encryptor after the other -func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, desc ocispec.Descriptor) (io.Reader, EncryptLayerFinalizer, error) { +func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, desc ocispec.Descriptor, previousLayersDigest []byte) (io.Reader, EncryptLayerFinalizer, []byte, error) { var ( encLayerReader io.Reader err error @@ -97,8 +112,18 @@ func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, des pubOptsData []byte ) + if len(previousLayersDigest) == 0 { + /* bottom-most layer MUST start with sha256.Sum(nil) */ + return nil, nil, nil, errors.New("previousLayersDigest must not be nil") + } + if ec == nil { - return nil, nil, errors.New("EncryptConfig must not be nil") + return nil, nil, nil, errors.New("EncryptConfig must not be nil") + } + + newLayersDigest, err := utils.GetNewLayersDigest(previousLayersDigest, desc.Digest) + if err != nil { + return nil, nil, nil, err } for annotationsID := range keyWrapperAnnotations { @@ -106,11 +131,11 @@ func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, des if annotation != "" { privOptsData, err = decryptLayerKeyOptsData(&ec.DecryptConfig, desc) if err != nil { - return nil, nil, err + return nil, nil, nil, err } pubOptsData, err = getLayerPubOpts(desc) if err != nil { - return nil, nil, err + return nil, nil, nil, err } // already encrypted! encrypted = true @@ -120,7 +145,7 @@ func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, des if !encrypted { encLayerReader, bcFin, err = commonEncryptLayer(encOrPlainLayerReader, desc.Digest, blockcipher.AES256CTR) if err != nil { - return nil, nil, err + return nil, nil, nil, err } } @@ -131,6 +156,8 @@ func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, des if err != nil { return nil, err } + + opts.Private.PreviousLayersDigest = digest.NewDigestFromBytes(digest.SHA256, previousLayersDigest) privOptsData, err = json.Marshal(opts.Private) if err != nil { return nil, errors.Wrapf(err, "could not JSON marshal opts") @@ -169,8 +196,7 @@ func EncryptLayer(ec *config.EncryptConfig, encOrPlainLayerReader io.Reader, des } // if nothing was encrypted, we just return encLayer = nil - return encLayerReader, encLayerFinalizer, err - + return encLayerReader, encLayerFinalizer, newLayersDigest, err } // preWrapKeys calls WrapKeys and handles the base64 encoding and concatenation of the @@ -190,22 +216,22 @@ func preWrapKeys(keywrapper keywrap.KeyWrapper, ec *config.EncryptConfig, b64Ann // 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) { +func DecryptLayer(dc *config.DecryptConfig, encLayerReader io.Reader, desc ocispec.Descriptor, unwrapOnly bool, previousLayersDigest []byte) (io.Reader, digest.Digest, []byte, error) { if dc == nil { - return nil, "", errors.New("DecryptConfig must not be nil") + return nil, "", nil, errors.New("DecryptConfig must not be nil") } privOptsData, err := decryptLayerKeyOptsData(dc, desc) if err != nil || unwrapOnly { - return nil, "", err + return nil, "", nil, err } var pubOptsData []byte pubOptsData, err = getLayerPubOpts(desc) if err != nil { - return nil, "", err + return nil, "", nil, err } - return commonDecryptLayer(encLayerReader, privOptsData, pubOptsData) + return commonDecryptLayer(encLayerReader, privOptsData, pubOptsData, previousLayersDigest) } func decryptLayerKeyOptsData(dc *config.DecryptConfig, desc ocispec.Descriptor) ([]byte, error) { @@ -301,23 +327,36 @@ func commonEncryptLayer(plainLayerReader io.Reader, d digest.Digest, typ blockci // commonDecryptLayer decrypts an encrypted layer previously encrypted with commonEncryptLayer // by passing along the optsData -func commonDecryptLayer(encLayerReader io.Reader, privOptsData []byte, pubOptsData []byte) (io.Reader, digest.Digest, error) { +func commonDecryptLayer(encLayerReader io.Reader, privOptsData []byte, pubOptsData []byte, previousLayersDigest []byte) (io.Reader, digest.Digest, []byte, error) { privOpts := blockcipher.PrivateLayerBlockCipherOptions{} err := json.Unmarshal(privOptsData, &privOpts) if err != nil { - return nil, "", errors.Wrapf(err, "could not JSON unmarshal privOptsData") + return nil, "", nil, errors.Wrapf(err, "could not JSON unmarshal privOptsData") + } + + if len(privOpts.PreviousLayersDigest) > 0 { + /* older images do not have this */ + err = comparePreviousLayersDigests(previousLayersDigest, privOpts.PreviousLayersDigest) + if err != nil { + return nil, "", nil, err + } + } + + newLayersDigest, err := utils.GetNewLayersDigest(previousLayersDigest, privOpts.Digest) + if err != nil { + return nil, "", nil, err } lbch, err := blockcipher.NewLayerBlockCipherHandler() if err != nil { - return nil, "", err + return nil, "", nil, err } pubOpts := blockcipher.PublicLayerBlockCipherOptions{} if len(pubOptsData) > 0 { err := json.Unmarshal(pubOptsData, &pubOpts) if err != nil { - return nil, "", errors.Wrapf(err, "could not JSON unmarshal pubOptsData") + return nil, "", nil, errors.Wrapf(err, "could not JSON unmarshal pubOptsData") } } @@ -328,10 +367,10 @@ func commonDecryptLayer(encLayerReader io.Reader, privOptsData []byte, pubOptsDa plainLayerReader, opts, err := lbch.Decrypt(encLayerReader, opts) if err != nil { - return nil, "", err + return nil, "", nil, err } - return plainLayerReader, opts.Private.Digest, nil + return plainLayerReader, opts.Private.Digest, newLayersDigest, nil } // FilterOutAnnotations filters out the annotations belonging to the image encryption 'namespace' diff --git a/encryption_test.go b/encryption_test.go index f76de5d..ff818e9 100644 --- a/encryption_test.go +++ b/encryption_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/containers/ocicrypt/config" + "github.com/containers/ocicrypt/utils" digest "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -100,12 +101,21 @@ func TestEncryptLayer(t *testing.T) { } dataReader := bytes.NewReader(data) + previousLayersDigest := utils.GetInitialPreviousLayersDigest() - encLayerReader, encLayerFinalizer, err := EncryptLayer(ec, dataReader, desc) + encLayerReader, encLayerFinalizer, newLayersDigest, err := EncryptLayer(ec, dataReader, desc, previousLayersDigest) if err != nil { t.Fatal(err) } + expDigest, err := utils.GetNewLayersDigest(previousLayersDigest, desc.Digest) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(expDigest, newLayersDigest) { + t.Fatal("Previous layer digest is wrong") + } + encLayer := make([]byte, 1024) encsize, err := encLayerReader.Read(encLayer) if err != io.EOF { @@ -126,7 +136,7 @@ func TestEncryptLayer(t *testing.T) { Annotations: annotations, } - decLayerReader, _, err := DecryptLayer(dc, encLayerReaderAt, newDesc, false) + decLayerReader, _, _, err := DecryptLayer(dc, encLayerReaderAt, newDesc, false, previousLayersDigest) if err != nil { t.Fatal(err) } diff --git a/utils/hashes.go b/utils/hashes.go new file mode 100644 index 0000000..bd641ca --- /dev/null +++ b/utils/hashes.go @@ -0,0 +1,48 @@ +/* + Copyright The ocicrypt 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 utils + +import ( + "crypto/sha256" + "encoding/hex" + + "github.com/opencontainers/go-digest" + "github.com/pkg/errors" +) + +// GetInitalPreviousLayersDigest returns the initial value for previousLayersDigest +func GetInitialPreviousLayersDigest() []byte { + digest := sha256.Sum256(nil) + return digest[:] +} + +// GetNewLayersDigest calculates the new layer digest from the previousLayersDigest and the layerDigest. +func GetNewLayersDigest(previousLayersDigest []byte, layerDigest digest.Digest) ([]byte, error) { + newDigest := sha256.New() + // never returns an error but linter requires us to look at it + _, err := newDigest.Write(previousLayersDigest) + if err != nil { + return nil, err + } + + digest, err := hex.DecodeString(layerDigest.Encoded()) + if err != nil { + return nil, errors.Wrap(err, "Hex decoding digest failed") + } + _, err = newDigest.Write(digest) + return newDigest.Sum(nil), err +}