cryptsetup: add Argon2id-based PIN mode for TPM2 enrollment (#41859)
The current TPM2 PIN mode is flawed as a compromised TPM directly exposes the sealed secret which is the LUKS volume key itself (https://github.com/systemd/systemd/pull/27502 and https://github.com/systemd/systemd/issues/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/cryptsetup/cryptsetup-tokens/luks2-tpm2.c: crypto-util.h for kdf_argon2id_derive()/kdf_hkdf_sha256() on the token unlock path
This commit is contained in:
@@ -677,13 +677,19 @@
|
||||
</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
|
||||
@@ -694,7 +700,51 @@
|
||||
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>
|
||||
|
||||
@@ -62,6 +62,10 @@ _systemd_cryptenroll() {
|
||||
--tpm2-device
|
||||
--tpm2-device-key
|
||||
--tpm2-seal-key-handle
|
||||
--tpm2-argon2id-memory
|
||||
--tpm2-argon2id-iter-time
|
||||
--tpm2-argon2id-iterations
|
||||
--tpm2-argon2id-parallelism
|
||||
--tpm2-pcrs
|
||||
--tpm2-public-key
|
||||
--tpm2-public-key-pcrs
|
||||
@@ -95,7 +99,7 @@ _systemd_cryptenroll() {
|
||||
comps="auto list $(__get_fido2_devices)"
|
||||
;;
|
||||
--fido2-parameters-in-header|--fido2-with-client-pin|--fido2-with-user-presence|--fido2-with-user-verification|--tpm2-with-pin)
|
||||
comps='yes no'
|
||||
comps='yes no direct'
|
||||
;;
|
||||
--tpm2-device)
|
||||
comps="auto list $(__get_tpm2_devices)"
|
||||
|
||||
@@ -65,10 +65,14 @@ _arguments -s \
|
||||
"($enroll)--tpm2-device=[Enroll a TPM2 device or list them]:TPM2 device:_systemd-cryptenroll_tpm2_device auto list" \
|
||||
"($enroll)--tpm2-device-key=[Enroll a TPM2 device using its public key]:public key file:_files" \
|
||||
'--tpm2-seal-key-handle=[Specify handle of key to use for sealing]:handle:' \
|
||||
'--tpm2-argon2id-memory=[Argon2id memory cost]:memory (bytes):' \
|
||||
'--tpm2-argon2id-iter-time=[Target time for Argon2id benchmark]:time (s):' \
|
||||
'--tpm2-argon2id-iterations=[Argon2id iterations]:iterations:' \
|
||||
'--tpm2-argon2id-parallelism=[Argon2id parallelism]:threads:' \
|
||||
'--tpm2-pcrs=[Specify TPM2 PCRs to seal against]:PCRs:' \
|
||||
'--tpm2-public-key=[Enroll signed TPM2 PCR policy against PEM public key]:public key file:_files' \
|
||||
'--tpm2-public-key-pcrs=[Enroll signed TPM2 PCR policy for specified TPM2 PCRs]:PCRs:' \
|
||||
'--tpm2-signature=[Validate public key enrollment works with JSON signature file]:signature file:_files' \
|
||||
'--tpm2-pcrlock=[Specify pcrlock policy to lock against]:pcrlock file:_files' \
|
||||
'--tpm2-with-pin=[Whether to require entering a PIN to unlock the volume]:boolean:(yes no)' \
|
||||
'--tpm2-with-pin=[Whether to require entering a PIN to unlock the volume]:mode:(yes no direct)' \
|
||||
'*::block device:_systemd-cryptenroll_devices'
|
||||
|
||||
@@ -56,6 +56,7 @@ executables += [
|
||||
libshared,
|
||||
],
|
||||
'dependencies' : [
|
||||
libopenssl_cflags,
|
||||
libseccomp_cflags,
|
||||
tpm2_cflags,
|
||||
],
|
||||
|
||||
@@ -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.");
|
||||
|
||||
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");
|
||||
_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");
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, \
|
||||
|
||||
@@ -103,6 +103,8 @@ _public_ int cryptsetup_token_open_pin(
|
||||
CLEANUP_ARRAY(blobs, n_blobs, iovec_array_free);
|
||||
CLEANUP_ARRAY(policy_hash, n_policy_hash, iovec_array_free);
|
||||
|
||||
Argon2IdParameters argon2id_params = {};
|
||||
|
||||
r = tpm2_parse_luks2_json(
|
||||
v,
|
||||
/* ret_keyslot= */ NULL,
|
||||
@@ -119,7 +121,8 @@ _public_ int cryptsetup_token_open_pin(
|
||||
&salt,
|
||||
&srk,
|
||||
&pcrlock_nv,
|
||||
&flags);
|
||||
&flags,
|
||||
&argon2id_params);
|
||||
if (r < 0)
|
||||
return log_debug_open_error(cd, token, r);
|
||||
|
||||
@@ -148,6 +151,7 @@ _public_ int cryptsetup_token_open_pin(
|
||||
&srk,
|
||||
&pcrlock_nv,
|
||||
flags,
|
||||
/* argon2id_params= */ FLAGS_SET(flags, TPM2_FLAGS_USE_ARGON2ID) ? &argon2id_params : NULL,
|
||||
&decrypted_key);
|
||||
if (r < 0)
|
||||
return log_debug_open_error(cd, token, r);
|
||||
@@ -227,7 +231,7 @@ _public_ void cryptsetup_token_dump(
|
||||
|
||||
r = tpm2_parse_luks2_json(
|
||||
v,
|
||||
NULL,
|
||||
/* ret_keyslot= */ NULL,
|
||||
&hash_pcr_mask,
|
||||
&pcr_bank,
|
||||
&pubkey,
|
||||
@@ -241,7 +245,8 @@ _public_ void cryptsetup_token_dump(
|
||||
&salt,
|
||||
&srk,
|
||||
&pcrlock_nv,
|
||||
&flags);
|
||||
&flags,
|
||||
/* ret_argon2id_params= */ NULL);
|
||||
if (r < 0)
|
||||
return (void) crypt_log_debug_errno(cd, r, "Failed to parse " TOKEN_NAME " JSON fields: %m");
|
||||
|
||||
@@ -266,6 +271,7 @@ _public_ void cryptsetup_token_dump(
|
||||
crypt_log(cd, "\ttpm2-primary-alg: %s\n", strna(tpm2_asym_alg_to_string(primary_alg)));
|
||||
crypt_log(cd, "\ttpm2-pin: %s\n", true_false(flags & TPM2_FLAGS_USE_PIN));
|
||||
crypt_log(cd, "\ttpm2-pcrlock: %s\n", true_false(flags & TPM2_FLAGS_USE_PCRLOCK));
|
||||
crypt_log(cd, "\ttpm2-argon2id: %s\n", true_false(flags & TPM2_FLAGS_USE_ARGON2ID));
|
||||
crypt_log(cd, "\ttpm2-salt: %s\n", true_false(iovec_is_set(&salt)));
|
||||
crypt_log(cd, "\ttpm2-srk: %s\n", true_false(iovec_is_set(&srk)));
|
||||
crypt_log(cd, "\ttpm2-pcrlock-nv: %s\n", true_false(iovec_is_set(&pcrlock_nv)));
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "ask-password-api.h"
|
||||
#include "crypto-util.h"
|
||||
#include "env-util.h"
|
||||
#include "hexdecoct.h"
|
||||
#include "log.h"
|
||||
@@ -33,11 +34,14 @@ int acquire_luks2_key(
|
||||
const struct iovec *srk,
|
||||
const struct iovec *pcrlock_nv,
|
||||
TPM2Flags flags,
|
||||
const Argon2IdParameters *argon2id_params,
|
||||
struct iovec *ret_decrypted_key) {
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *signature_json = NULL;
|
||||
_cleanup_free_ char *auto_device = NULL;
|
||||
_cleanup_(erase_and_freep) char *b64_salted_pin = NULL;
|
||||
_cleanup_(erase_and_freep) char *b64_pin = NULL;
|
||||
_cleanup_(iovec_done_erase) struct iovec key1 = {};
|
||||
bool argon2id = FLAGS_SET(flags, TPM2_FLAGS_USE_ARGON2ID);
|
||||
int r;
|
||||
|
||||
assert(iovec_is_valid(salt));
|
||||
@@ -56,17 +60,28 @@ int acquire_luks2_key(
|
||||
if ((flags & TPM2_FLAGS_USE_PIN) && !pin)
|
||||
return -ENOANO;
|
||||
|
||||
if (pin && iovec_is_set(salt)) {
|
||||
if (argon2id) {
|
||||
if (isempty(pin))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Argon2id PIN requires a non-empty PIN.");
|
||||
if (!iovec_is_set(salt))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argon2id PIN requires salt in LUKS2 token.");
|
||||
|
||||
r = tpm2_argon2id_derive_split(pin, salt, argon2id_params, &key1, &b64_pin);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to perform Argon2id: %m");
|
||||
|
||||
pin = b64_pin;
|
||||
} else if (pin && iovec_is_set(salt)) {
|
||||
uint8_t salted_pin[SHA256_DIGEST_SIZE] = {};
|
||||
CLEANUP_ERASE(salted_pin);
|
||||
r = tpm2_util_pbkdf2_hmac_sha256(pin, strlen(pin), salt->iov_base, salt->iov_len, salted_pin);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to perform PBKDF2: %m");
|
||||
|
||||
r = base64mem(salted_pin, sizeof(salted_pin), &b64_salted_pin);
|
||||
r = base64mem(salted_pin, sizeof(salted_pin), &b64_pin);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to base64 encode salted pin: %m");
|
||||
pin = b64_salted_pin;
|
||||
pin = b64_pin;
|
||||
}
|
||||
|
||||
if (pubkey_pcr_mask != 0) {
|
||||
@@ -81,7 +96,6 @@ int acquire_luks2_key(
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0) {
|
||||
/* Not found? Then search among passed credentials */
|
||||
r = tpm2_pcrlock_policy_from_credentials(srk, pcrlock_nv, &pcrlock_policy);
|
||||
if (r < 0)
|
||||
return r;
|
||||
@@ -95,6 +109,7 @@ int acquire_luks2_key(
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
_cleanup_(iovec_done_erase) struct iovec unsealed = {};
|
||||
r = tpm2_unseal(tpm2_context,
|
||||
hash_pcr_mask,
|
||||
pcr_bank,
|
||||
@@ -110,13 +125,14 @@ int acquire_luks2_key(
|
||||
policy_hash,
|
||||
n_policy_hash,
|
||||
srk,
|
||||
ret_decrypted_key);
|
||||
argon2id ? &unsealed : ret_decrypted_key);
|
||||
if (r == -EREMOTE)
|
||||
return log_warning_errno(r, "TPM key integrity check failed. Key enrolled in superblock most likely does not belong to this TPM.");
|
||||
if (r == -EADDRNOTAVAIL)
|
||||
return log_warning_errno(r, "NV index referenced by token is missing, unwritten, or unusable, it could be for another system.");
|
||||
if (r == -EILSEQ)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Bad PIN."); /* cryptsetup docs say we should return ENOANO on bad PIN */
|
||||
/* cryptsetup docs say we should return ENOANO on bad PIN */
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Bad %s.", argon2id ? "password" : "PIN");
|
||||
if (r == -ENOLCK)
|
||||
return log_error_errno(r, "TPM is in dictionary attack lock-out mode.");
|
||||
if (ERRNO_IS_NEG_TPM2_UNSEAL_BAD_PCR(r)) {
|
||||
@@ -129,5 +145,14 @@ int acquire_luks2_key(
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to unseal secret using TPM2: %m");
|
||||
|
||||
return r;
|
||||
if (argon2id) {
|
||||
_cleanup_(iovec_done_erase) struct iovec volume_key = {};
|
||||
r = tpm2_argon2id_hkdf(&key1, &unsealed, &volume_key);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to derive volume key via HKDF: %m");
|
||||
|
||||
*ret_decrypted_key = TAKE_STRUCT(volume_key);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -25,4 +25,5 @@ int acquire_luks2_key(
|
||||
const struct iovec *srk,
|
||||
const struct iovec *pcrlock_nv,
|
||||
TPM2Flags flags,
|
||||
const Argon2IdParameters *argon2id_params,
|
||||
struct iovec *decrypted_key);
|
||||
|
||||
@@ -48,6 +48,7 @@ modules += [
|
||||
'sources' : cryptsetup_token_systemd_tpm2_sources,
|
||||
'dependencies' : [
|
||||
libcryptsetup_cflags,
|
||||
libopenssl_cflags,
|
||||
tpm2_cflags,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -5840,6 +5840,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");
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
# 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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
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 = tpm2_util_pbkdf2_hmac_sha256(pin_str, strlen(pin_str), salt->iov_base, salt->iov_len, 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 perform PBKDF2: %m");
|
||||
return log_error_errno(r, "Failed to perform Argon2id: %m");
|
||||
|
||||
r = base64mem(salted_pin, sizeof(salted_pin), &b64_salted_pin);
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+120
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -300,7 +300,10 @@ executables += [
|
||||
},
|
||||
test_template + {
|
||||
'sources' : files('test-creds.c'),
|
||||
'dependencies' : tpm2_cflags,
|
||||
'dependencies' : [
|
||||
libopenssl_cflags,
|
||||
tpm2_cflags,
|
||||
],
|
||||
},
|
||||
test_template + {
|
||||
'sources' : files('test-crypto-util.c'),
|
||||
|
||||
@@ -23,6 +23,7 @@ executables += [
|
||||
'HAVE_TPM2',
|
||||
],
|
||||
'dependencies' : [
|
||||
libopenssl_cflags,
|
||||
tpm2_cflags,
|
||||
],
|
||||
},
|
||||
@@ -44,6 +45,7 @@ executables += [
|
||||
'HAVE_TPM2',
|
||||
],
|
||||
'dependencies' : [
|
||||
libopenssl_cflags,
|
||||
tpm2_cflags,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -111,6 +111,7 @@ udev_dependencies = [
|
||||
libblkid_cflags,
|
||||
libkmod_cflags,
|
||||
libmount_cflags,
|
||||
libopenssl_cflags,
|
||||
libselinux_cflags,
|
||||
tpm2_cflags,
|
||||
]
|
||||
|
||||
@@ -10,6 +10,7 @@ executables += [
|
||||
'sources' : files('veritysetup.c'),
|
||||
'dependencies' : [
|
||||
libcryptsetup_cflags,
|
||||
libopenssl_cflags,
|
||||
tpm2_cflags,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user