#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"
#include "sync-util.h"
#include "time-util.h"
#include "tpm2-pcr.h"
+#include "tmpfile-util.h"
#include "tpm2-util.h"
#include "virt.h"
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;
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),
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,
#endif
}
+#if HAVE_TPM2
const uint16_t tpm2_hash_algorithms[] = {
TPM2_ALG_SHA1,
TPM2_ALG_SHA256,
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",
#if HAVE_OPENSSL
static int tpm2_userspace_log_open(void) {
_cleanup_close_ int fd = -EBADF;
- struct stat st;
const char *e;
int r;
* 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 */
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 */
+}