]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
cryptsetup: Add fixate-volume-key option to pin the expected volume key hash
authorVitaly Kuznetsov <vkuznets@redhat.com>
Wed, 14 Jan 2026 08:51:24 +0000 (09:51 +0100)
committerVitaly Kuznetsov <vkuznets@redhat.com>
Mon, 19 Jan 2026 16:49:39 +0000 (17:49 +0100)
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
src/cryptsetup/cryptsetup.c
src/shared/cryptsetup-util.c
src/shared/cryptsetup-util.h

index a8a8242a62f2411dbfbf4d8be3c6ddad3fea152c..dbbeb3fd5bf00eccd8f9076dd4d8a85c1afd7dd7 100644 (file)
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><option>fixate-volume-key=</option></term>
+
+        <listitem><para>Pin the expected hash of the volume key.</para>
+
+        <para>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.
+        <option>fixate-volume-key=</option> 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 <option>tpm2-measure-pcr=</option> is used.</para>
+
+        <xi:include href="version-info.xml" xpointer="v260"/>
+        </listitem>
+      </varlistentry>
+
     </variablelist>
 
     <para>At early boot and when the system manager configuration is
index e56941adf2a37564820c8fc3d16a45e4c15dc542..f715494418b425fba532125cd77a5f004624583c 100644 (file)
@@ -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 *s = NULL;
-        s = strjoin("cryptsetup:", escaped, ":", strempty(crypt_get_uuid(cd)));
-        if (!s)
-                return log_oom();
-
-        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);
+        _cleanup_free_ char *prefix = NULL;
+
+        /* 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,
+                        /* 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. */
index 5d99edd52d1068b04f2ade43f8ad437513cf4177..a766a92b2037f1122915581e224d38c563b09f00 100644 (file)
@@ -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) {
index e42debeeb464f0c934c3d83172813f8a168c87a4..e9be8249fa1a00d3bf7dd5a8cf90592862ceca07 100644 (file)
@@ -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);