diff --git a/docs/libblockdev-sections.txt b/docs/libblockdev-sections.txt index 4a8f538d9..86fa3e9f1 100644 --- a/docs/libblockdev-sections.txt +++ b/docs/libblockdev-sections.txt @@ -69,6 +69,7 @@ bd_crypto_device_is_luks bd_crypto_device_seems_encrypted bd_crypto_luks_status bd_crypto_luks_format +bd_crypto_luks_format_volume_key bd_crypto_luks_extra_free bd_crypto_luks_extra_copy bd_crypto_luks_extra_new @@ -751,6 +752,14 @@ bd_s390_zfcp_sanitize_lun_input bd_s390_zfcp_online bd_s390_zfcp_scsi_offline bd_s390_zfcp_offline +bd_s390_zkey_generate +BDS390ZkeyInfo +bd_s390_zkey_info_copy +bd_s390_zkey_info_free +bd_s390_zkey_list +bd_s390_zkey_remove +bd_s390_zkey_cryptsetup_setvp +bd_s390_zkey_cryptsetup_validate BDS390Tech BDS390TechMode bd_s390_is_tech_avail diff --git a/misc/libblockdev-tasks.yml b/misc/libblockdev-tasks.yml index 8650ff81e..cb7a3a851 100644 --- a/misc/libblockdev-tasks.yml +++ b/misc/libblockdev-tasks.yml @@ -69,6 +69,14 @@ - xfsprogs when: ansible_facts['distribution'] == 'Fedora' and test_dependencies|bool +- name: Install s390utils (Fedora) + package: + state: present + name: + - s390utils + when: ansible_facts['distribution'] == 'Fedora' and ansible_facts['architecture'] == 's390x' and test_dependencies|bool + + ####### CentOS - name: Install basic build tools (CentOS) package: @@ -129,6 +137,13 @@ - xfsprogs when: ansible_facts['distribution'] == 'CentOS' and test_dependencies|bool +- name: Install s390utils (CentOS) + package: + state: present + name: + - s390utils + when: ansible_facts['distribution'] == 'CentOS' and ansible_facts['architecture'] == 's390x' and test_dependencies|bool + - name: Install pylint using pip (CentOS) pip: name: ['pylint'] diff --git a/src/lib/plugin_apis/crypto.api b/src/lib/plugin_apis/crypto.api index d790894c9..26db8e479 100644 --- a/src/lib/plugin_apis/crypto.api +++ b/src/lib/plugin_apis/crypto.api @@ -919,6 +919,36 @@ BDCryptoKeyslotContext* bd_crypto_keyslot_context_new_volume_key (const guint8 * */ gboolean bd_crypto_luks_format (const gchar *device, const gchar *cipher, guint64 key_size, BDCryptoKeyslotContext *context, guint64 min_entropy, BDCryptoLUKSVersion luks_version, BDCryptoLUKSExtra *extra,GError **error); +/** + * bd_crypto_luks_format_volume_key: + * @device: a device to format as LUKS + * @cipher: (nullable): cipher specification (type-mode, e.g. "aes-xts-plain64") or %NULL to use the default + * @key_size: size of the volume key in bits or 0 to use the size of @volume_key + * @context: key slot context (passphrase/keyfile...) for this LUKS device + * @volume_key: volume key context (created by %bd_crypto_keyslot_context_new_volume_key) to use + * as the LUKS volume (master) key instead of a randomly generated one + * @min_entropy: minimum random data entropy (in bits) required to format @device as LUKS + * @luks_version: whether to use LUKS v1 or LUKS v2 + * @extra: (nullable): extra arguments for LUKS format creation + * @error: (out) (optional): place to store error (if any) + * + * Formats the given @device as LUKS according to the other parameters given, using the + * provided @volume_key as the LUKS volume (master) key instead of letting cryptsetup + * generate a random one. This is primarily useful for pervasive encryption on IBM Z where + * the volume key is a secure key token generated by the 'zkey' utility and the cipher is + * "paes-xts-plain64". If @key_size is 0, the size of @volume_key is used. The @context is + * used for the key slot (passphrase/key file) as with %bd_crypto_luks_format. + * + * Supported @context types for this function: passphrase, key file + * Supported @volume_key context types for this function: volume key + * + * Returns: whether the given @device was successfully formatted as LUKS or not + * (the @error) contains the error in such cases) + * + * Tech category: %BD_CRYPTO_TECH_LUKS-%BD_CRYPTO_TECH_MODE_CREATE + */ +gboolean bd_crypto_luks_format_volume_key (const gchar *device, const gchar *cipher, guint64 key_size, BDCryptoKeyslotContext *context, BDCryptoKeyslotContext *volume_key, guint64 min_entropy, BDCryptoLUKSVersion luks_version, BDCryptoLUKSExtra *extra, GError **error); + /** * bd_crypto_luks_open: * @device: the device to open diff --git a/src/lib/plugin_apis/s390.api b/src/lib/plugin_apis/s390.api index ec8242ae6..8f50d1825 100644 --- a/src/lib/plugin_apis/s390.api +++ b/src/lib/plugin_apis/s390.api @@ -15,16 +15,19 @@ typedef enum { BD_S390_ERROR_FORMAT_FAILED, BD_S390_ERROR_DASDFMT, BD_S390_ERROR_IO, + BD_S390_ERROR_ZKEY, } BDS390Error; typedef enum { BD_S390_TECH_DASD = 0, BD_S390_TECH_ZFCP, + BD_S390_TECH_PAES, } BDS390Tech; typedef enum { BD_S390_TECH_MODE_MODIFY = 1 << 0, BD_S390_TECH_MODE_QUERY = 1 << 1, + BD_S390_TECH_MODE_CREATE = 1 << 2, } BDS390TechMode; /** @@ -175,4 +178,189 @@ gboolean bd_s390_zfcp_scsi_offline(const gchar *devno, const gchar *wwpn, const */ gboolean bd_s390_zfcp_offline (const gchar *devno, const gchar *wwpn, const gchar *lun, GError **error); +/** + * bd_s390_zkey_generate: + * @name: name of the secure key to generate in the secure key repository + * @key_type: (nullable): type of the secure key to generate (e.g. "CCA-AESCIPHER") or %NULL for the zkey default + * @keybits: size of the key in bits or 0 for the zkey default + * @volumes: (nullable) (array zero-terminated=1): list of volumes to associate with the key, each given + * in the "volume:dmname" format, or %NULL + * @apqns: (nullable) (array zero-terminated=1): list of cryptographic adapters (APQNs) to associate with + * the key, each given in the "card.domain" format, or %NULL + * @sector_size: sector size in bytes to use with dm-crypt or 0 for the system default + * @dummy_passphrase: whether to generate and associate a dummy passphrase with the key + * @extra: (nullable) (array zero-terminated=1): extra options for the key generation (right now + * passed to the 'zkey' utility) + * @error: (out) (optional): place to store error (if any) + * + * Generates a new secure key for pervasive encryption using the 'zkey' utility and stores it + * in the secure key repository. The key is generated as an XTS key with the 'LUKS2' volume type. + * + * Returns: whether the secure key was successfully generated or not + * + * Tech category: %BD_S390_TECH_PAES-%BD_S390_TECH_MODE_CREATE + */ +gboolean bd_s390_zkey_generate (const gchar *name, const gchar *key_type, guint64 keybits, const gchar **volumes, const gchar **apqns, guint64 sector_size, gboolean dummy_passphrase, const BDExtraArg **extra, GError **error); + +#define BD_S390_TYPE_ZKEY_INFO (bd_s390_zkey_info_get_type ()) +GType bd_s390_zkey_info_get_type(); + +/** + * BDS390ZkeyInfo: + * @name: name of the secure key in the secure key repository; + * @description: (nullable): user provided description of the key or %NULL if none was set; + * @secure_key_size: size of the secure key in bytes; + * @clear_key_size: size of the effective (clear) key in bits; + * @xts: whether the key is an XTS type key or not; + * @key_type: type of the secure key (e.g. "CCA-AESCIPHER"); + * @volumes: (array zero-terminated=1): volumes associated with the key, each in the "volume:dmname" format; + * @apqns: (array zero-terminated=1): cryptographic adapters (APQNs) associated with the key, each in the "card.domain" format; + * @key_file_name: full path to the file holding the secure key; + * @sector_size: sector size in bytes to use with dm-crypt or 0 for the system default; + * @volume_type: volume type the key is to be used with (e.g. "LUKS2"); + * @dummy_passphrase: (nullable): path to the dummy passphrase file associated with the key or %NULL if none is set; + */ +typedef struct BDS390ZkeyInfo { + gchar *name; + gchar *description; + guint64 secure_key_size; + guint64 clear_key_size; + gboolean xts; + gchar *key_type; + gchar **volumes; + gchar **apqns; + gchar *key_file_name; + guint64 sector_size; + gchar *volume_type; + gchar *dummy_passphrase; +} BDS390ZkeyInfo; + +/** + * bd_s390_zkey_info_free: (skip) + * @info: (nullable): %BDS390ZkeyInfo to free + * + * Frees @info. + */ +void bd_s390_zkey_info_free (BDS390ZkeyInfo *info) { + if (info == NULL) + return; + + g_free (info->name); + g_free (info->description); + g_free (info->key_type); + g_strfreev (info->volumes); + g_strfreev (info->apqns); + g_free (info->key_file_name); + g_free (info->volume_type); + g_free (info->dummy_passphrase); + g_free (info); +} + +/** + * bd_s390_zkey_info_copy: (skip) + * @info: (nullable): %BDS390ZkeyInfo to copy + * + * Creates a new copy of @info. + */ +BDS390ZkeyInfo* bd_s390_zkey_info_copy (BDS390ZkeyInfo *info) { + if (info == NULL) + return NULL; + + BDS390ZkeyInfo *new_info = g_new0 (BDS390ZkeyInfo, 1); + + new_info->name = g_strdup (info->name); + new_info->description = g_strdup (info->description); + new_info->secure_key_size = info->secure_key_size; + new_info->clear_key_size = info->clear_key_size; + new_info->xts = info->xts; + new_info->key_type = g_strdup (info->key_type); + new_info->volumes = g_strdupv (info->volumes); + new_info->apqns = g_strdupv (info->apqns); + new_info->key_file_name = g_strdup (info->key_file_name); + new_info->sector_size = info->sector_size; + new_info->volume_type = g_strdup (info->volume_type); + new_info->dummy_passphrase = g_strdup (info->dummy_passphrase); + + return new_info; +} + +GType bd_s390_zkey_info_get_type () { + static GType type = 0; + + if (G_UNLIKELY(type == 0)) { + type = g_boxed_type_register_static("BDS390ZkeyInfo", + (GBoxedCopyFunc) bd_s390_zkey_info_copy, + (GBoxedFreeFunc) bd_s390_zkey_info_free); + } + + return type; +} + +/** + * bd_s390_zkey_list: + * @name: (nullable): name of a single secure key to get information about or %NULL to list all keys + * @error: (out) (optional): place to store error (if any) + * + * Lists the secure keys stored in the secure key repository using the 'zkey' utility. If @name + * is given, only the information about the matching key is returned. + * + * Returns: (array zero-terminated=1) (transfer full): information about the secure keys in the + * repository (an empty list if there are none) or %NULL in case of error + * + * Tech category: %BD_S390_TECH_PAES-%BD_S390_TECH_MODE_QUERY + */ +BDS390ZkeyInfo** bd_s390_zkey_list (const gchar *name, GError **error); + +/** + * bd_s390_zkey_remove: + * @name: name of the secure key to remove from the secure key repository + * @error: (out) (optional): place to store error (if any) + * + * Removes the secure key @name from the secure key repository using the 'zkey' utility. + * + * Returns: whether the secure key was successfully removed or not + * + * Tech category: %BD_S390_TECH_PAES-%BD_S390_TECH_MODE_MODIFY + */ +gboolean bd_s390_zkey_remove (const gchar *name, GError **error); + +/** + * bd_s390_zkey_cryptsetup_setvp: + * @device: LUKS2 device to set the verification pattern on + * @key_file: path to a file containing the LUKS passphrase used to unlock a keyslot of @device + * @extra: (nullable) (array zero-terminated=1): extra options for setting the verification pattern + * (right now passed to the 'zkey-cryptsetup' utility) + * @error: (out) (optional): place to store error (if any) + * + * Sets the verification pattern of the secure key in the metadata of the LUKS2 volume @device + * using the 'zkey-cryptsetup' utility. The verification pattern is used to identify the secure + * key associated with the volume. Setting the verification pattern requires unlocking a keyslot + * of @device, so the LUKS passphrase needs to be provided in @key_file (this is a passphrase + * file, not the secure key file). + * + * Returns: whether the verification pattern was successfully set or not + * + * Tech category: %BD_S390_TECH_PAES-%BD_S390_TECH_MODE_MODIFY + */ +gboolean bd_s390_zkey_cryptsetup_setvp (const gchar *device, const gchar *key_file, const BDExtraArg **extra, GError **error); + +/** + * bd_s390_zkey_cryptsetup_validate: + * @device: LUKS2 device to validate + * @key_file: path to a file containing the LUKS passphrase used to unlock a keyslot of @device + * @extra: (nullable) (array zero-terminated=1): extra options for the validation + * (right now passed to the 'zkey-cryptsetup' utility) + * @error: (out) (optional): place to store error (if any) + * + * Validates that the LUKS2 volume @device is correctly set up for pervasive encryption using the + * 'zkey-cryptsetup' utility. Validation requires unlocking a keyslot of @device, so the LUKS + * passphrase needs to be provided in @key_file (this is a passphrase file, not the secure key + * file). + * + * Returns: whether the LUKS2 volume @device is correctly set up for pervasive encryption or not + * + * Tech category: %BD_S390_TECH_PAES-%BD_S390_TECH_MODE_QUERY + */ +gboolean bd_s390_zkey_cryptsetup_validate (const gchar *device, const gchar *key_file, const BDExtraArg **extra, GError **error); + #endif /* BD_S390_API */ diff --git a/src/plugins/crypto.c b/src/plugins/crypto.c index b7004af34..3328380bf 100644 --- a/src/plugins/crypto.c +++ b/src/plugins/crypto.c @@ -945,6 +945,7 @@ gboolean _crypto_luks_format (const gchar *device, const gchar *cipher, guint64 key_size, BDCryptoKeyslotContext *context, + BDCryptoKeyslotContext *volume_key, guint64 min_entropy, BDCryptoLUKSVersion luks_version, BDCryptoLUKSExtra *extra, @@ -965,6 +966,7 @@ gboolean _crypto_luks_format (const gchar *device, guint64 progress_id = 0; gchar *msg = NULL; const gchar* crypt_version = NULL; + const gchar *format_vk = NULL; GError *l_error = NULL; struct crypt_pbkdf_type *pbkdf = NULL; @@ -992,6 +994,25 @@ gboolean _crypto_luks_format (const gchar *device, return FALSE; } + if (volume_key) { + if (volume_key->type != BD_CRYPTO_KEYSLOT_CONTEXT_TYPE_VOLUME_KEY) { + g_set_error_literal (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_INVALID_CONTEXT, + "Only 'volume key' context type is valid for the volume key."); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + return FALSE; + } +#ifdef LIBCRYPTSETUP_27 + if (hw_encryption == BD_CRYPTO_LUKS_HW_ENCRYPTION_OPAL_HW_ONLY || hw_encryption == BD_CRYPTO_LUKS_HW_ENCRYPTION_OPAL_HW_AND_SW) { + g_set_error_literal (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_INVALID_PARAMS, + "Specifying the volume key is not supported for OPAL LUKS format."); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + return FALSE; + } +#endif + } + ret = crypt_init (&cd, device); if (ret != 0) { g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_DEVICE, @@ -1031,6 +1052,25 @@ gboolean _crypto_luks_format (const gchar *device, /* key_size should be in bytes */ key_size = key_size / 8; + if (volume_key) { + /* use the provided volume key instead of letting cryptsetup generate + a random one -- the key_size must match the volume key length */ + if (key_size == 0) + key_size = volume_key->u.volume_key.volume_key_size; + else if (key_size != volume_key->u.volume_key.volume_key_size) { + g_set_error (&l_error, BD_CRYPTO_ERROR, BD_CRYPTO_ERROR_INVALID_PARAMS, + "Specified key size (%"G_GUINT64_FORMAT" bytes) does not match the " + "provided volume key size (%"G_GSIZE_FORMAT" bytes).", + key_size, volume_key->u.volume_key.volume_key_size); + crypt_free (cd); + g_strfreev (cipher_specs); + bd_utils_report_finished (progress_id, l_error->message); + g_propagate_error (error, l_error); + return FALSE; + } + format_vk = (const gchar *) volume_key->u.volume_key.volume_key; + } + /* wait for enough random data entropy (if requested) */ if (min_entropy > 0) { dev_random_fd = open ("/dev/random", O_RDONLY); @@ -1143,7 +1183,7 @@ gboolean _crypto_luks_format (const gchar *device, params.data_alignment = extra->data_alignment; params.data_device = extra->data_device; ret = crypt_format (cd, crypt_version, cipher_specs[0], cipher_specs[1], - NULL, NULL, key_size, ¶ms); + NULL, format_vk, key_size, ¶ms); g_free (pbkdf); } else if (luks_version == BD_CRYPTO_LUKS_VERSION_LUKS2) { @@ -1175,7 +1215,7 @@ gboolean _crypto_luks_format (const gchar *device, else #endif ret = crypt_format (cd, crypt_version, cipher_specs[0], cipher_specs[1], - NULL, NULL, key_size, ¶ms); + NULL, format_vk, key_size, ¶ms); g_free (pbkdf); } } else { @@ -1188,7 +1228,7 @@ gboolean _crypto_luks_format (const gchar *device, } else #endif ret = crypt_format (cd, crypt_version, cipher_specs[0], cipher_specs[1], - NULL, NULL, key_size, NULL); + NULL, format_vk, key_size, NULL); } g_strfreev (cipher_specs); @@ -1278,7 +1318,39 @@ gboolean _crypto_luks_format (const gchar *device, * Tech category: %BD_CRYPTO_TECH_LUKS-%BD_CRYPTO_TECH_MODE_CREATE */ gboolean bd_crypto_luks_format (const gchar *device, const gchar *cipher, guint64 key_size, BDCryptoKeyslotContext *context, guint64 min_entropy, BDCryptoLUKSVersion luks_version, BDCryptoLUKSExtra *extra,GError **error) { - return _crypto_luks_format (device, cipher, key_size, context, min_entropy, luks_version, extra, BD_CRYPTO_LUKS_HW_ENCRYPTION_SW_ONLY, NULL, error); + return _crypto_luks_format (device, cipher, key_size, context, NULL, min_entropy, luks_version, extra, BD_CRYPTO_LUKS_HW_ENCRYPTION_SW_ONLY, NULL, error); +} + +/** + * bd_crypto_luks_format_volume_key: + * @device: a device to format as LUKS + * @cipher: (nullable): cipher specification (type-mode, e.g. "aes-xts-plain64") or %NULL to use the default + * @key_size: size of the volume key in bits or 0 to use the size of @volume_key + * @context: key slot context (passphrase/keyfile...) for this LUKS device + * @volume_key: volume key context (created by %bd_crypto_keyslot_context_new_volume_key) to use + * as the LUKS volume (master) key instead of a randomly generated one + * @min_entropy: minimum random data entropy (in bits) required to format @device as LUKS + * @luks_version: whether to use LUKS v1 or LUKS v2 + * @extra: (nullable): extra arguments for LUKS format creation + * @error: (out) (optional): place to store error (if any) + * + * Formats the given @device as LUKS according to the other parameters given, using the + * provided @volume_key as the LUKS volume (master) key instead of letting cryptsetup + * generate a random one. This is primarily useful for pervasive encryption on IBM Z where + * the volume key is a secure key token generated by the 'zkey' utility and the cipher is + * "paes-xts-plain64". If @key_size is 0, the size of @volume_key is used. The @context is + * used for the key slot (passphrase/key file) as with %bd_crypto_luks_format. + * + * Supported @context types for this function: passphrase, key file + * Supported @volume_key context types for this function: volume key + * + * Returns: whether the given @device was successfully formatted as LUKS or not + * (the @error) contains the error in such cases) + * + * Tech category: %BD_CRYPTO_TECH_LUKS-%BD_CRYPTO_TECH_MODE_CREATE + */ +gboolean bd_crypto_luks_format_volume_key (const gchar *device, const gchar *cipher, guint64 key_size, BDCryptoKeyslotContext *context, BDCryptoKeyslotContext *volume_key, guint64 min_entropy, BDCryptoLUKSVersion luks_version, BDCryptoLUKSExtra *extra, GError **error) { + return _crypto_luks_format (device, cipher, key_size, context, volume_key, min_entropy, luks_version, extra, BD_CRYPTO_LUKS_HW_ENCRYPTION_SW_ONLY, NULL, error); } static gboolean _is_dm_name_valid (const gchar *name, GError **error) { @@ -4125,7 +4197,7 @@ gboolean bd_crypto_opal_format (const gchar *device, const gchar *cipher, guint6 return FALSE; } - return _crypto_luks_format (device, cipher, key_size, context, min_entropy, BD_CRYPTO_LUKS_VERSION_LUKS2, extra, hw_encryption, opal_context, error); + return _crypto_luks_format (device, cipher, key_size, context, NULL, min_entropy, BD_CRYPTO_LUKS_VERSION_LUKS2, extra, hw_encryption, opal_context, error); } #endif diff --git a/src/plugins/crypto.h b/src/plugins/crypto.h index 892866a1b..da2548471 100644 --- a/src/plugins/crypto.h +++ b/src/plugins/crypto.h @@ -294,6 +294,7 @@ gboolean bd_crypto_device_is_luks (const gchar *device, GError **error); const gchar* bd_crypto_luks_status (const gchar *luks_device, GError **error); gboolean bd_crypto_luks_format (const gchar *device, const gchar *cipher, guint64 key_size, BDCryptoKeyslotContext *context, guint64 min_entropy, BDCryptoLUKSVersion luks_version, BDCryptoLUKSExtra *extra,GError **error); +gboolean bd_crypto_luks_format_volume_key (const gchar *device, const gchar *cipher, guint64 key_size, BDCryptoKeyslotContext *context, BDCryptoKeyslotContext *volume_key, guint64 min_entropy, BDCryptoLUKSVersion luks_version, BDCryptoLUKSExtra *extra, GError **error); gboolean bd_crypto_luks_open (const gchar *device, const gchar *name, BDCryptoKeyslotContext *context, gboolean read_only, GError **error); gboolean bd_crypto_luks_open_flags (const gchar *device, const gchar *name, BDCryptoKeyslotContext *context, BDCryptoOpenFlags flags, GError **error); gboolean bd_crypto_luks_close (const gchar *luks_device, GError **error); diff --git a/src/plugins/s390.c b/src/plugins/s390.c index 97f831da8..0af819648 100644 --- a/src/plugins/s390.c +++ b/src/plugins/s390.c @@ -53,11 +53,17 @@ static GMutex deps_check_lock; #define DEPS_DASDFMT 0 #define DEPS_DASDFMT_MASK (1 << DEPS_DASDFMT) -#define DEPS_LAST 1 +#define DEPS_ZKEY 1 +#define DEPS_ZKEY_MASK (1 << DEPS_ZKEY) +#define DEPS_ZKEY_CRYPTSETUP 2 +#define DEPS_ZKEY_CRYPTSETUP_MASK (1 << DEPS_ZKEY_CRYPTSETUP) +#define DEPS_LAST 3 static const UtilDep deps[DEPS_LAST] = { /* dasdfmt doesn't return version info */ {"dasdfmt", NULL, NULL, NULL}, + {"zkey", NULL, NULL, NULL}, + {"zkey-cryptsetup", NULL, NULL, NULL}, }; @@ -104,6 +110,15 @@ gboolean bd_s390_is_tech_avail (BDS390Tech tech, guint64 mode, GError **error) { return check_deps (&avail_deps, DEPS_DASDFMT_MASK, deps, DEPS_LAST, &deps_check_lock, error); else return TRUE; + case BD_S390_TECH_PAES: { + /* pervasive encryption support always requires the 'zkey' utility; querying and + modifying the pervasive encryption setup of an existing LUKS2 volume additionally + requires the 'zkey-cryptsetup' utility */ + guint req_deps = DEPS_ZKEY_MASK; + if (mode & (BD_S390_TECH_MODE_QUERY | BD_S390_TECH_MODE_MODIFY)) + req_deps |= DEPS_ZKEY_CRYPTSETUP_MASK; + return check_deps (&avail_deps, req_deps, deps, DEPS_LAST, &deps_check_lock, error); + } default: g_set_error_literal (error, BD_S390_ERROR, BD_S390_ERROR_TECH_UNAVAIL, "Unknown technology"); return FALSE; @@ -1055,3 +1070,396 @@ gboolean bd_s390_zfcp_offline (const gchar *devno, const gchar *wwpn, const gcha bd_utils_report_finished (progress_id, "Completed"); return TRUE; } + +/** + * bd_s390_zkey_generate: + * @name: name of the secure key to generate in the secure key repository + * @key_type: (nullable): type of the secure key to generate (e.g. "CCA-AESCIPHER") or %NULL for the zkey default + * @keybits: size of the key in bits or 0 for the zkey default + * @volumes: (nullable) (array zero-terminated=1): list of volumes to associate with the key, each given + * in the "volume:dmname" format, or %NULL + * @apqns: (nullable) (array zero-terminated=1): list of cryptographic adapters (APQNs) to associate with + * the key, each given in the "card.domain" format, or %NULL + * @sector_size: sector size in bytes to use with dm-crypt or 0 for the system default + * @dummy_passphrase: whether to generate and associate a dummy passphrase with the key + * @error: (out) (optional): place to store error (if any) + * + * Generates a new secure key for pervasive encryption using the 'zkey' utility and stores it + * in the secure key repository. The key is generated as an XTS key with the 'LUKS2' volume type. + * + * Returns: whether the secure key was successfully generated or not + * + * Tech category: %BD_S390_TECH_PAES-%BD_S390_TECH_MODE_CREATE + */ +gboolean bd_s390_zkey_generate (const gchar *name, const gchar *key_type, guint64 keybits, const gchar **volumes, const gchar **apqns, guint64 sector_size, gboolean dummy_passphrase, const BDExtraArg **extra, GError **error) { + gboolean success = FALSE; + GPtrArray *argv = NULL; + + if (!check_deps (&avail_deps, DEPS_ZKEY_MASK, deps, DEPS_LAST, &deps_check_lock, error)) + return FALSE; + + if (name == NULL || *name == '\0') { + g_set_error_literal (error, BD_S390_ERROR, BD_S390_ERROR_ZKEY, + "Key name must be specified"); + return FALSE; + } + + argv = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (argv, g_strdup ("zkey")); + g_ptr_array_add (argv, g_strdup ("generate")); + g_ptr_array_add (argv, g_strdup ("--name")); + g_ptr_array_add (argv, g_strdup (name)); + + if (key_type != NULL) { + g_ptr_array_add (argv, g_strdup ("--key-type")); + g_ptr_array_add (argv, g_strdup (key_type)); + } + if (keybits != 0) { + g_ptr_array_add (argv, g_strdup ("--keybits")); + g_ptr_array_add (argv, g_strdup_printf ("%"G_GUINT64_FORMAT, keybits)); + } + if (volumes != NULL && *volumes != NULL) { + /* zkey expects a single comma-separated list */ + g_ptr_array_add (argv, g_strdup ("--volumes")); + g_ptr_array_add (argv, g_strjoinv (",", (gchar **) volumes)); + } + if (apqns != NULL && *apqns != NULL) { + /* zkey expects a single comma-separated list */ + g_ptr_array_add (argv, g_strdup ("--apqns")); + g_ptr_array_add (argv, g_strjoinv (",", (gchar **) apqns)); + } + if (sector_size != 0) { + g_ptr_array_add (argv, g_strdup ("--sector-size")); + g_ptr_array_add (argv, g_strdup_printf ("%"G_GUINT64_FORMAT, sector_size)); + } + if (dummy_passphrase) + g_ptr_array_add (argv, g_strdup ("--gen-dummy-passphrase")); + + /* pervasive encryption of LUKS2 volumes always uses XTS secure keys */ + g_ptr_array_add (argv, g_strdup ("--xts")); + g_ptr_array_add (argv, g_strdup ("--volume-type")); + g_ptr_array_add (argv, g_strdup ("LUKS2")); + + g_ptr_array_add (argv, NULL); + + success = bd_utils_exec_and_report_error ((const gchar **) argv->pdata, extra, error); + g_ptr_array_free (argv, TRUE); + + return success; +} + +/** + * bd_s390_zkey_info_free: (skip) + * @info: (nullable): %BDS390ZkeyInfo to free + * + * Frees @info. + */ +void bd_s390_zkey_info_free (BDS390ZkeyInfo *info) { + if (info == NULL) + return; + + g_free (info->name); + g_free (info->description); + g_free (info->key_type); + g_strfreev (info->volumes); + g_strfreev (info->apqns); + g_free (info->key_file_name); + g_free (info->volume_type); + g_free (info->dummy_passphrase); + g_free (info); +} + +/** + * bd_s390_zkey_info_copy: (skip) + * @info: (nullable): %BDS390ZkeyInfo to copy + * + * Creates a new copy of @info. + */ +BDS390ZkeyInfo* bd_s390_zkey_info_copy (BDS390ZkeyInfo *info) { + if (info == NULL) + return NULL; + + BDS390ZkeyInfo *new_info = g_new0 (BDS390ZkeyInfo, 1); + + new_info->name = g_strdup (info->name); + new_info->description = g_strdup (info->description); + new_info->secure_key_size = info->secure_key_size; + new_info->clear_key_size = info->clear_key_size; + new_info->xts = info->xts; + new_info->key_type = g_strdup (info->key_type); + new_info->volumes = g_strdupv (info->volumes); + new_info->apqns = g_strdupv (info->apqns); + new_info->key_file_name = g_strdup (info->key_file_name); + new_info->sector_size = info->sector_size; + new_info->volume_type = g_strdup (info->volume_type); + new_info->dummy_passphrase = g_strdup (info->dummy_passphrase); + + return new_info; +} + +/** + * bd_s390_zkey_list: + * @name: (nullable): name of a single secure key to get information about or %NULL to list all keys + * @error: (out) (optional): place to store error (if any) + * + * Lists the secure keys stored in the secure key repository using the 'zkey' utility. If @name + * is given, only the information about the matching key is returned. + * + * Returns: (array zero-terminated=1) (transfer full): information about the secure keys in the + * repository (an empty list if there are none) or %NULL in case of error + * + * Tech category: %BD_S390_TECH_PAES-%BD_S390_TECH_MODE_QUERY + */ +BDS390ZkeyInfo** bd_s390_zkey_list (const gchar *name, GError **error) { + const gchar *argv[5] = {"zkey", "list", NULL, NULL, NULL}; + guint next = 2; + gchar *output = NULL; + gchar *stderr_data = NULL; + gint status = 0; + gboolean success = FALSE; + GPtrArray *keys = NULL; + gchar **lines = NULL; + BDS390ZkeyInfo *cur_info = NULL; + GPtrArray *cur_list = NULL; /* accumulates a multi-line list value (Volumes/APQNs) */ + gchar ***cur_list_dest = NULL; /* struct member to store the finalized NULL-terminated array in */ + gint value_col = -1; /* column (0-based) where field values start, learned from field lines */ + + if (!check_deps (&avail_deps, DEPS_ZKEY_MASK, deps, DEPS_LAST, &deps_check_lock, error)) + return NULL; + + if (name != NULL && *name != '\0') { + argv[next++] = "--name"; + argv[next++] = name; + } + + /* not using bd_utils_exec_and_capture_output because it treats an empty output + (i.e. no keys in the repository) as an error */ + success = bd_utils_exec_and_capture_output_no_progress (argv, NULL, &output, &stderr_data, &status, error); + if (!success) { + g_free (output); + g_free (stderr_data); + return NULL; + } + if (status != 0) { + g_set_error (error, BD_S390_ERROR, BD_S390_ERROR_ZKEY, + "Failed to list secure keys: %s", stderr_data ? stderr_data : ""); + g_free (output); + g_free (stderr_data); + return NULL; + } + g_free (stderr_data); + + keys = g_ptr_array_new (); + + lines = g_strsplit (output ? output : "", "\n", -1); + g_free (output); + + for (gchar **line_p = lines; *line_p != NULL; line_p++) { + gchar *line = *line_p; + gsize indent = strspn (line, " \t"); + gchar *colon = NULL; + gchar *label = NULL; + gchar *value = NULL; + + /* A multi-value field (Volumes/APQNs) can span several lines: the first item + follows the label, any further items appear on continuation lines that carry + no label and are indented to the value column. Such continuation lines cannot + be recognized by the absence of a colon -- a volume is formatted as + "volume:dmname" and thus contains one -- so they are detected by indentation + instead: everything left of the value column is whitespace. */ + if (cur_list != NULL && value_col >= 0 && (gint) indent >= value_col) { + gchar *item = g_strstrip (g_strdup (line + indent)); + if (*item != '\0') + g_ptr_array_add (cur_list, item); + else + g_free (item); + continue; + } + + /* any other line terminates the current multi-value field */ + if (cur_list != NULL) { + g_ptr_array_add (cur_list, NULL); + *cur_list_dest = (gchar **) g_ptr_array_free (cur_list, FALSE); + cur_list = NULL; + cur_list_dest = NULL; + } + + /* split the line on the first ':' -- the label never contains a colon; lines + without a colon are separators or blank lines */ + colon = strchr (line, ':'); + if (colon == NULL) + continue; + + /* "label : value" -- remember where values start so the continuation lines of + the next multi-value field can be recognized */ + value_col = (gint) (colon - line) + 2; + + label = g_strndup (line, colon - line); + label = g_strstrip (label); + value = g_strdup (colon + 1); + value = g_strstrip (value); + + if (g_strcmp0 (label, "Key") == 0) { + /* the "Key" line starts a new key record */ + cur_info = g_new0 (BDS390ZkeyInfo, 1); + cur_info->name = g_strdup (value); + g_ptr_array_add (keys, cur_info); + } else if (cur_info == NULL) { + /* a field line before any "Key" line -- ignore it */ + } else if (g_strcmp0 (label, "Description") == 0) { + if (*value != '\0') + cur_info->description = g_strdup (value); + } else if (g_strcmp0 (label, "Secure key size") == 0) { + cur_info->secure_key_size = g_ascii_strtoull (value, NULL, 0); + } else if (g_strcmp0 (label, "Clear key size") == 0) { + cur_info->clear_key_size = g_ascii_strtoull (value, NULL, 0); + } else if (g_strcmp0 (label, "XTS type key") == 0) { + cur_info->xts = (g_ascii_strcasecmp (value, "Yes") == 0); + } else if (g_strcmp0 (label, "Key type") == 0) { + cur_info->key_type = g_strdup (value); + } else if (g_strcmp0 (label, "Volumes") == 0) { + /* the value continues on the following continuation lines (one volume each) */ + cur_list = g_ptr_array_new (); + cur_list_dest = &cur_info->volumes; + if (*value != '\0') + g_ptr_array_add (cur_list, g_strdup (value)); + } else if (g_strcmp0 (label, "APQNs") == 0) { + /* the value continues on the following continuation lines (one APQN each) */ + cur_list = g_ptr_array_new (); + cur_list_dest = &cur_info->apqns; + if (*value != '\0') + g_ptr_array_add (cur_list, g_strdup (value)); + } else if (g_strcmp0 (label, "Key file name") == 0) { + cur_info->key_file_name = g_strdup (value); + } else if (g_strcmp0 (label, "Sector size") == 0) { + /* "(system default)" is reported as 0 */ + cur_info->sector_size = g_ascii_strtoull (value, NULL, 0); + } else if (g_strcmp0 (label, "Volume type") == 0) { + cur_info->volume_type = g_strdup (value); + } else if (g_strcmp0 (label, "Dummy passphrase") == 0) { + /* "(none)" means no dummy passphrase is set, otherwise it is a path to the passphrase file */ + if (g_strcmp0 (value, "(none)") != 0) + cur_info->dummy_passphrase = g_strdup (value); + } + + g_free (label); + g_free (value); + } + + /* finalize a multi-value field left open at the end of the output */ + if (cur_list != NULL) { + g_ptr_array_add (cur_list, NULL); + *cur_list_dest = (gchar **) g_ptr_array_free (cur_list, FALSE); + cur_list = NULL; + cur_list_dest = NULL; + } + + g_strfreev (lines); + + g_ptr_array_add (keys, NULL); + return (BDS390ZkeyInfo **) g_ptr_array_free (keys, FALSE); +} + +/** + * bd_s390_zkey_remove: + * @name: name of the secure key to remove from the secure key repository + * @error: (out) (optional): place to store error (if any) + * + * Removes the secure key @name from the secure key repository using the 'zkey' utility. + * + * Returns: whether the secure key was successfully removed or not + * + * Tech category: %BD_S390_TECH_PAES-%BD_S390_TECH_MODE_MODIFY + */ +gboolean bd_s390_zkey_remove (const gchar *name, GError **error) { + /* --force suppresses the interactive y/n confirmation */ + const gchar *argv[6] = {"zkey", "remove", "--name", name, "--force", NULL}; + + if (!check_deps (&avail_deps, DEPS_ZKEY_MASK, deps, DEPS_LAST, &deps_check_lock, error)) + return FALSE; + + if (name == NULL || *name == '\0') { + g_set_error_literal (error, BD_S390_ERROR, BD_S390_ERROR_ZKEY, + "Key name must be specified"); + return FALSE; + } + + return bd_utils_exec_and_report_error (argv, NULL, error); +} + +/** + * bd_s390_zkey_cryptsetup_setvp: + * @device: LUKS2 device to set the verification pattern on + * @key_file: path to a file containing the LUKS passphrase used to unlock a keyslot of @device + * @extra: (nullable) (array zero-terminated=1): extra options for setting the verification pattern + * (right now passed to the 'zkey-cryptsetup' utility) + * @error: (out) (optional): place to store error (if any) + * + * Sets the verification pattern of the secure key in the metadata of the LUKS2 volume @device + * using the 'zkey-cryptsetup' utility. The verification pattern is used to identify the secure + * key associated with the volume. Setting the verification pattern requires unlocking a keyslot + * of @device, so the LUKS passphrase needs to be provided in @key_file (this is a passphrase + * file, not the secure key file). + * + * Returns: whether the verification pattern was successfully set or not + * + * Tech category: %BD_S390_TECH_PAES-%BD_S390_TECH_MODE_MODIFY + */ +gboolean bd_s390_zkey_cryptsetup_setvp (const gchar *device, const gchar *key_file, const BDExtraArg **extra, GError **error) { + const gchar *argv[6] = {"zkey-cryptsetup", "setvp", device, "--key-file", key_file, NULL}; + + if (!check_deps (&avail_deps, DEPS_ZKEY_CRYPTSETUP_MASK, deps, DEPS_LAST, &deps_check_lock, error)) + return FALSE; + + if (device == NULL || *device == '\0') { + g_set_error_literal (error, BD_S390_ERROR, BD_S390_ERROR_ZKEY, + "Device must be specified"); + return FALSE; + } + + if (key_file == NULL || *key_file == '\0') { + g_set_error_literal (error, BD_S390_ERROR, BD_S390_ERROR_ZKEY, + "Key file must be specified"); + return FALSE; + } + + return bd_utils_exec_and_report_error (argv, extra, error); +} + +/** + * bd_s390_zkey_cryptsetup_validate: + * @device: LUKS2 device to validate + * @key_file: path to a file containing the LUKS passphrase used to unlock a keyslot of @device + * @extra: (nullable) (array zero-terminated=1): extra options for the validation + * (right now passed to the 'zkey-cryptsetup' utility) + * @error: (out) (optional): place to store error (if any) + * + * Validates that the LUKS2 volume @device is correctly set up for pervasive encryption using the + * 'zkey-cryptsetup' utility. Validation requires unlocking a keyslot of @device, so the LUKS + * passphrase needs to be provided in @key_file (this is a passphrase file, not the secure key + * file). + * + * Returns: whether the LUKS2 volume @device is correctly set up for pervasive encryption or not + * + * Tech category: %BD_S390_TECH_PAES-%BD_S390_TECH_MODE_QUERY + */ +gboolean bd_s390_zkey_cryptsetup_validate (const gchar *device, const gchar *key_file, const BDExtraArg **extra, GError **error) { + const gchar *argv[6] = {"zkey-cryptsetup", "validate", device, "--key-file", key_file, NULL}; + + if (!check_deps (&avail_deps, DEPS_ZKEY_CRYPTSETUP_MASK, deps, DEPS_LAST, &deps_check_lock, error)) + return FALSE; + + if (device == NULL || *device == '\0') { + g_set_error_literal (error, BD_S390_ERROR, BD_S390_ERROR_ZKEY, + "Device must be specified"); + return FALSE; + } + + if (key_file == NULL || *key_file == '\0') { + g_set_error_literal (error, BD_S390_ERROR, BD_S390_ERROR_ZKEY, + "Key file must be specified"); + return FALSE; + } + + return bd_utils_exec_and_report_error (argv, extra, error); +} diff --git a/src/plugins/s390.h b/src/plugins/s390.h index 1053fe4d8..e67b27e5a 100644 --- a/src/plugins/s390.h +++ b/src/plugins/s390.h @@ -12,18 +12,54 @@ typedef enum { BD_S390_ERROR_FORMAT_FAILED, BD_S390_ERROR_DASDFMT, BD_S390_ERROR_IO, + BD_S390_ERROR_ZKEY, } BDS390Error; typedef enum { BD_S390_TECH_DASD = 0, BD_S390_TECH_ZFCP, + BD_S390_TECH_PAES, } BDS390Tech; typedef enum { BD_S390_TECH_MODE_MODIFY = 1 << 0, BD_S390_TECH_MODE_QUERY = 1 << 1, + BD_S390_TECH_MODE_CREATE = 1 << 2, } BDS390TechMode; +/** + * BDS390ZkeyInfo: + * @name: name of the secure key in the secure key repository; + * @description: (nullable): user provided description of the key or %NULL if none was set; + * @secure_key_size: size of the secure key in bytes; + * @clear_key_size: size of the effective (clear) key in bits; + * @xts: whether the key is an XTS type key or not; + * @key_type: type of the secure key (e.g. "CCA-AESCIPHER"); + * @volumes: (array zero-terminated=1): volumes associated with the key, each in the "volume:dmname" format; + * @apqns: (array zero-terminated=1): cryptographic adapters (APQNs) associated with the key, each in the "card.domain" format; + * @key_file_name: full path to the file holding the secure key; + * @sector_size: sector size in bytes to use with dm-crypt or 0 for the system default; + * @volume_type: volume type the key is to be used with (e.g. "LUKS2"); + * @dummy_passphrase: (nullable): path to the dummy passphrase file associated with the key or %NULL if none is set; + */ +typedef struct BDS390ZkeyInfo { + gchar *name; + gchar *description; + guint64 secure_key_size; + guint64 clear_key_size; + gboolean xts; + gchar *key_type; + gchar **volumes; + gchar **apqns; + gchar *key_file_name; + guint64 sector_size; + gchar *volume_type; + gchar *dummy_passphrase; +} BDS390ZkeyInfo; + +void bd_s390_zkey_info_free (BDS390ZkeyInfo *info); +BDS390ZkeyInfo* bd_s390_zkey_info_copy (BDS390ZkeyInfo *info); + /* * If using the plugin as a standalone library, the following functions should * be called to: @@ -51,4 +87,10 @@ gboolean bd_s390_zfcp_online (const gchar *devno, const gchar *wwpn, const gchar gboolean bd_s390_zfcp_scsi_offline(const gchar *devno, const gchar *wwpn, const gchar *lun, GError **error); gboolean bd_s390_zfcp_offline(const gchar *devno, const gchar *wwpn, const gchar *lun, GError **error); +gboolean bd_s390_zkey_generate (const gchar *name, const gchar *key_type, guint64 keybits, const gchar **volumes, const gchar **apqns, guint64 sector_size, gboolean dummy_passphrase, const BDExtraArg **extra, GError **error); +BDS390ZkeyInfo** bd_s390_zkey_list (const gchar *name, GError **error); +gboolean bd_s390_zkey_remove (const gchar *name, GError **error); +gboolean bd_s390_zkey_cryptsetup_setvp (const gchar *device, const gchar *key_file, const BDExtraArg **extra, GError **error); +gboolean bd_s390_zkey_cryptsetup_validate (const gchar *device, const gchar *key_file, const BDExtraArg **extra, GError **error); + #endif /* BD_S390 */ diff --git a/src/python/gi/overrides/BlockDev.py b/src/python/gi/overrides/BlockDev.py index 03a32fe2d..f5bebe21d 100644 --- a/src/python/gi/overrides/BlockDev.py +++ b/src/python/gi/overrides/BlockDev.py @@ -310,6 +310,12 @@ def crypto_luks_format(device, cipher=None, key_size=0, context=None, min_entrop return _crypto_luks_format(device, cipher, key_size, context, min_entropy, luks_version, extra) __all__.append("crypto_luks_format") +_crypto_luks_format_volume_key = BlockDev.crypto_luks_format_volume_key +@override(BlockDev.crypto_luks_format_volume_key) +def crypto_luks_format_volume_key(device, cipher=None, key_size=0, context=None, volume_key=None, min_entropy=0, luks_version=BlockDev.CryptoLUKSVersion.LUKS2, extra=None): + return _crypto_luks_format_volume_key(device, cipher, key_size, context, volume_key, min_entropy, luks_version, extra) +__all__.append("crypto_luks_format_volume_key") + _crypto_luks_open = BlockDev.crypto_luks_open @override(BlockDev.crypto_luks_open) def crypto_luks_open(device, name, context, read_only=False): diff --git a/tests/Makefile.am b/tests/Makefile.am index a3ad561be..15c7bec00 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -55,6 +55,10 @@ if WITH_NVME PLUGINS += nvme endif +if WITH_S390 +PLUGINS += s390 +endif + if WITH_SMART PLUGINS += smart endif diff --git a/tests/crypto_test.py b/tests/crypto_test.py index b33349a86..f298f2111 100644 --- a/tests/crypto_test.py +++ b/tests/crypto_test.py @@ -268,6 +268,44 @@ def test_luks2_format(self): self.fail("Failed to get pbkdf information from:\n%s %s" % (out, err)) self.assertEqual(m.group(1), "pbkdf2") + @tag_test(TestTags.SLOW, TestTags.CORE) + def test_luks2_format_volume_key(self): + """Verify that formatting device as LUKS 2 with a specified volume key works""" + + volume_key = secrets.token_bytes(64) + pw_ctx = BlockDev.CryptoKeyslotContext(passphrase=PASSWD) + vk_ctx = BlockDev.CryptoKeyslotContext(volume_key=list(volume_key)) + + # key_size (in bits) must match the size of the provided volume key + with self.assertRaisesRegex(GLib.GError, "does not match"): + BlockDev.crypto_luks_format_volume_key(self.loop_devs[0], "aes-xts-plain64", 256, + pw_ctx, vk_ctx, 0, + BlockDev.CryptoLUKSVersion.LUKS2, None) + + # only 'volume key' context type is valid for the volume key argument + with self.assertRaisesRegex(GLib.GError, "Only 'volume key' context type is valid"): + BlockDev.crypto_luks_format_volume_key(self.loop_devs[0], "aes-xts-plain64", 0, + pw_ctx, pw_ctx, 0, + BlockDev.CryptoLUKSVersion.LUKS2, None) + + # format with the volume key, letting key_size default to the volume key size + succ = BlockDev.crypto_luks_format_volume_key(self.loop_devs[0], "aes-xts-plain64", 0, + pw_ctx, vk_ctx, 0, + BlockDev.CryptoLUKSVersion.LUKS2, None) + self.assertTrue(succ) + + # verify the volume key stored in the LUKS header is the one we specified + vk_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, vk_dir) + vk_path = os.path.join(vk_dir, "volume_key") + p = subprocess.run(["cryptsetup", "luksDump", "--dump-volume-key", + "--volume-key-file", vk_path, self.loop_devs[0]], + input=PASSWD.encode("utf-8"), + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self.assertEqual(p.returncode, 0, msg=p.stderr) + with open(vk_path, "rb") as f: + self.assertEqual(f.read(), volume_key) + @tag_test(TestTags.SLOW, TestTags.CORE) def test_luks1_format_pbkdf_options(self): """Verify that formatting device as LUKS 1 with extra PBKDF arguments works""" diff --git a/tests/fake_utils/zkey_list_multiple_apqns/zkey b/tests/fake_utils/zkey_list_multiple_apqns/zkey new file mode 100755 index 000000000..91f846338 --- /dev/null +++ b/tests/fake_utils/zkey_list_multiple_apqns/zkey @@ -0,0 +1,29 @@ +#!/bin/bash + +# fake 'zkey list' output with a single key associated with multiple APQNs (and a +# multi-line verification pattern); the extra APQNs are printed on continuation +# lines indented to the value column + +cat <