zkey-cryptsetup: Add new 'convert' command

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 <ifranzki@linux.ibm.com>
Reviewed-by: Jorg Schmidbauer <jschmidb@de.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
This commit is contained in:
Ingo Franzki
2025-02-14 10:00:40 +01:00
committed by Jan Höppner
parent c80a0c8af0
commit efdd34a56c
4 changed files with 441 additions and 19 deletions

View File

@@ -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))
{

View File

@@ -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);

View File

@@ -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

View File

@@ -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;