--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "extract-word.h"
+#include "parse-util.h"
+#include "tpm2-util.h"
+
+#if HAVE_TPM2
+#include "alloc-util.h"
+#include "dirent-util.h"
+#include "dlfcn-util.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "fs-util.h"
+#include "hexdecoct.h"
+#include "memory-util.h"
+#include "random-util.h"
+#include "time-util.h"
+
+static void *libtss2_esys_dl = NULL;
+static void *libtss2_rc_dl = NULL;
+static void *libtss2_mu_dl = NULL;
+
+TSS2_RC (*sym_Esys_Create)(ESYS_CONTEXT *esysContext, ESYS_TR parentHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_SENSITIVE_CREATE *inSensitive, const TPM2B_PUBLIC *inPublic, const TPM2B_DATA *outsideInfo, const TPML_PCR_SELECTION *creationPCR, TPM2B_PRIVATE **outPrivate, TPM2B_PUBLIC **outPublic, TPM2B_CREATION_DATA **creationData, TPM2B_DIGEST **creationHash, TPMT_TK_CREATION **creationTicket) = NULL;
+TSS2_RC (*sym_Esys_CreatePrimary)(ESYS_CONTEXT *esysContext, ESYS_TR primaryHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_SENSITIVE_CREATE *inSensitive, const TPM2B_PUBLIC *inPublic, const TPM2B_DATA *outsideInfo, const TPML_PCR_SELECTION *creationPCR, ESYS_TR *objectHandle, TPM2B_PUBLIC **outPublic, TPM2B_CREATION_DATA **creationData, TPM2B_DIGEST **creationHash, TPMT_TK_CREATION **creationTicket) = NULL;
+void (*sym_Esys_Finalize)(ESYS_CONTEXT **context) = NULL;
+TSS2_RC (*sym_Esys_FlushContext)(ESYS_CONTEXT *esysContext, ESYS_TR flushHandle) = NULL;
+void (*sym_Esys_Free)(void *ptr) = NULL;
+TSS2_RC (*sym_Esys_GetRandom)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, UINT16 bytesRequested, TPM2B_DIGEST **randomBytes) = NULL;
+TSS2_RC (*sym_Esys_Initialize)(ESYS_CONTEXT **esys_context, TSS2_TCTI_CONTEXT *tcti, TSS2_ABI_VERSION *abiVersion) = NULL;
+TSS2_RC (*sym_Esys_Load)(ESYS_CONTEXT *esysContext, ESYS_TR parentHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_PRIVATE *inPrivate, const TPM2B_PUBLIC *inPublic, ESYS_TR *objectHandle) = NULL;
+TSS2_RC (*sym_Esys_PolicyGetDigest)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2B_DIGEST **policyDigest) = NULL;
+TSS2_RC (*sym_Esys_PolicyPCR)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_DIGEST *pcrDigest, const TPML_PCR_SELECTION *pcrs) = NULL;
+TSS2_RC (*sym_Esys_StartAuthSession)(ESYS_CONTEXT *esysContext, ESYS_TR tpmKey, ESYS_TR bind, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_NONCE *nonceCaller, TPM2_SE sessionType, const TPMT_SYM_DEF *symmetric, TPMI_ALG_HASH authHash, ESYS_TR *sessionHandle) = NULL;
+TSS2_RC (*sym_Esys_Startup)(ESYS_CONTEXT *esysContext, TPM2_SU startupType) = NULL;
+TSS2_RC (*sym_Esys_Unseal)(ESYS_CONTEXT *esysContext, ESYS_TR itemHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2B_SENSITIVE_DATA **outData) = NULL;
+
+const char* (*sym_Tss2_RC_Decode)(TSS2_RC rc) = NULL;
+
+TSS2_RC (*sym_Tss2_MU_TPM2B_PRIVATE_Marshal)(TPM2B_PRIVATE const *src, uint8_t buffer[], size_t buffer_size, size_t *offset) = NULL;
+TSS2_RC (*sym_Tss2_MU_TPM2B_PRIVATE_Unmarshal)(uint8_t const buffer[], size_t buffer_size, size_t *offset, TPM2B_PRIVATE *dest) = NULL;
+TSS2_RC (*sym_Tss2_MU_TPM2B_PUBLIC_Marshal)(TPM2B_PUBLIC const *src, uint8_t buffer[], size_t buffer_size, size_t *offset) = NULL;
+TSS2_RC (*sym_Tss2_MU_TPM2B_PUBLIC_Unmarshal)(uint8_t const buffer[], size_t buffer_size, size_t *offset, TPM2B_PUBLIC *dest) = NULL;
+
+int dlopen_tpm2(void) {
+ int r, k = 0;
+
+ if (!libtss2_esys_dl) {
+ _cleanup_(dlclosep) void *dl = NULL;
+
+ dl = dlopen("libtss2-esys.so.0", RTLD_LAZY);
+ if (!dl)
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "TPM2 support is not installed: %s", dlerror());
+
+ r = dlsym_many_and_warn(
+ dl,
+ LOG_DEBUG,
+ DLSYM_ARG(Esys_Create),
+ DLSYM_ARG(Esys_CreatePrimary),
+ DLSYM_ARG(Esys_Finalize),
+ DLSYM_ARG(Esys_FlushContext),
+ DLSYM_ARG(Esys_Free),
+ DLSYM_ARG(Esys_GetRandom),
+ DLSYM_ARG(Esys_Initialize),
+ DLSYM_ARG(Esys_Load),
+ DLSYM_ARG(Esys_PolicyGetDigest),
+ DLSYM_ARG(Esys_PolicyPCR),
+ DLSYM_ARG(Esys_StartAuthSession),
+ DLSYM_ARG(Esys_Startup),
+ DLSYM_ARG(Esys_Unseal),
+ NULL);
+ if (r < 0)
+ return r;
+
+ libtss2_esys_dl = TAKE_PTR(dl);
+ k++;
+ }
+
+ if (!libtss2_rc_dl) {
+ _cleanup_(dlclosep) void *dl = NULL;
+
+ dl = dlopen("libtss2-rc.so.0", RTLD_LAZY);
+ if (!dl)
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "TPM2 support is not installed: %s", dlerror());
+
+ r = dlsym_many_and_warn(
+ dl,
+ LOG_DEBUG,
+ DLSYM_ARG(Tss2_RC_Decode),
+ NULL);
+ if (r < 0)
+ return r;
+
+ libtss2_rc_dl = TAKE_PTR(dl);
+ k++;
+ }
+
+ if (!libtss2_mu_dl) {
+ _cleanup_(dlclosep) void *dl = NULL;
+
+ dl = dlopen("libtss2-mu.so.0", RTLD_LAZY);
+ if (!dl)
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "TPM2 support is not installed: %s", dlerror());
+
+ r = dlsym_many_and_warn(
+ dl,
+ LOG_DEBUG,
+ DLSYM_ARG(Tss2_MU_TPM2B_PRIVATE_Marshal),
+ DLSYM_ARG(Tss2_MU_TPM2B_PRIVATE_Unmarshal),
+ DLSYM_ARG(Tss2_MU_TPM2B_PUBLIC_Marshal),
+ DLSYM_ARG(Tss2_MU_TPM2B_PUBLIC_Unmarshal),
+ NULL);
+ if (r < 0)
+ return r;
+
+ libtss2_mu_dl = TAKE_PTR(dl);
+ k++;
+ }
+
+ return k;
+}
+
+struct tpm2_context {
+ ESYS_CONTEXT *esys_context;
+ void *tcti_dl;
+ TSS2_TCTI_CONTEXT *tcti_context;
+};
+
+static void tpm2_context_destroy(struct tpm2_context *c) {
+ assert(c);
+
+ if (c->esys_context)
+ sym_Esys_Finalize(&c->esys_context);
+
+ c->tcti_context = mfree(c->tcti_context);
+
+ if (c->tcti_dl) {
+ dlclose(c->tcti_dl);
+ c->tcti_dl = NULL;
+ }
+}
+
+static inline void Esys_Finalize_wrapper(ESYS_CONTEXT **c) {
+ /* A wrapper around Esys_Finalize() for use with _cleanup_(). Only reasons we need this wrapper is
+ * because the function itself warn logs if we'd pass a pointer to NULL, and we don't want that. */
+ if (*c)
+ sym_Esys_Finalize(c);
+}
+
+static inline void Esys_Freep(void *p) {
+ if (*(void**) p)
+ sym_Esys_Free(*(void**) p);
+}
+
+static ESYS_TR flush_context_verbose(ESYS_CONTEXT *c, ESYS_TR handle) {
+ TSS2_RC rc;
+
+ if (!c || handle == ESYS_TR_NONE)
+ return ESYS_TR_NONE;
+
+ rc = sym_Esys_FlushContext(c, handle);
+ if (rc != TSS2_RC_SUCCESS) /* We ignore failures here (besides debug logging), since this is called
+ * in error paths, where we cannot do anything about failures anymore. And
+ * when it is called in successful codepaths by this time we already did
+ * what we wanted to do, and got the results we wanted so there's no
+ * reason to make this fail more loudly than necessary. */
+ log_debug("Failed to get flush context of TPM, ignoring: %s", sym_Tss2_RC_Decode(rc));
+
+ return ESYS_TR_NONE;
+}
+
+static int tpm2_init(const char *device, struct tpm2_context *ret) {
+ _cleanup_(Esys_Finalize_wrapper) ESYS_CONTEXT *c = NULL;
+ _cleanup_free_ TSS2_TCTI_CONTEXT *tcti = NULL;
+ _cleanup_(dlclosep) void *dl = NULL;
+ TSS2_RC rc;
+ int r;
+
+ r = dlopen_tpm2();
+ if (r < 0)
+ return log_error_errno(r, "TPM2 support not installed: %m");
+
+ if (!device)
+ device = secure_getenv("SYSTEMD_TPM2_DEVICE");
+
+ if (device) {
+ const char *param, *driver, *fn;
+ const TSS2_TCTI_INFO* info;
+ TSS2_TCTI_INFO_FUNC func;
+ size_t sz = 0;
+
+ param = strchr(device, ':');
+ if (param) {
+ driver = strndupa(device, param - device);
+ param++;
+ } else {
+ driver = "device";
+ param = device;
+ }
+
+ fn = strjoina("libtss2-tcti-", driver, ".so.0");
+
+ dl = dlopen(fn, RTLD_NOW);
+ if (!dl)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to load %s: %s", fn, dlerror());
+
+ func = dlsym(dl, TSS2_TCTI_INFO_SYMBOL);
+ if (!func)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to find TCTI info symbol " TSS2_TCTI_INFO_SYMBOL ": %s",
+ dlerror());
+
+ info = func();
+ if (!info)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to get TCTI info data.");
+
+
+ log_debug("Loaded TCTI module '%s' (%s) [Version %" PRIu32 "]", info->name, info->description, info->version);
+
+ rc = info->init(NULL, &sz, NULL);
+ if (rc != TPM2_RC_SUCCESS)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to initialize TCTI context: %s", sym_Tss2_RC_Decode(rc));
+
+ tcti = malloc0(sz);
+ if (!tcti)
+ return log_oom();
+
+ rc = info->init(tcti, &sz, device);
+ if (rc != TPM2_RC_SUCCESS)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to initialize TCTI context: %s", sym_Tss2_RC_Decode(rc));
+ }
+
+ rc = sym_Esys_Initialize(&c, tcti, NULL);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to initialize TPM context: %s", sym_Tss2_RC_Decode(rc));
+
+ rc = sym_Esys_Startup(c, TPM2_SU_CLEAR);
+ if (rc == TPM2_RC_INITIALIZE)
+ log_debug("TPM already started up.");
+ else if (rc == TSS2_RC_SUCCESS)
+ log_debug("TPM successfully started up.");
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to start up TPM: %s", sym_Tss2_RC_Decode(rc));
+
+ *ret = (struct tpm2_context) {
+ .esys_context = TAKE_PTR(c),
+ .tcti_context = TAKE_PTR(tcti),
+ .tcti_dl = TAKE_PTR(dl),
+ };
+
+ return 0;
+}
+
+static int tpm2_credit_random(ESYS_CONTEXT *c) {
+ size_t rps, done = 0;
+ TSS2_RC rc;
+ int r;
+
+ assert(c);
+
+ /* Pulls some entropy from the TPM and adds it into the kernel RNG pool. That way we can say that the
+ * key we will ultimately generate with the kernel random pool is at least as good as the TPM's RNG,
+ * but likely better. Note that we don't trust the TPM RNG very much, hence do not actually credit
+ * any entropy. */
+
+ for (rps = random_pool_size(); rps > 0;) {
+ _cleanup_(Esys_Freep) TPM2B_DIGEST *buffer = NULL;
+
+ rc = sym_Esys_GetRandom(
+ c,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ MIN(rps, 32U), /* 32 is supposedly a safe choice, given that AES 256bit keys are this long, and TPM2 baseline requires support for those. */
+ &buffer);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to acquire entropy from TPM: %s", sym_Tss2_RC_Decode(rc));
+
+ if (buffer->size == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Zero-sized entropy returned from TPM.");
+
+ r = random_write_entropy(-1, buffer->buffer, buffer->size, false);
+ if (r < 0)
+ return log_error_errno(r, "Failed wo write entropy to kernel: %m");
+
+ done += buffer->size;
+ rps = LESS_BY(rps, buffer->size);
+ }
+
+ log_debug("Added %zu bytes of entropy to the kernel random pool.", done);
+ return 0;
+}
+
+static int tpm2_make_primary(
+ ESYS_CONTEXT *c,
+ ESYS_TR *ret_primary) {
+
+ static const TPM2B_SENSITIVE_CREATE primary_sensitive = {};
+ static const TPM2B_PUBLIC primary_template = {
+ .size = sizeof(TPMT_PUBLIC),
+ .publicArea = {
+ .type = TPM2_ALG_ECC,
+ .nameAlg = TPM2_ALG_SHA256,
+ .objectAttributes = TPMA_OBJECT_RESTRICTED|TPMA_OBJECT_DECRYPT|TPMA_OBJECT_FIXEDTPM|TPMA_OBJECT_FIXEDPARENT|TPMA_OBJECT_SENSITIVEDATAORIGIN|TPMA_OBJECT_USERWITHAUTH,
+ .parameters = {
+ .eccDetail = {
+ .symmetric = {
+ .algorithm = TPM2_ALG_AES,
+ .keyBits.aes = 128,
+ .mode.aes = TPM2_ALG_CFB,
+ },
+ .scheme.scheme = TPM2_ALG_NULL,
+ .curveID = TPM2_ECC_NIST_P256,
+ .kdf.scheme = TPM2_ALG_NULL,
+ },
+ },
+ },
+ };
+ static const TPML_PCR_SELECTION creation_pcr = {};
+ ESYS_TR primary = ESYS_TR_NONE;
+ TSS2_RC rc;
+
+ log_debug("Creating primary key on TPM.");
+
+ rc = sym_Esys_CreatePrimary(
+ c,
+ ESYS_TR_RH_OWNER,
+ ESYS_TR_PASSWORD,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &primary_sensitive,
+ &primary_template,
+ NULL,
+ &creation_pcr,
+ &primary,
+ NULL,
+ NULL,
+ NULL,
+ NULL);
+
+ if (rc != TSS2_RC_SUCCESS)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to generate primary key in TPM: %s", sym_Tss2_RC_Decode(rc));
+
+ log_debug("Successfully created primary key on TPM.");
+
+ *ret_primary = primary;
+ return 0;
+}
+
+static int tpm2_make_pcr_session(
+ ESYS_CONTEXT *c,
+ uint32_t pcr_mask,
+ ESYS_TR *ret_session,
+ TPM2B_DIGEST **ret_policy_digest) {
+
+ static const TPMT_SYM_DEF symmetric = {
+ .algorithm = TPM2_ALG_AES,
+ .keyBits = {
+ .aes = 128
+ },
+ .mode = {
+ .aes = TPM2_ALG_CFB,
+ }
+ };
+ TPML_PCR_SELECTION pcr_selection = {
+ .count = 1,
+ .pcrSelections[0].hash = TPM2_ALG_SHA256,
+ .pcrSelections[0].sizeofSelect = 3,
+ .pcrSelections[0].pcrSelect[0] = pcr_mask & 0xFF,
+ .pcrSelections[0].pcrSelect[1] = (pcr_mask >> 8) & 0xFF,
+ .pcrSelections[0].pcrSelect[2] = (pcr_mask >> 16) & 0xFF,
+ };
+ _cleanup_(Esys_Freep) TPM2B_DIGEST *policy_digest = NULL;
+ ESYS_TR session = ESYS_TR_NONE;
+ TSS2_RC rc;
+ int r;
+
+ assert(c);
+
+ log_debug("Starting authentication session.");
+
+ rc = sym_Esys_StartAuthSession(
+ c,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ NULL,
+ TPM2_SE_POLICY,
+ &symmetric,
+ TPM2_ALG_SHA256,
+ &session);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to open session in TPM: %s", sym_Tss2_RC_Decode(rc));
+
+ log_debug("Configuring PCR policy.");
+
+ rc = sym_Esys_PolicyPCR(
+ c,
+ session,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ NULL,
+ &pcr_selection);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to add PCR policy to TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ if (DEBUG_LOGGING || ret_policy_digest) {
+ log_debug("Acquiring policy digest.");
+
+ rc = sym_Esys_PolicyGetDigest(
+ c,
+ session,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &policy_digest);
+
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to get policy digest from TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *h = NULL;
+
+ h = hexmem(policy_digest->buffer, policy_digest->size);
+ if (!h) {
+ r = log_oom();
+ goto finish;
+ }
+
+ log_debug("Session policy digest: %s", h);
+ }
+ }
+
+ if (ret_session) {
+ *ret_session = session;
+ session = ESYS_TR_NONE;
+ }
+
+ if (ret_policy_digest)
+ *ret_policy_digest = TAKE_PTR(policy_digest);
+
+ r = 0;
+
+finish:
+ session = flush_context_verbose(c, session);
+ return r;
+}
+
+int tpm2_seal(
+ const char *device,
+ uint32_t pcr_mask,
+ void **ret_secret,
+ size_t *ret_secret_size,
+ void **ret_blob,
+ size_t *ret_blob_size,
+ void **ret_pcr_hash,
+ size_t *ret_pcr_hash_size) {
+
+ _cleanup_(tpm2_context_destroy) struct tpm2_context c = {};
+ _cleanup_(Esys_Freep) TPM2B_DIGEST *policy_digest = NULL;
+ _cleanup_(Esys_Freep) TPM2B_PRIVATE *private = NULL;
+ _cleanup_(Esys_Freep) TPM2B_PUBLIC *public = NULL;
+ static const TPML_PCR_SELECTION creation_pcr = {};
+ _cleanup_(erase_and_freep) void *secret = NULL;
+ _cleanup_free_ void *blob = NULL, *hash = NULL;
+ TPM2B_SENSITIVE_CREATE hmac_sensitive;
+ ESYS_TR primary = ESYS_TR_NONE;
+ TPM2B_PUBLIC hmac_template;
+ size_t k, blob_size;
+ usec_t start;
+ TSS2_RC rc;
+ int r;
+
+ assert(ret_secret);
+ assert(ret_secret_size);
+ assert(ret_blob);
+ assert(ret_blob_size);
+ assert(ret_pcr_hash);
+ assert(ret_pcr_hash_size);
+
+ assert(pcr_mask < (UINT32_C(1) << TPM2_PCRS_MAX)); /* Support 24 PCR banks */
+
+ /* So here's what we do here: we connect to the TPM2 chip. It persistently contains a "seed" key that
+ * is randomized when the TPM2 is first initialized or reset and remains stable across boots. We
+ * generate a "primary" key pair derived from that (RSA). Given the seed remains fixed this will
+ * result in the same key pair whenever we specify the exact same parameters for it. We then create a
+ * PCR-bound policy session, which calculates a hash on the current PCR values of the indexes we
+ * specify. We then generate a randomized key on the host (which is the key we actually enroll in the
+ * LUKS2 keyslots), which we upload into the TPM2, where it is encrypted with the "primary" key,
+ * taking the PCR policy session into account. We then download the encrypted key from the TPM2
+ * ("sealing") and marshall it into binary form, which is ultimately placed in the LUKS2 JSON header.
+ *
+ * The TPM2 "seed" key and "primary" keys never leave the TPM2 chip (and cannot be extracted at
+ * all). The random key we enroll in LUKS2 we generate on the host using the Linux random device. It
+ * is stored in the LUKS2 JSON only in encrypted form with the "primary" key of the TPM2 chip, thus
+ * binding the unlocking to the TPM2 chip. */
+
+ start = now(CLOCK_MONOTONIC);
+
+ r = tpm2_init(device, &c);
+ if (r < 0)
+ return r;
+
+ r = tpm2_make_primary(c.esys_context, &primary);
+ if (r < 0)
+ return r;
+
+ r = tpm2_make_pcr_session(c.esys_context, pcr_mask, NULL, &policy_digest);
+ if (r < 0)
+ goto finish;
+
+ /* We use a keyed hash object (i.e. HMAC) to store the secret key we want to use for unlocking the
+ * LUKS2 volume with. We don't ever use for HMAC/keyed hash operations however, we just use it
+ * because it's a key type that is universally supported and suitable for symmetric binary blobs. */
+ hmac_template = (TPM2B_PUBLIC) {
+ .size = sizeof(TPMT_PUBLIC),
+ .publicArea = {
+ .type = TPM2_ALG_KEYEDHASH,
+ .nameAlg = TPM2_ALG_SHA256,
+ .objectAttributes = TPMA_OBJECT_FIXEDTPM | TPMA_OBJECT_FIXEDPARENT,
+ .parameters = {
+ .keyedHashDetail = {
+ .scheme.scheme = TPM2_ALG_NULL,
+ },
+ },
+ .unique = {
+ .keyedHash = {
+ .size = 32,
+ },
+ },
+ .authPolicy = *policy_digest,
+ },
+ };
+
+ hmac_sensitive = (TPM2B_SENSITIVE_CREATE) {
+ .size = sizeof(hmac_sensitive.sensitive),
+ .sensitive.data.size = 32,
+ };
+ assert(sizeof(hmac_sensitive.sensitive.data.buffer) >= hmac_sensitive.sensitive.data.size);
+
+ (void) tpm2_credit_random(c.esys_context);
+
+ log_debug("Generating secret key data.");
+
+ r = genuine_random_bytes(hmac_sensitive.sensitive.data.buffer, hmac_sensitive.sensitive.data.size, RANDOM_BLOCK);
+ if (r < 0) {
+ log_error_errno(r, "Failed to generate secret key: %m");
+ goto finish;
+ }
+
+ log_debug("Creating HMAC key.");
+
+ rc = sym_Esys_Create(
+ c.esys_context,
+ primary,
+ ESYS_TR_PASSWORD,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &hmac_sensitive,
+ &hmac_template,
+ NULL,
+ &creation_pcr,
+ &private,
+ &public,
+ NULL,
+ NULL,
+ NULL);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to generate HMAC key in TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ secret = memdup(hmac_sensitive.sensitive.data.buffer, hmac_sensitive.sensitive.data.size);
+ explicit_bzero_safe(hmac_sensitive.sensitive.data.buffer, hmac_sensitive.sensitive.data.size);
+ if (!secret) {
+ r = log_oom();
+ goto finish;
+ }
+
+ log_debug("Marshalling private and public part of HMAC key.");
+
+ k = ALIGN8(sizeof(*private)) + ALIGN8(sizeof(*public)); /* Some roughly sensible start value */
+ for (;;) {
+ _cleanup_free_ void *buf = NULL;
+ size_t offset = 0;
+
+ buf = malloc(k);
+ if (!buf) {
+ r = log_oom();
+ goto finish;
+ }
+
+ rc = sym_Tss2_MU_TPM2B_PRIVATE_Marshal(private, buf, k, &offset);
+ if (rc == TSS2_RC_SUCCESS) {
+ rc = sym_Tss2_MU_TPM2B_PUBLIC_Marshal(public, buf, k, &offset);
+ if (rc == TSS2_RC_SUCCESS) {
+ blob = TAKE_PTR(buf);
+ blob_size = offset;
+ break;
+ }
+ }
+ if (rc != TSS2_MU_RC_INSUFFICIENT_BUFFER) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to marshal private/public key: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ if (k > SIZE_MAX / 2) {
+ r = log_oom();
+ goto finish;
+ }
+
+ k *= 2;
+ }
+
+ hash = memdup(policy_digest->buffer, policy_digest->size);
+ if (!hash)
+ return log_oom();
+
+ if (DEBUG_LOGGING) {
+ char buf[FORMAT_TIMESPAN_MAX];
+ log_debug("Completed TPM2 key sealing in %s.", format_timespan(buf, sizeof(buf), now(CLOCK_MONOTONIC) - start, 1));
+ }
+
+ *ret_secret = TAKE_PTR(secret);
+ *ret_secret_size = hmac_sensitive.sensitive.data.size;
+ *ret_blob = TAKE_PTR(blob);
+ *ret_blob_size = blob_size;
+ *ret_pcr_hash = TAKE_PTR(hash);
+ *ret_pcr_hash_size = policy_digest->size;
+
+ r = 0;
+
+finish:
+ primary = flush_context_verbose(c.esys_context, primary);
+ return r;
+}
+
+int tpm2_unseal(
+ const char *device,
+ uint32_t pcr_mask,
+ const void *blob,
+ size_t blob_size,
+ const void *known_policy_hash,
+ size_t known_policy_hash_size,
+ void **ret_secret,
+ size_t *ret_secret_size) {
+
+ _cleanup_(tpm2_context_destroy) struct tpm2_context c = {};
+ ESYS_TR primary = ESYS_TR_NONE, session = ESYS_TR_NONE, hmac_key = ESYS_TR_NONE;
+ _cleanup_(Esys_Freep) TPM2B_SENSITIVE_DATA* unsealed = NULL;
+ _cleanup_(Esys_Freep) TPM2B_DIGEST *policy_digest = NULL;
+ _cleanup_(erase_and_freep) char *secret = NULL;
+ TPM2B_PRIVATE private = {};
+ TPM2B_PUBLIC public = {};
+ size_t offset = 0;
+ TSS2_RC rc;
+ usec_t start;
+ int r;
+
+ assert(blob);
+ assert(blob_size > 0);
+ assert(known_policy_hash_size == 0 || known_policy_hash);
+ assert(ret_secret);
+ assert(ret_secret_size);
+
+ assert(pcr_mask < (UINT32_C(1) << TPM2_PCRS_MAX)); /* Support 24 PCR banks */
+
+ /* So here's what we do here: We connect to the TPM2 chip. As we do when sealing we generate a
+ * "primary" key on the TPM2 chip, with the same parameters as well as a PCR-bound policy
+ * session. Given we pass the same parameters, this will result in the same "primary" key, and same
+ * policy hash (the latter of course, only if the PCR values didn't change in between). We unmarshal
+ * the encrypted key we stored in the LUKS2 JSON token header and upload it into the TPM2, where it
+ * is decrypted if the seed and the PCR policy were right ("unsealing"). We then download the result,
+ * and use it to unlock the LUKS2 volume. */
+
+ start = now(CLOCK_MONOTONIC);
+
+ log_debug("Unmarshalling private part of HMAC key.");
+
+ rc = sym_Tss2_MU_TPM2B_PRIVATE_Unmarshal(blob, blob_size, &offset, &private);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to unmarshal private key: %s", sym_Tss2_RC_Decode(rc));
+
+ log_debug("Unmarshalling public part of HMAC key.");
+
+ rc = sym_Tss2_MU_TPM2B_PUBLIC_Unmarshal(blob, blob_size, &offset, &public);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to unmarshal public key: %s", sym_Tss2_RC_Decode(rc));
+
+ r = tpm2_init(device, &c);
+ if (r < 0)
+ return r;
+
+ r = tpm2_make_pcr_session(c.esys_context, pcr_mask, &session, &policy_digest);
+ if (r < 0)
+ goto finish;
+
+ /* If we know the policy hash to expect, and it doesn't match, we can shortcut things here, and not
+ * wait until the TPM2 tells us to go away. */
+ if (known_policy_hash_size > 0 &&
+ memcmp_nn(policy_digest->buffer, policy_digest->size, known_policy_hash, known_policy_hash_size) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM),
+ "Current policy digest does not match stored policy digest, cancelling TPM2 authentication attempt.");
+
+ r = tpm2_make_primary(c.esys_context, &primary);
+ if (r < 0)
+ return r;
+
+ log_debug("Loading HMAC key into TPM.");
+
+ rc = sym_Esys_Load(
+ c.esys_context,
+ primary,
+ ESYS_TR_PASSWORD,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &private,
+ &public,
+ &hmac_key);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to load HMAC key in TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ log_debug("Unsealing HMAC key.");
+
+ rc = sym_Esys_Unseal(
+ c.esys_context,
+ hmac_key,
+ session,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &unsealed);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to unseal HMAC key in TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ secret = memdup(unsealed->buffer, unsealed->size);
+ explicit_bzero_safe(unsealed->buffer, unsealed->size);
+ if (!secret) {
+ r = log_oom();
+ goto finish;
+ }
+
+ if (DEBUG_LOGGING) {
+ char buf[FORMAT_TIMESPAN_MAX];
+ log_debug("Completed TPM2 key unsealing in %s.", format_timespan(buf, sizeof(buf), now(CLOCK_MONOTONIC) - start, 1));
+ }
+
+ *ret_secret = TAKE_PTR(secret);
+ *ret_secret_size = unsealed->size;
+
+ r = 0;
+
+finish:
+ primary = flush_context_verbose(c.esys_context, primary);
+ session = flush_context_verbose(c.esys_context, session);
+ hmac_key = flush_context_verbose(c.esys_context, hmac_key);
+ return r;
+}
+
+#endif
+
+int tpm2_list_devices(void) {
+#if HAVE_TPM2
+ _cleanup_(table_unrefp) Table *t = NULL;
+ _cleanup_(closedirp) DIR *d = NULL;
+ int r;
+
+ r = dlopen_tpm2();
+ if (r < 0)
+ return log_error_errno(r, "TPM2 support is not installed.");
+
+ t = table_new("path", "device", "driver");
+ if (!t)
+ return log_oom();
+
+ d = opendir("/sys/class/tpmrm");
+ if (!d) {
+ log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, "Failed to open /sys/class/tpmrm: %m");
+ if (errno != ENOENT)
+ return -errno;
+ } else {
+ for (;;) {
+ _cleanup_free_ char *device_path = NULL, *device = NULL, *driver_path = NULL, *driver = NULL, *node = NULL;
+ struct dirent *de;
+
+ de = readdir_no_dot(d);
+ if (!de)
+ break;
+
+ device_path = path_join("/sys/class/tpmrm", de->d_name, "device");
+ if (!device_path)
+ return log_oom();
+
+ r = readlink_malloc(device_path, &device);
+ if (r < 0)
+ log_debug_errno(r, "Failed to read device symlink %s, ignoring: %m", device_path);
+ else {
+ driver_path = path_join(device_path, "driver");
+ if (!driver_path)
+ return log_oom();
+
+ r = readlink_malloc(driver_path, &driver);
+ if (r < 0)
+ log_debug_errno(r, "Failed to read driver symlink %s, ignoring: %m", driver_path);
+ }
+
+ node = path_join("/dev", de->d_name);
+ if (!node)
+ return log_oom();
+
+ r = table_add_many(
+ t,
+ TABLE_PATH, node,
+ TABLE_STRING, device ? last_path_component(device) : NULL,
+ TABLE_STRING, driver ? last_path_component(driver) : NULL);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ }
+
+ if (table_get_rows(t) <= 1) {
+ log_info("No suitable TPM2 devices found.");
+ return 0;
+ }
+
+ r = table_print(t, stdout);
+ if (r < 0)
+ return log_error_errno(r, "Failed to show device table: %m");
+
+ return 0;
+#else
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "TPM2 not supported on this build.");
+#endif
+}
+
+int tpm2_find_device_auto(
+ int log_level, /* log level when no device is found */
+ char **ret) {
+#if HAVE_TPM2
+ _cleanup_(closedirp) DIR *d = NULL;
+ int r;
+
+ r = dlopen_tpm2();
+ if (r < 0)
+ return log_error_errno(r, "TPM2 support is not installed.");
+
+ d = opendir("/sys/class/tpmrm");
+ if (!d) {
+ log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno,
+ "Failed to open /sys/class/tpmrm: %m");
+ if (errno != ENOENT)
+ return -errno;
+ } else {
+ _cleanup_free_ char *node = NULL;
+
+ for (;;) {
+ struct dirent *de;
+
+ de = readdir_no_dot(d);
+ if (!de)
+ break;
+
+ if (node)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ),
+ "More than one TPM2 (tpmrm) device found.");
+
+ node = path_join("/dev", de->d_name);
+ if (!node)
+ return log_oom();
+ }
+
+ if (node) {
+ *ret = TAKE_PTR(node);
+ return 0;
+ }
+ }
+
+ return log_full_errno(log_level, SYNTHETIC_ERRNO(ENODEV), "No TPM2 (tpmrm) device found.");
+#else
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "TPM2 not supported on this build.");
+#endif
+}
+
+int tpm2_parse_pcrs(const char *s, uint32_t *ret) {
+ const char *p = s;
+ uint32_t mask = 0;
+ int r;
+
+ /* Parses a comma-separated list of PCR indexes */
+
+ for (;;) {
+ _cleanup_free_ char *pcr = NULL;
+ unsigned n;
+
+ r = extract_first_word(&p, &pcr, ",", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r == 0)
+ break;
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse PCR list: %s", s);
+
+ r = safe_atou(pcr, &n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse PCR number: %s", pcr);
+ if (n >= TPM2_PCRS_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE),
+ "PCR number out of range (valid range 0…23): %u", n);
+
+ mask |= UINT32_C(1) << n;
+ }
+
+ *ret = mask;
+ return 0;
+}
+
+int tpm2_make_luks2_json(
+ int keyslot,
+ uint32_t pcr_mask,
+ const void *blob,
+ size_t blob_size,
+ const void *policy_hash,
+ size_t policy_hash_size,
+ JsonVariant **ret) {
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *a = NULL;
+ _cleanup_free_ char *keyslot_as_string = NULL;
+ JsonVariant* pcr_array[TPM2_PCRS_MAX];
+ unsigned n_pcrs = 0;
+ int r;
+
+ assert(blob || blob_size == 0);
+ assert(policy_hash || policy_hash_size == 0);
+
+ if (asprintf(&keyslot_as_string, "%i", keyslot) < 0)
+ return -ENOMEM;
+
+ for (unsigned i = 0; i < ELEMENTSOF(pcr_array); i++) {
+ if ((pcr_mask & (UINT32_C(1) << i)) == 0)
+ continue;
+
+ r = json_variant_new_integer(pcr_array + n_pcrs, i);
+ if (r < 0) {
+ json_variant_unref_many(pcr_array, n_pcrs);
+ return -ENOMEM;
+ }
+
+ n_pcrs++;
+ }
+
+ r = json_variant_new_array(&a, pcr_array, n_pcrs);
+ json_variant_unref_many(pcr_array, n_pcrs);
+ if (r < 0)
+ return -ENOMEM;
+
+ r = json_build(&v,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("type", JSON_BUILD_STRING("systemd-tpm2")),
+ JSON_BUILD_PAIR("keyslots", JSON_BUILD_ARRAY(JSON_BUILD_STRING(keyslot_as_string))),
+ JSON_BUILD_PAIR("tpm2-blob", JSON_BUILD_BASE64(blob, blob_size)),
+ JSON_BUILD_PAIR("tpm2-pcrs", JSON_BUILD_VARIANT(a)),
+ JSON_BUILD_PAIR("tpm2-policy-hash", JSON_BUILD_HEX(policy_hash, policy_hash_size))));
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = TAKE_PTR(v);
+
+ return keyslot;
+}