/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#include <sys/file.h>
+
#include "alloc-util.h"
#include "constants.h"
#include "cryptsetup-util.h"
#include "hexdecoct.h"
#include "hmac.h"
#include "initrd-util.h"
+#include "io-util.h"
#include "lock-util.h"
#include "log.h"
#include "logarithm.h"
#include "memory-util.h"
+#include "mkdir.h"
#include "nulstr-util.h"
#include "parse-util.h"
#include "random-util.h"
#include "sort-util.h"
#include "stat-util.h"
#include "string-table.h"
+#include "sync-util.h"
#include "time-util.h"
#include "tpm2-util.h"
#include "virt.h"
}
#if HAVE_TPM2
+static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MAX] = {
+ [TPM2_EVENT_PHASE] = "phase",
+ [TPM2_EVENT_FILESYSTEM] = "filesystem",
+ [TPM2_EVENT_VOLUME_KEY] = "volume-key",
+ [TPM2_EVENT_MACHINE_ID] = "machine-id",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(tpm2_userspace_event_type, Tpm2UserspaceEventType);
+
+const char *tpm2_userspace_log_path(void) {
+ return secure_getenv("SYSTEMD_MEASURE_LOG_USERSPACE") ?: "/var/log/systemd/tpm2-measure.log";
+}
+
+static int tpm2_userspace_log_open(void) {
+ _cleanup_close_ int fd = -EBADF;
+ struct stat st;
+ const char *e;
+ int r;
+
+ e = tpm2_userspace_log_path();
+ (void) mkdir_parents(e, 0755);
+
+ /* We use access mode 0600 here (even though the measurements should not strictly be confidential),
+ * because we use BSD file locking on it, and if anyone but root can access the file they can also
+ * 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_warning_errno(errno, "Failed to open TPM log file '%s' for writing, ignoring: %m", e);
+
+ if (flock(fd, LOCK_EX) < 0)
+ return log_warning_errno(errno, "Failed to lock TPM log file '%s', ignoring: %m", e);
+
+ if (fstat(fd, &st) < 0)
+ return log_warning_errno(errno, "Failed to fstat TPM log file '%s', ignoring: %m", e);
+
+ r = stat_verify_regular(&st);
+ if (r < 0)
+ return log_warning_errno(r, "TPM log file '%s' is not regular, ignoring: %m", e);
+
+ /* 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_warning_errno(SYNTHETIC_ERRNO(ESTALE), "TPM log file '%s' aborted, ignoring.", e);
+
+ if (fchmod(fd, 0600 | S_ISVTX) < 0)
+ return log_warning_errno(errno, "Failed to chmod() TPM log file '%s', ignoring: %m", e);
+
+ return TAKE_FD(fd);
+}
+
+static int tpm2_userspace_log(
+ int fd,
+ unsigned pcr_index,
+ const TPML_DIGEST_VALUES *values,
+ Tpm2UserspaceEventType event_type,
+ const char *description) {
+
+ _cleanup_(json_variant_unrefp) JsonVariant *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 = json_variant_append_arrayb(
+ &array, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_STRING("hashAlg", a),
+ JSON_BUILD_PAIR("digest", JSON_BUILD_HEX(&values->digests[i].digest, EVP_MD_size(implementation)))));
+ if (r < 0)
+ return log_error_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_error_errno(r, "Failed to acquire boot ID: %m");
+
+ r = json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("pcr", JSON_BUILD_UNSIGNED(pcr_index)),
+ JSON_BUILD_PAIR("digests", JSON_BUILD_VARIANT(array)),
+ JSON_BUILD_PAIR("content_type", JSON_BUILD_STRING("systemd")),
+ JSON_BUILD_PAIR("content", JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_CONDITION(description, "string", JSON_BUILD_STRING(description)),
+ JSON_BUILD_PAIR("bootId", JSON_BUILD_ID128(boot_id)),
+ JSON_BUILD_PAIR("timestamp", JSON_BUILD_UNSIGNED(now(CLOCK_BOOTTIME))),
+ JSON_BUILD_PAIR_CONDITION(event_type >= 0, "eventType", JSON_BUILD_STRING(tpm2_userspace_event_type_to_string(event_type)))))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build log record JSON: %m");
+
+ r = json_variant_format(v, JSON_FORMAT_SEQ, &f);
+ if (r < 0)
+ return log_error_errno(r, "Failed to format JSON: %m");
+
+ if (lseek(fd, 0, SEEK_END) == (off_t) -1)
+ return log_error_errno(errno, "Failed to seek to end of JSON log: %m");
+
+ r = loop_write(fd, f, SIZE_MAX, /* do_poll= */ false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write JSON data to log: %m");
+
+ if (fsync(fd) < 0)
+ return log_error_errno(errno, "Failed to sync JSON data: %m");
+
+ /* Unset S_ISVTX again */
+ if (fchmod(fd, 0600) < 0)
+ return log_warning_errno(errno, "Failed to chmod() TPM log file, ignoring: %m");
+
+ r = fsync_full(fd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to sync JSON log: %m");
+
+ return 1;
+}
+
int tpm2_extend_bytes(
Tpm2Context *c,
char **banks,
const void *data,
size_t data_size,
const void *secret,
- size_t secret_size) {
+ size_t secret_size,
+ Tpm2UserspaceEventType event_type,
+ const char *description) {
#if HAVE_OPENSSL
+ _cleanup_close_ int log_fd = -EBADF;
TPML_DIGEST_VALUES values = {};
TSS2_RC rc;
values.count++;
}
+ /* Open + lock the log file *before* we start measuring, so that noone else can come between our log
+ * and our measurement and change either */
+ log_fd = tpm2_userspace_log_open();
+
rc = sym_Esys_PCR_Extend(
c->esys_context,
ESYS_TR_PCR0 + pcr_index,
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, &values, event_type, description);
+
return 0;
#else /* HAVE_OPENSSL */
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled.");