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 <berra...@redhat.com> --- 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