cryptsetup: add Argon2id-based PIN mode for TPM2 enrollment

The current TPM2 PIN mode is flawed as a compromised TPM directly exposes
the sealed secret which is the LUKS volume key itself (#27502 and #37386).

Goal: add Argon2id-based PIN hardening to TPM2 enrollment, making
the TPM a second factor rather than a single point of failure:

1. Password + salt -> Argon2id -> 512-bit key split into Key1 + Key2
2. Key2 (base64-encoded) is used as the PIN to seal a random secret
in the TPM
3. Key1 + unsealed secret -> HKDF-SHA256 -> final LUKS volume key

This implementation ensures that if the TPM is compromised, an attacker
still needs the password to derive Key1 and combine it with the unsealed
secret.

The --tpm2-with-pin= option now accepts three values:
- false (no PIN used)
- true (PIN hardened with Argon2id - default)
- "direct" (legacy PIN without Argon2id for backward compatibility)

Argon2id parameters are customizable via:

--tpm2-argon2id-memory=
--tpm2-argon2id-iterations=
--tpm2-argon2id-parallelism=
--tpm2-argon2id-iter-time=

These default to a function of available CPUs and physical memory, with
a benchmark that scales iterations to the target time (default: 2s) and
falls back to ARGON2ID_PARAMETERS_DEFAULT (64 MiB, 8 iter, 4 lanes) when
auto detection fails.
Also if the runtime OpenSSL lacks Argon2id support (< 3.2), the feature
silently falls back to direct PIN mode with a warning.

Added includes:
- src/cryptenroll/cryptenroll.c: cpu-set-util.h, limits-util.h, time-util.h
  for Argon2id benchmark auto-tuning (cpus_online, physical_memory_scale,
  now/usec_t)
- src/cryptenroll/cryptenroll-tpm2.c: crypto-util.h for Argon2IdParameters
  struct in load_volume_key_tpm2()
- src/shared/tpm2-util.h: crypto-util.h for Argon2IdParameters in
  tpm2_make_luks2_json() API
- src/shared/tpm2-util.c: limits-util.h, tpm2-util.h for physical_memory() validation
  of Argon2id memory cost and function prototypes
- src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c: crypto-util.h for
  kdf_argon2id_derive()/kdf_hkdf_sha256() on the token unlock path
This commit is contained in:
fuldeka
2026-07-12 23:31:29 +02:00
parent 9a09141cae
commit f3fe668cdf
16 changed files with 839 additions and 69 deletions
@@ -103,6 +103,8 @@ _public_ int cryptsetup_token_open_pin(
CLEANUP_ARRAY(blobs, n_blobs, iovec_array_free);
CLEANUP_ARRAY(policy_hash, n_policy_hash, iovec_array_free);
Argon2IdParameters argon2id_params = {};
r = tpm2_parse_luks2_json(
v,
/* ret_keyslot= */ NULL,
@@ -119,7 +121,8 @@ _public_ int cryptsetup_token_open_pin(
&salt,
&srk,
&pcrlock_nv,
&flags);
&flags,
&argon2id_params);
if (r < 0)
return log_debug_open_error(cd, token, r);
@@ -148,6 +151,7 @@ _public_ int cryptsetup_token_open_pin(
&srk,
&pcrlock_nv,
flags,
/* argon2id_params= */ FLAGS_SET(flags, TPM2_FLAGS_USE_ARGON2ID) ? &argon2id_params : NULL,
&decrypted_key);
if (r < 0)
return log_debug_open_error(cd, token, r);
@@ -227,7 +231,7 @@ _public_ void cryptsetup_token_dump(
r = tpm2_parse_luks2_json(
v,
NULL,
/* ret_keyslot= */ NULL,
&hash_pcr_mask,
&pcr_bank,
&pubkey,
@@ -241,7 +245,8 @@ _public_ void cryptsetup_token_dump(
&salt,
&srk,
&pcrlock_nv,
&flags);
&flags,
/* ret_argon2id_params= */ NULL);
if (r < 0)
return (void) crypt_log_debug_errno(cd, r, "Failed to parse " TOKEN_NAME " JSON fields: %m");
@@ -266,6 +271,7 @@ _public_ void cryptsetup_token_dump(
crypt_log(cd, "\ttpm2-primary-alg: %s\n", strna(tpm2_asym_alg_to_string(primary_alg)));
crypt_log(cd, "\ttpm2-pin: %s\n", true_false(flags & TPM2_FLAGS_USE_PIN));
crypt_log(cd, "\ttpm2-pcrlock: %s\n", true_false(flags & TPM2_FLAGS_USE_PCRLOCK));
crypt_log(cd, "\ttpm2-argon2id: %s\n", true_false(flags & TPM2_FLAGS_USE_ARGON2ID));
crypt_log(cd, "\ttpm2-salt: %s\n", true_false(iovec_is_set(&salt)));
crypt_log(cd, "\ttpm2-srk: %s\n", true_false(iovec_is_set(&srk)));
crypt_log(cd, "\ttpm2-pcrlock-nv: %s\n", true_false(iovec_is_set(&pcrlock_nv)));
+33 -8
View File
@@ -4,6 +4,7 @@
#include "alloc-util.h"
#include "ask-password-api.h"
#include "crypto-util.h"
#include "env-util.h"
#include "hexdecoct.h"
#include "log.h"
@@ -33,11 +34,14 @@ int acquire_luks2_key(
const struct iovec *srk,
const struct iovec *pcrlock_nv,
TPM2Flags flags,
const Argon2IdParameters *argon2id_params,
struct iovec *ret_decrypted_key) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *signature_json = NULL;
_cleanup_free_ char *auto_device = NULL;
_cleanup_(erase_and_freep) char *b64_salted_pin = NULL;
_cleanup_(erase_and_freep) char *b64_pin = NULL;
_cleanup_(iovec_done_erase) struct iovec key1 = {};
bool argon2id = FLAGS_SET(flags, TPM2_FLAGS_USE_ARGON2ID);
int r;
assert(iovec_is_valid(salt));
@@ -56,17 +60,28 @@ int acquire_luks2_key(
if ((flags & TPM2_FLAGS_USE_PIN) && !pin)
return -ENOANO;
if (pin && iovec_is_set(salt)) {
if (argon2id) {
if (isempty(pin))
return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Argon2id PIN requires a non-empty PIN.");
if (!iovec_is_set(salt))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argon2id PIN requires salt in LUKS2 token.");
r = tpm2_argon2id_derive_split(pin, salt, argon2id_params, &key1, &b64_pin);
if (r < 0)
return log_error_errno(r, "Failed to perform Argon2id: %m");
pin = b64_pin;
} else if (pin && iovec_is_set(salt)) {
uint8_t salted_pin[SHA256_DIGEST_SIZE] = {};
CLEANUP_ERASE(salted_pin);
r = tpm2_util_pbkdf2_hmac_sha256(pin, strlen(pin), salt->iov_base, salt->iov_len, salted_pin);
if (r < 0)
return log_error_errno(r, "Failed to perform PBKDF2: %m");
r = base64mem(salted_pin, sizeof(salted_pin), &b64_salted_pin);
r = base64mem(salted_pin, sizeof(salted_pin), &b64_pin);
if (r < 0)
return log_error_errno(r, "Failed to base64 encode salted pin: %m");
pin = b64_salted_pin;
pin = b64_pin;
}
if (pubkey_pcr_mask != 0) {
@@ -81,7 +96,6 @@ int acquire_luks2_key(
if (r < 0)
return r;
if (r == 0) {
/* Not found? Then search among passed credentials */
r = tpm2_pcrlock_policy_from_credentials(srk, pcrlock_nv, &pcrlock_policy);
if (r < 0)
return r;
@@ -95,6 +109,7 @@ int acquire_luks2_key(
if (r < 0)
return r;
_cleanup_(iovec_done_erase) struct iovec unsealed = {};
r = tpm2_unseal(tpm2_context,
hash_pcr_mask,
pcr_bank,
@@ -110,13 +125,14 @@ int acquire_luks2_key(
policy_hash,
n_policy_hash,
srk,
ret_decrypted_key);
argon2id ? &unsealed : ret_decrypted_key);
if (r == -EREMOTE)
return log_warning_errno(r, "TPM key integrity check failed. Key enrolled in superblock most likely does not belong to this TPM.");
if (r == -EADDRNOTAVAIL)
return log_warning_errno(r, "NV index referenced by token is missing, unwritten, or unusable, it could be for another system.");
if (r == -EILSEQ)
return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Bad PIN."); /* cryptsetup docs say we should return ENOANO on bad PIN */
/* cryptsetup docs say we should return ENOANO on bad PIN */
return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Bad %s.", argon2id ? "password" : "PIN");
if (r == -ENOLCK)
return log_error_errno(r, "TPM is in dictionary attack lock-out mode.");
if (ERRNO_IS_NEG_TPM2_UNSEAL_BAD_PCR(r)) {
@@ -129,5 +145,14 @@ int acquire_luks2_key(
if (r < 0)
return log_error_errno(r, "Failed to unseal secret using TPM2: %m");
return r;
if (argon2id) {
_cleanup_(iovec_done_erase) struct iovec volume_key = {};
r = tpm2_argon2id_hkdf(&key1, &unsealed, &volume_key);
if (r < 0)
return log_error_errno(r, "Failed to derive volume key via HKDF: %m");
*ret_decrypted_key = TAKE_STRUCT(volume_key);
}
return 0;
}
@@ -25,4 +25,5 @@ int acquire_luks2_key(
const struct iovec *srk,
const struct iovec *pcrlock_nv,
TPM2Flags flags,
const Argon2IdParameters *argon2id_params,
struct iovec *decrypted_key);
+5 -1
View File
@@ -2070,6 +2070,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2(
until,
"cryptsetup.tpm2-pin",
arg_ask_password_flags,
/* argon2id_params= */ NULL,
&decrypted_key);
if (r >= 0)
break;
@@ -2115,6 +2116,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2(
uint32_t hash_pcr_mask, pubkey_pcr_mask;
size_t n_blobs = 0, n_policy_hash = 0;
uint16_t pcr_bank, primary_alg;
Argon2IdParameters argon2id_params = {};
TPM2Flags tpm2_flags;
CLEANUP_ARRAY(blobs, n_blobs, iovec_array_free);
@@ -2139,7 +2141,8 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2(
&pcrlock_nv,
&tpm2_flags,
&keyslot,
&token);
&token,
&argon2id_params);
if (r == -ENXIO)
/* No further TPM2 tokens found in the LUKS2 header. */
return log_full_errno(found_some ? LOG_NOTICE : LOG_DEBUG,
@@ -2179,6 +2182,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2(
until,
"cryptsetup.tpm2-pin",
arg_ask_password_flags,
&argon2id_params,
&decrypted_key);
if (IN_SET(r, -EACCES, -ENOLCK))
return log_notice_errno(SYNTHETIC_ERRNO(EAGAIN), "TPM2 PIN unlock failed, falling back to traditional unlocking.");