From efdd34a56c914a326ec1c0586a2c68d310f663ae Mon Sep 17 00:00:00 2001 From: Ingo Franzki Date: Fri, 14 Feb 2025 10:00:40 +0100 Subject: [PATCH] zkey-cryptsetup: Add new 'convert' command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'convert' command converts a LUKS2 volume that uses a clear volume key and the 'aes' cipher to use a secure volume key and the 'paes' cipher. Optionally, the volume can use the integrity option with LUKS2 using a clear key integrity key. Signed-off-by: Ingo Franzki Reviewed-by: Jorg Schmidbauer Signed-off-by: Jan Höppner --- zkey/pkey.c | 38 ++++- zkey/pkey.h | 5 + zkey/zkey-cryptsetup.1 | 83 ++++++++++ zkey/zkey-cryptsetup.c | 334 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 441 insertions(+), 19 deletions(-) diff --git a/zkey/pkey.c b/zkey/pkey.c index 25deb05a..95ed7c98 100644 --- a/zkey/pkey.c +++ b/zkey/pkey.c @@ -1384,19 +1384,23 @@ int validate_secure_key(int pkey_fd, } /** - * Generate a key verification pattern of a secure key by encrypting the all + * Generate a key verification pattern of a secure AES key by encrypting the all * zero message with the secure key using the AF_ALG interface * * @param[in] key the secure key token * @param[in] key_size the size of the secure key * @param[in] vp buffer where the verification pattern is returned * @param[in] vp_len the size of the buffer + * @param[in] cipher the cipher to use ('cbc([p]aes)' or 'xts([p]aes)'), + * or null to determine it from the secure key. * @param[in] verbose if true, verbose messages are printed * * @returns 0 on success, a negative errno in case of an error */ -int generate_key_verification_pattern(const u8 *key, size_t key_size, - char *vp, size_t vp_len, bool verbose) +int generate_aes_key_verification_pattern(const u8 *key, size_t key_size, + char *vp, size_t vp_len, + const char *cipher, + bool verbose) { int tfmfd = -1, opfd = -1, rc = 0, retry_count = 0; char null_msg[ENC_ZERO_LEN]; @@ -1429,8 +1433,13 @@ int generate_key_verification_pattern(const u8 *key, size_t key_size, goto out; } - snprintf((char *)sa.salg_name, sizeof(sa.salg_name), "%s(paes)", - is_xts_key(key, key_size) ? "xts" : "cbc"); + if (cipher != NULL) { + util_strlcpy((char *)sa.salg_name, cipher, + sizeof(sa.salg_name)); + } else { + snprintf((char *)sa.salg_name, sizeof(sa.salg_name), "%s(paes)", + is_xts_key(key, key_size) ? "xts" : "cbc"); + } tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0); if (tfmfd < 0) { @@ -1541,6 +1550,25 @@ out: return rc; } +/** + * Generate a key verification pattern of a secure key by encrypting the all + * zero message with the secure key using the AF_ALG interface + * + * @param[in] key the secure key token + * @param[in] key_size the size of the secure key + * @param[in] vp buffer where the verification pattern is returned + * @param[in] vp_len the size of the buffer + * @param[in] verbose if true, verbose messages are printed + * + * @returns 0 on success, a negative errno in case of an error + */ +int generate_key_verification_pattern(const u8 *key, size_t key_size, + char *vp, size_t vp_len, bool verbose) +{ + return generate_aes_key_verification_pattern(key, key_size, vp, vp_len, + NULL, verbose); +} + int get_master_key_verification_pattern(const u8 *key, size_t key_size, u8 *mkvp, bool UNUSED(verbose)) { diff --git a/zkey/pkey.h b/zkey/pkey.h index ce3bd8bd..71061e8d 100644 --- a/zkey/pkey.h +++ b/zkey/pkey.h @@ -351,6 +351,11 @@ int validate_secure_key(int pkey_fd, int generate_key_verification_pattern(const u8 *key, size_t key_size, char *vp, size_t vp_len, bool verbose); +int generate_aes_key_verification_pattern(const u8 *key, size_t key_size, + char *vp, size_t vp_len, + const char *cipher, + bool verbose); + int get_master_key_verification_pattern(const u8 *key, size_t key_size, u8 *mkvp, bool verbose); diff --git a/zkey/zkey-cryptsetup.1 b/zkey/zkey-cryptsetup.1 index 20a3cd20..caf8d758 100644 --- a/zkey/zkey-cryptsetup.1 +++ b/zkey/zkey-cryptsetup.1 @@ -355,6 +355,73 @@ relevance. . . . +.SS "Convert a clear-key LUKS2 volume to use a secure AES volume key" +. +.B zkey\-cryptsetup +.BR convert | conv +.I device +.BR \-\-volume\-key\-file | \-m +.IR file\-name +.RB [ \-\-key\-file | \-d +.IR file\-name ] +.RB [ \-\-keyfile\-offset | \-o +.IR bytes ] +.RB [ \-\-keyfile\-size | \-l +.IR bytes ] +.RB [ \-\-tries | \-T +.IR number ] +.RB [ \-\-batch\-mode | \-q ] +.RB [ \-\-verbose | \-V ] +.RB [ \-\-debug | \-D ] +.PP +Use the +.B convert +command to convert a clear-key \fBLUKS2\fP volume to use a secure volume key +and the \fBpaes\fP cipher. You must provide a secure volume key that uses +the exact same effective key as the current volume key. The volume must not be +open when a conversion is performed, otherwise it fails. +.PP +.B ATTENTION: +It is strongly recommended to take a LUKS header backup using \fBcryptsetup +luksHeaderBackup\fP before converting a volume. The volume might not be +recoverable after a failing conversion, if you don't have a LUKS header backup. +.PP +Specify the secure key file with option +.B \-\-volume\-key\-file +to convert the volume to use this secure key as the new volume key. +.PP +To open a key slot contained in the LUKS2 header of the volume, a passphrase is +required. You are prompted for the passphrase, unless option +.B \-\-key\-file +is specified. Option +.B \-\-tries +specifies how often a passphrase can be re-entered. When option +.B \-\-key\-file +is specified, the passphrase is read from the specified file. You can specify +options +.B \-\-keyfile\-offset +and +.B \-\-keyfile\-size +to control which part of the key file is used as passphrase. These options +behave in the same way the same as with \fBcryptsetup\fP. +.PP +The +.B convert +command creates a new key slot with the specified secure AES volume key. +The new key slot uses the same password based key derivation function as +the key slot that is used to open the volume. LUKS2 volumes typically default to +.B Argon2i +as password based key derivation function, +but this might cause out-of-memory errors when multiple encrypted volumes are +unlocked automatically at boot through /etc/crypttab. Because PAES +uses secure AES keys as volume keys, the security of the key derivation +function used to encrypt the volume key in the LUKS key slots is of less +relevance. Use \fBcryptsetup luksConvertKey --pbkdf pbkdf2\fP to convert +the key slot(s) to use \fBPBKDF2\fP as password based key derivation function +after the conversion. +. +. +. . .SH OPTIONS . @@ -415,6 +482,22 @@ Suppresses all confirmation questions. Use with care! . . . +.SS "Options for the convert command" +.TP +.BR \-m ", " \-\-volume\-key\-file\~\fIfile\-name\fP +Specifies the name of a file containing the secure AES key that is used as +volume key for the converted volume. +.TP +.BR \-\-master\-key\-file\~\fIfile\-name\fP +Alias for the +.BR \-\-volume\-key\-file\~\fIfile\-name\fP +option. +.TP +.BR \-q ", " \-\-batch\-mode +Suppresses all confirmation questions. Use with care! +. +. +. .SS "Options for supplying the passphrase" .TP .BR \-d ", " \-\-key\-file\~\fIfile\-name\fP diff --git a/zkey/zkey-cryptsetup.c b/zkey/zkey-cryptsetup.c index 566cc076..e81e88a5 100644 --- a/zkey/zkey-cryptsetup.c +++ b/zkey/zkey-cryptsetup.c @@ -82,7 +82,7 @@ static const struct util_prg prg = { { .owner = "IBM Corp.", .pub_first = 2018, - .pub_last = 2024, + .pub_last = 2025, }, UTIL_PRG_COPYRIGHT_END } @@ -125,6 +125,7 @@ static struct zkey_cryptsetup_globals { #define COMMAND_VALIDATE "validate" #define COMMAND_SETVP "setvp" #define COMMAND_SETKEY "setkey" +#define COMMAND_CONVERT "convert" #define ZKEY_CRYPTSETUP_COMMAND_MAX_LEN 10 @@ -251,6 +252,33 @@ static struct util_opt opt_vec[] = { .desc = "Suppresses all confirmation questions. Use with care!", .command = COMMAND_SETKEY, }, + /***********************************************************/ + { + .flags = UTIL_OPT_FLAG_SECTION, + .desc = "OPTIONS", + .command = COMMAND_CONVERT, + }, + { + .option = {"volume-key-file", required_argument, NULL, 'm'}, + .argument = "FILE-NAME", + .desc = "Specifies the name of a file containing the secure " + "AES key that is set as new volume key", + .command = COMMAND_CONVERT, + }, + { + .option = {"master-key-file", required_argument, NULL, 'm'}, + .argument = "FILE-NAME", + .desc = "Alias for the '--volume-key-file'|'-m' option", + .command = COMMAND_CONVERT, + .flags = UTIL_OPT_FLAG_NOSHORT, + }, + OPT_PASSPHRASE_ENTRY(COMMAND_CONVERT), + { + .option = {"batch-mode", 0, NULL, 'q'}, + .desc = "Suppresses all confirmation questions. Use with care!", + .command = COMMAND_CONVERT, + }, + /***********************************************************/ { .flags = UTIL_OPT_FLAG_SECTION, @@ -288,12 +316,14 @@ struct zkey_cryptsetup_command { int has_options; char *pos_arg; int open_device; + const char *expected_cipher; }; static int command_reencipher(void); static int command_validate(void); static int command_setvp(void); static int command_setkey(void); +static int command_convert(void); static struct zkey_cryptsetup_command zkey_cryptsetup_commands[] = { { @@ -308,6 +338,7 @@ static struct zkey_cryptsetup_command zkey_cryptsetup_commands[] = { .has_options = 1, .pos_arg = "DEVICE", .open_device = 1, + .expected_cipher = "paes", }, { .command = COMMAND_VALIDATE, @@ -320,6 +351,7 @@ static struct zkey_cryptsetup_command zkey_cryptsetup_commands[] = { .has_options = 1, .pos_arg = "DEVICE", .open_device = 1, + .expected_cipher = "paes", }, { .command = COMMAND_SETVP, @@ -334,6 +366,7 @@ static struct zkey_cryptsetup_command zkey_cryptsetup_commands[] = { .has_options = 1, .pos_arg = "DEVICE", .open_device = 1, + .expected_cipher = "paes", }, { .command = COMMAND_SETKEY, @@ -346,6 +379,22 @@ static struct zkey_cryptsetup_command zkey_cryptsetup_commands[] = { .has_options = 1, .pos_arg = "DEVICE", .open_device = 1, + .expected_cipher = "paes", + }, + { + .command = COMMAND_CONVERT, + .abbrev_len = 4, + .function = command_convert, + .need_pkey_device = 1, + .short_desc = "Convert a clear-key LUKS2 volume to use a " + "secure volume key", + .long_desc = "Convert a LUKS2 volume that uses a clear volume " + "key and the 'aes' cipher to use a secure volume " + "key and the 'paes' cipher", + .has_options = 1, + .pos_arg = "DEVICE", + .open_device = 1, + .expected_cipher = "aes", }, { .command = NULL } }; @@ -1149,7 +1198,8 @@ static int put_vp_token(struct crypt_device *cd, int token, /* * Open the LUKS2 device */ -static int open_device(const char *device, struct crypt_device **cd) +static int open_device(const char *device, struct crypt_device **cd, + const char *expected_cipher) { const struct crypt_pbkdf_type pbkdf2 = { .type = CRYPT_KDF_PBKDF2, @@ -1180,9 +1230,9 @@ static int open_device(const char *device, struct crypt_device **cd) goto out; } - if (strcmp(crypt_get_cipher(cdev), "paes") != 0) { - warnx("Device '%s' is not encrypted using the 'paes' cipher", - device); + if (strcmp(crypt_get_cipher(cdev), expected_cipher) != 0) { + warnx("Device '%s' is not encrypted using the '%s' cipher", + device, expected_cipher); rc = -EINVAL; goto out; } @@ -1386,7 +1436,7 @@ static int check_keysize_and_cipher_mode(const u8 *key, size_t keysize) static int open_keyslot(int keyslot, char **key, size_t *keysize, size_t *integrity_keysize, char **password, size_t *password_len, - const char *prompt) + const char *prompt, bool no_keysize_check) { struct crypt_params_integrity ip = { 0 }; #ifdef HAVE_CRYPT_KEYSLOT_GET_PBKDF @@ -1414,10 +1464,13 @@ static int open_keyslot(int keyslot, char **key, size_t *keysize, pr_verbose("Integrity key size: %u", ip.integrity_key_size); } - rc = check_keysize_and_cipher_mode(NULL, - vkeysize - ip.integrity_key_size); - if (rc != 0) - return rc; + if (!no_keysize_check) { + rc = check_keysize_and_cipher_mode(NULL, + vkeysize - + ip.integrity_key_size); + if (rc != 0) + return rc; + } vkey = malloc(vkeysize); if (vkey == NULL) { @@ -1528,7 +1581,7 @@ static int validate_keyslot(int keyslot, char **key, size_t *keysize, int rc, is_old; rc = open_keyslot(keyslot, &vkey, &vkeysize, &ikeysize, - password, password_len, prompt); + password, password_len, prompt, false); if (rc < 0) return rc; @@ -1570,6 +1623,27 @@ out: return rc; } +/* + * Return the number of active key slots in a LUKS2 volume. + */ +static int count_active_keyslots(struct crypt_device *cd) +{ + crypt_keyslot_info info; + int i, n; + + for (i = 0, n = 0; ; i++) { + info = crypt_keyslot_status(cd, i); + if (info == CRYPT_SLOT_INVALID) + break; + if (info < CRYPT_SLOT_ACTIVE || info > CRYPT_SLOT_ACTIVE_LAST) + continue; + + n++; + } + + return n; +} + /* * Prepares for a re-enciphering of a secure volume key. Dependent on the * options specified by the user and the state of the volume key, it starts @@ -2002,7 +2076,7 @@ static int command_validate(void) util_asprintf(&prompt, "Enter passphrase for '%s': ", g.pos_arg); rc = open_keyslot(CRYPT_ANY_SLOT, &key, &keysize, &integrity_keysize, - NULL, NULL, prompt); + NULL, NULL, prompt, false); free(prompt); if (rc < 0) goto out; @@ -2228,7 +2302,7 @@ static int command_setkey(void) util_asprintf(&prompt, "Enter passphrase for '%s': ", g.pos_arg); rc = open_keyslot(CRYPT_ANY_SLOT, &key, &keysize, &integrity_keysize, - &password, &password_len, prompt); + &password, &password_len, prompt, false); free(prompt); if (rc < 0) goto out; @@ -2337,6 +2411,238 @@ out: } +/* + * Command handler for 'convert'. + * + * Convert a LUKS2 volume that uses a clear volume key and the 'aes' cipher + * to use a secure volume key and the 'paes' cipher. + */ +static int command_convert(void) +{ + struct crypt_params_integrity ip_new = { 0 }; + struct crypt_params_integrity ip = { 0 }; + struct crypt_params_luks2 luks2 = { 0 }; + char new_vp[VERIFICATION_PATTERN_LEN]; + struct crypt_device *new_cd = NULL; + char vp[VERIFICATION_PATTERN_LEN]; + struct vp_token vp_tok = { 0 }; + size_t integrity_keysize = 0; + size_t password_len = 0; + size_t newekey_size = 0; + size_t newkey_size = 0; + char *password = NULL; + size_t ekeysize = 0; + size_t keysize = 0; + u8 *newkey = NULL; + int num_keyslots; + u8 *key = NULL; + int is_old_mk; + char *prompt; + int is_xts; + char *msg; + int rc; + + /* Check preconditions */ + if (strcmp(crypt_get_cipher_mode(g.cd), "xts-plain64") != 0 && + strcmp(crypt_get_cipher_mode(g.cd), "cbc-plain64") != 0) { + warnx("The volume uses cipher mode '%s' and thus can not be " + "converted to use the 'paes' cipher", + crypt_get_cipher_mode(g.cd)); + return EXIT_FAILURE; + } + + is_xts = (strncmp(crypt_get_cipher_mode(g.cd), "xts-", 4) == 0); + + /* Read new volume key and validate it */ + if (g.volume_key_file == NULL) { + misc_print_required_parm("--volume-key-file/-m"); + return EXIT_FAILURE; + } + + newkey = read_secure_key(g.volume_key_file, &newkey_size, g.verbose); + if (newkey == NULL) + return EXIT_FAILURE; + + rc = crypt_get_integrity_info(g.cd, &ip); + if (rc == 0) + integrity_keysize = ip.integrity_key_size; + + newekey_size = newkey_size - integrity_keysize; + + rc = check_keysize_and_cipher_mode(newkey, newekey_size); + if (rc != 0) + goto out; + + rc = validate_secure_key(g.pkey_fd, newkey, newekey_size, NULL, + &is_old_mk, NULL, g.verbose); + if (rc != 0) { + warnx("The secure key in file '%s' is not valid", + g.volume_key_file); + goto out; + } + + if (is_secure_key(newkey, newekey_size) && + is_old_mk) { + util_asprintf(&msg, "The secure key in file '%s' is " + "enciphered with the master key in the OLD " + "master key register. Do you want to set this " + "key as the new volume key anyway [y/N]?", + g.volume_key_file); + util_print_indented(msg, 0); + free(msg); + + if (!_prompt_for_yes()) { + warnx("Device '%s' is left unchanged", g.pos_arg); + rc = -EINVAL; + goto out; + } + } + + rc = generate_key_verification_pattern(newkey, newekey_size, + new_vp, sizeof(new_vp), + g.verbose); + if (rc != 0) { + warnx("Failed to generate the verification pattern: %s", + strerror(-rc)); + warnx("Make sure that kernel module 'paes_s390' is loaded and " + "that the 'paes' cipher is available"); + goto out; + } + + /* Get current (clear) volume key from LUKS2 header */ + util_asprintf(&prompt, "Enter passphrase for '%s': ", g.pos_arg); + rc = open_keyslot(CRYPT_ANY_SLOT, (char **)&key, &keysize, NULL, + &password, &password_len, prompt, true); + free(prompt); + if (rc < 0) + goto out; + + ekeysize = keysize - integrity_keysize; + + if (integrity_keysize > 0 && + memcmp(newkey + newekey_size, key + ekeysize, + integrity_keysize) != 0) { + warnx("The secure key in file '%s' contains a different " + "integrity key (i.e. the last %lu bytes of the key) than " + "the current volume key.", g.volume_key_file, + integrity_keysize); + rc = -EINVAL; + goto out; + } + + rc = generate_aes_key_verification_pattern(key, ekeysize, + vp, sizeof(vp), + is_xts ? + "xts(aes)" : "cbc(aes)", + g.verbose); + if (rc != 0) { + warnx("Failed to generate the verification pattern: %s", + strerror(-rc)); + goto out; + } + + if (strcmp(vp, new_vp) != 0) { + warnx("The verification patterns of the new and old " + "volume keys do not match"); + rc = -EINVAL; + goto out; + } + + num_keyslots = count_active_keyslots(g.cd); + pr_verbose("Number of keyslots before conversion: %d", num_keyslots); + + util_print_indented("WARNING: It is strongly recommended to take a " + "LUKS header backup using 'cryptsetup " + "luksHeaderBackup' before continuing. The volume " + "might not be recoverable after a failing " + "conversion, if you don't have a LUKS header " + "backup.", 0); + util_asprintf(&msg, "Do you really want to convert device '%s' to " + "use a secure volume key? [y/N]?", g.pos_arg); + util_print_indented(msg, 0); + free(msg); + + if (!_prompt_for_yes()) { + warnx("Device '%s' is left unchanged", g.pos_arg); + rc = -EINVAL; + goto out; + } + + /* Open the device freshly for crypt_format */ + rc = crypt_init(&new_cd, g.pos_arg); + if (rc != 0) { + warnx("Failed to open device '%s': %s", g.pos_arg, + strerror(-rc)); + goto out; + } + + crypt_set_log_callback(new_cd, cryptsetup_log, NULL); + + /* Re-format the device with the same settings, but using 'paes' */ +#ifdef CRYPT_ACTIVATE_ERROR_AS_CORRUPTION /* Indicates libcryptsetup > 2.7.5 */ + ip_new.integrity_key_size = integrity_keysize; +#endif + + luks2.pbkdf = crypt_get_pbkdf_type(g.cd); + luks2.integrity = integrity_keysize > 0 ? ip.integrity : NULL; + luks2.integrity_params = integrity_keysize > 0 ? &ip_new : NULL; + luks2.data_alignment = 0; + luks2.data_device = NULL; + luks2.sector_size = crypt_get_sector_size(g.cd); + luks2.label = crypt_get_label(g.cd); + luks2.subsystem = crypt_get_subsystem(g.cd); + + rc = crypt_set_data_offset(new_cd, crypt_get_data_offset(g.cd)); + if (rc != 0) { + warnx("Failed to set data size: %s", strerror(-rc)); + goto out; + } + rc = crypt_format(new_cd, CRYPT_LUKS2, "paes", + crypt_get_cipher_mode(g.cd), crypt_get_uuid(g.cd), + (char *)newkey, newkey_size, &luks2); + if (rc != 0) { + warnx("Failed to reformat the LUKS header: %s", + strerror(-rc)); + goto out; + } + + /* Add a key slot with the current password */ + rc = crypt_keyslot_add_by_volume_key(new_cd, CRYPT_ANY_SLOT, NULL, 0, + password, password_len); + if (rc != 0) { + warnx("Failed to add a key slotr: %s", strerror(-rc)); + goto out; + } + + /* Add the verification pattern token */ + memcpy(vp_tok.verification_pattern, new_vp, + sizeof(vp_tok.verification_pattern)); + + rc = put_vp_token(new_cd, -1, &vp_tok); + if (rc < 0) + goto out; + + printf("The device '%s' has been successfully converted\n", g.pos_arg); + if (num_keyslots > 1) { + util_print_indented("\nWARNING: Before converting, the " + "volume's LUKS header had multiple active " + "key slots with the same key, but " + "different passwords. Use 'cryptsetup " + "luksAddKey' if you need more than one key " + "slot.", 0); + } + +out: + secure_free(password, password_len); + secure_free(newkey, newkey_size); + secure_free(key, keysize); + + if (new_cd != NULL) + crypt_free(new_cd); + + return rc < 0 ? EXIT_FAILURE : EXIT_SUCCESS; +} + static bool is_command(struct zkey_cryptsetup_command *command, const char *str) { char command_str[ZKEY_CRYPTSETUP_COMMAND_STR_LEN]; @@ -2535,7 +2841,7 @@ int main(int argc, char *argv[]) goto out; } - rc = open_device(g.pos_arg, &g.cd); + rc = open_device(g.pos_arg, &g.cd, command->expected_cipher); if (rc != 0) { g.cd = NULL; rc = EXIT_FAILURE;