]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
cryptsetup: add Argon2id-based PIN mode for TPM2 enrollment
authorfuldeka <260289618+fuldeka@users.noreply.github.com>
Sun, 28 Jun 2026 14:42:47 +0000 (16:42 +0200)
committerfuldeka <260289618+fuldeka@users.noreply.github.com>
Sun, 12 Jul 2026 21:31:29 +0000 (23:31 +0200)
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

16 files changed:
man/systemd-cryptenroll.xml
src/cryptenroll/cryptenroll-tpm2.c
src/cryptenroll/cryptenroll.c
src/cryptenroll/cryptenroll.h
src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c
src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c
src/cryptsetup/cryptsetup-tokens/luks2-tpm2.h
src/cryptsetup/cryptsetup.c
src/repart/repart.c
src/shared/crypto-util.c
src/shared/crypto-util.h
src/shared/cryptsetup-tpm2.c
src/shared/cryptsetup-tpm2.h
src/shared/tpm2-util.c
src/shared/tpm2-util.h
test/test-dlopen-note.py

index 7cbc783edccee2af377c687bb49a7919bdfdfebb..94cab54ad7ac8408638832c3d867129852efc21b 100644 (file)
       </varlistentry>
 
       <varlistentry>
-        <term><option>--tpm2-with-pin=<replaceable>BOOL</replaceable></option></term>
+        <term><option>--tpm2-with-pin=<replaceable>BOOL|direct</replaceable></option></term>
 
         <listitem><para>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 <literal>no</literal>. 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 <literal>direct</literal>. Defaults to <literal>no</literal>.
+        Despite being called PIN, any character can be used, not just numbers.
         </para>
 
+        <para>When enabled (<literal>yes</literal>), 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 <literal>direct</literal> is used, the PIN
+        is hashed with PBKDF2 and used directly (compatible with systemd v253 and later).</para>
+
         <para>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, <command>systemd-cryptenroll</command> does not control or
         project='mankier'><refentrytitle>tpm2_dictionarylockout</refentrytitle><manvolnum>1</manvolnum></citerefentry>
         commands, respectively.</para>
 
-        <xi:include href="version-info.xml" xpointer="v251"/></listitem>
+        <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--tpm2-argon2id-memory=<replaceable>BYTES</replaceable></option></term>
+
+        <listitem><para>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 <literal>K</literal>, <literal>M</literal>, <literal>G</literal>
+        (for kilobytes, megabytes, gigabytes).</para>
+
+        <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--tpm2-argon2id-iterations=<replaceable>NUM</replaceable></option></term>
+
+        <listitem><para>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.</para>
+
+        <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--tpm2-argon2id-parallelism=<replaceable>NUM</replaceable></option></term>
+
+        <listitem><para>When enrolling a TPM2 with PIN hardening enabled, sets the Argon2id
+        parallelism (lane count). Defaults to the number of online CPUs.</para>
+
+        <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--tpm2-argon2id-iter-time=<replaceable>SEC</replaceable></option></term>
+
+        <listitem><para>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.</para>
+
+        <xi:include href="version-info.xml" xpointer="v262"/></listitem>
       </varlistentry>
 
       <varlistentry>
index bc52a18c6ab7f9d032e29558f9abcaf4ba7bb7a3..6165a3164227809e79f8105112c805ab05f8065b 100644 (file)
@@ -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");
index ae708146615d2eeb40221d5cfea84e06dd993425..f295585ff9d1fca065077b69579496361b06ccf2 100644 (file)
@@ -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"
 #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;
index 9bc51bceadb5ad5dd328826c21a598574fe26aa1..4e96976f50a059dd5cb94ee3fc10d76f81b44a2d 100644 (file)
@@ -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,                                    \
index dc67ebeb7aa661eb271054a2b736773d6db2220f..126982b6b273ae060f9af4bf4655aa7f241b0711 100644 (file)
@@ -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)));
index e88d1594747491df2f3cb7105943e47c7f576a1f..de26e400e35e5e7ade02ac4cafe9ea07b5de00d2 100644 (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;
 }
index 7668d4bc932ddad8d78010998fbc28d0c3c2d72a..a12e8731f803711c648494e4ebfa3edfb89d838d 100644 (file)
@@ -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);
index db34b7e97473f641ac6a0951f7bc15995a6ac2ee..e105794bb75643c5b9f0a4b9d8827da7f636ceae 100644 (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.");
index 3eada3bc42a9ecf32e9c538c5307272e6d4ff3fd..b7992eefbe90a7ceba03931d28c51cca8b912742 100644 (file)
@@ -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");
index d285c16b8b3d38dea30d36fb7bc93e8ab2b7ff4f..55b28f213597a1a25044803628c5ebd096936034 100644 (file)
 #    include <openssl/ui.h>
 #  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,
index ebf36e2df035d4e4a3b68bc87b987b3ad42afbfb..b2705998a6ab2f652bbcc9a798d6ff262b9b8d1f 100644 (file)
@@ -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);
index a34c87cb917448221b8d32e9e57e8b1730f8c700..0d8f675b31340e6d8b93000e189256f7edfd4e36 100644 (file)
@@ -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;
                 }
 
index f27204dda1669030c894f3165b96478c59097d32..7c06ebd2005e93d2f2da56030c6bc32e55b591a8 100644 (file)
@@ -3,6 +3,7 @@
 
 #include <sys/uio.h>
 
+#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);
index 788b153c5abe7b4ff41cb06e6e53ec1f20330b6d..b57fb092bb33ad7122dc0a3f1a92b556b220606a 100644 (file)
@@ -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;
 }
 
index 4d2420ba03c2d17695272a05dcdfec3c05d7f93b..9daf2466a183d3b25ad577cdd3f4a93c4053db25 100644 (file)
@@ -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
index 7882ab3dd46f3c2ff0af917835af8b99ba0e360e..295a0bc69c30e6485d0cec0815e4e6da1da19b22 100755 (executable)
@@ -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',