qemu-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [Qemu-devel] [PATCH v1 12/15] qcow2: add support for LUKS encryption


From: Daniel P. Berrange
Subject: Re: [Qemu-devel] [PATCH v1 12/15] qcow2: add support for LUKS encryption format
Date: Tue, 24 Jan 2017 13:58:48 +0000
User-agent: Mutt/1.7.1 (2016-10-04)

On Sat, Jan 21, 2017 at 07:57:45PM +0100, Max Reitz wrote:
> On 03.01.2017 19:27, Daniel P. Berrange wrote:
> > This adds support for using LUKS as an encryption format
> > with the qcow2 file. The use of the existing 'encryption=on'
> > parameter is replaced by a new parameter 'encryption-format'
> > which takes the values 'aes' or 'luks'. e.g.
> > 
> >   # qemu-img create --object secret,data=123456,id=sec0 \
> >        -f qcow2 -o encryption-format=luks,luks-key-secret=sec0 \
> >        test.qcow2 10G
> > 
> > results in the creation of an image using the LUKS format.
> > Use of the legacy 'encryption=on' parameter still results
> > in creation of the old qcow2 AES format, and is equivalent
> > to the new 'encryption-format=aes'. e.g. the following are
> > equivalent:
> > 
> >   # qemu-img create --object secret,data=123456,id=sec0 \
> >        -f qcow2 -o encryption=on,aes-key-secret=sec0 \
> >        test.qcow2 10G
> > 
> >  # qemu-img create --object secret,data=123456,id=sec0 \
> >        -f qcow2 -o encryption-format=aes,aes-key-secret=sec0 \
> >        test.qcow2 10G
> > 
> > With the LUKS format it is necessary to store the LUKS
> > partition header and key material in the QCow2 file. This
> > data can be many MB in size, so cannot go into the QCow2
> > header region directly. Thus the spec defines a FDE
> > (Full Disk Encryption) header extension that specifies
> > the offset of a set of clusters to hold the FDE headers,
> > as well as the length of that region. The LUKS header is
> > thus stored in these extra allocated clusters before the
> > main image payload.
> > 
> > Aside from all the cryptographic differences implied by
> > use of the LUKS format, there is one further key difference
> > between the use of legacy AES and LUKS encryption in qcow2.
> > For LUKS, the initialiazation vectors are generated using
> > the host physical sector as the input, rather than the
> > guest virtual sector. This guarantees unique initialization
> > vectors for all sectors when qcow2 internal snapshots are
> > used, thus giving stronger protection against watermarking
> > attacks.
> > 
> > Signed-off-by: Daniel P. Berrange <address@hidden>
> > ---
> >  block/qcow2-cluster.c      |   4 +-
> >  block/qcow2-refcount.c     |  10 ++
> >  block/qcow2.c              | 236 
> > ++++++++++++++++++++++++++++++++++++++-------
> >  block/qcow2.h              |   9 ++
> >  docs/specs/qcow2.txt       |  99 +++++++++++++++++++
> 
> I'd personally rather have specification changes separate from the
> implementation.
> 
> >  qapi/block-core.json       |   6 +-
> >  tests/qemu-iotests/049     |   2 +-
> >  tests/qemu-iotests/082.out | 216 +++++++++++++++++++++++++++++++++++++++++
> >  tests/qemu-iotests/087     |  65 ++++++++++++-
> >  tests/qemu-iotests/087.out |  22 ++++-
> >  tests/qemu-iotests/134     |   4 +-
> >  tests/qemu-iotests/158     |   8 +-
> >  tests/qemu-iotests/174     |  76 +++++++++++++++
> >  tests/qemu-iotests/174.out |  19 ++++
> >  tests/qemu-iotests/175     |  85 ++++++++++++++++
> >  tests/qemu-iotests/175.out |  26 +++++
> >  tests/qemu-iotests/group   |   2 +
> >  17 files changed, 840 insertions(+), 49 deletions(-)
> >  create mode 100755 tests/qemu-iotests/174
> >  create mode 100644 tests/qemu-iotests/174.out
> >  create mode 100755 tests/qemu-iotests/175
> >  create mode 100644 tests/qemu-iotests/175.out
> 
> Also, in order to help reviewing by making this patch less scary (840
> insertions are kind of... Urgh.), it would make sense to add these two
> tests in one or two separate patches.

Ok, will split it in three - spec change, code change and test
additions.


> > @@ -80,6 +81,73 @@ static int qcow2_probe(const uint8_t *buf, int buf_size, 
> > const char *filename)
> 
> [...]
> 
> > +static ssize_t qcow2_crypto_hdr_init_func(QCryptoBlock *block, size_t 
> > headerlen,
> > +                                          Error **errp, void *opaque)
> > +{
> > +    BlockDriverState *bs = opaque;
> > +    BDRVQcow2State *s = bs->opaque;
> > +    int64_t ret;
> > +
> > +    ret = qcow2_alloc_clusters(bs, headerlen);
> > +    if (ret < 0) {
> > +        error_setg_errno(errp, -ret,
> > +                         "Cannot allocate cluster for LUKS header size 
> > %zu",
> > +                         headerlen);
> > +        return -1;
> > +    }
> > +
> > +    s->crypto_header.length = headerlen;
> > +    s->crypto_header.offset = ret;
> 
> The specification says any unused space in the last cluster has to be
> set to zero. This isn't done here.
> 
> I don't have a preference whether you write zeroes to the range in
> question here or whether you simply relax the specification to say that
> anything beyond crypto_header.length is undefined.

I'll explicitly fill it with zeros.

> > +static ssize_t qcow2_crypto_hdr_write_func(QCryptoBlock *block, size_t 
> > offset,
> > +                                           const uint8_t *buf, size_t 
> > buflen,
> > +                                           Error **errp, void *opaque)
> > +{
> > +    BlockDriverState *bs = opaque;
> > +    BDRVQcow2State *s = bs->opaque;
> > +    ssize_t ret;
> > +
> > +    if ((offset + buflen) > s->crypto_header.length) {
> > +        error_setg(errp, "Request for data outside of extension header");
> > +        return -1;
> > +    }
> > +
> > +    ret = bdrv_pwrite(bs->file,
> > +                      s->crypto_header.offset + offset, buf, buflen);
> 
> Theoretically, there should be a qcow2_pre_write_overlap_check() here.
> But in theory, qcow2_check_metadata_overlap() should also check overlaps
> with this new block of metadata.
> 
> I'll leave it up to you as the discussion about the future of those
> overlap checks is still up in the air. I really don't have a preference
> on what to do in this patch.

Ok, i'll leave as is for now.

> > @@ -165,6 +233,45 @@ static int qcow2_read_extensions(BlockDriverState *bs, 
> > uint64_t start_offset,
> >              }
> >              break;
> >  
> > +        case QCOW2_EXT_MAGIC_CRYPTO_HEADER: {
> > +            unsigned int cflags = 0;
> > +            if (s->crypt_method_header != QCOW_CRYPT_LUKS) {
> > +                error_setg(errp, "CRYPTO header extension only "
> > +                           "expected with LUKS encryption method");
> > +                return -EINVAL;
> > +            }
> > +            if (ext.len != sizeof(Qcow2CryptoHeaderExtension)) {
> > +                error_setg(errp, "CRYPTO header extension size %u, "
> > +                           "but expected size %zu", ext.len,
> > +                           sizeof(Qcow2CryptoHeaderExtension));
> > +                return -EINVAL;
> > +            }
> > +
> > +            ret = bdrv_pread(bs->file, offset, &s->crypto_header, ext.len);
> > +            if (ret < 0) {
> > +                error_setg_errno(errp, -ret,
> > +                                 "Unable to read CRYPTO header extension");
> > +                return ret;
> > +            }
> > +            be64_to_cpu(s->crypto_header.offset);
> > +            be64_to_cpu(s->crypto_header.length);
> 
> Shouldn't you use the result somehow (or use be64_to_cpus)...?

Sigh, yes, intention was to modify in-place

> Also, you should check the validity of s->crypto_header.offset here,
> i.e. whether it is indeed aligned to a cluster boundary.

Ok, wil add that.

> 
> > +
> > +            if (flags & BDRV_O_NO_IO) {
> > +                cflags |= QCRYPTO_BLOCK_OPEN_NO_IO;
> > +            }
> > +            /* XXX how do we pass the same crypto opts down to the
> 
> Just as in the previous patch, a TODO would probably be sufficient here.

Yes

> > +             * backing file by default, so we don't have to manually
> > +             * provide the same key-secret property against the full
> > +             * backing chain
> > +             */
> > +            s->crypto = qcrypto_block_open(s->crypto_opts,
> > +                                           qcow2_crypto_hdr_read_func,
> > +                                           bs, cflags, errp);
> > +            if (!s->crypto) {
> > +                return -EINVAL;
> > +            }
> > +            s->crypt_physical_offset = true;
> 
> I think this should be set depending on the crypt_method_header (i.e. in
> qcow2_open()), not depending on whether this extension exists or not.

Yes, makes sense.

> > @@ -988,12 +1101,6 @@ static int qcow2_open(BlockDriverState *bs, QDict 
> > *options, int flags,
> >      s->refcount_max = UINT64_C(1) << (s->refcount_bits - 1);
> >      s->refcount_max += s->refcount_max - 1;
> >  
> > -    if (header.crypt_method > QCOW_CRYPT_AES) {
> > -        error_setg(errp, "Unsupported encryption method: %" PRIu32,
> > -                   header.crypt_method);
> > -        ret = -EINVAL;
> > -        goto fail;
> > -    }
> >      s->crypt_method_header = header.crypt_method;
> >      if (s->crypt_method_header) {
> >          if (bdrv_uses_whitelist() &&
> 
> You could put the assignment of crypt_physical_offset into this block
> (at its bottom where bs->encrypted is set).

Yep will do.


> > @@ -1929,6 +2053,22 @@ int qcow2_update_header(BlockDriverState *bs)
> >          buflen -= ret;
> >      }
> >  
> > +    /* Full disk encryption header pointer extension */
> > +    if (s->crypto_header.offset != 0) {
> > +        cpu_to_be64(s->crypto_header.offset);
> > +        cpu_to_be64(s->crypto_header.length);
> 
> Should be cpu_to_be64s() or you should store the result somewhere.
> 
> > +        ret = header_ext_add(buf, QCOW2_EXT_MAGIC_CRYPTO_HEADER,
> > +                             &s->crypto_header, sizeof(s->crypto_header),
> > +                             buflen);
> > +        be64_to_cpu(s->crypto_header.offset);
> > +        be64_to_cpu(s->crypto_header.length);
> 
> The same with be64_to_cpus().

Yes intention was obviously to modify in-place.

> > @@ -3426,7 +3582,19 @@ static QemuOptsList qcow2_create_opts = {
> >              .help = "Width of a reference count entry in bits",
> >              .def_value_str = "16"
> >          },
> > +        {
> > +            .name = QCOW2_OPT_ENCRYPTION_FORMAT,
> > +            .type = QEMU_OPT_STRING,
> > +            .help = "Encryption data format 'luks' (default) or 'aes'",
> 
> In what way is LUKS the default? No encryption is the default; and the
> default encryption is the old AES-CBC because that is what you get when
> specifying encryption=on.

This is left-over language from a previous approach.

> I would agree if you replaced "default" by "recommended". In addition,
> the help text for the "encryption" option should probably now simply say:
> 
> "Deprecated, use the " QCOW2_OPT_ENCRYPTION_FORMAT " option instead"

Yes, will do that.


> > diff --git a/tests/qemu-iotests/087 b/tests/qemu-iotests/087
> > index fe30383..6690715 100755
> > --- a/tests/qemu-iotests/087
> > +++ b/tests/qemu-iotests/087
> 
> [...]
> 
> > @@ -169,7 +169,62 @@ run_qemu <<EOF
> >            "driver": "file",
> >            "filename": "$TEST_IMG"
> >        },
> > -      "qcow-key-secret": "sec0"
> > +      "aes-key-secret": "sec0"
> > +    }
> > +  }
> > +{ "execute": "quit" }
> > +EOF
> > +
> > +echo
> > +echo === Encrypted image LUKS ===
> > +echo
> > +
> > +_make_test_img --object secret,id=sec0,data=123456 -o 
> > encryption-format=luks,luks-key-secret=sec0 $size
> > +run_qemu -S <<EOF
> > +{ "execute": "qmp_capabilities" }
> > +{ "execute": "object-add",
> > +  "arguments": {
> > +      "qom-type": "secret",
> > +      "id": "sec0",
> > +      "props": {
> > +          "data": "123456"
> > +      }
> > +  }
> > +}
> > +{ "execute": "blockdev-add",
> > +  "arguments": {
> > +      "driver": "$IMGFMT",
> > +      "node-name": "disk",
> > +      "file": {
> > +          "driver": "file",
> > +          "filename": "$TEST_IMG"
> > +      },
> > +      "luks-key-secret": "sec0"
> > +    }
> > +  }
> > +{ "execute": "quit" }
> > +EOF
> > +
> > +run_qemu <<EOF
> 
> I think we can drop one of the duplicated test cases with and without -S
> now. The reason for having two originally was, as far as I can remember,
> to test that you could blockdev-add encrypted images when the VM was not
> paused. However, that is no longer the case since we are no longer
> relying on that old infrastructure (as of this series at least).

Yes, and in fact I can drop the existing one for qcow2 AES in the
earlier patch where I remove prompting for passwords.


> > diff --git a/tests/qemu-iotests/174 b/tests/qemu-iotests/174
> > new file mode 100755
> > index 0000000..27d4870
> > --- /dev/null
> > +++ b/tests/qemu-iotests/174

> > +_supported_fmt qcow2
> > +_supported_proto generic
> > +_supported_os Linux
> > +
> > +
> > +size=128M
> > +
> > +SECRET="secret,id=sec0,data=astrochicken"
> > +SECRETALT="secret,id=sec0,data=platypus"
> > +
> > +_make_test_img --object $SECRET -o 
> > "encryption-format=luks,luks-key-secret=sec0" $size
> > +
> > +IMGSPEC="driver=$IMGFMT,file.filename=$TEST_IMG,luks-key-secret=sec0"
> > +
> > +QEMU_IO_OPTIONS=$QEMU_IO_OPTIONS_NO_FMT
> > +
> > +echo
> > +echo "== reading whole image =="
> > +$QEMU_IO --object $SECRET -c "read 0 $size" --image-opts $IMGSPEC | 
> > _filter_qemu_io | _filter_testdir
> 
> Shouldn't "read -P 0 0 $size" work here, too?

The underlying disk image contents will be zeros, but we'll then decrypt
those zeros and get random garbage.

We could only use -P 0 if we explicitly fill with encrypted-zeros.

> > +echo
> > +echo "== rewriting whole image =="
> > +$QEMU_IO --object $SECRET -c "write -P 0xa 0 $size" --image-opts $IMGSPEC 
> > | _filter_qemu_io | _filter_testdir
> > +
> > +echo
> > +echo "== verify pattern =="
> > +$QEMU_IO --object $SECRET -c "read -P 0xa 0 $size"  --image-opts $IMGSPEC 
> > | _filter_qemu_io | _filter_testdir
> 
> But do you really want to read and write 128 MB...? I mean, even on an
> HDD, it will only take a couple of seconds at most, but won't 16 or 32
> MB suffice? That way, it should always take less than a second.

I'll drop it to 16 MB, but this reminds me the real performance
benefit comes from telling luks to not force 1 second running
time for the PBKDF algorithm. So I'll drop PBKDK down to 10ms
instead of 1sec


> > +== verify pattern failure with wrong password ==
> > +can't open: Invalid password, cannot unlock any keyslot
> 
> Well, that's not quite a pattern failure. It's the result we want, but
> you should probably change the heading for this test case.

Good point.


> > +# get standard environment, filters and checks
> > +. ./common.rc
> > +. ./common.filter
> > +
> > +_supported_fmt qcow2
> > +_supported_proto generic
> > +_supported_os Linux
> > +
> > +
> > +size=128M
> 
> Same as in last test, I'm not sure 128 MB are actually necessary.
> 
> > +TEST_IMG_BASE=$TEST_IMG.base
> 
> Hmmm, does this work with spaces in $TEST_IMG?
> 
> > +SECRET="secret,id=sec0,data=astrochicken"
> 
> How about using different secrets for the base and the overlay?

Yes, that's a good idea.


Regards,
Daniel
-- 
|: http://berrange.com      -o-    http://www.flickr.com/photos/dberrange/ :|
|: http://libvirt.org              -o-             http://virt-manager.org :|
|: http://entangle-photo.org       -o-    http://search.cpan.org/~danberr/ :|



reply via email to

[Prev in Thread] Current Thread [Next in Thread]