Skip to content

Commit

Permalink
Add computation and verification of previous layers' hashes
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
stefanberger committed Mar 9, 2021
1 parent 059f6b1 commit 22cb4e2
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 23 deletions.
3 changes: 3 additions & 0 deletions blockcipher/blockcipher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 60 additions & 21 deletions encryption.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -97,20 +112,30 @@ 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 {
annotation := desc.Annotations[annotationsID]
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
Expand All @@ -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
}
}

Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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")
}
}

Expand All @@ -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'
Expand Down
14 changes: 12 additions & 2 deletions encryption_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down
26 changes: 26 additions & 0 deletions scripts/calc_next_previous_layers_digest
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash


echo "Enter PreviousLayersDigest or leave empty for initial one"
read previousLayersDigest
if [ -z "$previousLayersDigest" ]; then
previousLayersDigest=$(echo -en | sha256sum | gawk '{print $1}')
fi
if ! [[ $previousLayersDigest =~ ^[0-9a-fA-F]{64}$ ]]; then
echo "previousLayersDigest '$previousLayersDigest' must be a sha256"
exit 1
fi
while :; do
echo "Enter current layer's digest"
read currentLayerDigest
if ! [[ $currentLayerDigest =~ ^[0-9a-fA-F]{64}$ ]]; then
echo "current layer digest must be a sha256"
exit 1
fi

dig=$(echo -n "${previousLayersDigest}${currentLayerDigest}" |
sed -n 's/[0-9a-fA-F]\{2\}/\\x\0/pg' )

previousLayersDigest=$(echo -en "${dig}" | sha256sum | gawk '{print $1}')
echo "digest: ${previousLayersDigest}"
done
48 changes: 48 additions & 0 deletions utils/hashes.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 22cb4e2

Please sign in to comment.