From 5e521624f292e2d469abe5e256db374dc17136ba Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 28 Nov 2020 15:27:34 +0100 Subject: [PATCH] cryptenroll: add support for TPM2 enrolling --- meson.build | 18 + meson_options.txt | 2 + src/cryptenroll/cryptenroll-tpm2.c | 131 ++++ src/cryptenroll/cryptenroll-tpm2.h | 16 + src/cryptenroll/cryptenroll.c | 61 ++ src/shared/meson.build | 2 + src/shared/tpm2-util.c | 998 +++++++++++++++++++++++++++++ src/shared/tpm2-util.h | 51 ++ 8 files changed, 1279 insertions(+) create mode 100644 src/cryptenroll/cryptenroll-tpm2.c create mode 100644 src/cryptenroll/cryptenroll-tpm2.h create mode 100644 src/shared/tpm2-util.c create mode 100644 src/shared/tpm2-util.h diff --git a/meson.build b/meson.build index f434d68542..70696a724b 100644 --- a/meson.build +++ b/meson.build @@ -1185,6 +1185,17 @@ else endif conf.set10('HAVE_LIBFIDO2', have) +want_tpm2 = get_option('tpm2') +if want_tpm2 != 'false' and not skip_deps + tpm2 = dependency('tss2-esys tss2-rc tss2-mu', + required : want_tpm2 == 'true') + have = tpm2.found() +else + have = false + tpm2 = [] +endif +conf.set10('HAVE_TPM2', have) + want_elfutils = get_option('elfutils') if want_elfutils != 'false' and not skip_deps libdw = dependency('libdw', @@ -2428,6 +2439,7 @@ if conf.get('HAVE_LIBCRYPTSETUP') == 1 src/cryptenroll/cryptenroll-pkcs11.h src/cryptenroll/cryptenroll-recovery.c src/cryptenroll/cryptenroll-recovery.h + src/cryptenroll/cryptenroll-tpm2.h src/cryptenroll/cryptenroll.c '''.split()) @@ -2439,12 +2451,17 @@ if conf.get('HAVE_LIBCRYPTSETUP') == 1 systemd_cryptenroll_sources += files('src/cryptenroll/cryptenroll-fido2.c') endif + if conf.get('HAVE_TPM2') == 1 + systemd_cryptenroll_sources += files('src/cryptenroll/cryptenroll-tpm2.c') + endif + executable( 'systemd-cryptenroll', systemd_cryptenroll_sources, include_directories : includes, link_with : [libshared], dependencies : [libcryptsetup, + libdl, libopenssl, libp11kit], install_rpath : rootlibexecdir, @@ -3770,6 +3787,7 @@ foreach tuple : [ ['libfdisk'], ['p11kit'], ['libfido2'], + ['tpm2'], ['AUDIT'], ['IMA'], ['AppArmor'], diff --git a/meson_options.txt b/meson_options.txt index eed3596b9b..6e104d21c3 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -327,6 +327,8 @@ option('p11kit', type : 'combo', choices : ['auto', 'true', 'false'], description : 'p11kit support') option('libfido2', type : 'combo', choices : ['auto', 'true', 'false'], description : 'FIDO2 support') +option('tpm2', type : 'combo', choices : ['auto', 'true', 'false'], + description : 'TPM2 support') option('elfutils', type : 'combo', choices : ['auto', 'true', 'false'], description : 'elfutils support') option('zlib', type : 'combo', choices : ['auto', 'true', 'false'], diff --git a/src/cryptenroll/cryptenroll-tpm2.c b/src/cryptenroll/cryptenroll-tpm2.c new file mode 100644 index 0000000000..211f8f9874 --- /dev/null +++ b/src/cryptenroll/cryptenroll-tpm2.c @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "cryptenroll-tpm2.h" +#include "hexdecoct.h" +#include "json.h" +#include "memory-util.h" +#include "tpm2-util.h" + +static int search_policy_hash( + struct crypt_device *cd, + const void *hash, + size_t hash_size) { + + int r; + + assert(cd); + assert(hash || hash_size == 0); + + if (hash_size == 0) + return 0; + + for (int token = 0; token < LUKS2_TOKENS_MAX; token ++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ void *thash = NULL; + size_t thash_size = 0; + int keyslot; + JsonVariant *w; + + r = cryptsetup_get_token_as_json(cd, token, "systemd-tpm2", &v); + if (IN_SET(r, -ENOENT, -EINVAL, -EMEDIUMTYPE)) + continue; + if (r < 0) + return log_error_errno(r, "Failed to read JSON token data off disk: %m"); + + keyslot = cryptsetup_get_keyslot_from_token(v); + if (keyslot < 0) + return log_error_errno(keyslot, "Failed to determine keyslot of JSON token: %m"); + + w = json_variant_by_key(v, "tpm2-policy-hash"); + if (!w || !json_variant_is_string(w)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "TPM2 token data lacks 'tpm2-policy-hash' field."); + + r = unhexmem(json_variant_string(w), (size_t) -1, &thash, &thash_size); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid base64 data in 'tpm2-policy-hash' field."); + + if (memcmp_nn(hash, hash_size, thash, thash_size) == 0) + return keyslot; /* Found entry with same hash. */ + } + + return -ENOENT; /* Not found */ +} + +int enroll_tpm2(struct crypt_device *cd, + const void *volume_key, + size_t volume_key_size, + const char *device, + uint32_t pcr_mask) { + + _cleanup_(erase_and_freep) void *secret = NULL, *secret2 = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *a = NULL; + _cleanup_(erase_and_freep) char *base64_encoded = NULL; + size_t secret_size, secret2_size, blob_size, hash_size; + _cleanup_free_ void *blob = NULL, *hash = NULL; + const char *node; + int r, keyslot; + + assert(cd); + assert(volume_key); + assert(volume_key_size > 0); + assert(pcr_mask < (1U << TPM2_PCRS_MAX)); /* Support 24 PCR banks */ + + assert_se(node = crypt_get_device_name(cd)); + + r = tpm2_seal(device, pcr_mask, &secret, &secret_size, &blob, &blob_size, &hash, &hash_size); + if (r < 0) + return r; + + /* Let's see if we already have this specific PCR policy hash enrolled, if so, exit early. */ + r = search_policy_hash(cd, hash, hash_size); + if (r == -ENOENT) + log_debug_errno(r, "PCR policy hash not yet enrolled, enrolling now."); + else if (r < 0) + return r; + else { + log_info("This PCR set is already enrolled, executing no operation."); + return r; /* return existing keyslot, so that wiping won't kill it */ + } + + /* Quick verification that everything is in order, we are not in a hurry after all. */ + log_debug("Unsealing for verification..."); + r = tpm2_unseal(device, pcr_mask, blob, blob_size, hash, hash_size, &secret2, &secret2_size); + if (r < 0) + return r; + + if (memcmp_nn(secret, secret_size, secret2, secret2_size) != 0) + 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. */ + r = base64mem(secret, secret_size, &base64_encoded); + if (r < 0) + return log_error_errno(r, "Failed to base64 encode secret key: %m"); + + r = cryptsetup_set_minimal_pbkdf(cd); + if (r < 0) + return log_error_errno(r, "Failed to set minimal PBKDF: %m"); + + keyslot = crypt_keyslot_add_by_volume_key( + cd, + CRYPT_ANY_SLOT, + volume_key, + volume_key_size, + base64_encoded, + strlen(base64_encoded)); + if (keyslot < 0) + return log_error_errno(keyslot, "Failed to add new TPM2 key to %s: %m", node); + + r = tpm2_make_luks2_json(keyslot, pcr_mask, blob, blob_size, hash, hash_size, &v); + if (r < 0) + return log_error_errno(r, "Failed to prepare TPM2 JSON token object: %m"); + + r = cryptsetup_add_token_json(cd, v); + if (r < 0) + return log_error_errno(r, "Failed to add TPM2 JSON token to LUKS2 header: %m"); + + log_info("New TPM2 token enrolled as key slot %i.", keyslot); + return keyslot; +} diff --git a/src/cryptenroll/cryptenroll-tpm2.h b/src/cryptenroll/cryptenroll-tpm2.h new file mode 100644 index 0000000000..d5dd1b0003 --- /dev/null +++ b/src/cryptenroll/cryptenroll-tpm2.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "cryptsetup-util.h" +#include "log.h" + +#if HAVE_TPM2 +int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t pcr_mask); +#else +static inline int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t pcr_mask) { + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "TPM2 key enrollment not supported."); +} +#endif diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index e08f810b9b..e3a660d021 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -7,22 +7,26 @@ #include "cryptenroll-password.h" #include "cryptenroll-pkcs11.h" #include "cryptenroll-recovery.h" +#include "cryptenroll-tpm2.h" #include "cryptsetup-util.h" #include "escape.h" #include "libfido2-util.h" #include "main-func.h" #include "memory-util.h" +#include "parse-util.h" #include "path-util.h" #include "pkcs11-util.h" #include "pretty-print.h" #include "strv.h" #include "terminal-util.h" +#include "tpm2-util.h" typedef enum EnrollType { ENROLL_PASSWORD, ENROLL_RECOVERY, ENROLL_PKCS11, ENROLL_FIDO2, + ENROLL_TPM2, _ENROLL_TYPE_MAX, _ENROLL_TYPE_INVALID = -1, } EnrollType; @@ -31,6 +35,7 @@ static EnrollType arg_enroll_type = _ENROLL_TYPE_INVALID; static char *arg_pkcs11_token_uri = NULL; static char *arg_fido2_device = NULL; static char *arg_tpm2_device = NULL; +static uint32_t arg_tpm2_pcr_mask = UINT32_MAX; static char *arg_node = NULL; STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, freep); @@ -56,6 +61,10 @@ static int help(void) { " Specify PKCS#11 security token URI\n" " --fido2-device=PATH\n" " Enroll a FIDO2-HMAC security token\n" + " --tpm2-device=PATH\n" + " Enroll a TPM2 device\n" + " --tpm2-pcrs=PCR1,PCR2,PCR3,…\n" + " Specifiy TPM2 PCRs to seal against\n" "\nSee the %s for details.\n" , program_invocation_short_name , ansi_highlight(), ansi_normal() @@ -73,6 +82,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_RECOVERY_KEY, ARG_PKCS11_TOKEN_URI, ARG_FIDO2_DEVICE, + ARG_TPM2_DEVICE, + ARG_TPM2_PCRS, }; static const struct option options[] = { @@ -82,6 +93,8 @@ static int parse_argv(int argc, char *argv[]) { { "recovery-key", no_argument, NULL, ARG_RECOVERY_KEY }, { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI }, { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE }, + { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, + { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS }, {} }; @@ -169,6 +182,47 @@ static int parse_argv(int argc, char *argv[]) { break; } + case ARG_TPM2_DEVICE: { + _cleanup_free_ char *device = NULL; + + if (streq(optarg, "list")) + return tpm2_list_devices(); + + if (arg_enroll_type >= 0 || arg_tpm2_device) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Multiple operations specified at once, refusing."); + + if (!streq(optarg, "auto")) { + device = strdup(optarg); + if (!device) + return log_oom(); + } + + arg_enroll_type = ENROLL_TPM2; + arg_tpm2_device = TAKE_PTR(device); + break; + } + + case ARG_TPM2_PCRS: { + uint32_t mask; + + if (isempty(optarg)) { + arg_tpm2_pcr_mask = 0; + break; + } + + r = tpm2_parse_pcrs(optarg, &mask); + if (r < 0) + return r; + + if (arg_tpm2_pcr_mask == UINT32_MAX) + arg_tpm2_pcr_mask = mask; + else + arg_tpm2_pcr_mask |= mask; + + break; + } + case '?': return -EINVAL; @@ -193,6 +247,9 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; + if (arg_tpm2_pcr_mask == UINT32_MAX) + arg_tpm2_pcr_mask = TPM2_PCR_MASK_DEFAULT; + return 1; } @@ -348,6 +405,10 @@ static int run(int argc, char *argv[]) { r = enroll_fido2(cd, vk, vks, arg_fido2_device); break; + case ENROLL_TPM2: + r = enroll_tpm2(cd, vk, vks, arg_tpm2_device, arg_tpm2_pcr_mask); + break; + default: return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Operation not implemented yet."); } diff --git a/src/shared/meson.build b/src/shared/meson.build index 45b8d1b07c..ec4f3e882a 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -237,6 +237,8 @@ shared_sources = files(''' tmpfile-util-label.h tomoyo-util.c tomoyo-util.h + tpm2-util.c + tpm2-util.h udev-util.c udev-util.h uid-range.c diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c new file mode 100644 index 0000000000..8a0f45c2db --- /dev/null +++ b/src/shared/tpm2-util.c @@ -0,0 +1,998 @@ +/* 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; +} diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h new file mode 100644 index 0000000000..82cd186e11 --- /dev/null +++ b/src/shared/tpm2-util.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "json.h" +#include "macro.h" + +#if HAVE_TPM2 + +#include +#include +#include + +extern 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); +extern 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); +extern void (*sym_Esys_Finalize)(ESYS_CONTEXT **context); +extern TSS2_RC (*sym_Esys_FlushContext)(ESYS_CONTEXT *esysContext, ESYS_TR flushHandle); +extern void (*sym_Esys_Free)(void *ptr); +extern TSS2_RC (*sym_Esys_GetRandom)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, UINT16 bytesRequested, TPM2B_DIGEST **randomBytes); +extern TSS2_RC (*sym_Esys_Initialize)(ESYS_CONTEXT **esys_context, TSS2_TCTI_CONTEXT *tcti, TSS2_ABI_VERSION *abiVersion); +extern 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); +extern TSS2_RC (*sym_Esys_PolicyGetDigest)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2B_DIGEST **policyDigest); +extern 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); +extern 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); +extern TSS2_RC (*sym_Esys_Startup)(ESYS_CONTEXT *esysContext, TPM2_SU startupType); +extern TSS2_RC (*sym_Esys_Unseal)(ESYS_CONTEXT *esysContext, ESYS_TR itemHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2B_SENSITIVE_DATA **outData); + +extern const char* (*sym_Tss2_RC_Decode)(TSS2_RC rc); + +extern TSS2_RC (*sym_Tss2_MU_TPM2B_PRIVATE_Marshal)(TPM2B_PRIVATE const *src, uint8_t buffer[], size_t buffer_size, size_t *offset); +extern TSS2_RC (*sym_Tss2_MU_TPM2B_PRIVATE_Unmarshal)(uint8_t const buffer[], size_t buffer_size, size_t *offset, TPM2B_PRIVATE *dest); +extern TSS2_RC (*sym_Tss2_MU_TPM2B_PUBLIC_Marshal)(TPM2B_PUBLIC const *src, uint8_t buffer[], size_t buffer_size, size_t *offset); +extern TSS2_RC (*sym_Tss2_MU_TPM2B_PUBLIC_Unmarshal)(uint8_t const buffer[], size_t buffer_size, size_t *offset, TPM2B_PUBLIC *dest); + +int dlopen_tpm2(void); + +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); +int tpm2_unseal(const char *device, uint32_t pcr_mask, const void *blob, size_t blob_size, const void *pcr_hash, size_t pcr_hash_size, void **ret_secret, size_t *ret_secret_size); + +#endif + +int tpm2_list_devices(void); +int tpm2_find_device_auto(int log_level, char **ret); + +int tpm2_parse_pcrs(const char *s, uint32_t *ret); + +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); + +#define TPM2_PCRS_MAX 24 + +/* Default to PCR 7 only */ +#define TPM2_PCR_MASK_DEFAULT (UINT32_C(1) << 7)