From 15b1c334dff04708e2ba6ba712a1474f0128563c Mon Sep 17 00:00:00 2001 From: Vitaly Kuznetsov Date: Mon, 12 Jan 2026 17:05:59 +0100 Subject: [PATCH 1/5] repart: Generate fstab and crypttab late The immediate need for the change is to allow to capture the expected LUKS volume key hash and record it to the generated crypttab but it also seems to make sense to not generate crypttab/fstab before we know that partition creation succeeded as fstab/crypttab entries are bogus otherwise. --- src/repart/repart.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/repart/repart.c b/src/repart/repart.c index 19bd3441ca..7c19f10287 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -10719,14 +10719,6 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - r = context_fstab(context); - if (r < 0) - return r; - - r = context_crypttab(context); - if (r < 0) - return r; - r = context_update_verity_size(context); if (r < 0) return r; @@ -10782,6 +10774,14 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; + r = context_fstab(context); + if (r < 0) + return r; + + r = context_crypttab(context); + if (r < 0) + return r; + (void) context_dump(context, /* late= */ true); context_disarm_auto_removal(context); From f99f843f24caae612a2727bbe7511db2a38944da Mon Sep 17 00:00:00 2001 From: Vitaly Kuznetsov Date: Mon, 19 Jan 2026 13:42:34 +0100 Subject: [PATCH 2/5] tpm2-util: make tpm2_pcr_extend_bytes() declaration match its implementation tpm2_pcr_extend_bytes() has differences in parameter names between the declaration and the implementation, in particular, 'event' in the header is named 'event_type' in the implementation. --- src/shared/tpm2-util.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 1dc3326b30..6827719e86 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -151,7 +151,7 @@ typedef enum Tpm2UserspaceEventType { DECLARE_STRING_TABLE_LOOKUP(tpm2_userspace_event_type, Tpm2UserspaceEventType); -int tpm2_pcr_extend_bytes(Tpm2Context *c, char **banks, unsigned pcr_index, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event, const char *description); +int tpm2_pcr_extend_bytes(Tpm2Context *c, char **banks, unsigned pcr_index, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event_type, const char *description); int tpm2_nvpcr_get_index(const char *name, uint32_t *ret); int tpm2_nvpcr_extend_bytes(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event_type, const char *description); int tpm2_nvpcr_acquire_anchor_secret(struct iovec *ret, bool sync_secondary); From 69fab29ec4b9ce29e4494aff22ae8a17784021c8 Mon Sep 17 00:00:00 2001 From: Vitaly Kuznetsov Date: Wed, 14 Jan 2026 09:51:24 +0100 Subject: [PATCH 3/5] cryptsetup: Add fixate-volume-key option to pin the expected volume key hash The expected hash (SHA265 HMAC signature) uses the exact same algorithm which is used to calculate sha256 PCR bank digest when 'tpm2-measure-pcr=' is used. --- man/crypttab.xml | 15 ++++++++ src/cryptsetup/cryptsetup.c | 67 ++++++++++++++++++++++++++++-------- src/shared/cryptsetup-util.c | 61 ++++++++++++++++++++++++++++++++ src/shared/cryptsetup-util.h | 3 ++ 4 files changed, 131 insertions(+), 15 deletions(-) diff --git a/man/crypttab.xml b/man/crypttab.xml index a8a8242a62..dbbeb3fd5b 100644 --- a/man/crypttab.xml +++ b/man/crypttab.xml @@ -1012,6 +1012,21 @@ + + + + Pin the expected hash of the volume key. + + In certain cases, e.g. for LUKS volumes where the key is sealed to the TPM2, this may be required + to provide a guarantee that the volume being attached is the volume which was previously created. + can be used to set the expected volume key hash and refuse to attach the volume + if it has a different one. The expected hash matches the digest which is measured to the sha256 PCR bank of the + TPM2 when is used. + + + + + At early boot and when the system manager configuration is diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index e56941adf2..f715494418 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -126,6 +126,7 @@ static char *arg_tpm2_measure_keyslot_nvpcr = NULL; static char *arg_link_keyring = NULL; static char *arg_link_key_type = NULL; static char *arg_link_key_description = NULL; +static char *arg_fixate_volume_key = NULL; STATIC_DESTRUCTOR_REGISTER(arg_cipher, freep); STATIC_DESTRUCTOR_REGISTER(arg_hash, freep); @@ -143,6 +144,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_tpm2_pcrlock, freep); STATIC_DESTRUCTOR_REGISTER(arg_link_keyring, freep); STATIC_DESTRUCTOR_REGISTER(arg_link_key_type, freep); STATIC_DESTRUCTOR_REGISTER(arg_link_key_description, freep); +STATIC_DESTRUCTOR_REGISTER(arg_fixate_volume_key, freep); static const char* const passphrase_type_table[_PASSPHRASE_TYPE_MAX] = { [PASSPHRASE_REGULAR] = "passphrase", @@ -651,6 +653,11 @@ static int parse_one_option(const char *option) { #else log_error("Build lacks libcryptsetup support for linking volume keys in user specified kernel keyrings upon device activation, ignoring: %s", option); #endif + } else if ((val = startswith(option, "fixate-volume-key="))) { + r = free_and_strdup(&arg_fixate_volume_key, val); + if (r < 0) + return log_oom(); + } else if (!streq(option, "x-initrd.attach")) log_warning("Encountered unknown /etc/crypttab option '%s', ignoring.", option); @@ -1050,24 +1057,29 @@ static int measure_volume_key( * unprotected direct hash of the secret volume key over the wire to the TPM. Hence let's instead * send a HMAC signature instead. */ - _cleanup_free_ char *escaped = NULL; - escaped = xescape(name, ":"); /* avoid ambiguity around ":" once we join things below */ - if (!escaped) - return log_oom(); + _cleanup_free_ char *prefix = NULL; - _cleanup_free_ char *s = NULL; - s = strjoin("cryptsetup:", escaped, ":", strempty(crypt_get_uuid(cd))); - if (!s) - return log_oom(); + /* Note: what is extended to the SHA256 bank here must match the expected hash of 'fixate-volume-key=' + * calculated by cryptsetup_get_volume_key_id(). */ + r = cryptsetup_get_volume_key_prefix(cd, name, &prefix); + if (r) + return log_error_errno(r, "Could not verify pcr banks: %m"); - r = tpm2_pcr_extend_bytes(c, l ?: arg_tpm2_measure_banks, arg_tpm2_measure_pcr, &IOVEC_MAKE_STRING(s), &IOVEC_MAKE(volume_key, volume_key_size), TPM2_EVENT_VOLUME_KEY, s); + r = tpm2_pcr_extend_bytes( + c, + /* banks= */ l ?: arg_tpm2_measure_banks, + /* pcr_index = */ arg_tpm2_measure_pcr, + /* data = */ &IOVEC_MAKE_STRING(prefix), + /* secret = */ &IOVEC_MAKE(volume_key, volume_key_size), + /* event_type = */ TPM2_EVENT_VOLUME_KEY, + /* description = */ prefix); if (r < 0) return log_error_errno(r, "Could not extend PCR: %m"); log_struct(LOG_INFO, LOG_MESSAGE_ID(SD_MESSAGE_TPM_PCR_EXTEND_STR), - LOG_MESSAGE("Successfully extended PCR index %u with '%s' and volume key (banks %s).", arg_tpm2_measure_pcr, s, joined), - LOG_ITEM("MEASURING=%s", s), + LOG_MESSAGE("Successfully extended PCR index %u with '%s' and volume key (banks %s).", arg_tpm2_measure_pcr, prefix, joined), + LOG_ITEM("MEASURING=%s", prefix), LOG_ITEM("PCR=%u", arg_tpm2_measure_pcr), LOG_ITEM("BANKS=%s", joined)); @@ -1173,12 +1185,36 @@ static int measured_crypt_activate_by_volume_key( /* A wrapper around crypt_activate_by_volume_key() which also measures to a PCR if that's requested. */ + /* First, check if volume key digest matches the expectation. */ + if (arg_fixate_volume_key) { + _cleanup_free_ char *key_id = NULL; + + r = cryptsetup_get_volume_key_id( + cd, + /* volume_name= */ name, + /* volume_key= */ volume_key, + /* volume_key_size= */ volume_key_size, + /* ret= */ &key_id); + if (r < 0) + return log_error_errno(r, "Failed to get volume key id."); + + if (!streq(arg_fixate_volume_key, key_id)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Volume key id: '%s' does not match the expectation: '%s'.", + key_id, arg_fixate_volume_key); + } + r = crypt_activate_by_volume_key(cd, name, volume_key, volume_key_size, flags); if (r == -EEXIST) /* volume is already active */ return log_external_activation(r, name); if (r < 0) return r; + if (arg_tpm2_measure_pcr == UINT_MAX) { + log_debug("Not measuring volume key, deactivated."); + return 0; + } + if (volume_key_size > 0) (void) measure_volume_key(cd, name, volume_key, volume_key_size); /* OK if fails */ else @@ -1204,14 +1240,12 @@ static int measured_crypt_activate_by_passphrase( assert(cd); /* A wrapper around crypt_activate_by_passphrase() which also measures to a PCR if that's - * requested. Note that we need the volume key for the measurement, and + * requested. Note that we may need the volume key for the measurement and/or for the comparison, and * crypt_activate_by_passphrase() doesn't give us access to this. Hence, we operate indirectly, and * retrieve the volume key first, and then activate through that. */ - if (arg_tpm2_measure_pcr == UINT_MAX) { - log_debug("Not measuring volume key, deactivated."); + if (arg_tpm2_measure_pcr == UINT_MAX && !arg_fixate_volume_key) goto shortcut; - } r = crypt_get_volume_key_size(cd); if (r < 0) @@ -1453,6 +1487,9 @@ static bool use_token_plugins(void) { return false; if (arg_tpm2_measure_keyslot_nvpcr) return false; + /* Volume key is also needed if the expected key id is set */ + if (arg_fixate_volume_key) + return false; #endif /* Disable tokens if we're in FIDO2 mode with manual parameters. */ diff --git a/src/shared/cryptsetup-util.c b/src/shared/cryptsetup-util.c index 5d99edd52d..a766a92b20 100644 --- a/src/shared/cryptsetup-util.c +++ b/src/shared/cryptsetup-util.c @@ -7,6 +7,9 @@ #include "alloc-util.h" #include "cryptsetup-util.h" #include "dlfcn-util.h" +#include "escape.h" +#include "hexdecoct.h" +#include "hmac.h" #include "log.h" #include "parse-util.h" #include "string-util.h" @@ -198,6 +201,64 @@ int cryptsetup_add_token_json(struct crypt_device *cd, sd_json_variant *v) { return 0; } + +int cryptsetup_get_volume_key_prefix( + struct crypt_device *cd, + const char *volume_name, + char **ret) { + + _cleanup_free_ char *volume = NULL; + const char *uuid; + char *s; + + uuid = sym_crypt_get_uuid(cd); + if (!uuid) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to get LUKS UUID."); + + if (volume_name) + /* avoid ambiguity around ":" once we join things below */ + volume = xescape(volume_name, ":"); + else + volume = strjoin("luks-", uuid); + if (!volume) + return log_oom_debug(); + + s = strjoin("cryptsetup:", volume, ":", uuid); + if (!s) + return log_oom_debug(); + + *ret = s; + + return 0; +} + +/* The hash must match what measure_volume_key() extends to the SHA256 bank of the TPM2. */ +int cryptsetup_get_volume_key_id( + struct crypt_device *cd, + const char *volume_name, + const void *volume_key, + size_t volume_key_size, + char **ret) { + + _cleanup_free_ char *prefix = NULL; + uint8_t digest[SHA256_DIGEST_SIZE]; + char *hex; + int r; + + r = cryptsetup_get_volume_key_prefix(cd, volume_name, &prefix); + if (r < 0) + return log_debug_errno(r, "Failed to get LUKS volume key prefix."); + + hmac_sha256(volume_key, volume_key_size, prefix, strlen(prefix), digest); + + hex = hexmem(digest, sizeof(digest)); + if (!hex) + return log_oom_debug(); + + *ret = hex; + + return 0; +} #endif int dlopen_cryptsetup(void) { diff --git a/src/shared/cryptsetup-util.h b/src/shared/cryptsetup-util.h index e42debeeb4..e9be8249fa 100644 --- a/src/shared/cryptsetup-util.h +++ b/src/shared/cryptsetup-util.h @@ -66,6 +66,9 @@ int cryptsetup_set_minimal_pbkdf(struct crypt_device *cd); int cryptsetup_get_token_as_json(struct crypt_device *cd, int idx, const char *verify_type, sd_json_variant **ret); int cryptsetup_add_token_json(struct crypt_device *cd, sd_json_variant *v); +int cryptsetup_get_volume_key_prefix(struct crypt_device *cd, const char *volume_name, char **ret); +int cryptsetup_get_volume_key_id(struct crypt_device *cd, const char *volume_name, const void *volume_key, + size_t volume_key_size, char **ret); #endif int dlopen_cryptsetup(void); From 4754289ddf8bce0cee4876639d1f1c009a2c111a Mon Sep 17 00:00:00 2001 From: Vitaly Kuznetsov Date: Wed, 14 Jan 2026 09:51:33 +0100 Subject: [PATCH 4/5] repart: Add fixate_volume_key crypttab option generation Add an option to capture the expected LUKS volume key hash and record it to the generated crypttab. --- man/crypttab.xml | 7 +++++ man/repart.d.xml | 9 +++++-- src/repart/repart.c | 63 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/man/crypttab.xml b/man/crypttab.xml index dbbeb3fd5b..f0d0ead947 100644 --- a/man/crypttab.xml +++ b/man/crypttab.xml @@ -1023,6 +1023,11 @@ if it has a different one. The expected hash matches the digest which is measured to the sha256 PCR bank of the TPM2 when is used. + For newly created LUKS volumes, the expected hash can be generated by + systemd-repart8. + For additional details, see the description at + repart.d5. + @@ -1136,6 +1141,8 @@ external /dev/sda3 keyfile:LABEL=keydev keyfile-timeout=10s,cipher=xchac systemd-cryptsetup@.service8 systemd-cryptsetup-generator8 systemd-cryptenroll1 + systemd-repart8 + repart.d5 fstab5 cryptsetup8 mkswap8 diff --git a/man/repart.d.xml b/man/repart.d.xml index 5d32557745..5cdf2b0f34 100644 --- a/man/repart.d.xml +++ b/man/repart.d.xml @@ -869,13 +869,18 @@ EncryptedVolume= Specifies how the encrypted partition should be set up. Takes at least one and at most - three fields separated with a colon (:). The first field specifies the encrypted + four fields separated with a colon (:). The first field specifies the encrypted volume name under /dev/mapper/. If not specified, luks-UUID will be used where UUID is the LUKS UUID. The second field specifies the keyfile to use following the same format as specified in crypttab. The third field specifies a - comma-delimited list of crypttab options. These fields correspond to the first, third and fourth + comma-delimited list of crypttab options. These three fields correspond to the first, third and fourth column of the crypttab5 format. + The fourth field contains extra options, which are not directly reflected to crypttab options. Currently, + the only supported extra option is fixate-volume-key. When specified, the volume key + hash will be taken upon LUKS volume formatting and the result will be added to the list of crypttab options, + i.e. fixate-volume-key=expected-hash will be added to the fourth column of the generated + crypttab. Note that this setting is only taken into account when diff --git a/src/repart/repart.c b/src/repart/repart.c index 7c19f10287..48e05d3769 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -318,6 +318,7 @@ typedef struct PartitionEncryptedVolume { char *name; char *keyfile; char *options; + bool fixate_volume_key; } PartitionEncryptedVolume; static PartitionEncryptedVolume* partition_encrypted_volume_free(PartitionEncryptedVolume *c) { @@ -2547,7 +2548,8 @@ static int config_parse_encrypted_volume( void *data, void *userdata) { - _cleanup_free_ char *volume = NULL, *keyfile = NULL, *options = NULL; + _cleanup_free_ char *volume = NULL, *keyfile = NULL, *options = NULL, *extra = NULL; + bool fixate_volume_key = false; Partition *p = ASSERT_PTR(data); int r; @@ -2558,7 +2560,7 @@ static int config_parse_encrypted_volume( const char *q = rvalue; r = extract_many_words(&q, ":", EXTRACT_CUNESCAPE|EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_UNQUOTE, - &volume, &keyfile, &options); + &volume, &keyfile, &options, &extra); if (r == -ENOMEM) return log_oom(); if (r < 0) { @@ -2589,10 +2591,29 @@ static int config_parse_encrypted_volume( if (!p->encrypted_volume) return log_oom(); + for (const char *e = extra;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&e, &word, ",", EXTRACT_DONT_COALESCE_SEPARATORS | EXTRACT_UNESCAPE_SEPARATORS); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Failed to parse extra options '%s', ignoring", word); + break; + } + if (r == 0) + break; + + if (streq(word, "fixate-volume-key")) + fixate_volume_key = true; + else + log_syntax(unit, LOG_WARNING, filename, line, 0, "Unknown extra option '%s', ignoring", word); + } + *p->encrypted_volume = (PartitionEncryptedVolume) { .name = TAKE_PTR(volume), .keyfile = TAKE_PTR(keyfile), .options = TAKE_PTR(options), + .fixate_volume_key = fixate_volume_key, }; return 0; @@ -5174,6 +5195,44 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta if (r < 0) return log_error_errno(r, "Failed to LUKS2 format future partition: %m"); + if (p->encrypted_volume && p->encrypted_volume->fixate_volume_key) { + _cleanup_free_ char *key_id = NULL, *hash_option = NULL; + + r = sym_crypt_get_volume_key_size(cd); + if (r < 0) + return log_error_errno(r, "Failed to determine volume key size: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Volume key has zero size and 'fixate-volume-key' is used"); + + _cleanup_(iovec_done) struct iovec vk = { + .iov_base = malloc(r), + .iov_len = r, + }; + + if (!vk.iov_base) + return log_oom(); + + r = sym_crypt_volume_key_get(cd, CRYPT_ANY_SLOT, (char *) vk.iov_base, &vk.iov_len, NULL, 0); + if (r < 0) + return log_error_errno(r, "Failed to get volume key: %m"); + + r = cryptsetup_get_volume_key_id( + cd, + /* volume_name= */ p->encrypted_volume->name, + /* volume_key= */ vk.iov_base, + /* volume_key_size= */ vk.iov_len, + /* ret= */ &key_id); + if (r < 0) + return log_error_errno(r, "Failed to get volume key hash: %m"); + + hash_option = strjoin("fixate-volume-key=", key_id); + if (!hash_option) + return log_oom(); + + if (!strextend_with_separator(&p->encrypted_volume->options, ",", hash_option)) + return log_oom(); + } + if (IN_SET(p->encrypt, ENCRYPT_KEY_FILE, ENCRYPT_KEY_FILE_TPM2)) { /* Use partition-specific key if available, otherwise fall back to global key */ struct iovec *iovec_key = arg_key.iov_base ? &arg_key : &p->key; From 398acccb2ac97c99a8bef6b542b4516e158266a8 Mon Sep 17 00:00:00 2001 From: Vitaly Kuznetsov Date: Tue, 13 Jan 2026 17:43:22 +0100 Subject: [PATCH 5/5] TEST-58-REPART: Add a test for fixate_folume_key The test checks that the expected hash is correctly recorded to the generated crypttab and also checks that systemd-cryptsetup handles the option correctly. --- test/units/TEST-58-REPART.sh | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh index 23548d9cea..9802f145bc 100755 --- a/test/units/TEST-58-REPART.sh +++ b/test/units/TEST-58-REPART.sh @@ -1942,6 +1942,61 @@ EOF cmp "$imgs/test1.img" "$imgs/test2.img" } +testcase_luks2_keyhash() { + local defs imgs output root + + defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")" + imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")" + root="$(mktemp --directory "/var/test-repart.root.XXXXXXXXXX")" + # shellcheck disable=SC2064 + trap "rm -rf '$defs' '$imgs' '$root'" RETURN + chmod 0755 "$defs" + + echo "*** testcase for fixate-volume-key ***" + + volume="test-repart-lukskeyhash-$RANDOM" + + tee "$defs/root.conf" <