From: fuldeka <260289618+fuldeka@users.noreply.github.com>
Date: Sun, 28 Jun 2026 14:42:47 +0000 (+0200)
Subject: cryptsetup: add Argon2id-based PIN mode for TPM2 enrollment
X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f3fe668cdf71b42485a6a2151d4f025788a35a95;p=thirdparty%2Fsystemd.git
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
---
diff --git a/man/systemd-cryptenroll.xml b/man/systemd-cryptenroll.xml
index 7cbc783edcc..94cab54ad7a 100644
--- a/man/systemd-cryptenroll.xml
+++ b/man/systemd-cryptenroll.xml
@@ -677,13 +677,19 @@
-
+
When enrolling a TPM2 device, controls whether to require the user to enter a PIN
- when unlocking the volume in addition to PCR binding, based on TPM2 policy authentication. Defaults
- to no. Despite being called PIN, any character can be used, not just numbers.
+ when unlocking the volume in addition to PCR binding, based on TPM2 policy authentication. Takes a
+ boolean or the special value direct. Defaults to no.
+ Despite being called PIN, any character can be used, not just numbers.
+ When enabled (yes), the PIN is hardened with Argon2id before it is passed
+ to the TPM, making the TPM a second factor rather than a single point of failure: a compromised TPM
+ alone does not expose the volume key without the PIN. When direct is used, the PIN
+ is hashed with PBKDF2 and used directly (compatible with systemd v253 and later).
+
Note that incorrect PIN entry when unlocking increments the TPM dictionary attack lockout
mechanism, and may lock out users for a prolonged time, depending on its configuration. The lockout
mechanism is a global property of the TPM, systemd-cryptenroll does not control or
@@ -694,7 +700,51 @@
project='mankier'>tpm2_dictionarylockout1
commands, respectively.
-
+
+
+
+
+
+
+ When enrolling a TPM2 with PIN hardening enabled, sets the Argon2id memory cost,
+ in bytes. By default, the memory cost is auto-detected from available RAM (up to 50% of total
+ physical memory), with a minimum of 64 MiB. When specified, the benchmark calibrates only
+ iterations to the target time; the memory cost is used as specified. To express large values
+ comfortably, use suffixes such as K, M, G
+ (for kilobytes, megabytes, gigabytes).
+
+
+
+
+
+
+
+ When enrolling a TPM2 with PIN hardening enabled, sets the Argon2id iteration
+ count. Defaults to auto-calibrated by a benchmark to reach the target unlock time. When
+ specified, the benchmark calibrates only memory cost to the target time; the iteration count
+ is used as specified.
+
+
+
+
+
+
+
+ When enrolling a TPM2 with PIN hardening enabled, sets the Argon2id
+ parallelism (lane count). Defaults to the number of online CPUs.
+
+
+
+
+
+
+
+ When enrolling a TPM2 with PIN hardening enabled, sets the target benchmark
+ time for the Argon2id parameter calibration, in seconds. Defaults to 2 seconds. The benchmark
+ adjusts memory cost and/or iteration count so that the actual derivation time falls within a
+ tolerance window of the specified target time.
+
+
diff --git a/src/cryptenroll/cryptenroll-tpm2.c b/src/cryptenroll/cryptenroll-tpm2.c
index bc52a18c6ab..6165a316422 100644
--- a/src/cryptenroll/cryptenroll-tpm2.c
+++ b/src/cryptenroll/cryptenroll-tpm2.c
@@ -5,6 +5,7 @@
#include "alloc-util.h"
#include "ask-password-api.h"
#include "cryptenroll-tpm2.h"
+#include "crypto-util.h"
#include "cryptsetup-tpm2.h"
#include "cryptsetup-util.h"
#include "env-util.h"
@@ -194,6 +195,7 @@ int load_volume_key_tpm2(
size_t n_blobs = 0, n_policy_hash = 0;
uint32_t hash_pcr_mask, pubkey_pcr_mask;
uint16_t pcr_bank, primary_alg;
+ Argon2IdParameters ap = {};
TPM2Flags tpm2_flags;
int keyslot;
@@ -219,7 +221,8 @@ int load_volume_key_tpm2(
&pcrlock_nv,
&tpm2_flags,
&keyslot,
- &token);
+ &token,
+ &ap);
if (r == -ENXIO)
return log_full_errno(LOG_NOTICE,
SYNTHETIC_ERRNO(EAGAIN),
@@ -257,6 +260,7 @@ int load_volume_key_tpm2(
/* until= */ 0,
"cryptenroll.tpm2-pin",
c->interactive ? 0 : ASK_PASSWORD_HEADLESS,
+ /* argon2id_params= */ FLAGS_SET(tpm2_flags, TPM2_FLAGS_USE_ARGON2ID) ? &ap : NULL,
&decrypted_key);
if (IN_SET(r, -EACCES, -ENOLCK))
return log_notice_errno(SYNTHETIC_ERRNO(EAGAIN), "TPM2 PIN unlock failed");
@@ -304,6 +308,7 @@ int enroll_tpm2(const EnrollContext *c,
_cleanup_(iovec_done_erase) struct iovec secret = {};
const char *node;
_cleanup_(erase_and_freep) char *pin_str = NULL;
+ _cleanup_(iovec_done_erase) struct iovec key1 = {};
ssize_t base64_encoded_size;
int r, keyslot, slot_to_wipe = -1;
TPM2Flags flags = 0;
@@ -328,7 +333,7 @@ int enroll_tpm2(const EnrollContext *c,
assert_se(node = sym_crypt_get_device_name(cd));
- if (c->tpm2_pin) {
+ if (IN_SET(c->tpm2_pin, TPM2_WITH_PIN_YES, TPM2_WITH_PIN_DIRECT)) {
r = get_pin(&pin_str, &flags);
if (r < 0)
return r;
@@ -337,17 +342,38 @@ int enroll_tpm2(const EnrollContext *c,
if (r < 0)
return log_error_errno(r, "Failed to acquire random salt: %m");
- uint8_t salted_pin[SHA256_DIGEST_SIZE] = {};
- CLEANUP_ERASE(salted_pin);
- r = tpm2_util_pbkdf2_hmac_sha256(pin_str, strlen(pin_str), binary_salt, sizeof(binary_salt), salted_pin);
- if (r < 0)
- return log_error_errno(r, "Failed to perform PBKDF2: %m");
+ if (c->tpm2_pin == TPM2_WITH_PIN_YES) {
+ if (isempty(pin_str))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Argon2id PIN requires a non-empty PIN.");
+
+ _cleanup_(erase_and_freep) char *b64_tpm_pin = NULL;
+ r = tpm2_argon2id_derive_split(
+ pin_str,
+ &IOVEC_MAKE(binary_salt, sizeof(binary_salt)),
+ &c->tpm2_argon2id_params,
+ &key1,
+ &b64_tpm_pin);
+ if (r < 0)
+ return log_error_errno(r, "Failed to perform Argon2id: %m");
+
+ /* Key2 = last 32 bytes, used as TPM PIN */
+ pin_str = erase_and_free(pin_str);
+ pin_str = TAKE_PTR(b64_tpm_pin);
+
+ flags |= TPM2_FLAGS_USE_ARGON2ID;
+ } else {
+ uint8_t salted_pin[SHA256_DIGEST_SIZE] = {};
+ CLEANUP_ERASE(salted_pin);
+ r = tpm2_util_pbkdf2_hmac_sha256(pin_str, strlen(pin_str), binary_salt, sizeof(binary_salt), salted_pin);
+ if (r < 0)
+ return log_error_errno(r, "Failed to perform PBKDF2: %m");
- pin_str = erase_and_free(pin_str);
- /* re-stringify pin_str */
- base64_encoded_size = base64mem(salted_pin, sizeof(salted_pin), &pin_str);
- if (base64_encoded_size < 0)
- return log_error_errno(base64_encoded_size, "Failed to base64 encode salted pin: %m");
+ pin_str = erase_and_free(pin_str);
+ /* re-stringify pin_str */
+ base64_encoded_size = base64mem(salted_pin, sizeof(salted_pin), &pin_str);
+ if (base64_encoded_size < 0)
+ return log_error_errno(base64_encoded_size, "Failed to base64 encode salted pin: %m");
+ }
}
TPM2B_PUBLIC public = {};
@@ -462,7 +488,7 @@ int enroll_tpm2(const EnrollContext *c,
c->tpm2_n_hash_pcr_values,
iovec_is_set(&pubkey) ? &public : NULL,
iovec_is_set(&pubkey) ? c->tpm2_public_key_policyref : NULL,
- c->tpm2_pin,
+ IN_SET(c->tpm2_pin, TPM2_WITH_PIN_YES, TPM2_WITH_PIN_DIRECT),
c->tpm2_pcrlock && !iovec_is_set(&pubkey) ? &pcrlock_policy : NULL,
policy_hash + 0);
if (r < 0)
@@ -474,7 +500,7 @@ int enroll_tpm2(const EnrollContext *c,
c->tpm2_n_hash_pcr_values,
/* public= */ NULL, /* This one is off now */
/* pubkey_policy_ref= */ NULL,
- c->tpm2_pin,
+ IN_SET(c->tpm2_pin, TPM2_WITH_PIN_YES, TPM2_WITH_PIN_DIRECT),
&pcrlock_policy, /* And this one on instead. */
policy_hash + 1);
if (r < 0)
@@ -533,7 +559,7 @@ int enroll_tpm2(const EnrollContext *c,
log_debug_errno(r, "PCR policy hash not yet enrolled, enrolling now.");
else if (r < 0)
return r;
- else if (c->tpm2_pin) {
+ else if (IN_SET(c->tpm2_pin, TPM2_WITH_PIN_YES, TPM2_WITH_PIN_DIRECT)) {
log_debug("This PCR set is already enrolled, re-enrolling anyway to update PIN.");
slot_to_wipe = r;
} else {
@@ -570,8 +596,16 @@ int enroll_tpm2(const EnrollContext *c,
return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 seal/unseal verification failed.");
}
- /* let's base64 encode the key to use, for compat with homed (and it's easier to every type it in by keyboard, if that might end up being necessary. */
- base64_encoded_size = base64mem(secret.iov_base, secret.iov_len, &base64_encoded);
+ if (FLAGS_SET(flags, TPM2_FLAGS_USE_ARGON2ID)) {
+ _cleanup_(iovec_done_erase) struct iovec final_key = {};
+ r = tpm2_argon2id_hkdf(&key1, &secret, &final_key);
+ if (r < 0)
+ return log_error_errno(r, "Failed to derive final volume key via HKDF: %m");
+
+ base64_encoded_size = base64mem(final_key.iov_base, final_key.iov_len, &base64_encoded);
+ } else
+ /* let's base64 encode the key to use, for compat with homed (and it's easier to every type it in by keyboard, if that might end up being necessary. */
+ base64_encoded_size = base64mem(secret.iov_base, secret.iov_len, &base64_encoded);
if (base64_encoded_size < 0)
return log_error_errno(base64_encoded_size, "Failed to base64 encode secret key: %m");
@@ -601,10 +635,11 @@ int enroll_tpm2(const EnrollContext *c,
n_blobs,
policy_hash_as_iovec,
n_policy_hash,
- c->tpm2_pin ? &IOVEC_MAKE(binary_salt, sizeof(binary_salt)) : NULL,
+ IN_SET(c->tpm2_pin, TPM2_WITH_PIN_YES, TPM2_WITH_PIN_DIRECT) ? &IOVEC_MAKE(binary_salt, sizeof(binary_salt)) : NULL,
&srk,
c->tpm2_pcrlock ? &pcrlock_policy.nv_handle : NULL,
flags,
+ &c->tpm2_argon2id_params,
&v);
if (r < 0)
return log_error_errno(r, "Failed to prepare TPM2 JSON token object: %m");
diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c
index ae708146615..f295585ff9d 100644
--- a/src/cryptenroll/cryptenroll.c
+++ b/src/cryptenroll/cryptenroll.c
@@ -10,6 +10,7 @@
#include "blockdev-util.h"
#include "build.h"
#include "cleanup-util.h"
+#include "cpu-set-util.h"
#include "cryptenroll.h"
#include "cryptenroll-fido2.h"
#include "cryptenroll-interactive.h"
@@ -27,6 +28,7 @@
#include "help-util.h"
#include "initrd-util.h"
#include "libfido2-util.h"
+#include "limits-util.h"
#include "log.h"
#include "main-func.h"
#include "memory-util.h"
@@ -40,9 +42,25 @@
#include "string-table.h"
#include "string-util.h"
#include "terminal-util.h"
+#include "time-util.h"
#include "tpm2-pcr.h"
#include "tpm2-util.h"
+#define ARGON2ID_BENCHMARK_DEFAULT_TARGET_MS 2000U
+#define ARGON2ID_BENCHMARK_MAX_ATTEMPTS 8U
+#define ARGON2ID_BENCHMARK_MIN_MEMORY (64ULL * 1024 * 1024)
+#define ARGON2ID_BENCHMARK_MIN_MS 250U
+#define ARGON2ID_BENCHMARK_PERCENT_MAX 110U
+#define ARGON2ID_BENCHMARK_PERCENT_MIN 95U
+
+static const char * const tpm2_with_pin_table[_TPM2_WITH_PIN_MAX] = {
+ [TPM2_WITH_PIN_NO] = "no",
+ [TPM2_WITH_PIN_YES] = "yes", /* with argon2id */
+ [TPM2_WITH_PIN_DIRECT] = "direct", /* without argon2id, i.e. traditional mode as in v251 and before */
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(tpm2_with_pin, Tpm2WithPin, TPM2_WITH_PIN_YES);
+
static EnrollType arg_enroll_type = _ENROLL_TYPE_INVALID;
static char *arg_unlock_keyfile = NULL;
static UnlockType arg_unlock_type = UNLOCK_PASSWORD;
@@ -57,7 +75,9 @@ static uint32_t arg_tpm2_seal_key_handle = 0;
static char *arg_tpm2_device_key = NULL;
static Tpm2PCRValue *arg_tpm2_hash_pcr_values = NULL;
static size_t arg_tpm2_n_hash_pcr_values = 0;
-static bool arg_tpm2_pin = false;
+static Tpm2WithPin arg_tpm2_pin = _TPM2_WITH_PIN_INVALID;
+static Argon2IdParameters arg_tpm2_argon2id_params = {};
+static usec_t arg_tpm2_argon2id_iter_time = 0;
static char *arg_tpm2_public_key = NULL;
static bool arg_tpm2_load_public_key = true;
static char *arg_tpm2_public_key_policyref = NULL;
@@ -715,11 +735,56 @@ static int parse_argv(int argc, char *argv[]) {
auto_pcrlock = false;
break;
- OPTION_LONG("tpm2-with-pin", "BOOL",
- "Whether to require entering a PIN to unlock the volume"):
- r = parse_boolean_argument("--tpm2-with-pin=", opts.arg, &arg_tpm2_pin);
+ OPTION_LONG("tpm2-with-pin", "BOOL|direct",
+ "Whether to require entering a PIN to unlock the volume. "
+ "Takes a boolean or the special value \"direct\". "
+ "When enabled (true), Argon2id is used for PIN hardening. "
+ "When \"direct\", the PIN is used directly without Argon2id "
+ "(compatible with older systemd versions)"): {
+ Tpm2WithPin v = tpm2_with_pin_from_string(opts.arg);
+ if (v < 0)
+ return log_error_errno(v, "Failed to parse --tpm2-with-pin=: %s", opts.arg);
+ arg_tpm2_pin = v;
+ break;
+ }
+
+ OPTION_LONG("tpm2-argon2id-memory", "BYTES",
+ "Argon2id memory cost in bytes (default: 64M)"): {
+ uint64_t mem;
+ r = parse_size(opts.arg, 1024, &mem);
if (r < 0)
- return r;
+ return log_error_errno(r, "Failed to parse --tpm2-argon2id-memory=: %s", opts.arg);
+ if (mem == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argon2id memory must be non-zero.");
+ arg_tpm2_argon2id_params.memcost_bytes = mem;
+ break;
+ }
+
+ OPTION_LONG("tpm2-argon2id-iterations", "NUM",
+ "Argon2id iteration count (default: 8)"):
+ r = safe_atou(opts.arg, &arg_tpm2_argon2id_params.iterations);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --tpm2-argon2id-iterations=: %s", opts.arg);
+ if (arg_tpm2_argon2id_params.iterations == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argon2id iterations must be non-zero.");
+ break;
+
+ OPTION_LONG("tpm2-argon2id-parallelism", "NUM",
+ "Argon2id parallelism/lane count (default: 4)"):
+ r = safe_atou(opts.arg, &arg_tpm2_argon2id_params.lanes);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --tpm2-argon2id-parallelism=: %s", opts.arg);
+ if (arg_tpm2_argon2id_params.lanes == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argon2id parallelism must be non-zero.");
+ break;
+
+ OPTION_LONG("tpm2-argon2id-iter-time", "TIME",
+ "Target Argon2id benchmark time in seconds (default: 2s)"):
+ r = parse_sec(opts.arg, &arg_tpm2_argon2id_iter_time);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --tpm2-argon2id-iter-time=: %s", opts.arg);
+ if (arg_tpm2_argon2id_iter_time == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Target time must be non-zero.");
break;
}
@@ -783,13 +848,22 @@ static int parse_argv(int argc, char *argv[]) {
}
if (arg_tpm2_n_hash_pcr_values == 0 &&
- !arg_tpm2_pin &&
+ !IN_SET(arg_tpm2_pin, TPM2_WITH_PIN_YES, TPM2_WITH_PIN_DIRECT) &&
arg_tpm2_public_key_pcr_mask == 0 &&
!arg_tpm2_pcrlock)
log_notice("Notice: enrolling TPM2 with an empty policy, i.e. without any state or access restrictions.\n"
"Use --tpm2-public-key=, --tpm2-pcrlock=, --tpm2-with-pin= or --tpm2-pcrs= to enable one or more restrictions.");
}
+ if (arg_tpm2_pin < 0)
+ arg_tpm2_pin = TPM2_WITH_PIN_NO;
+
+ if (arg_tpm2_pin == TPM2_WITH_PIN_YES && !dlopen_libcrypto_has_argon2id()) {
+ log_warning("Argon2id not supported by libcrypto (OpenSSL >= 3.2 required), "
+ "falling back to direct PIN mode.");
+ arg_tpm2_pin = TPM2_WITH_PIN_DIRECT;
+ }
+
return 1;
}
@@ -923,6 +997,8 @@ static int enroll_context_from_args(EnrollContext *c) {
c->tpm2_pin = arg_tpm2_pin;
c->tpm2_load_public_key = arg_tpm2_load_public_key;
c->tpm2_public_key_pcr_mask = arg_tpm2_public_key_pcr_mask;
+ c->tpm2_argon2id_params = arg_tpm2_argon2id_params;
+ c->tpm2_argon2id_iter_time = arg_tpm2_argon2id_iter_time;
c->wipe_slots_scope = arg_wipe_slots_scope;
c->wipe_slots_mask = arg_wipe_slots_mask;
@@ -1014,6 +1090,166 @@ int enroll_now(
}
}
+static void argon2id_parameters_init_autotune(Argon2IdParameters *ret_params) {
+ assert(ret_params);
+
+ unsigned lanes = ARGON2ID_PARAMETERS_DEFAULT.lanes;
+ (void) cpus_online(&lanes);
+ *ret_params = (Argon2IdParameters) {
+ .lanes = lanes,
+ };
+}
+
+static int argon2id_benchmark_once(
+ const struct iovec *password,
+ const struct iovec *salt,
+ uint64_t memcost_bytes,
+ uint32_t iterations,
+ uint32_t lanes,
+ usec_t *ret_elapsed) {
+
+ assert(ret_elapsed);
+
+ _cleanup_(iovec_done_erase) struct iovec result = {};
+ Argon2IdParameters bp = {
+ .memcost_bytes = memcost_bytes,
+ .iterations = iterations,
+ .lanes = lanes,
+ };
+
+ usec_t start = now(CLOCK_MONOTONIC);
+ int r = kdf_argon2id_derive(password, salt, &bp, /* derive_size= */ 64, &result);
+ *ret_elapsed = now(CLOCK_MONOTONIC) - start;
+
+ return r;
+}
+
+static void argon2id_parameters_benchmark(Argon2IdParameters *p, usec_t target_time) {
+ int r;
+
+ assert(p);
+ assert(target_time > 0);
+
+ bool mem_fixed = p->memcost_bytes > 0;
+ bool iter_fixed = p->iterations > 0;
+
+ if (mem_fixed && iter_fixed)
+ return;
+
+ struct iovec password = IOVEC_MAKE_STRING("benchmark");
+ struct iovec salt = IOVEC_MAKE_STRING("benchmark-salt");
+
+ uint64_t target_ms = MAX(target_time / USEC_PER_MSEC, 1U);
+
+ uint32_t iterations = iter_fixed ? p->iterations : 2;
+ uint64_t memcost_bytes = mem_fixed ? p->memcost_bytes : ARGON2ID_BENCHMARK_MIN_MEMORY;
+
+ uint64_t max_mem_bytes;
+ if (mem_fixed)
+ max_mem_bytes = memcost_bytes;
+ else {
+ max_mem_bytes = physical_memory_scale(1, 2);
+ if (max_mem_bytes == 0 || max_mem_bytes == UINT64_MAX)
+ max_mem_bytes = ARGON2ID_PARAMETERS_DEFAULT.memcost_bytes;
+ if (memcost_bytes > max_mem_bytes)
+ memcost_bytes = max_mem_bytes;
+ }
+
+ target_ms = MIN(target_ms, UINT64_MAX / MAX(1u, MAX(max_mem_bytes, (uint64_t) UINT32_MAX)));
+
+ usec_t actual_elapsed = 0;
+
+ for (;;) {
+ usec_t elapsed;
+ r = argon2id_benchmark_once(&password, &salt, memcost_bytes, iterations, p->lanes, &elapsed);
+ log_debug("Benchmarking Argon2id with %"PRIu64" memory, %u iterations, %u lanesâ¦",
+ memcost_bytes, iterations, p->lanes);
+ if (r < 0) {
+ log_debug_errno(r, "Argon2id benchmark failed, using default parameters: %m");
+ *p = ARGON2ID_PARAMETERS_DEFAULT;
+ return;
+ }
+
+ actual_elapsed = elapsed;
+
+ if (elapsed >= ARGON2ID_BENCHMARK_MIN_MS * USEC_PER_MSEC)
+ break;
+
+ if (!mem_fixed && memcost_bytes < max_mem_bytes) {
+ uint64_t new_mem = MIN(memcost_bytes * 2, max_mem_bytes);
+ if (new_mem > memcost_bytes)
+ memcost_bytes = new_mem;
+ else
+ memcost_bytes = max_mem_bytes;
+ } else if (!iter_fixed) {
+ uint32_t new_iter = MIN(2u * iterations, UINT32_MAX / 2u);
+ if (new_iter > iterations)
+ iterations = new_iter;
+ else
+ break;
+ } else
+ break;
+ }
+
+ p->memcost_bytes = memcost_bytes;
+
+ for (unsigned attempt = 0; attempt < ARGON2ID_BENCHMARK_MAX_ATTEMPTS; attempt++) {
+ usec_t elapsed;
+ r = argon2id_benchmark_once(&password, &salt, memcost_bytes, iterations, p->lanes, &elapsed);
+ if (r < 0) {
+ log_debug_errno(r, "Argon2id fine-tuning failed, keeping coarse parameters: %m");
+ break;
+ }
+
+ actual_elapsed = elapsed;
+
+ uint64_t ms = MAX(elapsed / USEC_PER_MSEC, 1U);
+
+ uint64_t lower = target_ms * ARGON2ID_BENCHMARK_PERCENT_MIN / 100;
+ uint64_t upper = target_ms * ARGON2ID_BENCHMARK_PERCENT_MAX / 100;
+ if (ms >= lower && ms <= upper)
+ break;
+
+ uint64_t new_mem = memcost_bytes;
+ uint32_t new_iter = iterations;
+
+ if (ms < target_ms) {
+ if (!mem_fixed) {
+ new_mem = MIN(memcost_bytes * target_ms / ms, max_mem_bytes);
+ if (new_mem >= max_mem_bytes && !iter_fixed)
+ new_iter = (uint32_t) MIN(
+ (uint64_t) iterations * target_ms / ms,
+ UINT32_MAX);
+ } else if (!iter_fixed)
+ new_iter = (uint32_t) MIN(iterations * target_ms / ms, UINT32_MAX);
+ } else {
+ if (!iter_fixed) {
+ new_iter = MAX((uint64_t) iterations * target_ms / ms, 2ULL);
+ if (new_iter <= 2 && !mem_fixed)
+ new_mem = MAX(
+ memcost_bytes * target_ms / ms,
+ ARGON2ID_BENCHMARK_MIN_MEMORY);
+ } else if (!mem_fixed)
+ new_mem = MAX(
+ memcost_bytes * target_ms / ms,
+ ARGON2ID_BENCHMARK_MIN_MEMORY);
+ }
+
+ if (new_iter == iterations && new_mem == memcost_bytes)
+ break;
+
+ iterations = new_iter;
+ memcost_bytes = new_mem;
+ }
+
+ p->memcost_bytes = memcost_bytes;
+ p->iterations = iterations;
+
+ log_notice("Argon2id benchmark: %u iterations, %"PRIu64" MiB, %u lanes, ~%"PRIu64"ms.",
+ p->iterations, p->memcost_bytes / 1024 / 1024, p->lanes,
+ actual_elapsed > 0 ? actual_elapsed / USEC_PER_MSEC : target_ms);
+}
+
static int run(int argc, char *argv[]) {
_cleanup_(crypt_freep) struct crypt_device *cd = NULL;
_cleanup_(iovec_done_erase) struct iovec vk = {};
@@ -1040,6 +1276,8 @@ static int run(int argc, char *argv[]) {
if (r > 0)
return cryptenroll_varlink_server();
+ argon2id_parameters_init_autotune(&arg_tpm2_argon2id_params);
+
r = parse_argv(argc, argv);
if (r <= 0)
return r;
@@ -1089,6 +1327,14 @@ static int run(int argc, char *argv[]) {
if (r < 0)
goto finish;
+ /* Benchmark Argon2id parameters before TPM2 enrollment with PIN */
+ if (c.enroll_type == ENROLL_TPM2 && c.tpm2_pin == TPM2_WITH_PIN_YES)
+ argon2id_parameters_benchmark(
+ &c.tpm2_argon2id_params,
+ c.tpm2_argon2id_iter_time > 0
+ ? c.tpm2_argon2id_iter_time
+ : (usec_t) ARGON2ID_BENCHMARK_DEFAULT_TARGET_MS * USEC_PER_MSEC);
+
slot = enroll_now(&c, cd, &vk, /* ret_recovery_key= */ NULL);
if (slot < 0) {
r = slot;
diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h
index 9bc51bceadb..4e96976f50a 100644
--- a/src/cryptenroll/cryptenroll.h
+++ b/src/cryptenroll/cryptenroll.h
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
+#include "crypto-util.h"
#include "libfido2-util.h"
#include "shared-forward.h"
@@ -33,6 +34,14 @@ typedef enum WipeScope {
_WIPE_SCOPE_INVALID = -EINVAL,
} WipeScope;
+typedef enum Tpm2WithPin {
+ TPM2_WITH_PIN_NO,
+ TPM2_WITH_PIN_YES, /* with argon2id */
+ TPM2_WITH_PIN_DIRECT, /* without argon2id (legacy mode, v251 and before) */
+ _TPM2_WITH_PIN_MAX,
+ _TPM2_WITH_PIN_INVALID = -EINVAL,
+} Tpm2WithPin;
+
DECLARE_STRING_TABLE_LOOKUP(enroll_type, EnrollType);
DECLARE_STRING_TABLE_LOOKUP(luks2_token_type, EnrollType);
@@ -74,7 +83,9 @@ typedef struct EnrollContext {
char *tpm2_device_key;
Tpm2PCRValue *tpm2_hash_pcr_values;
size_t tpm2_n_hash_pcr_values;
- bool tpm2_pin;
+ Tpm2WithPin tpm2_pin;
+ Argon2IdParameters tpm2_argon2id_params;
+ usec_t tpm2_argon2id_iter_time;
char *tpm2_public_key;
bool tpm2_load_public_key;
char *tpm2_public_key_policyref;
@@ -106,7 +117,9 @@ typedef struct EnrollContext {
.unlock_type = UNLOCK_PASSWORD, \
.fido2_parameters_in_header = true, \
.fido2_lock_with = FIDO2ENROLL_PIN | FIDO2ENROLL_UP, \
+ .tpm2_pin = _TPM2_WITH_PIN_INVALID, \
.tpm2_load_public_key = true, \
+ .tpm2_argon2id_params = ARGON2ID_PARAMETERS_DEFAULT, \
.wipe_slots_scope = WIPE_EXPLICIT, \
.wipe_except_slot = -1, \
.interactive = true, \
diff --git a/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c b/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c
index dc67ebeb7aa..126982b6b27 100644
--- a/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c
+++ b/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c
@@ -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)));
diff --git a/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c b/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c
index e88d1594747..de26e400e35 100644
--- a/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c
+++ b/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c
@@ -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;
}
diff --git a/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.h b/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.h
index 7668d4bc932..a12e8731f80 100644
--- a/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.h
+++ b/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.h
@@ -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);
diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c
index db34b7e9747..e105794bb75 100644
--- a/src/cryptsetup/cryptsetup.c
+++ b/src/cryptsetup/cryptsetup.c
@@ -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.");
diff --git a/src/repart/repart.c b/src/repart/repart.c
index 3eada3bc42a..b7992eefbe9 100644
--- a/src/repart/repart.c
+++ b/src/repart/repart.c
@@ -5816,6 +5816,7 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta
&srk,
&pcrlock_policy.nv_handle,
flags,
+ &(Argon2IdParameters) {},
&v);
if (r < 0)
return log_error_errno(r, "Failed to prepare TPM2 JSON token object: %m");
diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c
index d285c16b8b3..55b28f21359 100644
--- a/src/shared/crypto-util.c
+++ b/src/shared/crypto-util.c
@@ -27,6 +27,11 @@
# include
# endif
+/* Forward declarations for OpenSSL thread pool API (optional, available in OpenSSL >= 3.2). These
+ * are resolved at runtime via DLSYM_OPTIONAL later. */
+int OSSL_set_max_threads(OSSL_LIB_CTX *ctx, uint64_t max_threads);
+uint64_t OSSL_get_max_threads(OSSL_LIB_CTX *ctx);
+
struct OpenSSLAskPasswordUI {
AskPasswordRequest request;
UI_METHOD *method;
@@ -226,8 +231,11 @@ DLSYM_PROTOTYPE(OSSL_PARAM_BLD_free) = NULL;
DLSYM_PROTOTYPE(OSSL_PARAM_BLD_new) = NULL;
DLSYM_PROTOTYPE(OSSL_PARAM_BLD_push_BN) = NULL;
static DLSYM_PROTOTYPE(OSSL_PARAM_BLD_push_octet_string) = NULL;
+static DLSYM_PROTOTYPE(OSSL_PARAM_BLD_push_uint) = NULL;
DLSYM_PROTOTYPE(OSSL_PARAM_BLD_push_utf8_string) = NULL;
DLSYM_PROTOTYPE(OSSL_PARAM_BLD_to_param) = NULL;
+static DLSYM_PROTOTYPE(OSSL_set_max_threads) = NULL;
+static DLSYM_PROTOTYPE(OSSL_get_max_threads) = NULL;
DLSYM_PROTOTYPE(OSSL_PARAM_construct_BN) = NULL;
DLSYM_PROTOTYPE(OSSL_PARAM_construct_end) = NULL;
DLSYM_PROTOTYPE(OSSL_PARAM_construct_octet_string) = NULL;
@@ -547,6 +555,7 @@ int dlopen_libcrypto(int log_level) {
DLSYM_ARG(OSSL_PARAM_BLD_new),
DLSYM_ARG(OSSL_PARAM_BLD_push_BN),
DLSYM_ARG(OSSL_PARAM_BLD_push_octet_string),
+ DLSYM_ARG(OSSL_PARAM_BLD_push_uint),
DLSYM_ARG(OSSL_PARAM_BLD_push_utf8_string),
DLSYM_ARG(OSSL_PARAM_BLD_to_param),
DLSYM_ARG(OSSL_PARAM_construct_BN),
@@ -660,6 +669,11 @@ int dlopen_libcrypto(int log_level) {
DLSYM_OPTIONAL(libcrypto_dl, ENGINE_load_private_key);
#endif
+ /* Optional thread pool API (OpenSSL >= 3.2). These symbols are resolved at runtime â if the
+ * running libcrypto doesn't provide them, the function pointers stay NULL and threading is
+ * skipped in kdf_argon2id_derive(). */
+ DLSYM_OPTIONAL(libcrypto_dl, OSSL_set_max_threads);
+ DLSYM_OPTIONAL(libcrypto_dl, OSSL_get_max_threads);
return r;
#else
return log_full_errno(log_level, SYNTHETIC_ERRNO(EOPNOTSUPP),
@@ -667,6 +681,18 @@ int dlopen_libcrypto(int log_level) {
#endif
}
+bool dlopen_libcrypto_has_argon2id(void) {
+#if HAVE_OPENSSL
+ if (dlopen_libcrypto(LOG_DEBUG) < 0)
+ return false;
+
+ _cleanup_(EVP_KDF_freep) EVP_KDF *kdf = sym_EVP_KDF_fetch(/* propq= */ NULL, "ARGON2ID", /* propq= */ NULL);
+ return !!kdf;
+#else
+ return false;
+#endif
+}
+
#if HAVE_OPENSSL
int openssl_to_errno(unsigned long e) {
@@ -1223,6 +1249,168 @@ int kdf_kb_hmac_derive(
return 0;
}
+/* Perform Argon2id KDF, producing derive_size bytes of output.
+ *
+ * For more details see: https://docs.openssl.org/master/man7/EVP_KDF-ARGON2/ */
+int kdf_argon2id_derive(
+ const struct iovec *password,
+ const struct iovec *salt,
+ const Argon2IdParameters *params,
+ size_t derive_size,
+ struct iovec *ret) {
+
+ int r;
+
+ assert(!password || password->iov_len > 0);
+ assert(!salt || salt->iov_len > 0);
+ assert(derive_size > 0);
+ assert(ret);
+
+ r = dlopen_libcrypto(LOG_DEBUG);
+ if (r < 0)
+ return r;
+
+ _cleanup_(EVP_KDF_freep) EVP_KDF *kdf = sym_EVP_KDF_fetch(/* propq= */ NULL, "ARGON2ID", /* propq= */ NULL);
+ if (!kdf)
+ return log_openssl_errors(LOG_DEBUG, "Failed to create new EVP_KDF for ARGON2ID");
+
+ _cleanup_(EVP_KDF_CTX_freep) EVP_KDF_CTX *ctx = sym_EVP_KDF_CTX_new(kdf);
+ if (!ctx)
+ return log_openssl_errors(LOG_DEBUG, "Failed to create new EVP_KDF_CTX");
+
+ _cleanup_(OSSL_PARAM_BLD_freep) OSSL_PARAM_BLD *bld = sym_OSSL_PARAM_BLD_new();
+ if (!bld)
+ return log_openssl_errors(LOG_DEBUG, "Failed to create new OSSL_PARAM_BLD");
+
+ assert(params);
+ assert(params->memcost_bytes > 0);
+ assert(params->iterations > 0);
+ assert(params->lanes > 0);
+
+ _cleanup_(erase_and_freep) void *buf = malloc(derive_size);
+ if (!buf)
+ return log_oom_debug();
+
+ if (password && !sym_OSSL_PARAM_BLD_push_octet_string(bld, "pass", password->iov_base, password->iov_len))
+ return log_openssl_errors(LOG_DEBUG, "Failed to add ARGON2ID pass");
+
+ if (salt && !sym_OSSL_PARAM_BLD_push_octet_string(bld, "salt", salt->iov_base, salt->iov_len))
+ return log_openssl_errors(LOG_DEBUG, "Failed to add ARGON2ID salt");
+
+ uint64_t memcost_kb = params->memcost_bytes / 1024;
+
+ if (memcost_kb > UINT_MAX)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Argon2id memory cost too large: %"PRIu64" bytes", params->memcost_bytes);
+
+ if (!sym_OSSL_PARAM_BLD_push_uint(bld, "memcost", (unsigned) memcost_kb))
+ return log_openssl_errors(LOG_DEBUG, "Failed to add ARGON2ID memcost");
+
+ if (!sym_OSSL_PARAM_BLD_push_uint(bld, "iter", params->iterations))
+ return log_openssl_errors(LOG_DEBUG, "Failed to add ARGON2ID iter");
+
+ if (!sym_OSSL_PARAM_BLD_push_uint(bld, "lanes", params->lanes))
+ return log_openssl_errors(LOG_DEBUG, "Failed to add ARGON2ID lanes");
+
+ /* FIXME: drop sym_OSSL_set_max_threads() conditionalization once OpenSSL 3.2 becomes the minimum baseline */
+ if (params->lanes > 1 && sym_OSSL_set_max_threads && !sym_OSSL_PARAM_BLD_push_uint(bld, "threads", params->lanes))
+ return log_openssl_errors(LOG_DEBUG, "Failed to add ARGON2ID threads");
+
+ _cleanup_(OSSL_PARAM_freep) OSSL_PARAM *openssl_params = sym_OSSL_PARAM_BLD_to_param(bld);
+ if (!openssl_params)
+ return log_openssl_errors(LOG_DEBUG, "Failed to build ARGON2ID OSSL_PARAM");
+
+ /* FIXME: drop sym_OSSL_set_max_threads() conditionalization once OpenSSL 3.2 becomes the minimum baseline */
+ uint64_t saved_max_threads = 0;
+ if (params->lanes > 1 && sym_OSSL_set_max_threads) {
+ if (sym_OSSL_get_max_threads)
+ saved_max_threads = sym_OSSL_get_max_threads(/* ctx= */ NULL);
+ if (!sym_OSSL_set_max_threads(/* ctx= */ NULL, params->lanes))
+ return log_openssl_errors(LOG_DEBUG, "Failed to set Argon2id thread pool size");
+ }
+
+ if (sym_EVP_KDF_derive(ctx, buf, derive_size, openssl_params) <= 0) {
+ if (params->lanes > 1 && sym_OSSL_set_max_threads)
+ sym_OSSL_set_max_threads(/* ctx= */ NULL, saved_max_threads);
+ return log_openssl_errors(LOG_DEBUG, "OpenSSL ARGON2ID derive failed");
+ }
+
+ if (params->lanes > 1 && sym_OSSL_set_max_threads)
+ sym_OSSL_set_max_threads(/* ctx= */ NULL, saved_max_threads);
+
+ ret->iov_base = TAKE_PTR(buf);
+ ret->iov_len = derive_size;
+
+ return 0;
+}
+
+/* Perform HKDF-SHA256 derivation, producing derive_size bytes of output.
+ *
+ * For more details see: https://docs.openssl.org/master/man7/EVP_KDF-HKDF.html */
+int kdf_hkdf_sha256(
+ const struct iovec *key,
+ const struct iovec *salt,
+ const struct iovec *info,
+ size_t derive_size,
+ struct iovec *ret) {
+
+ int r;
+
+ assert(!key || key->iov_len > 0);
+ assert(!salt || salt->iov_len > 0);
+ assert(!info || info->iov_len > 0);
+ assert(derive_size > 0);
+ assert(ret);
+
+ r = dlopen_libcrypto(LOG_DEBUG);
+ if (r < 0)
+ return r;
+
+ _cleanup_(EVP_KDF_freep) EVP_KDF *kdf = sym_EVP_KDF_fetch(/* propq= */ NULL, "HKDF", /* propq= */ NULL);
+ if (!kdf)
+ return log_openssl_errors(LOG_DEBUG, "Failed to create new EVP_KDF for HKDF");
+
+ _cleanup_(EVP_KDF_CTX_freep) EVP_KDF_CTX *ctx = sym_EVP_KDF_CTX_new(kdf);
+ if (!ctx)
+ return log_openssl_errors(LOG_DEBUG, "Failed to create new EVP_KDF_CTX");
+
+ _cleanup_(OSSL_PARAM_BLD_freep) OSSL_PARAM_BLD *bld = sym_OSSL_PARAM_BLD_new();
+ if (!bld)
+ return log_openssl_errors(LOG_DEBUG, "Failed to create new OSSL_PARAM_BLD");
+
+ _cleanup_(erase_and_freep) void *buf = malloc(derive_size);
+ if (!buf)
+ return log_oom_debug();
+
+ if (!sym_OSSL_PARAM_BLD_push_utf8_string(bld, "digest", "SHA256", 0))
+ return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF digest");
+
+ if (key)
+ if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, "key", key->iov_base, key->iov_len))
+ return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF key");
+
+ if (salt)
+ if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, "salt", salt->iov_base, salt->iov_len))
+ return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF salt");
+
+ if (info)
+ if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, "info", info->iov_base, info->iov_len))
+ return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF info");
+
+ _cleanup_(OSSL_PARAM_freep) OSSL_PARAM *openssl_params = sym_OSSL_PARAM_BLD_to_param(bld);
+ if (!openssl_params)
+ return log_openssl_errors(LOG_DEBUG, "Failed to build HKDF OSSL_PARAM");
+
+ if (sym_EVP_KDF_derive(ctx, buf, derive_size, openssl_params) <= 0)
+ return log_openssl_errors(LOG_DEBUG, "OpenSSL HKDF derive failed");
+
+ ret->iov_base = TAKE_PTR(buf);
+ ret->iov_len = derive_size;
+
+ return 0;
+}
+
+/* Encrypt the key data using RSA-OAEP with the provided label and specified digest algorithm. Returns 0 on
+ * success, -EOPNOTSUPP if the digest algorithm is not supported, or < 0 for any other error. */
int rsa_oaep_encrypt_bytes(
const EVP_PKEY *pkey,
const char *digest_alg,
@@ -2413,6 +2601,28 @@ int openssl_load_x509_certificate(
}
#endif
+#if !HAVE_OPENSSL
+int kdf_argon2id_derive(
+ const struct iovec *password,
+ const struct iovec *salt,
+ const Argon2IdParameters *params,
+ size_t derive_size,
+ struct iovec *ret) {
+
+ return -EOPNOTSUPP;
+}
+
+int kdf_hkdf_sha256(
+ const struct iovec *key,
+ const struct iovec *salt,
+ const struct iovec *info,
+ size_t derive_size,
+ struct iovec *ret) {
+
+ return -EOPNOTSUPP;
+}
+#endif
+
int parse_openssl_certificate_source_argument(
const char *argument,
char **certificate_source,
diff --git a/src/shared/crypto-util.h b/src/shared/crypto-util.h
index ebf36e2df03..b2705998a6a 100644
--- a/src/shared/crypto-util.h
+++ b/src/shared/crypto-util.h
@@ -447,9 +447,27 @@ DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(OpenSSLAskPasswordUI*, openssl_ask_password_ui_
#endif
int dlopen_libcrypto(int log_level) _dlopen_loader_;
+bool dlopen_libcrypto_has_argon2id(void);
#define DLOPEN_LIBCRYPTO(log_level, priority) \
({ \
LIBCRYPTO_NOTE(priority); \
dlopen_libcrypto(log_level); \
})
+
+typedef struct Argon2IdParameters {
+ uint64_t memcost_bytes;
+ uint32_t iterations;
+ uint32_t lanes;
+} Argon2IdParameters;
+
+#define ARGON2ID_PARAMETERS_DEFAULT \
+ (Argon2IdParameters) { \
+ .memcost_bytes = 64ULL * 1024 * 1024, \
+ .iterations = 8, \
+ .lanes = 4, \
+ }
+
+int kdf_argon2id_derive(const struct iovec *password, const struct iovec *salt, const Argon2IdParameters *params, size_t derive_size, struct iovec *ret);
+
+int kdf_hkdf_sha256(const struct iovec *key, const struct iovec *salt, const struct iovec *info, size_t derive_size, struct iovec *ret);
diff --git a/src/shared/cryptsetup-tpm2.c b/src/shared/cryptsetup-tpm2.c
index a34c87cb917..0d8f675b313 100644
--- a/src/shared/cryptsetup-tpm2.c
+++ b/src/shared/cryptsetup-tpm2.c
@@ -4,6 +4,7 @@
#include "alloc-util.h"
#include "ask-password-api.h"
+#include "crypto-util.h"
#include "cryptsetup-tpm2.h"
#include "cryptsetup-util.h"
#include "env-util.h"
@@ -88,6 +89,7 @@ int acquire_tpm2_key(
usec_t until,
const char *askpw_credential,
AskPasswordFlags askpw_flags,
+ const Argon2IdParameters *argon2id_params,
struct iovec *ret_decrypted_key) {
#if HAVE_LIBCRYPTSETUP && HAVE_TPM2
@@ -96,8 +98,6 @@ int acquire_tpm2_key(
_cleanup_free_ char *auto_device = NULL;
int r;
- assert(iovec_is_valid(salt));
-
if (!device) {
r = tpm2_find_device_auto(&auto_device);
if (r == -ENODEV)
@@ -190,33 +190,53 @@ int acquire_tpm2_key(
return r;
}
+ bool argon2id = FLAGS_SET(flags, TPM2_FLAGS_USE_ARGON2ID);
+
for (int i = 5;; i--) {
- _cleanup_(erase_and_freep) char *pin_str = NULL, *b64_salted_pin = NULL;
+ _cleanup_(erase_and_freep) char *input_str = NULL, *b64_key2 = NULL;
+ _cleanup_(iovec_done_erase) struct iovec key1 = {};
+ const char *pin_used;
if (i <= 0)
return -EACCES;
- r = get_pin(until, askpw_credential, askpw_flags, &pin_str);
+ r = get_pin(until, askpw_credential, askpw_flags, &input_str);
if (r < 0)
return r;
askpw_flags &= ~ASK_PASSWORD_ACCEPT_CACHED;
- if (iovec_is_set(salt)) {
- uint8_t salted_pin[SHA256_DIGEST_SIZE] = {};
- CLEANUP_ERASE(salted_pin);
-
- r = tpm2_util_pbkdf2_hmac_sha256(pin_str, strlen(pin_str), salt->iov_base, salt->iov_len, salted_pin);
- if (r < 0)
- return log_error_errno(r, "Failed to perform PBKDF2: %m");
+ if (argon2id) {
+ if (isempty(input_str))
+ 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 = base64mem(salted_pin, sizeof(salted_pin), &b64_salted_pin);
+ r = tpm2_argon2id_derive_split(input_str, salt, argon2id_params, &key1, &b64_key2);
if (r < 0)
- return log_error_errno(r, "Failed to base64 encode salted pin: %m");
- } else
- /* no salting needed, backwards compat with non-salted pins */
- b64_salted_pin = TAKE_PTR(pin_str);
+ return log_error_errno(r, "Failed to perform Argon2id: %m");
+
+ pin_used = b64_key2;
+ } else {
+ if (iovec_is_set(salt)) {
+ uint8_t salted_pin[SHA256_DIGEST_SIZE] = {};
+ CLEANUP_ERASE(salted_pin);
+
+ r = tpm2_util_pbkdf2_hmac_sha256(input_str, strlen(input_str), 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_key2);
+ if (r < 0)
+ return log_error_errno(r, "Failed to base64 encode salted pin: %m");
+
+ pin_used = b64_key2;
+ } else
+ /* else: no salting needed, backwards compat with non-salted pins */
+ pin_used = input_str;
+ }
+ _cleanup_(iovec_done_erase) struct iovec unsealed = {};
r = tpm2_unseal(tpm2_context,
hash_pcr_mask,
pcr_bank,
@@ -224,7 +244,7 @@ int acquire_tpm2_key(
pubkey_policy_ref,
pubkey_pcr_mask,
signature_json,
- b64_salted_pin,
+ pin_used,
FLAGS_SET(flags, TPM2_FLAGS_USE_PCRLOCK) ? &pcrlock_policy : NULL,
primary_alg,
blobs,
@@ -232,7 +252,7 @@ int acquire_tpm2_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)
@@ -247,13 +267,22 @@ int acquire_tpm2_key(
if (r == -ENOLCK)
return log_error_errno(r, "TPM is in dictionary attack lock-out mode.");
if (r == -EILSEQ) {
- log_warning_errno(r, "Bad PIN.");
+ log_warning_errno(r, "Bad %s.", argon2id ? "password" : "PIN");
continue;
}
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;
}
#else
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support not available.");
@@ -279,7 +308,8 @@ int find_tpm2_auto_data(
struct iovec *ret_pcrlock_nv,
TPM2Flags *ret_flags,
int *ret_keyslot,
- int *ret_token) {
+ int *ret_token,
+ Argon2IdParameters *ret_argon2id_params) {
#if HAVE_LIBCRYPTSETUP && HAVE_TPM2
int r, token;
@@ -310,6 +340,7 @@ int find_tpm2_auto_data(
size_t n_blobs = 0, n_policy_hash = 0;
uint32_t hash_pcr_mask, pubkey_pcr_mask;
uint16_t pcr_bank, primary_alg;
+ Argon2IdParameters ap = {};
TPM2Flags flags;
int keyslot;
@@ -338,7 +369,8 @@ int find_tpm2_auto_data(
&salt,
&srk,
&pcrlock_nv,
- &flags);
+ &flags,
+ &ap);
if (r == -EUCLEAN) /* Gracefully handle issues in JSON fields not owned by us */
continue;
if (r < 0)
@@ -366,6 +398,8 @@ int find_tpm2_auto_data(
*ret_srk = TAKE_STRUCT(srk);
*ret_pcrlock_nv = TAKE_STRUCT(pcrlock_nv);
*ret_flags = flags;
+ if (ret_argon2id_params)
+ *ret_argon2id_params = ap;
return 0;
}
diff --git a/src/shared/cryptsetup-tpm2.h b/src/shared/cryptsetup-tpm2.h
index f27204dda16..7c06ebd2005 100644
--- a/src/shared/cryptsetup-tpm2.h
+++ b/src/shared/cryptsetup-tpm2.h
@@ -3,6 +3,7 @@
#include
+#include "crypto-util.h"
#include "shared-forward.h"
int acquire_tpm2_key(
@@ -30,6 +31,7 @@ int acquire_tpm2_key(
usec_t until,
const char *askpw_credential,
AskPasswordFlags askpw_flags,
+ const Argon2IdParameters *argon2id_params,
struct iovec *ret_decrypted_key);
int find_tpm2_auto_data(
@@ -51,4 +53,5 @@ int find_tpm2_auto_data(
struct iovec *ret_pcrlock_nv,
TPM2Flags *ret_flags,
int *ret_keyslot,
- int *ret_token);
+ int *ret_token,
+ Argon2IdParameters *ret_argon2id_params);
diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c
index 788b153c5ab..b57fb092bb3 100644
--- a/src/shared/tpm2-util.c
+++ b/src/shared/tpm2-util.c
@@ -30,6 +30,7 @@
#include "initrd-util.h"
#include "io-util.h"
#include "json-util.h"
+#include "limits-util.h"
#include "log.h"
#include "logarithm.h"
#include "memory-util.h"
@@ -9658,6 +9659,63 @@ int tpm2_hmac_key_from_pin(Tpm2Context *c, const Tpm2Handle *session, const TPM2
}
#endif
+int tpm2_argon2id_derive_split(
+ const char *pin,
+ const struct iovec *salt,
+ const Argon2IdParameters *argon2id_params,
+ struct iovec *ret_key1,
+ char **ret_b64_pin) {
+
+ int r;
+ _cleanup_(iovec_done_erase) struct iovec derived = {};
+
+ assert(pin);
+ assert(salt);
+ assert(argon2id_params);
+ assert(ret_key1);
+ assert(ret_b64_pin);
+
+ r = kdf_argon2id_derive(
+ &IOVEC_MAKE(pin, strlen(pin)),
+ salt,
+ argon2id_params,
+ /* derive_size= */ 64, &derived);
+ if (r < 0)
+ return r;
+
+ assert(derived.iov_len == 64);
+
+ const uint8_t *derived_bytes = derived.iov_base;
+
+ ret_key1->iov_base = memdup(derived_bytes, 32);
+ if (!ret_key1->iov_base)
+ return log_oom();
+ ret_key1->iov_len = 32;
+
+ ssize_t n = base64mem(derived_bytes + 32, 32, ret_b64_pin);
+ if (n < 0)
+ return log_oom();
+
+ return 0;
+}
+
+int tpm2_argon2id_hkdf(
+ const struct iovec *key1,
+ const struct iovec *unsealed,
+ struct iovec *ret_volume_key) {
+
+ assert(key1);
+ assert(key1->iov_len > 0);
+ assert(unsealed);
+ assert(ret_volume_key);
+
+ return kdf_hkdf_sha256(
+ key1,
+ unsealed,
+ &IOVEC_MAKE_STRING("systemd-tpm2-argon2id-lock"),
+ /* derive_size= */ 32, ret_volume_key);
+}
+
char* tpm2_pcr_mask_to_string(uint32_t mask) {
_cleanup_free_ char *s = NULL;
@@ -9781,6 +9839,7 @@ int tpm2_make_luks2_json(
const struct iovec *srk,
const struct iovec *pcrlock_nv,
TPM2Flags flags,
+ const Argon2IdParameters *argon2id_params,
sd_json_variant **ret) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL, *hmj = NULL, *pkmj = NULL;
@@ -9792,6 +9851,8 @@ int tpm2_make_luks2_json(
assert(n_blobs >= 1);
assert(n_policy_hash >= 1);
+ POINTER_MAY_BE_NULL(argon2id_params);
+
if (asprintf(&keyslot_as_string, "%i", keyslot) < 0)
return -ENOMEM;
@@ -9815,6 +9876,11 @@ int tpm2_make_luks2_json(
if (r < 0)
return r;
+ const Argon2IdParameters argon2id_params_safe =
+ FLAGS_SET(flags, TPM2_FLAGS_USE_ARGON2ID) && argon2id_params
+ ? *argon2id_params
+ : (Argon2IdParameters) {};
+
/* Note: We made the mistake of using "-" in the field names, which isn't particular compatible with
* other programming languages. Let's not make things worse though, i.e. future additions to the JSON
* object should use "_" rather than "-" in field names. */
@@ -9835,7 +9901,10 @@ int tpm2_make_luks2_json(
SD_JSON_BUILD_PAIR_CONDITION(pubkey_policy_ref != NULL, "tpm2_pubkey_ref", SD_JSON_BUILD_STRING(pubkey_policy_ref)),
SD_JSON_BUILD_PAIR_CONDITION(iovec_is_set(salt), "tpm2_salt", JSON_BUILD_IOVEC_BASE64(salt)),
SD_JSON_BUILD_PAIR_CONDITION(iovec_is_set(srk), "tpm2_srk", JSON_BUILD_IOVEC_BASE64(srk)),
- SD_JSON_BUILD_PAIR_CONDITION(iovec_is_set(pcrlock_nv), "tpm2_pcrlock_nv", JSON_BUILD_IOVEC_BASE64(pcrlock_nv)));
+ SD_JSON_BUILD_PAIR_CONDITION(iovec_is_set(pcrlock_nv), "tpm2_pcrlock_nv", JSON_BUILD_IOVEC_BASE64(pcrlock_nv)),
+ SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(flags, TPM2_FLAGS_USE_ARGON2ID), "tpm2_argon2id_memcost", SD_JSON_BUILD_UNSIGNED(argon2id_params_safe.memcost_bytes)),
+ SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(flags, TPM2_FLAGS_USE_ARGON2ID), "tpm2_argon2id_iterations", SD_JSON_BUILD_UNSIGNED(argon2id_params_safe.iterations)),
+ SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(flags, TPM2_FLAGS_USE_ARGON2ID), "tpm2_argon2id_lanes", SD_JSON_BUILD_UNSIGNED(argon2id_params_safe.lanes)));
if (r < 0)
return r;
@@ -9918,7 +9987,8 @@ int tpm2_parse_luks2_json(
struct iovec *ret_salt,
struct iovec *ret_srk,
struct iovec *ret_pcrlock_nv,
- TPM2Flags *ret_flags) {
+ TPM2Flags *ret_flags,
+ Argon2IdParameters *ret_argon2id_params) {
_cleanup_(iovec_done) struct iovec pubkey = {}, salt = {}, srk = {}, pcrlock_nv = {};
_cleanup_free_ char *pubkey_ref = NULL;
@@ -10068,6 +10138,52 @@ int tpm2_parse_luks2_json(
return log_debug_errno(r, "Invalid base64 data in 'tpm2_pcrlock_nv' field.");
}
+ Argon2IdParameters ap = {};
+
+ w = sd_json_variant_by_key(v, "tpm2_argon2id_memcost");
+ if (w) {
+ if (!sd_json_variant_is_unsigned(w))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "tpm2_argon2id_memcost is not an unsigned integer.");
+ ap.memcost_bytes = sd_json_variant_unsigned(w);
+ if (ap.memcost_bytes == 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EUCLEAN), "tpm2_argon2id_memcost must be non-zero.");
+ }
+
+ w = sd_json_variant_by_key(v, "tpm2_argon2id_iterations");
+ if (w) {
+ uint64_t raw_iterations;
+
+ if (!sd_json_variant_is_unsigned(w))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "tpm2_argon2id_iterations is not an unsigned integer.");
+ raw_iterations = sd_json_variant_unsigned(w);
+ if (raw_iterations == 0 || raw_iterations > UINT32_MAX)
+ return log_debug_errno(SYNTHETIC_ERRNO(EUCLEAN), "tpm2_argon2id_iterations value out of range.");
+ ap.iterations = (uint32_t) raw_iterations;
+ }
+
+ w = sd_json_variant_by_key(v, "tpm2_argon2id_lanes");
+ if (w) {
+ uint64_t raw_lanes;
+
+ if (!sd_json_variant_is_unsigned(w))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "tpm2_argon2id_lanes is not an unsigned integer.");
+ raw_lanes = sd_json_variant_unsigned(w);
+ if (raw_lanes == 0 || raw_lanes > UINT32_MAX)
+ return log_debug_errno(SYNTHETIC_ERRNO(EUCLEAN), "tpm2_argon2id_lanes value out of range.");
+ ap.lanes = (uint32_t) raw_lanes;
+ }
+
+ if (ap.memcost_bytes > 0 && ap.iterations > 0 && ap.lanes > 0) {
+ if (!iovec_is_set(&salt))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Argon2id PIN requires salt in LUKS2 token.");
+
+ if (ap.memcost_bytes > physical_memory())
+ return log_debug_errno(SYNTHETIC_ERRNO(EUCLEAN), "Argon2id memory cost exceeds physical memory.");
+
+ SET_FLAG(flags, TPM2_FLAGS_USE_ARGON2ID, true);
+ } else if (ap.memcost_bytes > 0 || ap.iterations > 0 || ap.lanes > 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EUCLEAN), "Incomplete Argon2id parameters in LUKS2 token.");
+
if (ret_keyslot)
*ret_keyslot = keyslot;
if (ret_hash_pcr_mask)
@@ -10098,6 +10214,8 @@ int tpm2_parse_luks2_json(
*ret_pcrlock_nv = TAKE_STRUCT(pcrlock_nv);
if (ret_flags)
*ret_flags = flags;
+ if (ret_argon2id_params)
+ *ret_argon2id_params = ap;
return 0;
}
diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h
index 4d2420ba03c..9daf2466a18 100644
--- a/src/shared/tpm2-util.h
+++ b/src/shared/tpm2-util.h
@@ -2,14 +2,16 @@
#pragma once
#include "bitfield.h"
+#include "crypto-util.h"
#include "dlopen-note.h"
#include "iovec-util.h"
#include "shared-forward.h"
#include "sha256.h"
typedef enum TPM2Flags {
- TPM2_FLAGS_USE_PIN = 1 << 0,
- TPM2_FLAGS_USE_PCRLOCK = 1 << 1,
+ TPM2_FLAGS_USE_PIN = 1 << 0,
+ TPM2_FLAGS_USE_PCRLOCK = 1 << 1,
+ TPM2_FLAGS_USE_ARGON2ID = 1 << 2,
} TPM2Flags;
/* As per https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClient_PFP_r1p05_v23_pub.pdf a
@@ -467,14 +469,17 @@ static inline int tpm2_pcrlock_search_file(const char *path, FILE **ret_file, ch
}
#endif /* HAVE_TPM2 */
+int tpm2_argon2id_derive_split(const char *pin, const struct iovec *salt, const Argon2IdParameters *argon2id_params, struct iovec *ret_key1, char **ret_b64_pin);
+int tpm2_argon2id_hkdf(const struct iovec *key1, const struct iovec *unsealed, struct iovec *ret_volume_key);
+
int tpm2_list_devices(bool legend, bool quiet);
int tpm2_find_device_auto(char **ret);
int tpm2_make_pcr_json_array(uint32_t pcr_mask, sd_json_variant **ret);
int tpm2_parse_pcr_json_array(sd_json_variant *v, uint32_t *ret);
-int tpm2_make_luks2_json(int keyslot, uint32_t hash_pcr_mask, uint16_t pcr_bank, const struct iovec *pubkey, const char *pubkey_policy_ref, uint32_t pubkey_pcr_mask, uint16_t primary_alg, const struct iovec blobs[], size_t n_blobs, const struct iovec policy_hash[], size_t n_policy_hash, const struct iovec *salt, const struct iovec *srk, const struct iovec *pcrlock_nv, TPM2Flags flags, sd_json_variant **ret);
-int tpm2_parse_luks2_json(sd_json_variant *v, int *ret_keyslot, uint32_t *ret_hash_pcr_mask, uint16_t *ret_pcr_bank, struct iovec *ret_pubkey, char **ret_pubkey_policy_ref, uint32_t *ret_pubkey_pcr_mask, uint16_t *ret_primary_alg, struct iovec **ret_blobs, size_t *ret_n_blobs, struct iovec **ret_policy_hash, size_t *ret_n_policy_hash, struct iovec *ret_salt, struct iovec *ret_srk, struct iovec *ret_pcrlock_nv, TPM2Flags *ret_flags);
+int tpm2_make_luks2_json(int keyslot, uint32_t hash_pcr_mask, uint16_t pcr_bank, const struct iovec *pubkey, const char *pubkey_policy_ref, uint32_t pubkey_pcr_mask, uint16_t primary_alg, const struct iovec blobs[], size_t n_blobs, const struct iovec policy_hash[], size_t n_policy_hash, const struct iovec *salt, const struct iovec *srk, const struct iovec *pcrlock_nv, TPM2Flags flags, const Argon2IdParameters *argon2id_params, sd_json_variant **ret);
+int tpm2_parse_luks2_json(sd_json_variant *v, int *ret_keyslot, uint32_t *ret_hash_pcr_mask, uint16_t *ret_pcr_bank, struct iovec *ret_pubkey, char **ret_pubkey_policy_ref, uint32_t *ret_pubkey_pcr_mask, uint16_t *ret_primary_alg, struct iovec **ret_blobs, size_t *ret_n_blobs, struct iovec **ret_policy_hash, size_t *ret_n_policy_hash, struct iovec *ret_salt, struct iovec *ret_srk, struct iovec *ret_pcrlock_nv, TPM2Flags *ret_flags, Argon2IdParameters *ret_argon2id_params);
/* Before v258 we used to bind to PCR 7 by default at various places if no explicit PCR mask was set. With
* v258 we stopped doing that (since the SecureBoot DB is as much subject to regular updates by tools such as
diff --git a/test/test-dlopen-note.py b/test/test-dlopen-note.py
index 7882ab3dd46..295a0bc69c3 100755
--- a/test/test-dlopen-note.py
+++ b/test/test-dlopen-note.py
@@ -37,6 +37,7 @@ FUNCTION_FILTER = {
'dlopen_verbose': [],
'dlopen_many_sym_or_warn_sentinel': [],
'dlopen_dw_has_dwfl_set_sysroot': [],
+ 'dlopen_libcrypto_has_argon2id': [],
# Mapped to multiple functions (expands generic wrappers into specific sub-layers)
'dlopen_tpm2': [
'dlopen_tpm2_esys',