qemu-devel
[Top][All Lists]
Advanced

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

[Qemu-devel] [PATCH WIP 30/30] qcow2: add LUKS full disk encryption supp


From: Daniel P. Berrange
Subject: [Qemu-devel] [PATCH WIP 30/30] qcow2: add LUKS full disk encryption support
Date: Fri, 20 Nov 2015 18:04:30 +0000

The QCow2 format currently has support for built-in AES
encryption, however, this is fundamentally flawed from a
cryptographic security POV, so its use is deprecated.
The previously added generic full disk encryption driver
could be used to encrypt QCow2 files by either laying it
above or below the QCow2 driver in the QEMU BlockBackend
tree.

If it is layered above (FDE -> QCow2 -> File), then only
the image payload will be encrypted. There is no safe way
to auto-detect use of FDE for the image payload, as you
cannot safely distinguish between a QCow2 image that has
the FDE driver layered above in the host, from a QCow2
image where the guest is using LUKS over a partitionless
drive. Layering above the image is a valid use case, but
this auto-detection limitation makes it undesirable as
a default approach for QCow2 encryption.

If it is layered below (QCow2 -> FDE -> File), then both
the image payload and QCow2 headers are encrypted. This
makes it impossible to query the disk image to determine
its logical disk size, or backing file requirements
without first unlocking the decryption key. This is again
a valid use case for scenarios where it is desirable to
avoid any leakage of information about the underling disk
format, but it is undesirable as a default approach for
QCow2 encryption.

Thus this patch takes a third approach of integrating LUKS
support directly into the QCow2 file format. Only the image
payload is encrypted, the QCow2 file header remainins in
clear text. Thus makes it possible to probe info about the
disk size, backing files, etc without needing decryption
keys. Since use of LUKS is encoded in the QCow2 header, it
is still possible to reliabily distinguish host side encryption
from guest side encryption.

First a new QCow2 encryption scheme is defined to represent
the LUKS format, with a value '2' (0 == plain text, 1 == the
legacy AES format). A corresponding new QCow2 header extension
is defined to hold the LUKS partition header data. This stores
the various encryption parameters and key slot metadata and the
encrypted master keys. The payload of the QCow2 file does not
change in structure. Sectors are simply processed via the
QCryptoBlock object to apply/remove encryption when required.

Signed-off-by: Daniel P. Berrange <address@hidden>
---
 block/qcow2.c        | 294 +++++++++++++++++++++++++++++++++++++++++++++++++--
 block/qcow2.h        |  11 +-
 docs/specs/qcow2.txt |  53 ++++++++++
 3 files changed, 347 insertions(+), 11 deletions(-)

diff --git a/block/qcow2.c b/block/qcow2.c
index 9158dd0..1650345 100644
--- a/block/qcow2.c
+++ b/block/qcow2.c
@@ -35,6 +35,8 @@
 #include "trace.h"
 #include "qemu/option_int.h"
 #include "crypto/secret.h"
+#include "qapi/opts-visitor.h"
+#include "qapi-visit.h"
 
 /*
   Differences with QCOW:
@@ -61,6 +63,7 @@ typedef struct {
 #define  QCOW2_EXT_MAGIC_END 0
 #define  QCOW2_EXT_MAGIC_BACKING_FORMAT 0xE2792ACA
 #define  QCOW2_EXT_MAGIC_FEATURE_TABLE 0x6803f857
+#define  QCOW2_EXT_MAGIC_LUKS_HEADER 0x4c554b53
 
 static int qcow2_probe(const uint8_t *buf, int buf_size, const char *filename)
 {
@@ -75,6 +78,63 @@ static int qcow2_probe(const uint8_t *buf, int buf_size, 
const char *filename)
 }
 
 
+struct QCow2FDEData {
+    BlockDriverState *bs;
+    size_t hdr_ext_offset; /* Offset of encryption header extension data */
+    size_t hdr_ext_length; /* Length of encryption header extension data */
+};
+
+static ssize_t qcow2_header_read_func(QCryptoBlock *block,
+                                      size_t offset,
+                                      uint8_t *buf,
+                                      size_t buflen,
+                                      Error **errp,
+                                      void *opaque)
+{
+    struct QCow2FDEData *data = opaque;
+    ssize_t ret;
+
+    if ((offset + buflen) > data->hdr_ext_length) {
+        error_setg_errno(errp, EINVAL,
+                         "Request for data outside of extension header");
+        return -1;
+    }
+
+    ret = bdrv_pread(data->bs->file->bs,
+                     data->hdr_ext_offset + offset, buf, buflen);
+    if (ret < 0) {
+        error_setg_errno(errp, -ret, "Could not read encryption header");
+        return ret;
+    }
+    return ret;
+}
+
+
+static ssize_t qcow2_header_write_func(QCryptoBlock *block,
+                                       size_t offset,
+                                       const uint8_t *buf,
+                                       size_t buflen,
+                                       Error **errp,
+                                       void *opaque)
+{
+    struct QCow2FDEData *data = opaque;
+    ssize_t ret;
+
+    if ((offset + buflen) > data->hdr_ext_length) {
+        error_setg_errno(errp, EINVAL,
+                         "Request for data outside of extension header");
+        return -1;
+    }
+
+    ret = bdrv_pwrite(data->bs, data->hdr_ext_offset + offset, buf, buflen);
+    if (ret < 0) {
+        error_setg_errno(errp, -ret, "Could not read encryption header");
+        return ret;
+    }
+    return ret;
+}
+
+
 /* 
  * read qcow2 extension and fill bs
  * start reading from start_offset
@@ -90,6 +150,7 @@ static int qcow2_read_extensions(BlockDriverState *bs, 
uint64_t start_offset,
     QCowExtension ext;
     uint64_t offset;
     int ret;
+    struct QCow2FDEData fdedata;
 
 #ifdef DEBUG_EXT
     printf("qcow2_read_extensions: start=%ld end=%ld\n", start_offset, 
end_offset);
@@ -160,6 +221,24 @@ static int qcow2_read_extensions(BlockDriverState *bs, 
uint64_t start_offset,
             }
             break;
 
+        case QCOW2_EXT_MAGIC_LUKS_HEADER:
+            if (s->crypt_method_header != QCOW_CRYPT_LUKS) {
+                error_setg(errp, "LUKS header extension only "
+                           "expected with LUKS encryption method");
+                return -EINVAL;
+            }
+            fdedata.bs = bs;
+            fdedata.hdr_ext_offset = offset;
+            fdedata.hdr_ext_length = ext.len;
+
+            s->fde = qcrypto_block_open(s->fde_opts,
+                                        qcow2_header_read_func,
+                                        &fdedata,
+                                        errp);
+            if (!s->fde) {
+                return -EINVAL;
+            }
+            break;
         default:
             /* unknown magic - save it in case we need to rewrite the header */
             {
@@ -474,7 +553,7 @@ static QemuOptsList qcow2_runtime_opts = {
             .help = "Clean unused cache entries after this time (in seconds)",
         },
         {
-            .name = QCOW2_OPT_KEY_ID,
+            .name = QCOW2_OPT_FDE_KEY_ID,
             .type = QEMU_OPT_STRING,
             .help = "ID of the secret that provides the encryption key",
         },
@@ -595,6 +674,98 @@ static void read_cache_sizes(BlockDriverState *bs, 
QemuOpts *opts,
     }
 }
 
+
+static QCryptoBlockOpenOptions *
+qcow2_fde_open_opts_init(QCryptoBlockFormat format,
+                         QemuOpts *opts,
+                         Error **errp)
+{
+    OptsVisitor *ov;
+    QCryptoBlockOpenOptions *ret;
+    Error *local_err = NULL;
+
+    ret = g_new0(QCryptoBlockOpenOptions, 1);
+    ret->format = format;
+
+    ov = opts_visitor_new(opts);
+
+    switch (format) {
+    case Q_CRYPTO_BLOCK_FORMAT_QCOWAES:
+        ret->u.qcowaes = g_new0(QCryptoBlockOptionsQCowAES, 1);
+        visit_type_QCryptoBlockOptionsQCowAES(opts_get_visitor(ov),
+                                              &ret->u.qcowaes,
+                                              "qcowaes", &local_err);
+        break;
+
+    case Q_CRYPTO_BLOCK_FORMAT_LUKS:
+        ret->u.luks = g_new0(QCryptoBlockOptionsLUKS, 1);
+        visit_type_QCryptoBlockOptionsLUKS(opts_get_visitor(ov),
+                                           &ret->u.luks, "luks", &local_err);
+        break;
+
+    default:
+        error_setg(&local_err, "Unsupported block format %d", format);
+        break;
+    }
+
+    if (local_err) {
+        error_propagate(errp, local_err);
+        opts_visitor_cleanup(ov);
+        qapi_free_QCryptoBlockOpenOptions(ret);
+        return NULL;
+    }
+
+    opts_visitor_cleanup(ov);
+    return ret;
+}
+
+
+static QCryptoBlockCreateOptions *
+qcow2_fde_create_opts_init(QCryptoBlockFormat format,
+                           QemuOpts *opts,
+                           Error **errp)
+{
+    OptsVisitor *ov;
+    QCryptoBlockCreateOptions *ret;
+    Error *local_err = NULL;
+
+    ret = g_new0(QCryptoBlockCreateOptions, 1);
+    ret->format = format;
+
+    ov = opts_visitor_new(opts);
+
+    switch (format) {
+    case Q_CRYPTO_BLOCK_FORMAT_QCOWAES:
+        ret->u.qcowaes = g_new0(QCryptoBlockOptionsQCowAES, 1);
+        visit_type_QCryptoBlockOptionsQCowAES(opts_get_visitor(ov),
+                                              &ret->u.qcowaes,
+                                              "qcowaes", &local_err);
+        break;
+
+    case Q_CRYPTO_BLOCK_FORMAT_LUKS:
+        ret->u.luks = g_new0(QCryptoBlockCreateOptionsLUKS, 1);
+        visit_type_QCryptoBlockCreateOptionsLUKS(opts_get_visitor(ov),
+                                                 &ret->u.luks,
+                                                 "luks", &local_err);
+        break;
+
+    default:
+        error_setg(&local_err, "Unsupported block format %d", format);
+        break;
+    }
+
+    if (local_err) {
+        error_propagate(errp, local_err);
+        opts_visitor_cleanup(ov);
+        qapi_free_QCryptoBlockCreateOptions(ret);
+        return NULL;
+    }
+
+    opts_visitor_cleanup(ov);
+    return ret;
+}
+
+
 typedef struct Qcow2ReopenState {
     Qcow2Cache *l2_table_cache;
     Qcow2Cache *refcount_block_cache;
@@ -761,13 +932,30 @@ static int qcow2_update_options_prepare(BlockDriverState 
*bs,
     r->discard_passthrough[QCOW2_DISCARD_OTHER] =
         qemu_opt_get_bool(opts, QCOW2_OPT_DISCARD_OTHER, false);
 
-    if (s->crypt_method_header) {
-        r->fde_opts = g_new0(QCryptoBlockOpenOptions, 1);
-        r->fde_opts->format = Q_CRYPTO_BLOCK_FORMAT_QCOWAES;
-        r->fde_opts->u.qcowaes = g_new0(QCryptoBlockOptionsQCowAES, 1);
+    switch (s->crypt_method_header) {
+    case QCOW_CRYPT_NONE:
+        break;
+
+    case QCOW_CRYPT_AES:
+        r->fde_opts = qcow2_fde_open_opts_init(
+            Q_CRYPTO_BLOCK_FORMAT_QCOWAES,
+            opts,
+            errp);
+        break;
 
-        r->fde_opts->u.qcowaes->keyid =
-            g_strdup(qemu_opt_get(opts, QCOW2_OPT_KEY_ID));
+    case QCOW_CRYPT_LUKS:
+        r->fde_opts = qcow2_fde_open_opts_init(
+            Q_CRYPTO_BLOCK_FORMAT_QCOWAES,
+            opts,
+            errp);
+        break;
+
+    default:
+        g_assert_not_reached();
+    }
+    if (s->crypt_method_header &&
+        !r->fde_opts) {
+        goto fail;
     }
 
     ret = 0;
@@ -1102,7 +1290,7 @@ static int qcow2_open(BlockDriverState *bs, QDict 
*options, int flags,
     }
 
     if (!(flags & BDRV_O_NO_IO) &&
-        bs->encrypted) {
+        s->crypt_method_header == QCOW_CRYPT_AES) {
         s->fde = qcrypto_block_open(s->fde_opts,
                                     NULL, NULL,
                                     errp);
@@ -1143,6 +1331,13 @@ static int qcow2_open(BlockDriverState *bs, QDict 
*options, int flags,
         goto fail;
     }
 
+    if (!(flags & BDRV_O_NO_IO) &&
+        bs->encrypted && !s->fde) {
+        error_setg(errp, "No encryption layer was initiailized");
+        ret = -EINVAL;
+        goto fail;
+    }
+
     /* read the backing file name */
     if (header.backing_file_offset != 0) {
         len = header.backing_file_size;
@@ -2013,6 +2208,10 @@ static int qcow2_create2(const char *filename, int64_t 
total_size,
 {
     int cluster_bits;
     QDict *options;
+    const char *fdestr;
+    QCryptoBlockCreateOptions *fdeopts = NULL;
+    QCryptoBlock *fde = NULL;
+    size_t i;
 
     /* Calculate cluster_bits */
     cluster_bits = ctz32(cluster_size);
@@ -2135,8 +2334,35 @@ static int qcow2_create2(const char *filename, int64_t 
total_size,
         .header_length              = cpu_to_be32(sizeof(*header)),
     };
 
-    if (flags & BLOCK_FLAG_ENCRYPT) {
-        header->crypt_method = cpu_to_be32(QCOW_CRYPT_AES);
+    fdestr = qemu_opt_get(opts, QCOW2_OPT_FDE);
+    if (fdestr) {
+        for (i = 0; i < Q_CRYPTO_BLOCK_FORMAT_MAX; i++) {
+            if (g_str_equal(QCryptoBlockFormat_lookup[i],
+                            fdestr)) {
+                fdeopts = qcow2_fde_create_opts_init(i,
+                                                     opts,
+                                                     errp);
+                if (!fdeopts) {
+                    goto out;
+                }
+                break;
+            }
+        }
+        if (!fdeopts) {
+            error_setg(errp, "Unknown fde format %s", fdestr);
+            goto out;
+        }
+        switch (fdeopts->format) {
+        case Q_CRYPTO_BLOCK_FORMAT_QCOWAES:
+            header->crypt_method = cpu_to_be32(QCOW_CRYPT_AES);
+            break;
+        case Q_CRYPTO_BLOCK_FORMAT_LUKS:
+            header->crypt_method = cpu_to_be32(QCOW_CRYPT_LUKS);
+            break;
+        default:
+            error_setg(errp, "Unsupported fde format %s", fdestr);
+            goto out;
+        }
     } else {
         header->crypt_method = cpu_to_be32(QCOW_CRYPT_NONE);
     }
@@ -2153,6 +2379,24 @@ static int qcow2_create2(const char *filename, int64_t 
total_size,
         goto out;
     }
 
+    /* XXXX this is roughly where we need to write the LUKS header,
+     * but its not going to fit inside the first cluster. Need to
+     * allow qcow2 header extensions to consume >1 cluster....
+     */
+    if (fdeopts && 0) {
+        struct QCow2FDEData fdedata;
+        fdedata.bs = bs;
+        fdedata.hdr_ext_offset = cluster_size;
+
+        fde = qcrypto_block_create(fdeopts,
+                                   qcow2_header_write_func,
+                                   &fdedata,
+                                   errp);
+        if (!fde) {
+            goto out;
+        }
+    }
+
     /* Write a refcount table with one refcount block */
     refcount_table = g_malloc0(2 * cluster_size);
     refcount_table[0] = cpu_to_be64(2 * cluster_size);
@@ -3136,6 +3380,36 @@ static QemuOptsList qcow2_create_opts = {
             .help = "Width of a reference count entry in bits",
             .def_value_str = "16"
         },
+        {
+            .name = QCOW2_OPT_FDE_KEY_ID,
+            .type = QEMU_OPT_STRING,
+            .help = "ID of the secret that provides the encryption key",
+        },
+        {
+            .name = QCOW2_OPT_FDE_CIPHER_ALG,
+            .type = QEMU_OPT_STRING,
+            .help = "Name of encryption cipher algorithm",
+        },
+        {
+            .name = QCOW2_OPT_FDE_CIPHER_MODE,
+            .type = QEMU_OPT_STRING,
+            .help = "Name of encryption cipher mode",
+        },
+        {
+            .name = QCOW2_OPT_FDE_IVGEN_ALG,
+            .type = QEMU_OPT_STRING,
+            .help = "Name of IV generator algorithm",
+        },
+        {
+            .name = QCOW2_OPT_FDE_IVGEN_HASH_ALG,
+            .type = QEMU_OPT_STRING,
+            .help = "Name of IV generator hash algorithm",
+        },
+        {
+            .name = QCOW2_OPT_FDE_HASH_ALG,
+            .type = QEMU_OPT_STRING,
+            .help = "Name of encryption hash algorithm",
+        },
         { /* end of list */ }
     }
 };
diff --git a/block/qcow2.h b/block/qcow2.h
index a41a1e3..69b99ae 100644
--- a/block/qcow2.h
+++ b/block/qcow2.h
@@ -36,6 +36,7 @@
 
 #define QCOW_CRYPT_NONE 0
 #define QCOW_CRYPT_AES  1
+#define QCOW_CRYPT_LUKS 2
 
 #define QCOW_MAX_CRYPT_CLUSTERS 32
 #define QCOW_MAX_SNAPSHOTS 65536
@@ -97,7 +98,15 @@
 #define QCOW2_OPT_L2_CACHE_SIZE "l2-cache-size"
 #define QCOW2_OPT_REFCOUNT_CACHE_SIZE "refcount-cache-size"
 #define QCOW2_OPT_CACHE_CLEAN_INTERVAL "cache-clean-interval"
-#define QCOW2_OPT_KEY_ID "keyid"
+
+#define QCOW2_OPT_FDE "fde"
+#define QCOW2_OPT_FDE_KEY_ID "keyid"
+#define QCOW2_OPT_FDE_CIPHER_ALG "cipher_alg"
+#define QCOW2_OPT_FDE_CIPHER_MODE "cipher_mode"
+#define QCOW2_OPT_FDE_IVGEN_ALG "ivgen_alg"
+#define QCOW2_OPT_FDE_IVGEN_HASH_ALG "ivgen_hash_alg"
+#define QCOW2_OPT_FDE_HASH_ALG "hash_alg"
+
 
 typedef struct QCowHeader {
     uint32_t magic;
diff --git a/docs/specs/qcow2.txt b/docs/specs/qcow2.txt
index f236d8c..3742f01 100644
--- a/docs/specs/qcow2.txt
+++ b/docs/specs/qcow2.txt
@@ -45,6 +45,7 @@ The first cluster of a qcow2 image contains the file header:
          32 - 35:   crypt_method
                     0 for no encryption
                     1 for AES encryption
+                    2 for LUKS encryption
 
          36 - 39:   l1_size
                     Number of entries in the active L1 table
@@ -123,6 +124,7 @@ be stored. Each extension has a structure like the 
following:
                         0x00000000 - End of the header extension area
                         0xE2792ACA - Backing file format name
                         0x6803f857 - Feature name table
+                        0x4c554b53 - LUKS partition header + key data
                         other      - Unknown header extension, can be safely
                                      ignored
 
@@ -166,6 +168,57 @@ the header extension data. Each entry look like this:
                     terminated if it has full length)
 
 
+== LUKS header and key slots ==
+
+If the 'crypt_method' header field specifies LUKS ( value == 2), the qcow2
+LUKS header extension is mandatory, to provide the data tables used by the
+LUKS encryption format.
+
+The first 592 bytes contain the LUKS partition header. This is then followed
+by the key material data areas. The size of the key material data areas is
+determined by the number of stripes in the key slot and key size.
+
+Refer to the LUKS format specification ('docs/on-disk-format.pdf' in the
+cryptsetup source package) for details of the LUKS partition header format.
+
+In the LUKS partition header, the "payload-offset" field does not refer
+to the offset of the QCow2 payload. Instead it simply refers to the
+total required length of the QCow2 header extension.
+
+In the LUKS key slots header, the "key-material-offset" is an absolute
+location in the qcow2 container.
+
+Logically the layout looks like
+
+  +-----------------------------+
+  | QCow2 header                |
+  +-----------------------------+
+  | QCow2 header extension X    |
+  +-----------------------------+
+  | QCow2 header extension LUKS |
+  | +-------------------------+ |
+  | | LUKS partition header   | |
+  | +-------------------------+ |
+  | | LUKS key material 1     | |
+  | +-------------------------+ |
+  | | LUKS key material 2     | |
+  | +-------------------------+ |
+  | | LUKS key material ...   | |
+  | +-------------------------+ |
+  | | LUKS key material 8     | |
+  | +-------------------------+ |
+  +-----------------------------+
+  | QCow2 header extension ...  |
+  +-----------------------------+
+  | QCow2 header extension Z    |
+  +-----------------------------+
+  | QCow2 cluster payload       |
+  .                             .
+  .                             .
+  .                             .
+  |                             |
+  +-----------------------------+
+
 == Host cluster management ==
 
 qcow2 manages the allocation of host clusters by maintaining a reference count
-- 
2.5.0




reply via email to

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