From: Lennart Poettering Date: Mon, 10 Jun 2024 13:55:54 +0000 (+0200) Subject: tpm2-util: add infra for allocating nvindex-based PCRs (aka "NvPCRs") X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=b0c5c6aad8d5834fda9d04a64265a35d1a984a26;p=thirdparty%2Fsystemd.git tpm2-util: add infra for allocating nvindex-based PCRs (aka "NvPCRs") We'd like to measure various additional things into PCRs, but all available ones to the OS are already used for various purposes. Hence, let's introduce a new concept of "NV Index based PCRs", i.e. let's use TPM2 nv indexes of type TPM2_NT_EXTEND that mostly behave like real PCRs, but which we can allocate relatively freely from the nv index space. Let's call these "fake" PCRs "NvPCRs". My original intention was to get a fixed NV index range assigned from the TCG, either for Linux or for systemd as a project, but this stalled with no further updates from the TCG for more than a year and a half now. I was told an NV index range to use though, even if it never was officially assigned, hence this PR uses this by default. But the range is configurable at build time, on purpose, so that downstreams have some flexibility to change this if they want. To abstract the actual nvindex number away we introduce a naming concept, so that nvindexes are referenced by name string rather than number. NvPCRs are defined in little JSON snippets in /usr/lib/nvpcr/*.nvpcr, that match up index number and name, as well as pick a hash algorithm. There's one complication: these nvindex (like any nvindex) can be deleted by anyone with access to the TPM, and then be recreated. This could be used to reset the NvPCRs to zero during runtime, which defeats the whole point of them. Our way out: we measure a secret as first thing after creation into the NvPCRs. (Or actually, we measure a per-NvPCR secret we derive from a system secret via an HMAC of the NvPCR name) and the nvindex handle). This "anchoring" secret is stored in /run/ + /var/lib/ + ESP/XBOOTLDR (the latter encrypted as credential, locked to the TPM), to make it available at the whole runtime of the OS. --- diff --git a/meson.build b/meson.build index 24efc9f7c3b..6c4b67d28d3 100644 --- a/meson.build +++ b/meson.build @@ -1343,6 +1343,7 @@ tpm2 = dependency('tss2-esys tss2-rc tss2-mu tss2-tcti-device', tpm2_cflags = tpm2.partial_dependency(includes: true, compile_args: true) conf.set10('HAVE_TPM2', tpm2.found()) conf.set10('HAVE_TSS2_ESYS3', tpm2.found() and tpm2.version().version_compare('>= 3.0.0')) +conf.set('TPM2_NVPCR_BASE', get_option('tpm2-nvpcr-base')) libdw = dependency('libdw', required : get_option('elfutils')) @@ -3028,6 +3029,7 @@ summary({ 'default user $PATH' : default_user_path != '' ? default_user_path : '(same as system services)', 'systemd service watchdog' : service_watchdog == '' ? 'disabled' : service_watchdog, 'time epoch' : f'@time_epoch@ (@alt_time_epoch@)', + 'TPM2 nvpcr base' : run_command(sh, '-c', 'printf 0x%x @0@'.format(get_option('tpm2-nvpcr-base')), check : true).stdout() }) # TODO: diff --git a/meson_options.txt b/meson_options.txt index f7ec39322c8..d44030ef8be 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -449,6 +449,8 @@ option('libfido2', type : 'feature', deprecated : { 'true' : 'enabled', 'false' description : 'FIDO2 support') option('tpm2', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, description : 'TPM2 support') +option('tpm2-nvpcr-base', type : 'integer', value: 0x01d10200, + description : 'Base for TPM2 nvindex based PCRs') option('elfutils', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, description : 'elfutils support') option('zlib', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 86fe3839568..49da1852719 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6,12 +6,15 @@ #include "alloc-util.h" #include "ansi-color.h" #include "bitfield.h" +#include "bootspec.h" +#include "boot-entry.h" #include "constants.h" #include "creds-util.h" #include "cryptsetup-util.h" #include "dirent-util.h" #include "dlfcn-util.h" #include "efi-api.h" +#include "errno-util.h" #include "extract-word.h" #include "fd-util.h" #include "fileio.h" @@ -39,6 +42,7 @@ #include "sync-util.h" #include "time-util.h" #include "tpm2-pcr.h" +#include "tmpfile-util.h" #include "tpm2-util.h" #include "virt.h" @@ -65,6 +69,9 @@ static DLSYM_PROTOTYPE(Esys_Initialize) = NULL; static DLSYM_PROTOTYPE(Esys_Load) = NULL; static DLSYM_PROTOTYPE(Esys_LoadExternal) = NULL; static DLSYM_PROTOTYPE(Esys_NV_DefineSpace) = NULL; +static DLSYM_PROTOTYPE(Esys_NV_Extend) = NULL; +static DLSYM_PROTOTYPE(Esys_NV_Read) = NULL; +static DLSYM_PROTOTYPE(Esys_NV_ReadPublic) = NULL; static DLSYM_PROTOTYPE(Esys_NV_UndefineSpace) = NULL; static DLSYM_PROTOTYPE(Esys_NV_Write) = NULL; static DLSYM_PROTOTYPE(Esys_PCR_Extend) = NULL; @@ -139,6 +146,9 @@ static int dlopen_tpm2_esys(void) { DLSYM_ARG(Esys_Load), DLSYM_ARG(Esys_LoadExternal), DLSYM_ARG(Esys_NV_DefineSpace), + DLSYM_ARG(Esys_NV_Extend), + DLSYM_ARG(Esys_NV_Read), + DLSYM_ARG(Esys_NV_ReadPublic), DLSYM_ARG(Esys_NV_UndefineSpace), DLSYM_ARG(Esys_NV_Write), DLSYM_ARG(Esys_PCR_Extend), @@ -5915,6 +5925,224 @@ int tpm2_undefine_nv_index( return 0; } +int tpm2_define_nvpcr_nv_index( + Tpm2Context *c, + const Tpm2Handle *session, + TPM2_HANDLE nv_index, + TPMI_ALG_HASH algorithm, + Tpm2Handle **ret_nv_handle) { + + _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL; + TSS2_RC rc; + int r; + + assert(c); + + /* Allocates an nvindex to use as a "fake" PCR. We call these "NvPCR" in our codebase */ + + if (algorithm == 0) + algorithm = TPM2_ALG_SHA256; + + int digest_size = tpm2_hash_alg_to_size(algorithm); + if (digest_size < 0) + return digest_size; + + if ((size_t) digest_size > sizeof_field(TPM2B_MAX_NV_BUFFER, buffer)) + return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Digest too large for extension."); + + r = tpm2_handle_new(c, &new_handle); + if (r < 0) + return r; + + new_handle->flush = false; /* This is a persistent NV index, don't flush hence */ + + TPM2B_NV_PUBLIC public_info = { + .size = sizeof_field(TPM2B_NV_PUBLIC, nvPublic), + .nvPublic = { + .nvIndex = nv_index, + .nameAlg = algorithm, + .attributes = TPMA_NV_CLEAR_STCLEAR | + TPMA_NV_ORDERLY | + TPMA_NV_OWNERWRITE | + TPMA_NV_AUTHWRITE | + TPMA_NV_OWNERREAD | + TPMA_NV_AUTHREAD | + (TPM2_NT_EXTEND << TPMA_NV_TPM2_NT_SHIFT), + .dataSize = digest_size, + }, + }; + + rc = sym_Esys_NV_DefineSpace( + c->esys_context, + /* authHandle= */ ESYS_TR_RH_OWNER, + /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + /* auth= */ NULL, + &public_info, + &new_handle->esys_handle); + if (rc == TPM2_RC_NV_DEFINED) { + log_debug("NV index 0x%" PRIu32 " already registered.", nv_index); + + new_handle = tpm2_handle_free(new_handle); + + r = tpm2_index_to_handle( + c, + nv_index, + session, + /* ret_public= */ NULL, + /* ret_name= */ NULL, + /* ret_qname= */ NULL, + &new_handle); + if (r < 0) + return log_debug_errno(r, "Failed to acquire handle to existing NV index 0x%" PRIu32 ".", nv_index); + + log_debug("Successfully acquired handle to existing NV index 0x%" PRIx32 ".", nv_index); + + _cleanup_(Esys_Freep) TPM2B_NV_PUBLIC *nv_public_real = NULL; + rc = sym_Esys_NV_ReadPublic( + c->esys_context, + /* nvIndex= */ new_handle->esys_handle, + /* shandle1= */ ESYS_TR_NONE, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + &nv_public_real, + /* ret_nv_name= */ NULL); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed read public data of nvindex 0x%x: %s", nv_index, sym_Tss2_RC_Decode(rc)); + + log_debug("Read public info for nvindex 0x%x.", nv_index); + + if (nv_public_real->size < endoffsetof_field(TPMS_NV_PUBLIC, attributes) + sizeof_field(TPMS_NV_PUBLIC, dataSize) || + nv_public_real->nvPublic.nvIndex != public_info.nvPublic.nvIndex || + nv_public_real->nvPublic.nameAlg != public_info.nvPublic.nameAlg || + ((nv_public_real->nvPublic.attributes ^ public_info.nvPublic.attributes) & ~TPMA_NV_WRITTEN) != 0 || + nv_public_real->nvPublic.dataSize != public_info.nvPublic.dataSize) + return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), + "Public data of nvindex 0x%x does not match our expectations.", nv_index); + + log_debug("Public info for nvindex 0x%x checks out, using.", nv_index); + + if (ret_nv_handle) + *ret_nv_handle = TAKE_PTR(new_handle); + + return 0; + } + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to allocate NV index: %s", sym_Tss2_RC_Decode(rc)); + + log_debug("NV Index 0x%" PRIx32 " successfully allocated.", nv_index); + + if (ret_nv_handle) + *ret_nv_handle = TAKE_PTR(new_handle); + + return 1; +} + +int tpm2_extend_nvpcr_nv_index( + Tpm2Context *c, + TPM2_HANDLE nv_index, + const Tpm2Handle *nv_handle, + const struct iovec *digest) { + + TPM2_RC rc; + + assert(c); + assert(nv_index); + assert(nv_handle); + assert(iovec_is_set(digest)); + + if (digest->iov_len > sizeof_field(TPM2B_MAX_NV_BUFFER, buffer)) + return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Hash value to extend too long."); + + TPM2B_MAX_NV_BUFFER buf = { + .size = digest->iov_len, + }; + memcpy(buf.buffer, digest->iov_base, digest->iov_len); + + rc = sym_Esys_NV_Extend( + c->esys_context, + /* authHandle= */ nv_handle->esys_handle, + /* nvIndex= */ nv_handle->esys_handle, + /* shandle1= */ ESYS_TR_PASSWORD, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + &buf); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to extend NV index: %s", sym_Tss2_RC_Decode(rc)); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *h = NULL; + h = hexmem(digest->iov_base, digest->iov_len); + log_debug("Written hash %s to NV index 0x%x", strnull(h), nv_index); + } + + return 0; +} + +int tpm2_read_nv_index( + Tpm2Context *c, + const Tpm2Handle *session, + TPM2_HANDLE nv_index, + const Tpm2Handle *nv_handle, + struct iovec *ret_value) { + + TPM2_RC rc; + + assert(c); + assert(nv_index); + assert(nv_handle); + + _cleanup_(Esys_Freep) TPM2B_NV_PUBLIC *nv_public = NULL; + rc = sym_Esys_NV_ReadPublic( + c->esys_context, + /* nvIndex= */ nv_handle->esys_handle, + /* shandle1= */ ESYS_TR_NONE, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + &nv_public, + /* ret_nv_name= */ NULL); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed read public data of nvindex 0x%x: %s", nv_index, sym_Tss2_RC_Decode(rc)); + + log_debug("Read public info for nvindex 0x%x, value size is %zu", nv_index, (size_t) nv_public->nvPublic.dataSize); + + _cleanup_(Esys_Freep) TPM2B_MAX_NV_BUFFER *value = NULL; + rc = sym_Esys_NV_Read( + c->esys_context, + /* authHandle= */ nv_handle->esys_handle, + /* nvIndex= */ nv_handle->esys_handle, + /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + nv_public->nvPublic.dataSize, + /* offset= */ 0, + &value); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed read contents of nvindex 0x%x: %s", nv_index, sym_Tss2_RC_Decode(rc)); + + if (ret_value) { + assert(value); + + struct iovec result = { + .iov_base = memdup(value->buffer, value->size), + .iov_len = value->size, + }; + + if (!result.iov_base) + return log_oom_debug(); + + *ret_value = TAKE_STRUCT(result); + } + + return 0; +} + int tpm2_seal_data( Tpm2Context *c, const struct iovec *data, @@ -6160,6 +6388,7 @@ int tpm2_find_device_auto(char **ret) { #endif } +#if HAVE_TPM2 const uint16_t tpm2_hash_algorithms[] = { TPM2_ALG_SHA1, TPM2_ALG_SHA256, @@ -6190,7 +6419,6 @@ static int json_dispatch_tpm2_algorithm(const char *name, sd_json_variant *varia return 0; } -#if HAVE_TPM2 static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MAX] = { [TPM2_EVENT_PHASE] = "phase", [TPM2_EVENT_FILESYSTEM] = "filesystem", @@ -6211,7 +6439,6 @@ const char* tpm2_firmware_log_path(void) { #if HAVE_OPENSSL static int tpm2_userspace_log_open(void) { _cleanup_close_ int fd = -EBADF; - struct stat st; const char *e; int r; @@ -6223,214 +6450,974 @@ static int tpm2_userspace_log_open(void) { * lock it, which we want to avoid. */ fd = open(e, O_CREAT|O_WRONLY|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600); if (fd < 0) - return log_debug_errno(errno, "Failed to open TPM log file '%s' for writing, ignoring: %m", e); + return log_debug_errno(errno, "Failed to open TPM log file '%s' for writing, ignoring: %m", e); + + if (flock(fd, LOCK_EX) < 0) + return log_debug_errno(errno, "Failed to lock TPM log file '%s', ignoring: %m", e); + + r = fd_verify_regular(fd); + if (r < 0) + return log_debug_errno(r, "TPM log file '%s' is not regular, ignoring: %m", e); + + return TAKE_FD(fd); +} + +static int tpm2_userspace_log_dirty(int fd) { + struct stat st; + + if (fd < 0) /* Apparently tpm2_local_log_open() failed earlier, let's not complain again */ + return 0; + + /* We set the sticky bit when we are about to append to the log file. We'll unset it afterwards + * again. If we manage to take a lock on a file that has it set we know we didn't write it fully and + * it is corrupted. Ideally we'd like to use user xattrs for this, but unfortunately tmpfs (which is + * our assumed backend fs) doesn't know user xattrs. */ + + if (fstat(fd, &st) < 0) + return log_debug_errno(errno, "Failed to fstat TPM log file, ignoring: %m"); + + if (st.st_mode & S_ISVTX) + return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "TPM log file aborted, ignoring."); + + if (fchmod(fd, 0600 | S_ISVTX) < 0) + return log_debug_errno(errno, "Failed to chmod() TPM log file, ignoring: %m"); + + return 0; +} + +static int tpm2_userspace_log_clean(int fd) { + int r; + + if (fd < 0) /* Apparently tpm2_local_log_open() failed earlier, let's not complain again */ + return 0; + + if (fsync(fd) < 0) + return log_debug_errno(errno, "Failed to sync JSON data: %m"); + + /* Unset S_ISVTX again */ + if (fchmod(fd, 0600) < 0) + return log_debug_errno(errno, "Failed to chmod() TPM log file, ignoring: %m"); + + r = fsync_full(fd); + if (r < 0) + return log_debug_errno(r, "Failed to sync JSON log: %m"); + + return 0; +} + +static int tpm2_userspace_log( + int fd, + unsigned pcr_index, + uint32_t nv_index, + const char *nv_index_name, + const TPML_DIGEST_VALUES *values, + Tpm2UserspaceEventType event_type, + const char *description) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL, *array = NULL; + _cleanup_free_ char *f = NULL; + sd_id128_t boot_id; + int r; + + assert(values); + assert(values->count > 0); + + /* We maintain a local PCR measurement log. This implements a subset of the TCG Canonical Event Log + * Format – the JSON flavour – + * (https://trustedcomputinggroup.org/resource/canonical-event-log-format/), but departs in certain + * ways from it, specifically: + * + * - We don't write out a recnum. It's a bit too vaguely defined which means we'd have to read + * through the whole logs (include firmware logs) before knowing what the next value is we should + * use. Hence we simply don't write this out as append-time, and instead expect a consumer to add + * it in when it uses the data. + * + * - We write this out in RFC 7464 application/json-seq rather than as a JSON array. Writing this as + * JSON array would mean that for each appending we'd have to read the whole log file fully into + * memory before writing it out again. We prefer a strictly append-only write pattern however. (RFC + * 7464 is what jq --seq eats.) Conversion into a proper JSON array is trivial. + * + * It should be possible to convert this format in a relatively straight-forward way into the + * official TCG Canonical Event Log Format on read, by simply adding in a few more fields that can be + * determined from the full dataset. + * + * We set the 'content_type' field to "systemd" to make clear this data is generated by us, and + * include various interesting fields in the 'content' subobject, including a CLOCK_BOOTTIME + * timestamp which can be used to order this measurement against possibly other measurements + * independently done by other subsystems on the system. + */ + + if (fd < 0) /* Apparently tpm2_local_log_open() failed earlier, let's not complain again */ + return 0; + + for (size_t i = 0; i < values->count; i++) { + const EVP_MD *implementation; + const char *a; + + assert_se(a = tpm2_hash_alg_to_string(values->digests[i].hashAlg)); + assert_se(implementation = EVP_get_digestbyname(a)); + + r = sd_json_variant_append_arraybo( + &array, + SD_JSON_BUILD_PAIR_STRING("hashAlg", a), + SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(&values->digests[i].digest, EVP_MD_size(implementation)))); + if (r < 0) + return log_debug_errno(r, "Failed to append digest object to JSON array: %m"); + } + + assert(array); + + r = sd_id128_get_boot(&boot_id); + if (r < 0) + return log_debug_errno(r, "Failed to acquire boot ID: %m"); + + r = sd_json_buildo( + &v, + SD_JSON_BUILD_PAIR_CONDITION(pcr_index != UINT_MAX, "pcr", SD_JSON_BUILD_UNSIGNED(pcr_index)), + SD_JSON_BUILD_PAIR_CONDITION(nv_index != UINT32_MAX, "nv_index", SD_JSON_BUILD_UNSIGNED(nv_index)), + SD_JSON_BUILD_PAIR("digests", SD_JSON_BUILD_VARIANT(array)), + SD_JSON_BUILD_PAIR("content_type", SD_JSON_BUILD_STRING("systemd")), + SD_JSON_BUILD_PAIR("content", SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_CONDITION(!!nv_index_name, "nvIndexName", SD_JSON_BUILD_STRING(nv_index_name)), + SD_JSON_BUILD_PAIR_CONDITION(!!description, "string", SD_JSON_BUILD_STRING(description)), + SD_JSON_BUILD_PAIR("bootId", SD_JSON_BUILD_ID128(boot_id)), + SD_JSON_BUILD_PAIR("timestamp", SD_JSON_BUILD_UNSIGNED(now(CLOCK_BOOTTIME))), + SD_JSON_BUILD_PAIR_CONDITION(event_type >= 0, "eventType", SD_JSON_BUILD_STRING(tpm2_userspace_event_type_to_string(event_type)))))); + if (r < 0) + return log_debug_errno(r, "Failed to build log record JSON: %m"); + + r = sd_json_variant_format(v, SD_JSON_FORMAT_SEQ, &f); + if (r < 0) + return log_debug_errno(r, "Failed to format JSON: %m"); + + if (lseek(fd, 0, SEEK_END) < 0) + return log_debug_errno(errno, "Failed to seek to end of JSON log: %m"); + + r = loop_write(fd, f, SIZE_MAX); + if (r < 0) + return log_debug_errno(r, "Failed to write JSON data to log: %m"); + + r = tpm2_userspace_log_clean(fd); + if (r < 0) + return r; + + return 1; +} +#endif + +int tpm2_pcr_extend_bytes( + Tpm2Context *c, + char **banks, + unsigned pcr_index, + const struct iovec *data, + const struct iovec *secret, + Tpm2UserspaceEventType event_type, + const char *description) { + +#if HAVE_OPENSSL + _cleanup_close_ int log_fd = -EBADF; + TPML_DIGEST_VALUES values = {}; + TSS2_RC rc; + + assert(c); + assert(iovec_is_valid(data)); + assert(iovec_is_valid(secret)); + + if (pcr_index >= TPM2_PCRS_MAX) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Can't measure into unsupported PCR %u, refusing.", pcr_index); + + if (!iovec_is_set(data)) + data = &iovec_empty; + + if (strv_isempty(banks)) + return 0; + + STRV_FOREACH(bank, banks) { + const EVP_MD *implementation; + int id; + + assert_se(implementation = EVP_get_digestbyname(*bank)); + + if (values.count >= ELEMENTSOF(values.digests)) + return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Too many banks selected."); + + if ((size_t) EVP_MD_size(implementation) > sizeof(values.digests[values.count].digest)) + return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Hash result too large for TPM2."); + + id = tpm2_hash_alg_from_string(EVP_MD_name(implementation)); + if (id < 0) + return log_debug_errno(id, "Can't map hash name to TPM2."); + + values.digests[values.count].hashAlg = id; + + /* So here's a twist: sometimes we want to measure secrets (e.g. root file system volume + * key), but we'd rather not leak a literal hash of the secret to the TPM (given that the + * wire is unprotected, and some other subsystem might use the simple, literal hash of the + * secret for other purposes, maybe because it needs a shorter secret derived from it for + * some unrelated purpose, who knows). Hence we instead measure an HMAC signature of a + * private non-secret string instead. */ + if (iovec_is_set(secret) > 0) { + if (!HMAC(implementation, secret->iov_base, secret->iov_len, data->iov_base, data->iov_len, (unsigned char*) &values.digests[values.count].digest, NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to calculate HMAC of data to measure."); + } else if (EVP_Digest(data->iov_base, data->iov_len, (unsigned char*) &values.digests[values.count].digest, NULL, implementation, NULL) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash data to measure."); + + values.count++; + } + + /* Open + lock the log file *before* we start measuring, so that no one else can come between our log + * and our measurement and change either */ + log_fd = tpm2_userspace_log_open(); + + (void) tpm2_userspace_log_dirty(log_fd); + rc = sym_Esys_PCR_Extend( + c->esys_context, + ESYS_TR_PCR0 + pcr_index, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + &values); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno( + SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to measure into PCR %u: %s", + pcr_index, + sym_Tss2_RC_Decode(rc)); + + /* Now, write what we just extended to the log, too. */ + (void) tpm2_userspace_log( + log_fd, + pcr_index, + /* nv_index= */ UINT32_MAX, + /* nv_index_name= */ NULL, + &values, + event_type, + description); + + return 0; +#else /* HAVE_OPENSSL */ + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled."); +#endif +} + +typedef struct NvPCRData { + char *name; + uint16_t algorithm; + uint32_t nv_index; +} NvPCRData; + +static void nvpcr_data_done(NvPCRData *d) { + assert(d); + + free(d->name); +} + +static int nvpcr_data_load(const char *name, NvPCRData *ret) { + int r; + + assert(ret); + + if (!tpm2_nvpcr_name_is_valid(name)) + return -EINVAL; + + const char *fname = strjoina(name, ".nvpcr"); + + _cleanup_free_ char *path = NULL; + _cleanup_fclose_ FILE *f = NULL; + r = search_and_fopen_nulstr(fname, "re", /* root= */ NULL, CONF_PATHS_NULSTR("nvpcr"), &f, &path); + if (r < 0) + return r; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + r = sd_json_parse_file( + f, + path, + /* flags= */ 0, + &v, + /* reterr_line= */ NULL, + /* reterr_column= */ NULL); + if (r < 0) + return log_debug_errno(r, "Failed to load '%s': %m", path); + + static const sd_json_dispatch_field dispatch_table[] = { + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(NvPCRData, name), SD_JSON_MANDATORY }, + { "algorithm", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_tpm2_algorithm, offsetof(NvPCRData, algorithm), 0 }, + { "nvIndex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(NvPCRData, nv_index), SD_JSON_MANDATORY }, + {}, + }; + + _cleanup_(nvpcr_data_done) NvPCRData p = { + .algorithm = TPM2_ALG_SHA256, + }; + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.name, name)) + return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "NvPCR name doesn't match filename, refusing."); + + *ret = TAKE_STRUCT(p); + return 0; +} + +int tpm2_nvpcr_get_index(const char *name, uint32_t *ret) { + int r; + + _cleanup_(nvpcr_data_done) NvPCRData p = {}; + r = nvpcr_data_load(name, &p); + if (r < 0) + return r; + + if (ret) + *ret = p.nv_index; + + return 0; +} + +int tpm2_nvpcr_extend_bytes( + Tpm2Context *c, + const Tpm2Handle *session, + const char *name, + const struct iovec *data, + const struct iovec *secret, + Tpm2UserspaceEventType event_type, + const char *description) { + +#if HAVE_OPENSSL + _cleanup_close_ int log_fd = -EBADF; + int r; + + assert(c); + assert(name); + assert(iovec_is_valid(data)); + assert(iovec_is_valid(secret)); + + _cleanup_(nvpcr_data_done) NvPCRData p = {}; + r = nvpcr_data_load(name, &p); + if (r < 0) + return r; + + /* Open + lock the log file *before* we start measuring, so that no one else can come between our log + * and our measurement and change either */ + log_fd = tpm2_userspace_log_open(); + + /* Check if this NvPCR is already anchored */ + const char *anchor_fname = strjoina("/run/systemd/nvpcr/", name, ".anchor"); + if (faccessat(AT_FDCWD, anchor_fname, F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + return log_debug_errno(errno, "Failed to check if '%s' exists: %m", anchor_fname); + + return log_debug_errno(SYNTHETIC_ERRNO(ENETDOWN), "NvPCR '%s' not anchored yet, refusing.", name); + } + + const char *an = tpm2_hash_alg_to_string(p.algorithm); + if (!an) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported algorithm for NvPCR, refusing."); + + const EVP_MD *implementation; + assert_se(implementation = EVP_get_digestbyname(an)); + + _cleanup_(iovec_done) struct iovec digest = { + .iov_len = EVP_MD_size(implementation), + }; + + digest.iov_base = malloc(digest.iov_len); + if (!digest.iov_base) + return log_oom_debug(); + + if (!iovec_is_set(data)) + data = &iovec_empty; + + if (iovec_is_set(secret)) { + if (!HMAC(implementation, secret->iov_base, secret->iov_len, data->iov_base, data->iov_len, digest.iov_base, NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to calculate HMAC of data to measure."); + } else if (EVP_Digest(data->iov_base, data->iov_len, digest.iov_base, NULL, implementation, NULL) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash data to measure."); + + _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; + r = tpm2_index_to_handle( + c, + p.nv_index, + session, + /* ret_public= */ NULL, + /* ret_name= */ NULL, + /* ret_qname= */ NULL, + &nv_handle); + if (r < 0) + return log_debug_errno(r, "Failed to acquire handle to NV index 0x%" PRIu32 ".", p.nv_index); + + log_debug("Successfully acquired handle to existing NV index 0x%" PRIx32 ".", p.nv_index); + + (void) tpm2_userspace_log_dirty(log_fd); + + r = tpm2_extend_nvpcr_nv_index( + c, + p.nv_index, + nv_handle, + &digest); + if (r < 0) + return r; + + TPML_DIGEST_VALUES digest_values = { + .count = 1, + .digests[0].hashAlg = p.algorithm, + }; + memcpy(&digest_values.digests[0].digest, digest.iov_base, digest.iov_len); + + /* Now, write what we just extended to the log, too. */ + (void) tpm2_userspace_log( + log_fd, + /* pcr_index= */ UINT_MAX, + p.nv_index, + name, + &digest_values, + event_type, + description); + + return 0; +#else /* HAVE_OPENSSL */ + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled."); +#endif +} + +#if HAVE_OPENSSL +static int tpm2_nvpcr_write_anchor_secret( + const char *dir, + const char *fname, + const struct iovec *credential) { + + int r; + + assert(dir); + assert(fname); + assert(iovec_is_set(credential)); + + /* Writes the encrypted credential of the anchor secret to directory 'dir' and file 'fname' */ + + _cleanup_close_ int dfd = open_mkdir(dir, O_CLOEXEC, 0755); + if (dfd < 0) + return log_error_errno(dfd, "Failed to create '%s' directory: %m", dir); + + _cleanup_free_ char *joined = path_join(dir, fname); + if (!joined) + return log_oom(); + + _cleanup_(iovec_done) struct iovec existing = {}; + r = read_full_file_full( + dfd, + fname, + /* offset= */ UINT64_MAX, + CREDENTIAL_ENCRYPTED_SIZE_MAX, + READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, + /* bind_name= */ NULL, + (char**) &existing.iov_base, + &existing.iov_len); + if (r < 0) { + if (r != -ENOENT) + return log_error_errno(r, "Failed to read '%s' file: %m", joined); + } else if (iovec_memcmp(&existing, credential) == 0) { + log_debug("Anchor secret file '%s' already matches expectations, not updating.", joined); + return 0; + } else + log_notice("Anchor secret file '%s' different from current anchor secret, updating.", joined); + + r = write_base64_file_at( + dfd, + fname, + credential, + WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_SYNC); + if (r < 0) + return log_error_errno(r, "Failed to write anchor secret file to '%s': %m", joined); + + log_info("Successfully written anchor secret to '%s'.", joined); + return 1; +} + +static int tpm2_nvpcr_write_anchor_secret_to_var(const struct iovec *credential) { + return tpm2_nvpcr_write_anchor_secret("/var/lib/systemd/nvpcr", "nvpcr-anchor.cred", credential); +} + +static int tpm2_nvpcr_write_anchor_secret_to_boot(const struct iovec *credential) { + int r; + + assert(iovec_is_set(credential)); + + _cleanup_free_ char *dir = NULL; + r = get_global_boot_credentials_path(&dir); + if (r < 0) + return r; + if (r == 0) { + log_debug("No XBOOTLDR/ESP partition found, not writing boot anchor secret file."); + return 0; + } + + sd_id128_t machine_id; + r = sd_id128_get_machine(&machine_id); + if (r < 0) + return log_error_errno(r, "Failed to read machine ID: %m"); + + BootEntryTokenType entry_token_type = BOOT_ENTRY_TOKEN_AUTO; + _cleanup_free_ char *entry_token = NULL; + r = boot_entry_token_ensure( + /* root= */ NULL, + /* conf_root= */ NULL, + machine_id, + /* machine_id_is_random = */ false, + &entry_token_type, + &entry_token); + if (r < 0) + return r; + + _cleanup_free_ char *fname = strjoin("nvpcr-anchor.", entry_token, ".cred"); + if (!fname) + return log_oom(); + + if (!filename_is_valid(fname)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential name '%s' would not be a valid file name, refusing.", fname); + + return tpm2_nvpcr_write_anchor_secret(dir, fname, credential); +} + +static int tpm2_nvpcr_acquire_anchor_secret_from_var(struct iovec *ret_credential) { + int r; + + assert(ret_credential); + + r = read_full_file_full( + AT_FDCWD, + "/var/lib/systemd/nvpcr/nvpcr-anchor.cred", + /* offset= */ UINT64_MAX, + CREDENTIAL_ENCRYPTED_SIZE_MAX, + READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER|READ_FULL_FILE_VERIFY_REGULAR, + /* bind_name= */ NULL, + (char**) &ret_credential->iov_base, + &ret_credential->iov_len); + if (r == -ENOENT) { + log_debug_errno(r, "No '/var/lib/systemd/nvpcr/nvpcr-anchor.cred' file."); + *ret_credential = (struct iovec) {}; + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to read '/var/lib/systemd/nvpcr/nvpcr-anchor.cred': %m"); + + return 1; +} + +static int tpm2_nvpcr_acquire_anchor_secret_from_credential(struct iovec *ret_credential, struct iovec *ret_secret) { + int r; + + assert(ret_credential); + assert(ret_secret); + + /* We need the anchor secret before the first measurement into an NvPCR. That means very early. Hence + * we'll try to pass it into the system via the system credentials logic. Because we must expect a + * multi-boot scenario it's hard to know which secret to use for which system. Hence we'll just try + * to unlock all of the available ones, until we can decrypt one of them, and then we'll use that. */ + + const char *dp; + r = get_encrypted_system_credentials_dir(&dp); + if (r < 0) + return log_error_errno(r, "Failed to get encrypted system credentials directory: %m"); + + /* Define early, so that it is definitely initialized, even if we take "goto not_found" branch below. */ + _cleanup_free_ DirectoryEntries *de = NULL; + + _cleanup_close_ int dfd = open(dp, O_CLOEXEC|O_DIRECTORY); + if (dfd < 0) { + if (errno == ENOENT) { + log_debug("No encrypted system credentials passed."); + goto not_found; + } + + return log_error_errno(errno, "Failed to open system credentials directory."); + } + + r = readdir_all(dfd, RECURSE_DIR_IGNORE_DOT, &de); + if (r < 0) + return log_error_errno(r, "Failed to enumerate system credentials: %m"); + + FOREACH_ARRAY(i, de->entries, de->n_entries) { + _cleanup_(iovec_done) struct iovec credential = {}; + struct dirent *d = *i; + + if (!startswith_no_case(d->d_name, "nvpcr-anchor.")) /* VFAT is case-insensitive, hence don't be too strict here */ + continue; + + r = read_full_file_full( + dfd, + d->d_name, + /* offset= */ UINT64_MAX, + CREDENTIAL_ENCRYPTED_SIZE_MAX, + READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, + /* bind_name= */ NULL, + (char**) &credential.iov_base, + &credential.iov_len); + if (r == -ENOENT) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to read anchor secret file '%s/%s', skipping: %m", dp, d->d_name); + continue; + } + + r = decrypt_credential_and_warn( + "nvpcr-anchor.cred", + now(CLOCK_REALTIME), + /* tpm2_device= */ NULL, + /* tpm2_signature_path= */ NULL, + /* uid= */ UID_INVALID, + &credential, + /* flags= */ 0, + ret_secret); + if (r < 0) + log_debug_errno(r, "Failed to decrypt anchor secret file '%s' passed in as system credential, skipping: %m", d->d_name); + else { + *ret_credential = TAKE_STRUCT(credential); + return 1; + } + } + + log_debug("No suitable anchor secret passed as system credential."); + +not_found: + *ret_credential = (struct iovec) {}; + *ret_secret = (struct iovec) {}; + return 0; +} +#endif + +#define ANCHOR_SECRET_SIZE 4096U + +int tpm2_nvpcr_acquire_anchor_secret(struct iovec *ret, bool sync_secondary) { +#if HAVE_OPENSSL + _cleanup_close_ int fd = -EBADF; + int r; + + /* Acquires the anchor secret. We store it in a credential. The primary location (and primary truth) + * for it is /run/systemd/nvpcr/ (i.e. volatile) [this file also doubles as lock file for the whole + * logic]. But something has to place it there once. We do keep two copies of it: one in + * /var/lib/systemd/nvpcr/, which is the persistent place for it, but which is only available at late + * boot, potentially. And one in the ESP/XBOOTLDR which will make it available in the initrd + * already via system credentials. */ + + _cleanup_close_ int dfd = open_mkdir("/run/systemd/nvpcr", O_CLOEXEC, 0755); + if (dfd < 0) + return log_error_errno(dfd, "Failed to open directory '/run/systemd/nvpcr': %m"); + + /* Use restrictive access mode of 0600. Not because the data inside needs to be kept inaccessible + * (it's encrypted, hence that'd be fine), but because we need to lock it, and unprivileged clients + * shouldn't be permitted to lock it. */ + fd = openat(dfd, "nvpcr-anchor.cred", O_RDWR|O_CLOEXEC|O_CREAT|O_NOCTTY, 0644); + if (fd < 0) + return log_error_errno(errno, "Failed to open anchor secret: %m"); - if (flock(fd, LOCK_EX) < 0) - return log_debug_errno(errno, "Failed to lock TPM log file '%s', ignoring: %m", e); + r = lock_generic(fd, LOCK_BSD, LOCK_SH); + if (r < 0) + return log_error_errno(r, "Failed to lock anchor secret file: %m"); + struct stat st; if (fstat(fd, &st) < 0) - return log_debug_errno(errno, "Failed to fstat TPM log file '%s', ignoring: %m", e); + return log_error_errno(errno, "Failed to stat() anchor secret: %m"); r = stat_verify_regular(&st); if (r < 0) - return log_debug_errno(r, "TPM log file '%s' is not regular, ignoring: %m", e); + return log_error_errno(r, "Anchor secret file is not a regular file: %m"); - /* We set the sticky bit when we are about to append to the log file. We'll unset it afterwards - * again. If we manage to take a lock on a file that has it set we know we didn't write it fully and - * it is corrupted. Ideally we'd like to use user xattrs for this, but unfortunately tmpfs (which is - * our assumed backend fs) doesn't know user xattrs. */ - if (st.st_mode & S_ISVTX) - return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "TPM log file '%s' aborted, ignoring.", e); + if (st.st_size == 0) { + /* If this is not initialized yet, then let's update the lock to an exclusive lock */ + r = lock_generic(fd, LOCK_BSD, LOCK_EX); + if (r < 0) + return log_error_errno(r, "Failed to upgrade lock on anchor secret file: %m"); - if (fchmod(fd, 0600 | S_ISVTX) < 0) - return log_debug_errno(errno, "Failed to chmod() TPM log file '%s', ignoring: %m", e); + /* Refresh size info, in case someone else has initialized it by now */ + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat() anchor secret: %m"); + } - return TAKE_FD(fd); -} + bool copy_to_var = true, copy_to_boot = true; -static int tpm2_userspace_log( - int fd, - unsigned pcr_index, - const TPML_DIGEST_VALUES *values, - Tpm2UserspaceEventType event_type, - const char *description) { + _cleanup_(iovec_done) struct iovec credential = {}; + _cleanup_(iovec_done_erase) struct iovec secret = {}; + if (st.st_size == 0) { /* No initialized yet? */ - _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL, *array = NULL; - _cleanup_free_ char *f = NULL; - sd_id128_t boot_id; - int r; + /* Check if we have a secret in /var/lib/systemd/nvpcr/. If so, import the secret from there */ + if (!sync_secondary) { + r = tpm2_nvpcr_acquire_anchor_secret_from_var(&credential); + if (r < 0) + return r; + if (r > 0) + copy_to_var = false; /* We read the secret from /var/, hence we don't have to copy it there. */ + } - assert(values); - assert(values->count > 0); + /* Did the copy_source logic work? If not, let's search for the secret among passed system credentials. */ + if (!iovec_is_set(&credential)) { + r = tpm2_nvpcr_acquire_anchor_secret_from_credential(&credential, &secret); + if (r < 0) + return r; + if (r > 0) + copy_to_boot = false; /* We read the secret from the boot partition, hence we don't have to copy it there. */ + } - /* We maintain a local PCR measurement log. This implements a subset of the TCG Canonical Event Log - * Format – the JSON flavour – - * (https://trustedcomputinggroup.org/resource/canonical-event-log-format/), but departs in certain - * ways from it, specifically: - * - * - We don't write out a recnum. It's a bit too vaguely defined which means we'd have to read - * through the whole logs (include firmware logs) before knowing what the next value is we should - * use. Hence we simply don't write this out as append-time, and instead expect a consumer to add - * it in when it uses the data. - * - * - We write this out in RFC 7464 application/json-seq rather than as a JSON array. Writing this as - * JSON array would mean that for each appending we'd have to read the whole log file fully into - * memory before writing it out again. We prefer a strictly append-only write pattern however. (RFC - * 7464 is what jq --seq eats.) Conversion into a proper JSON array is trivial. - * - * It should be possible to convert this format in a relatively straight-forward way into the - * official TCG Canonical Event Log Format on read, by simply adding in a few more fields that can be - * determined from the full dataset. - * - * We set the 'content_type' field to "systemd" to make clear this data is generated by us, and - * include various interesting fields in the 'content' subobject, including a CLOCK_BOOTTIME - * timestamp which can be used to order this measurement against possibly other measurements - * independently done by other subsystems on the system. - */ + /* Did the copy_source or system credential logic work? If not, let's generate a new random one */ + if (!iovec_is_set(&credential)) { + r = crypto_random_bytes_allocate_iovec(ANCHOR_SECRET_SIZE, &secret); + if (r < 0) + return log_error_errno(r, "Failed to acquire entropy for anchor secret: %m"); + + r = encrypt_credential_and_warn( + _CRED_AUTO_TPM2, + "nvpcr-anchor.cred", + now(CLOCK_REALTIME), + /* not_after= */ USEC_INFINITY, + /* tpm2_device= */ NULL, + /* tpm2_hash_pcr_mask= */ 0, + /* tpm2_pubkey_path= */ NULL, + /* tpm2_pubkey_pcrs= */ UINT32_MAX, + /* uid= */ UID_INVALID, + &secret, + /* flags= */ 0, + &credential); + if (r < 0) + return r; + } - if (fd < 0) /* Apparently tpm2_local_log_open() failed earlier, let's not complain again */ - return 0; + _cleanup_free_ char *encoded = NULL; + ssize_t n = base64mem_full(credential.iov_base, credential.iov_len, 79, &encoded); + if (n < 0) + return log_error_errno(n, "Failed to base64 encode credential: %m"); - for (size_t i = 0; i < values->count; i++) { - const EVP_MD *implementation; - const char *a; + if (!strextend(&encoded, "\n")) + return log_oom(); - assert_se(a = tpm2_hash_alg_to_string(values->digests[i].hashAlg)); - assert_se(implementation = EVP_get_digestbyname(a)); + n++; - r = sd_json_variant_append_arraybo( - &array, - SD_JSON_BUILD_PAIR_STRING("hashAlg", a), - SD_JSON_BUILD_PAIR("digest", SD_JSON_BUILD_HEX(&values->digests[i].digest, EVP_MD_size(implementation)))); + r = loop_write(fd, encoded, n); if (r < 0) - return log_debug_errno(r, "Failed to append digest object to JSON array: %m"); + return log_error_errno(r, "Failed to write anchor secret to disk: %m"); + } else { + /* The file was already initialized? Then just read it. */ + r = read_full_file_full( + fd, + /* filename= */ NULL, + /* offset= */ UINT64_MAX, + CREDENTIAL_ENCRYPTED_SIZE_MAX, + READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, + /* bind_name= */ NULL, + (char**) &credential.iov_base, + &credential.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to read anchor secret file: %m"); } - assert(array); - - r = sd_id128_get_boot(&boot_id); - if (r < 0) - return log_debug_errno(r, "Failed to acquire boot ID: %m"); - - r = sd_json_buildo( - &v, - SD_JSON_BUILD_PAIR("pcr", SD_JSON_BUILD_UNSIGNED(pcr_index)), - SD_JSON_BUILD_PAIR("digests", SD_JSON_BUILD_VARIANT(array)), - SD_JSON_BUILD_PAIR("content_type", SD_JSON_BUILD_STRING("systemd")), - SD_JSON_BUILD_PAIR("content", SD_JSON_BUILD_OBJECT( - SD_JSON_BUILD_PAIR_CONDITION(!!description, "string", SD_JSON_BUILD_STRING(description)), - SD_JSON_BUILD_PAIR("bootId", SD_JSON_BUILD_ID128(boot_id)), - SD_JSON_BUILD_PAIR("timestamp", SD_JSON_BUILD_UNSIGNED(now(CLOCK_BOOTTIME))), - SD_JSON_BUILD_PAIR_CONDITION(event_type >= 0, "eventType", SD_JSON_BUILD_STRING(tpm2_userspace_event_type_to_string(event_type)))))); - if (r < 0) - return log_debug_errno(r, "Failed to build log record JSON: %m"); - - r = sd_json_variant_format(v, SD_JSON_FORMAT_SEQ, &f); - if (r < 0) - return log_debug_errno(r, "Failed to format JSON: %m"); - - if (lseek(fd, 0, SEEK_END) < 0) - return log_debug_errno(errno, "Failed to seek to end of JSON log: %m"); - - r = loop_write(fd, f, SIZE_MAX); - if (r < 0) - return log_debug_errno(r, "Failed to write JSON data to log: %m"); - - if (fsync(fd) < 0) - return log_debug_errno(errno, "Failed to sync JSON data: %m"); + /* if we don't have the plaintext secret yet, then decrypt it now. */ + if (!iovec_is_set(&secret)) { + assert(iovec_is_set(&credential)); + + r = decrypt_credential_and_warn( + "nvpcr-anchor.cred", + now(CLOCK_REALTIME), + /* tpm2_device= */ NULL, + /* tpm2_signature_path= */ NULL, + /* uid= */ UID_INVALID, + &credential, + /* flags= */ 0, + &secret); + if (r < 0) + return r; + } - /* Unset S_ISVTX again */ - if (fchmod(fd, 0600) < 0) - return log_debug_errno(errno, "Failed to chmod() TPM log file, ignoring: %m"); + if (sync_secondary) { + if (copy_to_var) { + r = tpm2_nvpcr_write_anchor_secret_to_var(&credential); + if (r < 0) + return r; + } - r = fsync_full(fd); - if (r < 0) - return log_debug_errno(r, "Failed to sync JSON log: %m"); + if (copy_to_boot) { + r = tpm2_nvpcr_write_anchor_secret_to_boot(&credential); + if (r < 0) + return r; + } + } - return 1; -} + if (ret) + *ret = TAKE_STRUCT(secret); + return 0; +#else /* HAVE_OPENSSL */ + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled."); #endif +} -int tpm2_pcr_extend_bytes( +int tpm2_nvpcr_initialize( Tpm2Context *c, - char **banks, - unsigned pcr_index, - const struct iovec *data, - const struct iovec *secret, - Tpm2UserspaceEventType event_type, - const char *description) { + const Tpm2Handle *session, + const char *name, + const struct iovec *anchor_secret) { #if HAVE_OPENSSL - _cleanup_close_ int log_fd = -EBADF; - TPML_DIGEST_VALUES values = {}; - TSS2_RC rc; + TPM2_RC rc; + int r; assert(c); - assert(iovec_is_valid(data)); - assert(iovec_is_valid(secret)); + assert(name); - if (pcr_index >= TPM2_PCRS_MAX) - return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Can't measure into unsupported PCR %u, refusing.", pcr_index); + _cleanup_(nvpcr_data_done) NvPCRData p = {}; + r = nvpcr_data_load(name, &p); + if (r < 0) + return r; - if (!iovec_is_set(data)) - data = &iovec_empty; + /* Open + lock the log file *before* we check for the *.anchor flag file. */ + _cleanup_close_ int log_fd = tpm2_userspace_log_open(); - if (strv_isempty(banks)) + _cleanup_close_ int dfd = open_mkdir("/run/systemd/nvpcr", O_CLOEXEC, 0755); + if (dfd < 0) + return log_error_errno(dfd, "Failed to open directory '/run/systemd/nvpcr': %m"); + + const char *anchor_fname = strjoina(name, ".anchor"); + if (faccessat(dfd, anchor_fname, F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + return log_debug_errno(errno, "Failed to check if /run/systemd/nvpcr/%s exists: %m", anchor_fname); + } else { + log_debug("NvPCR '%s' is already anchored.", name); return 0; + } - STRV_FOREACH(bank, banks) { - const EVP_MD *implementation; - int id; + if (!iovec_is_set(anchor_secret)) + return log_debug_errno(SYNTHETIC_ERRNO(EUNATCH), "Need anchor secret."); - assert_se(implementation = EVP_get_digestbyname(*bank)); + const char *an = tpm2_hash_alg_to_string(p.algorithm); + if (!an) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported algorithm for NvPCR, refusing."); - if (values.count >= ELEMENTSOF(values.digests)) - return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Too many banks selected."); + const EVP_MD *implementation; + assert_se(implementation = EVP_get_digestbyname(an)); - if ((size_t) EVP_MD_size(implementation) > sizeof(values.digests[values.count].digest)) - return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Hash result too large for TPM2."); + int digest_size = EVP_MD_get_size(implementation); + assert_se(digest_size > 0); - id = tpm2_hash_alg_from_string(EVP_MD_name(implementation)); - if (id < 0) - return log_debug_errno(id, "Can't map hash name to TPM2."); + if ((size_t) digest_size > sizeof_field(TPM2B_MAX_NV_BUFFER, buffer)) + return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Hash function result too large for TPM, refusing."); - values.digests[values.count].hashAlg = id; + /* Put together a buffer consisting if the nvindex number and the NvPCR name, that we can calculate an HMAC() off, see below */ + size_t hmac_buffer_size = sizeof(le32_t) + strlen(p.name); + _cleanup_free_ void* hmac_buffer = malloc(hmac_buffer_size); + if (!hmac_buffer) + return log_oom_debug(); - /* So here's a twist: sometimes we want to measure secrets (e.g. root file system volume - * key), but we'd rather not leak a literal hash of the secret to the TPM (given that the - * wire is unprotected, and some other subsystem might use the simple, literal hash of the - * secret for other purposes, maybe because it needs a shorter secret derived from it for - * some unrelated purpose, who knows). Hence we instead measure an HMAC signature of a - * private non-secret string instead. */ - if (iovec_is_set(secret) > 0) { - if (!HMAC(implementation, secret->iov_base, secret->iov_len, data->iov_base, data->iov_len, (unsigned char*) &values.digests[values.count].digest, NULL)) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to calculate HMAC of data to measure."); - } else if (EVP_Digest(data->iov_base, data->iov_len, (unsigned char*) &values.digests[values.count].digest, NULL, implementation, NULL) != 1) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash data to measure."); + *(le32_t*) hmac_buffer = htole32(p.nv_index); + memcpy((uint8_t*) hmac_buffer + sizeof(le32_t), name, strlen(name)); - values.count++; - } + TPM2B_MAX_NV_BUFFER buf = { + .size = digest_size, + }; + CLEANUP_ERASE(buf); - /* Open + lock the log file *before* we start measuring, so that no one else can come between our log - * and our measurement and change either */ - log_fd = tpm2_userspace_log_open(); + /* We measure HMAC(anchor_secret, name) into the NvPCR to anchor it on our secret. */ + if (!HMAC(implementation, anchor_secret->iov_base, anchor_secret->iov_len, hmac_buffer, hmac_buffer_size, buf.buffer, NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to calculate HMAC of data to measure."); - rc = sym_Esys_PCR_Extend( + _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; + r = tpm2_define_nvpcr_nv_index( + c, + session, + p.nv_index, + p.algorithm, + &nv_handle); + if (r < 0) + return r; + + log_debug("Successfully acquired handle to NV index 0x%" PRIx32 ".", p.nv_index); + + tpm2_userspace_log_dirty(log_fd); + rc = sym_Esys_NV_Extend( c->esys_context, - ESYS_TR_PCR0 + pcr_index, - ESYS_TR_PASSWORD, - ESYS_TR_NONE, - ESYS_TR_NONE, - &values); + /* authHandle= */ nv_handle->esys_handle, + /* nvIndex= */ nv_handle->esys_handle, + /* shandle1= */ ESYS_TR_PASSWORD, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + &buf); if (rc != TSS2_RC_SUCCESS) - return log_debug_errno( - SYNTHETIC_ERRNO(ENOTRECOVERABLE), - "Failed to measure into PCR %u: %s", - pcr_index, - sym_Tss2_RC_Decode(rc)); + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to extend NV index: %s", sym_Tss2_RC_Decode(rc)); - /* Now, write what we just extended to the log, too. */ - (void) tpm2_userspace_log(log_fd, pcr_index, &values, event_type, description); + log_debug("Successfully extended NvPCR '%s' with anchor secret.", name); + + /* Now pre-calculate the initial measurement of an "anchor" secret. This makes sure that others + * cannot delete and reproduce the same fake PCR, unless they also know the "anchor" secret. */ + TPM2B_DIGEST start = { /* initialize to zero */ + .size = digest_size, + }; + r = tpm2_digest_buffer( + p.algorithm, + &start, + buf.buffer, + buf.size, + /* extend= */ true); + if (r < 0) + return log_debug_errno(r, "Failed to calculate initial value: %m"); + + /* Now create the anchor flag file */ + _cleanup_free_ char *h = hexmem(start.buffer, start.size); + if (!h) + return log_oom_debug(); + + r = write_string_file_at(dfd, anchor_fname, h, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); + if (r < 0) + return log_debug_errno(r, "Failed to write anchor file: %m"); + + tpm2_userspace_log_clean(log_fd); + return 1; +#else /* HAVE_OPENSSL */ + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled."); +#endif +} + +int tpm2_nvpcr_read( + Tpm2Context *c, + const Tpm2Handle *session, + const char *name, + struct iovec *ret_value, + uint32_t *ret_nv_index) { + +#if HAVE_OPENSSL + int r; + + assert(c); + assert(name); + + if (!tpm2_nvpcr_name_is_valid(name)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Attempt to read from NvPCR with invalid name, refusing: %s", name); + + _cleanup_(nvpcr_data_done) NvPCRData p = {}; + r = nvpcr_data_load(name, &p); + if (r < 0) + return r; + + _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; + r = tpm2_index_to_handle( + c, + p.nv_index, + session, + /* ret_public= */ NULL, + /* ret_name= */ NULL, + /* ret_qname= */ NULL, + &nv_handle); + if (r < 0) + return log_debug_errno(r, "Failed to acquire handle to NV index 0x%" PRIu32 ".", p.nv_index); + + log_debug("Successfully acquired handle to NV index 0x%" PRIx32 ".", p.nv_index); + + r = tpm2_read_nv_index( + c, + /* session= */ NULL, + p.nv_index, + nv_handle, + ret_value); + if (r < 0) + return r; + + if (ret_nv_index) + *ret_nv_index = p.nv_index; return 0; #else /* HAVE_OPENSSL */ @@ -8140,3 +9127,9 @@ static const char* const tpm2_pcr_index_table[_TPM2_PCR_INDEX_MAX_DEFINED] = { DEFINE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_FALLBACK(tpm2_pcr_index, int, TPM2_PCRS_MAX - 1); DEFINE_STRING_TABLE_LOOKUP_TO_STRING(tpm2_pcr_index, int); + +bool tpm2_nvpcr_name_is_valid(const char *name) { + return filename_is_valid(name) && + string_is_safe(name) && + tpm2_pcr_index_from_string(name) < 0; /* don't allow nvpcrs to be name like pcrs */ +} diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 67b035b442f..564a3e46ad3 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -150,6 +150,11 @@ const char* tpm2_userspace_event_type_to_string(Tpm2UserspaceEventType type) _co Tpm2UserspaceEventType tpm2_userspace_event_type_from_string(const char *s) _pure_; int tpm2_pcr_extend_bytes(Tpm2Context *c, char **banks, unsigned pcr_index, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event, const char *description); +int tpm2_nvpcr_get_index(const char *name, uint32_t *ret); +int tpm2_nvpcr_extend_bytes(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event_type, const char *description); +int tpm2_nvpcr_acquire_anchor_secret(struct iovec *ret, bool sync_secondary); +int tpm2_nvpcr_initialize(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *anchor_secret); +int tpm2_nvpcr_read(Tpm2Context *c, const Tpm2Handle *session, const char *name, struct iovec *ret, uint32_t *ret_nv_index); uint32_t tpm2_tpms_pcr_selection_to_mask(const TPMS_PCR_SELECTION *s); void tpm2_tpms_pcr_selection_from_mask(uint32_t mask, TPMI_ALG_HASH hash, TPMS_PCR_SELECTION *ret); @@ -296,7 +301,10 @@ int tpm2_tpm2b_public_to_fingerprint(const TPM2B_PUBLIC *public, void **ret_fing int tpm2_define_policy_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE requested_nv_index, const TPM2B_DIGEST *write_policy, TPM2_HANDLE *ret_nv_index, Tpm2Handle **ret_nv_handle, TPM2B_NV_PUBLIC *ret_nv_public); int tpm2_write_policy_nv_index(Tpm2Context *c, const Tpm2Handle *policy_session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, const TPM2B_DIGEST *policy_digest); +int tpm2_define_nvpcr_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, TPMI_ALG_HASH algorithm, Tpm2Handle **ret_nv_handle); +int tpm2_extend_nvpcr_nv_index(Tpm2Context *c, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, const struct iovec *digest); int tpm2_undefine_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle); +int tpm2_read_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, struct iovec *ret_value); int tpm2_seal_data(Tpm2Context *c, const struct iovec *data, const Tpm2Handle *primary_handle, const Tpm2Handle *encryption_session, const TPM2B_DIGEST *policy, struct iovec *ret_public, struct iovec *ret_private); int tpm2_unseal_data(Tpm2Context *c, const struct iovec *public, const struct iovec *private, const Tpm2Handle *primary_handle, const Tpm2Handle *policy_session, const Tpm2Handle *encryption_session, struct iovec *ret_data); @@ -510,3 +518,5 @@ const char* tpm2_pcr_index_to_string(int pcr) _const_; assert_cc(TPM2_NV_INDEX_UNASSIGNED_FIRST >= TPM2_NV_INDEX_FIRST); assert_cc(TPM2_NV_INDEX_UNASSIGNED_LAST <= TPM2_NV_INDEX_LAST); #endif + +bool tpm2_nvpcr_name_is_valid(const char *name); diff --git a/src/tpm2-setup/meson.build b/src/tpm2-setup/meson.build index 39fc97b91e7..36082486d30 100644 --- a/src/tpm2-setup/meson.build +++ b/src/tpm2-setup/meson.build @@ -25,6 +25,23 @@ executables += [ generator_template + { 'name' : 'systemd-tpm2-generator', 'sources' : files('tpm2-generator.c'), + 'conditions' : [ + 'ENABLE_BOOTLOADER', + 'HAVE_OPENSSL', + 'HAVE_TPM2', + ], }, ] + +if conf.get('ENABLE_BOOTLOADER') == 1 and conf.get('HAVE_OPENSSL') == 1 and conf.get('HAVE_TPM2') == 1 + nvpcrs = [] + foreach n : nvpcrs + custom_target( + input : 'nvpcr/' + n + '.nvpcr.in', + output : n + '.nvpcr', + command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'], + install : true, + install_dir : prefixdir / 'lib/nvpcr') + endforeach +endif