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:
@@ -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)));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.");
|
||||
|
||||
Reference in New Issue
Block a user