diff --git a/man/rules/meson.build b/man/rules/meson.build
index d3cb5e4292..35b025c89e 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -1214,6 +1214,10 @@ manpages = [
'8',
['systemd-report-sign-plain', 'systemd-report-sign-plain.socket'],
'HAVE_OPENSSL'],
+ ['systemd-report-sign-tpm2@.service',
+ '8',
+ ['systemd-report-sign-tpm2', 'systemd-report-sign-tpm2.socket'],
+ 'HAVE_OPENSSL HAVE_TPM2'],
['systemd-report-sign-tsm@.service',
'8',
['systemd-report-sign-tsm', 'systemd-report-sign-tsm.socket'],
diff --git a/man/systemd-report-sign-tpm2@.service.xml b/man/systemd-report-sign-tpm2@.service.xml
new file mode 100644
index 0000000000..f4917dd945
--- /dev/null
+++ b/man/systemd-report-sign-tpm2@.service.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+ systemd-report-sign-tpm2@.service
+ systemd
+
+
+
+ systemd-report-sign-tpm2@.service
+ 8
+
+
+
+ systemd-report-sign-tpm2@.service
+ systemd-report-sign-tpm2.socket
+ systemd-report-sign-tpm2
+ Sign system reports with a set of signed TPM2 attestations
+
+
+
+ systemd-report-sign-tpm2@.service
+ systemd-report-sign-tpm2.socket
+ /usr/lib/systemd/systemd-report-sign-tpm2
+
+
+
+ Description
+
+ systemd-report-sign-tpm2@.service is a system service that signs system reports
+ generated by
+ systemd-report1. It is
+ a signing backend for the logic of that tool: it implements the
+ io.systemd.Report.Signer.Sign() Varlink method and is reached via a socket linked into
+ the /run/systemd/report.sign/ directory, named tpm2.
+
+ The service is socket-activated (one instance per connection) via
+ systemd-report-sign-tpm2.socket. Rather than returning a single signature, it uses a
+ TPM2 device to generate a set of signed attestations that also bind the report digest passed to it,
+ together providing a verifier with evidence about the state of the system that produced the report.
+
+ The returned report consists of several signed components, each carrying a JSON encoded
+ attestation structure and its corresponding signature. The signature is provided as a JSON encoded
+ TPMT_SIGNATURE structure and in PEM encoded form. The signed component types are:
+
+
+ A PCR quote, generated with TPM2_Quote
+
+ One attestation per NvPCR, generated with TPM2_NV_Certify. Each
+ component also includes the readable name of the NvPCR, the NV index public area (as a JSON encoded
+ TPMS_NV_PUBLIC structure), and a JSON blob of authenticated data (the NvPCR name
+ and priority) that is digested and supplied as the qualifying data to the certify command.
+
+ An audit session attestation, generated with
+ TPM2_GetSessionAuditDigest. This is always the last component. The report digest
+ passed in via Varlink is supplied as its qualifying data.
+
+
+ The report additionally includes the measurement event log, and the public area of the signing key.
+ The public area is provided as a JSON encoded TPMT_PUBLIC structure, and in PEM
+ encooded form.
+
+ The preceding components are all executed under the audit session, and the report is only generated
+ if that session remains exclusive throughout. Because the audit session attestation records whether the
+ session was still exclusive when it was signed, this provides evidence to a verifier that the report is a
+ single, consistent snapshot of the system, with no other TPM commands interleaved while it was
+ generated.
+
+ The set of attested PCRs and NvPCRs is currently fixed. It covers all firmware PCRs except PCR 6,
+ all other S-RTM PCRs except the IMA PCR (which is not useful without the IMA log), and all defined
+ NvPCRs.
+
+
+
+ Signing key
+
+ The attestation signing key is stored as a JSON file named default.key in
+ /var/lib/systemd/report.sign.tpm2/. Three kinds of key are supported: an ordinary
+ object protected by an existing persistent object such as the SRK or EK, a persistent object, and a
+ primary object that is recreated from a template on each use. In all cases the key must be a restricted
+ signing key. If no key exists, an ordinary key is generated automatically on first use.
+
+ For ordinary and primary keys, the loaded object context is cached in the service's runtime
+ directory as /run/systemd/report.sign.tpm2/default.context and reused on subsequent
+ invocations, avoiding a repeated TPM2_Load (which requires authorization with the
+ parent key) or primary key recreation.
+
+ The key may optionally have an associated voucher, stored alongside it as
+ default.voucher. This is an opaque credential — for example one issued by a CA during
+ onboarding — that is attached to the signed report and presented to a verifier as proof that the
+ attestation key is authentic. It is deliberately not required to be any particular format (such as an
+ X.509 certificate).
+
+
+
+
+
+ See Also
+
+ systemd1
+ systemd-report1
+
+
+
+
diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset
index bce96b7a6d..bf6006c35c 100644
--- a/presets/90-systemd.preset
+++ b/presets/90-systemd.preset
@@ -36,6 +36,7 @@ enable systemd-report-basic.socket
enable systemd-report-cgroup.socket
enable systemd-report-files.socket
enable systemd-report-sign-plain.socket
+enable systemd-report-sign-tpm2.socket
enable systemd-report-sign-tsm.socket
enable systemd-report.socket
enable systemd-resolved.service
diff --git a/src/report/meson.build b/src/report/meson.build
index 76182a4131..c471fd00e6 100644
--- a/src/report/meson.build
+++ b/src/report/meson.build
@@ -44,6 +44,20 @@ executables += [
'dependencies' : libopenssl_cflags,
'conditions' : ['HAVE_OPENSSL'],
},
+ libexec_template + {
+ 'name' : 'systemd-report-sign-tpm2',
+ 'export': files(
+ 'report-sign-tpm2.c',
+ ),
+ 'dependencies' : [
+ libopenssl_cflags,
+ tpm2_cflags,
+ ],
+ 'conditions' : [
+ 'HAVE_OPENSSL',
+ 'HAVE_TPM2',
+ ],
+ },
libexec_template + {
'name' : 'systemd-report-sign-tsm',
'export' : files(
diff --git a/src/report/report-sign-tpm2.c b/src/report/report-sign-tpm2.c
new file mode 100644
index 0000000000..a742639336
--- /dev/null
+++ b/src/report/report-sign-tpm2.c
@@ -0,0 +1,784 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "sd-json.h"
+#include "sd-varlink.h"
+
+#include "build.h"
+#include "dlopen-note.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-table.h"
+#include "fs-util.h"
+#include "help-util.h"
+#include "iovec-util.h"
+#include "json-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "options.h"
+#include "string-table.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+#include "tpm2-pcr.h"
+#include "tpm2-report.h"
+#include "tpm2-util.h"
+#include "varlink-io.systemd.Report.Signer.h"
+#include "varlink-util.h"
+
+#define REPORT_SIGN_TPM2_PERSISTENT_DIR "/var/lib/systemd/report.sign.tpm2"
+#define REPORT_SIGN_TPM2_RUNTIME_DIR "/run/systemd/report.sign.tpm2"
+
+/* File containing the TPM signing key data. */
+#define REPORT_SIGN_TPM2_KEY "default.key"
+
+/* The voucher is some credential optionally issued as part of onboarding in order to certify the
+ * attestation key, and which is attached to the report signature. It's intentionally not "certificate" as
+ * we don't prescribe that it has to be a X.509 certificate - it's entirely opaque to us and an implementation
+ * detail of whatever platform we onboard with. It's also not "cred" or "credential" because those terms
+ * already have other meanings in systemd. */
+#define REPORT_SIGN_TPM2_VOUCHER "default.voucher"
+
+/* The key context contains the TPM context blob generated by TPM2_ContextSave, and
+ * is persisted between signing invocations for ordinary and primary signing keys.
+ * This avoids having to recreate primary keys, or reload ordinary keys (which requires
+ * authorization with the parent storage key, and which requires a policy session in
+ * some circumstances. */
+#define REPORT_SIGN_TPM2_KEYCONTEXT "default.context"
+
+typedef struct SignRequestParameters {
+ struct iovec digest;
+ const char *algorithm;
+} SignRequestParameters;
+
+static void sign_request_parameters_done(SignRequestParameters *p) {
+ assert(p);
+
+ iovec_done(&p->digest);
+ p->algorithm = NULL;
+}
+
+static int make_default_report_options(Tpm2ReportOptions *ret) {
+ int r;
+
+ assert(ret);
+
+ /* Include all NvPCRs. */
+ _cleanup_strv_free_ char **nv_pcrs = NULL;
+ r = tpm2_nvpcr_all_names(&nv_pcrs);
+ if (r < 0)
+ return log_error_errno(r, "Failed to obtain all NvPCR names");
+
+ /* Include all firmware PCRs except 6, and all on the "Linux TPM PCR Registry"
+ * except for the IMA PCR as we're not including the log for that. */
+ static const uint32_t pcr_mask =
+ (UINT32_C(1) << TPM2_PCR_PLATFORM_CODE) |
+ (UINT32_C(1) << TPM2_PCR_PLATFORM_CONFIG) |
+ (UINT32_C(1) << TPM2_PCR_EXTERNAL_CODE) |
+ (UINT32_C(1) << TPM2_PCR_EXTERNAL_CONFIG) |
+ (UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CODE) |
+ (UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CONFIG) |
+ (UINT32_C(1) << TPM2_PCR_SECURE_BOOT_POLICY) |
+ (UINT32_C(1) << TPM2_PCR_KERNEL_INITRD) |
+ (UINT32_C(1) << TPM2_PCR_KERNEL_BOOT) |
+ (UINT32_C(1) << TPM2_PCR_KERNEL_CONFIG) |
+ (UINT32_C(1) << TPM2_PCR_SYSEXTS) |
+ (UINT32_C(1) << TPM2_PCR_SHIM_POLICY) |
+ (UINT32_C(1) << TPM2_PCR_SYSTEM_IDENTITY);
+
+ *ret = (Tpm2ReportOptions) {
+ .pcr_mask = pcr_mask,
+ .nv_pcrs = TAKE_PTR(nv_pcrs),
+ };
+ return 0;
+}
+
+typedef enum SigningKeyType {
+ SIGNING_KEY_ORDINARY,
+ SIGNING_KEY_PERSISTENT,
+ SIGNING_KEY_PRIMARY,
+
+ _SIGNING_KEY_TYPE_MAX,
+ _SIGNING_KEY_TYPE_INVALID = -EINVAL,
+} SigningKeyType;
+
+static const char* const signing_key_type_table[_SIGNING_KEY_TYPE_MAX] = {
+ [SIGNING_KEY_ORDINARY] = "ordinary",
+ [SIGNING_KEY_PERSISTENT] = "persistent",
+ [SIGNING_KEY_PRIMARY] = "primary",
+};
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP(signing_key_type, SigningKeyType);
+
+static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_signing_key_type, SigningKeyType, signing_key_type_from_string);
+
+typedef struct SigningKeyData {
+ SigningKeyType type;
+
+ struct iovec parent_handle;
+ struct iovec public;
+ struct iovec private;
+
+ struct iovec handle;
+
+ uint32_t hierarchy;
+ struct iovec template;
+} SigningKeyData;
+
+static void signing_key_data_done(SigningKeyData *d) {
+ assert(d);
+
+ iovec_done(&d->parent_handle);
+ iovec_done(&d->public);
+ iovec_done(&d->private);
+
+ iovec_done(&d->handle);
+
+ iovec_done(&d->template);
+}
+
+static int load_signing_key_data(int dir_fd, SigningKeyData *ret) {
+ int r;
+
+ assert(dir_fd >= 0);
+ assert(ret);
+
+ _cleanup_fclose_ FILE *f = NULL;
+ r = xfopenat(dir_fd, REPORT_SIGN_TPM2_KEY, "re", /* open_flags= */ 0, &f);
+ if (r == -ENOENT)
+ return r;
+ if (r < 0)
+ return log_error_errno(r, "Failed to open TPM signing key data: %m");
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ r = sd_json_parse_file(
+ f,
+ /* path= */ NULL,
+ /* flags= */ 0,
+ &v,
+ /* reterr_line= */ NULL,
+ /* reterr_column= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load TPM signing key data: %m");
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "type", SD_JSON_VARIANT_STRING, json_dispatch_signing_key_type, offsetof(SigningKeyData, type), SD_JSON_MANDATORY },
+ { "parentHandle", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(SigningKeyData, parent_handle), 0 },
+ { "public", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(SigningKeyData, public), 0 },
+ { "private", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(SigningKeyData, private), 0 },
+ { "handle", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(SigningKeyData, handle), 0 },
+ { "hierarchy", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(SigningKeyData, hierarchy), 0 },
+ { "template", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(SigningKeyData, template), 0 },
+ {},
+ };
+
+ _cleanup_(signing_key_data_done) SigningKeyData data = {};
+ r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &data);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_STRUCT(data);
+
+ return 1;
+}
+
+static int generate_key(Tpm2Context *c, int dir_fd) {
+ int r;
+
+ assert(c);
+ assert(dir_fd >= 0);
+
+ TPMT_PUBLIC template;
+ r = tpm2_get_best_attestation_key_template(c, &template);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get attestation key template: %m");
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *ek_handle = NULL;
+ r = tpm2_get_or_create_ek(c, /* session= */ NULL, /* ret_public= */ NULL, /* ret_name= */ NULL, /* ret_qname= */ NULL, &ek_handle);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get handle to persistent EK: %m");
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *session = NULL;
+ r = tpm2_open_ek_user_policy_session(c, /* session= */ NULL, ek_handle, /* tpm_key= */ NULL, &session);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open policy session for EK: %m");
+
+ _cleanup_(Esys_Freep) TPM2B_PUBLIC *public = NULL;
+ _cleanup_(Esys_Freep) TPM2B_PRIVATE *private = NULL;
+ r = tpm2_create(c, ek_handle, session, &template, /* sensitive= */ NULL, &public, &private);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create new ordinary signing key: %m");
+
+ _cleanup_(iovec_done) struct iovec ek_handle_buf = {};
+ r = tpm2_serialize(c, ek_handle, &ek_handle_buf);
+ if (r < 0)
+ return log_error_errno(r, "Failed to serialize EK handle: %m");
+
+ _cleanup_free_ void *public_buf = NULL;
+ size_t public_sz;
+ r = tpm2_marshal_public(public, &public_buf, &public_sz);
+ if (r < 0)
+ return log_error_errno(r, "Failed to marshal public area for new ordinary signing key: %m");
+
+ _cleanup_free_ void *private_buf = NULL;
+ size_t private_sz;
+ r = tpm2_marshal_private(private, &private_buf, &private_sz);
+ if (r < 0)
+ return log_error_errno(r, "Failed to marshal private area for new ordinary signing key: %m");
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ r = sd_json_buildo(
+ &v,
+ SD_JSON_BUILD_PAIR_STRING("type", signing_key_type_to_string(SIGNING_KEY_ORDINARY)),
+ JSON_BUILD_PAIR_IOVEC_BASE64("parentHandle", &ek_handle_buf),
+ SD_JSON_BUILD_PAIR_BASE64("public", public_buf, public_sz),
+ SD_JSON_BUILD_PAIR_BASE64("private", private_buf, private_sz));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build signing key data: %m");
+
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *temp = NULL;
+ r = fopen_tmpfile_linkable_at(dir_fd, REPORT_SIGN_TPM2_KEY, O_WRONLY, &temp, &f);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open signing key data file '%s/%s' for writing: %m",
+ REPORT_SIGN_TPM2_PERSISTENT_DIR, REPORT_SIGN_TPM2_KEY);
+
+ CLEANUP_TMPFILE_AT(dir_fd, temp);
+
+ r = sd_json_variant_dump(v, SD_JSON_FORMAT_NEWLINE, f, /* prefix= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write signing key data: %m");
+
+ r = flink_tmpfile_at(f, dir_fd, temp, REPORT_SIGN_TPM2_KEY, LINK_TMPFILE_SYNC);
+ if (r < 0)
+ return log_error_errno(r, "Failed to move signing key data file '%s/%s' into place: %m",
+ REPORT_SIGN_TPM2_PERSISTENT_DIR, REPORT_SIGN_TPM2_KEY);
+
+ temp = mfree(temp);
+
+ return 0;
+}
+
+static int recreate_primary_key(Tpm2Context *c, uint32_t hierarchy, const struct iovec *template, Tpm2Handle **ret_handle) {
+ int r;
+
+ assert(c);
+ assert(iovec_is_valid(template));
+ assert(ret_handle);
+
+ ESYS_TR hierarchy_esys;
+ switch (hierarchy) {
+ case TPM2_RH_OWNER:
+ hierarchy_esys = ESYS_TR_RH_OWNER;
+ break;
+ case TPM2_RH_ENDORSEMENT:
+ hierarchy_esys = ESYS_TR_RH_ENDORSEMENT;
+ break;
+ default:
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unsupported hierarchy for primary key");
+ }
+
+ TPM2B_PUBLIC template_tpm2b;
+ r = tpm2_unmarshal_public(template->iov_base, template->iov_len, &template_tpm2b);
+ if (r < 0)
+ return log_error_errno(r, "Failed to unmarshal primary key template: %m");
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+ r = tpm2_create_primary(
+ c,
+ /* session= */ NULL,
+ hierarchy_esys,
+ &template_tpm2b,
+ /* sensitive= */ NULL,
+ /* ret_public= */ NULL,
+ &handle);
+ if (r < 0)
+ return log_error_errno(r, "Failed to recreate primary signing key: %m");
+
+ log_debug("Acquired primary signing key handle.");
+
+ *ret_handle = TAKE_PTR(handle);
+
+ return 0;
+}
+
+static int acquire_persistent_key(Tpm2Context *c, const struct iovec *handle_data, Tpm2Handle **ret_handle) {
+ int r;
+
+ assert(c);
+ assert(iovec_is_valid(handle_data));
+ assert(ret_handle);
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+ r = tpm2_deserialize(c, handle_data, &handle);
+ if (r < 0)
+ return log_error_errno(r, "Failed to deserialize persistent signing key handle: %m");
+
+ /* Make sure that the loaded handle matches the TPM object. */
+
+ _cleanup_(Esys_Freep) TPM2B_NAME *handle_name = NULL;
+ r = tpm2_get_name(c, handle, &handle_name);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get name of persistent signing key handle: %m");
+
+ _cleanup_(Esys_Freep) TPM2B_NAME *real_name = NULL;
+ r = tpm2_read_public(c, /* session= */ NULL, handle, /* ret_public= */ NULL, &real_name, /* ret_qname= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read name of persistent signing key from TPM: %m");
+
+ if (memcmp_nn(handle_name->name, handle_name->size, real_name->name, real_name->size) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Persistent signing key handle does not match object in TPM");
+
+ log_debug("Acquired persistent signing key handle.");
+
+ *ret_handle = TAKE_PTR(handle);
+
+ return 0;
+}
+
+static int load_ordinary_key(
+ Tpm2Context *c,
+ const struct iovec *parent_handle_data,
+ const struct iovec *public,
+ const struct iovec *private,
+ Tpm2Handle **ret_handle) {
+ int r;
+
+ assert(c);
+ assert(iovec_is_valid(parent_handle_data));
+ assert(iovec_is_valid(public));
+ assert(iovec_is_valid(private));
+ assert(ret_handle);
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *parent = NULL;
+ r = tpm2_deserialize(c, parent_handle_data, &parent);
+ if (r < 0)
+ return log_error_errno(r, "Failed to deserialize ordinary signing key parent handle: %m");
+
+ TPM2B_PUBLIC public_tpm2b;
+ r = tpm2_unmarshal_public(public->iov_base, public->iov_len, &public_tpm2b);
+ if (r < 0)
+ return log_error_errno(r, "Failed to unmarshal ordinary signing key public area: %m");
+
+ TPM2B_PRIVATE private_tpm2b;
+ r = tpm2_unmarshal_private(private->iov_base, private->iov_len, &private_tpm2b);
+ if (r < 0)
+ return log_error_errno(r, "Failed to unmarshal ordinary signing key private area: %m");
+
+ /* We pass in the EK handle as the tpmKey parameter for the policy session. This is just to make
+ * sure that the policy session HMAC check will fail on the TPM if the handle we've deserialized
+ * from disk isn't consistent with the object that's actually in the TPM now. */
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *session = NULL;
+ r = tpm2_open_ek_user_policy_session(c, /* session= */ NULL, parent, parent, &session);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open policy session for EK: %m");
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+ r = tpm2_load(c, parent, session, &public_tpm2b, &private_tpm2b, &handle);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load ordinary attestation key into the TPM: %m");
+
+ log_debug("Acquired ordinary signing key handle.");
+
+ *ret_handle = TAKE_PTR(handle);
+
+ return 0;
+}
+
+static int load_key_context(Tpm2Context *c, int dir_fd, Tpm2Handle **ret_handle) {
+ int r;
+
+ assert(c);
+ assert(dir_fd >= 0);
+ assert(ret_handle);
+
+ _cleanup_free_ char *buf = NULL;
+ size_t buf_sz;
+ r = read_full_file_at(dir_fd, REPORT_SIGN_TPM2_KEYCONTEXT, &buf, &buf_sz);
+ if (r == -ENOENT)
+ return r;
+ if (r < 0)
+ return log_error_errno(r, "Failed to read saved signing key context: %m");
+
+ TPMS_CONTEXT context;
+ r = tpm2_unmarshal_saved_handle_context(buf, buf_sz, &context);
+ if (r < 0)
+ return log_error_errno(r, "Failed to unmarshal saved signing key context: %m");
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+ r = tpm2_load_saved_handle_context(c, &context, /* ret_name= */ NULL, &handle);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load saved signing key context: %m");
+
+ log_debug("Acquired signing key handle from saved context.");
+
+ *ret_handle = TAKE_PTR(handle);
+
+ return 1;
+}
+
+static int save_key_context(Tpm2Context *c, int dir_fd, const Tpm2Handle *handle) {
+ int r;
+
+ assert(c);
+ assert(dir_fd >= 0);
+ assert(handle);
+
+ _cleanup_(Esys_Freep) TPMS_CONTEXT *context = NULL;
+ r = tpm2_save_handle_context(c, handle, &context);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to save signing key context: %m");
+
+ _cleanup_free_ void *buf = NULL;
+ size_t buf_sz;
+ r = tpm2_marshal_saved_handle_context(context, &buf, &buf_sz);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to marshal signing key context: %m");
+
+ r = write_data_file_atomic_at(
+ dir_fd,
+ REPORT_SIGN_TPM2_KEYCONTEXT,
+ &IOVEC_MAKE(buf, buf_sz),
+ /* flags= */ 0);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to write saved signing key context to '%s/%s': %m",
+ REPORT_SIGN_TPM2_RUNTIME_DIR, REPORT_SIGN_TPM2_KEYCONTEXT);
+
+ log_debug("Saved signing key context to '%s/%s'.", REPORT_SIGN_TPM2_RUNTIME_DIR, REPORT_SIGN_TPM2_KEYCONTEXT);
+
+ return 0;
+}
+
+/* Acquire the signing key handle. This can acquire several types of signing key:
+ * - An ordinary object that is protected by an existing persistent object (like the EK or SRK). These are
+ * loaded from a saved context if one exists, otherwise falling back to doing TPM2_Load again.
+ * - A persistent object.
+ * - A primary object that is created from a template on each use.
+ *
+ * If there is no existing signing key, then one is created automatically as an ordinary child of the EK.
+ *
+ * In the future, it would be nice to support multiple signing keys so that one can generate reports for
+ * different verifiers. Perhaps each signing key could have a corresponding set of report options rather
+ * than hardcoding the options as we do at the moment.
+ *
+ * It might also be nice to have some way of customizing what type of signing key is automatically created if
+ * one doesn't exist. */
+static int acquire_key_handle(Tpm2Context *c, Tpm2Handle **ret_handle, struct iovec *ret_voucher) {
+ int r;
+
+ assert(c);
+ assert(ret_handle);
+ assert(ret_voucher);
+
+ /* Open (creating if necessary) and exclusively lock the key directory, so that loading/generating the
+ * key is safe against parallel invocations (we are spawned once per connection). Like with the plain
+ * signer, the directory has 0700 permissions but this is just to be conservative - there's no
+ * unencrypted key material here. */
+ _cleanup_close_ int dir_fd = xopenat_lock_full(
+ AT_FDCWD,
+ REPORT_SIGN_TPM2_PERSISTENT_DIR,
+ O_CLOEXEC|O_DIRECTORY|O_CREAT,
+ /* xopen_flags= */ 0,
+ /* mode= */ 0700,
+ LOCK_BSD,
+ LOCK_EX);
+ if (dir_fd < 0)
+ return log_error_errno(dir_fd, "Failed to open and lock directory '%s': %m", REPORT_SIGN_TPM2_PERSISTENT_DIR);
+
+ /* Also open the runtime directory which we use for loading and saving TPM key contexts. */
+ _cleanup_close_ int runtime_dir_fd = xopenat_lock_full(
+ AT_FDCWD,
+ REPORT_SIGN_TPM2_RUNTIME_DIR,
+ O_CLOEXEC|O_DIRECTORY|O_CREAT,
+ /* xopen_flags = */ 0,
+ /* mode= */ 0700,
+ LOCK_BSD,
+ LOCK_EX);
+ if (runtime_dir_fd < 0)
+ return log_error_errno(runtime_dir_fd, "Failed to open and lock directory '%s': %m", REPORT_SIGN_TPM2_RUNTIME_DIR);
+
+ _cleanup_free_ char *voucher = NULL;
+ size_t voucher_sz = 0;
+ r = read_full_file_at(dir_fd, REPORT_SIGN_TPM2_VOUCHER, &voucher, &voucher_sz);
+ if (r < 0 && r != -ENOENT)
+ return log_error_errno(r, "Failed to read signing key voucher: %m");
+
+ _cleanup_(signing_key_data_done) SigningKeyData data = {};
+ r = load_signing_key_data(dir_fd, &data);
+ if (r == -ENOENT && !voucher) {
+ r = generate_key(c, dir_fd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate key: %m");
+
+ r = load_signing_key_data(dir_fd, &data);
+ }
+ if (r < 0)
+ return r;
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+ switch (data.type) {
+ case SIGNING_KEY_ORDINARY:
+ r = load_key_context(c, runtime_dir_fd, &handle);
+ if (r == -ENOENT) {
+ r = load_ordinary_key(c, &data.parent_handle, &data.public, &data.private, &handle);
+ if (r < 0)
+ return r;
+
+ (void) save_key_context(c, runtime_dir_fd, handle);
+ } else if (r < 0)
+ return r;
+ break;
+ case SIGNING_KEY_PERSISTENT:
+ r = acquire_persistent_key(c, &data.handle, &handle);
+ if (r < 0)
+ return r;
+ break;
+ case SIGNING_KEY_PRIMARY:
+ r = load_key_context(c, runtime_dir_fd, &handle);
+ if (r == -ENOENT) {
+ r = recreate_primary_key(c, data.hierarchy, &data.template, &handle);
+ if (r < 0)
+ return r;
+
+ (void) save_key_context(c, runtime_dir_fd, handle);
+ } else if (r < 0)
+ return r;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ *ret_handle = TAKE_PTR(handle);
+ *ret_voucher = IOVEC_MAKE(TAKE_PTR(voucher), voucher_sz);
+
+ return 0;
+}
+
+static int reply_report(sd_varlink *link, const Tpm2Report *report, const struct iovec *voucher) {
+ int r;
+
+ assert(link);
+ assert(report);
+ assert(iovec_is_valid(voucher));
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *pubkey = NULL;
+ r = tpm2_tpmt_public_to_json(report->public_key, &pubkey);
+ if (r < 0)
+ return log_error_errno(r, "Cannot convert public key to JSON: %m");
+
+ _cleanup_free_ char *pubkey_pem = NULL;
+ r = tpm2_tpmt_public_to_pem(report->public_key, &pubkey_pem);
+ if (r < 0)
+ return log_error_errno(r, "Cannot convert public key to PEM: %m");
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *cv = NULL;
+ FOREACH_ARRAY(c, report->components, report->n_components) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *nv_public = NULL;
+ if (c->type == TPM2_REPORT_TYPE_NVPCR) {
+ r = tpm2_tpms_nv_public_to_json(c->nv_public, &nv_public);
+ if (r < 0)
+ return log_error_errno(r, "Cannot convert NvPCR public area to JSON: %m");
+ }
+
+ TPMT_SIG_SCHEME scheme = {
+ .scheme = c->signature->sigAlg,
+ };
+ if (IN_SET(scheme.scheme, TPM2_ALG_RSASSA, TPM2_ALG_RSAPSS, TPM2_ALG_ECDSA))
+ scheme.details.any = c->signature->signature.any;
+ else if (scheme.scheme != TPM2_ALG_NULL)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Unsupported signature scheme 0x%" PRIx16, scheme.scheme);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *attest_info = NULL;
+ r = tpm2_attest_info_to_json(&scheme, c->attestation, &attest_info);
+ if (r < 0)
+ return log_error_errno(r, "Cannot convert attestation info to JSON: %m");
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *sig = NULL;
+ r = tpm2_tpmt_signature_to_json(c->signature, &sig);
+ if (r < 0)
+ return log_error_errno(r, "Cannot convert signature to JSON: %m");
+
+ _cleanup_free_ char *sig_pem = NULL;
+ r = tpm2_tpmt_signature_to_pem(c->signature, &sig_pem);
+ if (r < 0)
+ return log_error_errno(r, "Cannot convert signature to PEM: %m");
+
+ r = sd_json_variant_append_arraybo(
+ &cv,
+ SD_JSON_BUILD_PAIR_STRING("type", tpm2_report_component_type_to_string(c->type)),
+ SD_JSON_BUILD_PAIR_CONDITION(c->type == TPM2_REPORT_TYPE_NVPCR, "nvpcrName", SD_JSON_BUILD_STRING(c->nv_pcr_name)),
+ SD_JSON_BUILD_PAIR_CONDITION(c->type == TPM2_REPORT_TYPE_NVPCR, "nvPublic", SD_JSON_BUILD_VARIANT(nv_public)),
+ SD_JSON_BUILD_PAIR_CONDITION(c->type != TPM2_REPORT_TYPE_SESSION_AUDIT && c->authenticated_data, "authenticatedData", SD_JSON_BUILD_STRING(c->authenticated_data)),
+ SD_JSON_BUILD_PAIR_VARIANT("attestInfo", attest_info),
+ SD_JSON_BUILD_PAIR_VARIANT("signature", sig),
+ SD_JSON_BUILD_PAIR_STRING("signaturePEM", sig_pem));
+ if (r < 0)
+ return log_error_errno(r, "Cannot add component to report array: %m");
+ }
+
+ return sd_varlink_replybo(
+ link,
+ SD_JSON_BUILD_PAIR(
+ "data",
+ SD_JSON_BUILD_ARRAY(
+ SD_JSON_BUILD_OBJECT(
+ SD_JSON_BUILD_PAIR_VARIANT("eventLog", report->event_log),
+ SD_JSON_BUILD_PAIR_VARIANT("publicKey", pubkey),
+ SD_JSON_BUILD_PAIR_STRING("publicKeyPEM", pubkey_pem),
+ SD_JSON_BUILD_PAIR_CONDITION(iovec_is_set(voucher), "voucher", JSON_BUILD_IOVEC_BASE64(voucher)),
+ SD_JSON_BUILD_PAIR_VARIANT("components", cv)))));
+}
+
+static int vl_method_sign(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ sd_varlink_method_flags_t flags,
+ void *userdata) {
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "digest", SD_JSON_VARIANT_STRING, json_dispatch_unhex_iovec, offsetof(SignRequestParameters, digest), SD_JSON_MANDATORY },
+ { "algorithm", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(SignRequestParameters, algorithm), SD_JSON_MANDATORY },
+ {}
+ };
+
+ int r;
+ _cleanup_(sign_request_parameters_done) SignRequestParameters sign_params = {};
+
+ assert(link);
+ assert(parameters);
+
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &sign_params);
+ if (r != 0)
+ return r;
+
+ /* Serialize the supplied digest to a TPM2B_DATA structure prefixed with the algorithm ID,
+ * and include this in the TPM report. Note that tpm2_digest_buf_to_data checks that the
+ * supplied digest has the correct size. */
+ r = tpm2_hash_alg_from_string(sign_params.algorithm);
+ if (r < 0)
+ return sd_varlink_error_invalid_parameter_name(link, "algorithm");
+
+ TPM2B_DATA external_data;
+ r = tpm2_digest_iovec_to_data((TPMI_ALG_HASH) r, &sign_params.digest, &external_data);
+ if (r == -EINVAL)
+ return sd_varlink_error_invalid_parameter_name(link, "digest");
+ if (r < 0)
+ return log_error_errno(r, "Failed to marshal digest: %m");
+
+ /* Use fixed options. Maybe it would be good to have a way to configure this. */
+ _cleanup_(tpm2_report_options_done) Tpm2ReportOptions opts = {};
+ r = make_default_report_options(&opts);
+ if (r < 0)
+ return r;
+
+ _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL;
+ r = tpm2_context_new_or_warn(/* device= */ NULL, &c);
+ if (r < 0)
+ return r;
+
+ /* Load a signing key. */
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *key_handle = NULL;
+ _cleanup_(iovec_done) struct iovec key_voucher = {};
+ r = acquire_key_handle(c, &key_handle, &key_voucher);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire a signing key handle: %m");
+
+ _cleanup_(tpm2_report_freep) Tpm2Report *report = NULL;
+ r = tpm2_generate_report(c, &opts, key_handle, &external_data, &report);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate TPM report: %m");
+
+ return reply_report(link, report, &key_voucher);
+}
+
+static int vl_server(void) {
+ _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *vs = NULL;
+ int r;
+
+ r = varlink_server_new(&vs, SD_VARLINK_SERVER_ROOT_ONLY|SD_VARLINK_SERVER_MYSELF_ONLY, /* userdata= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate Varlink server: %m");
+
+ r = sd_varlink_server_add_interface(vs, &vl_interface_io_systemd_Report_Signer);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add Varlink interface: %m");
+
+ r = sd_varlink_server_bind_method(vs, "io.systemd.Report.Signer.Sign", vl_method_sign);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind Varlink methods: %m");
+
+ r = sd_varlink_server_loop_auto(vs);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run Varlink event loop: %m");
+
+ return 0;
+}
+
+static int help(void) {
+ _cleanup_(table_unrefp) Table *options = NULL;
+ int r;
+
+ r = option_parser_get_help_table(&options);
+ if (r < 0)
+ return r;
+
+ help_cmdline("[OPTIONS...]");
+ help_abstract("Get an attestation report from a Trusted Platform Module (TPM) "
+ "that includes the hash of the system report.");
+ help_section("Options");
+
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+
+ help_man_page_reference("systemd-report-sign-tpm2@.service", "8");
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ int r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ OptionParser opts = { argc, argv };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
+ switch (c) {
+ OPTION_COMMON_HELP:
+ return help();
+ OPTION_COMMON_VERSION:
+ return version();
+ }
+
+ if (option_parser_get_n_args(&opts) > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "This program takes no arguments.");
+
+ r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "This program can only run as a Varlink service.");
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ TPM2_NOTE(suggested);
+ LIBCRYPTO_NOTE(suggested);
+
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ return vl_server();
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/report/report-sign.c b/src/report/report-sign.c
index 27665fe25f..a2986534da 100644
--- a/src/report/report-sign.c
+++ b/src/report/report-sign.c
@@ -238,10 +238,6 @@ int context_sign_report(
if (r < 0)
return log_error_errno(r, "Failed to build JSON data: %m");
- r = sd_json_variant_normalize(&sig);
- if (r < 0)
- return log_error_errno(r, "Failed to normalize JSON object: %m");
-
r = sd_json_variant_dump(sig, format_flags, output, /* prefix= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to dump json object: %m");
diff --git a/src/shared/crypto-util.c b/src/shared/crypto-util.c
index 7689a4209f..3258f8f906 100644
--- a/src/shared/crypto-util.c
+++ b/src/shared/crypto-util.c
@@ -256,6 +256,7 @@ DLSYM_PROTOTYPE(PEM_read_PrivateKey) = NULL;
DLSYM_PROTOTYPE(PEM_read_X509) = NULL;
static DLSYM_PROTOTYPE(PEM_read_bio_PrivateKey) = NULL;
static DLSYM_PROTOTYPE(PEM_read_bio_X509) = NULL;
+DLSYM_PROTOTYPE(PEM_write) = NULL;
DLSYM_PROTOTYPE(PEM_write_PUBKEY) = NULL;
DLSYM_PROTOTYPE(PEM_write_PrivateKey) = NULL;
DLSYM_PROTOTYPE(PEM_write_X509) = NULL;
@@ -580,6 +581,7 @@ int dlopen_libcrypto(int log_level) {
DLSYM_ARG(PEM_read_X509),
DLSYM_ARG(PEM_read_bio_PrivateKey),
DLSYM_ARG(PEM_read_bio_X509),
+ DLSYM_ARG(PEM_write),
DLSYM_ARG(PEM_write_PUBKEY),
DLSYM_ARG(PEM_write_PrivateKey),
DLSYM_ARG(PEM_write_X509),
diff --git a/src/shared/crypto-util.h b/src/shared/crypto-util.h
index 28a0eaf5bd..fbe02bfb2c 100644
--- a/src/shared/crypto-util.h
+++ b/src/shared/crypto-util.h
@@ -224,6 +224,7 @@ extern DLSYM_PROTOTYPE(OSSL_PARAM_free);
extern DLSYM_PROTOTYPE(PEM_read_PUBKEY);
extern DLSYM_PROTOTYPE(PEM_read_PrivateKey);
extern DLSYM_PROTOTYPE(PEM_read_X509);
+extern DLSYM_PROTOTYPE(PEM_write);
extern DLSYM_PROTOTYPE(PEM_write_PUBKEY);
extern DLSYM_PROTOTYPE(PEM_write_PrivateKey);
extern DLSYM_PROTOTYPE(PEM_write_X509);
diff --git a/src/shared/meson.build b/src/shared/meson.build
index 466d86bb6f..ee395ffbe1 100644
--- a/src/shared/meson.build
+++ b/src/shared/meson.build
@@ -308,6 +308,10 @@ if conf.get('HAVE_LIBBPF') == 1
shared_sources += files('bpf-link.c')
endif
+if conf.get('HAVE_TPM2') == 1
+ shared_sources += files('tpm2-report.c')
+endif
+
generate_ip_protocol_list = files('generate-ip-protocol-list.sh')
ip_protocol_list_txt = custom_target(
input : [generate_ip_protocol_list, ipproto_sources],
diff --git a/src/shared/tpm2-report.c b/src/shared/tpm2-report.c
new file mode 100644
index 0000000000..58f54f4858
--- /dev/null
+++ b/src/shared/tpm2-report.c
@@ -0,0 +1,472 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "alloc-util.h"
+#include "crypto-util.h"
+#include "json-util.h"
+#include "log.h"
+#include "sd-json.h"
+#include "sd-varlink.h"
+#include "string-table.h"
+#include "strv.h"
+#include "tpm2-report.h"
+#include "tpm2-util.h"
+
+void tpm2_report_options_done(Tpm2ReportOptions *opts) {
+ assert(opts);
+
+ opts->nv_pcrs = strv_free(opts->nv_pcrs);
+}
+
+static const char* const tpm2_report_component_type_table[_TPM2_REPORT_TYPE_MAX] = {
+ [TPM2_REPORT_TYPE_PCR] = "pcr",
+ [TPM2_REPORT_TYPE_NVPCR] = "nvpcr",
+ [TPM2_REPORT_TYPE_SESSION_AUDIT] = "session-audit",
+};
+DEFINE_STRING_TABLE_LOOKUP(tpm2_report_component_type, Tpm2ReportComponentType);
+
+static void tpm2_report_component_done(Tpm2ReportComponent *c) {
+ assert(c);
+
+ c->nv_pcr_name = mfree(c->nv_pcr_name);
+ c->nv_public = mfree(c->nv_public);
+
+ c->authenticated_data = mfree(c->authenticated_data);
+
+ c->attestation = mfree(c->attestation);
+ sym_Esys_Free(c->signature);
+ c->signature = NULL;
+}
+
+static void tpm2_report_component_array_free(Tpm2ReportComponent *comps, size_t n_comps) {
+ assert(comps || n_comps == 0);
+
+ FOREACH_ARRAY(c, comps, n_comps)
+ tpm2_report_component_done(c);
+ free(comps);
+}
+
+Tpm2Report *tpm2_report_free(Tpm2Report *rp) {
+ if (!rp)
+ return NULL;
+
+ rp->event_log = sd_json_variant_unref(rp->event_log);
+ rp->public_key = mfree(rp->public_key);
+ tpm2_report_component_array_free(rp->components, rp->n_components);
+ rp->components = NULL;
+ rp->n_components = 0;
+
+ return mfree(rp);
+}
+
+#if HAVE_OPENSSL
+
+typedef struct NvPCRReportRequest {
+ char *name;
+
+ TPM2B_NV_PUBLIC *public;
+ Tpm2Handle *handle;
+
+ char *authenticated_data;
+} NvPCRReportRequest;
+
+static void nvpcr_report_request_done(NvPCRReportRequest *req) {
+ assert(req);
+
+ req->name = mfree(req->name);
+ sym_Esys_Free(req->public);
+ req->public = NULL;
+ req->handle = tpm2_handle_free(req->handle);
+ req->authenticated_data = mfree(req->authenticated_data);
+}
+
+static void nvpcr_report_request_array_free(NvPCRReportRequest *reqs, size_t n_reqs) {
+ assert(reqs || n_reqs == 0);
+
+ FOREACH_ARRAY(r, reqs, n_reqs)
+ nvpcr_report_request_done(r);
+ mfree(reqs);
+}
+
+static int make_qualifying_data(TPMI_ALG_HASH alg, const char *authenticated_data, TPM2B_DATA *ret) {
+ int r;
+
+ assert(authenticated_data);
+ assert(ret);
+
+ const char *md_alg = tpm2_hash_alg_to_string(alg);
+ if (!md_alg)
+ return -EINVAL;
+
+ _cleanup_free_ void *digest = NULL;
+ size_t digest_sz;
+ r = openssl_digest(md_alg, authenticated_data, strlen(authenticated_data), &digest, &digest_sz);
+ if (r < 0)
+ return r;
+
+ TPM2B_DATA digest_data;
+ r = tpm2_digest_buf_to_data(alg, digest, digest_sz, &digest_data);
+ if (r < 0)
+ return r;
+
+ *ret = digest_data;
+ return 0;
+}
+
+static int make_nvpcr_report_authenticated_data(const char *name, uint64_t priority, char **ret) {
+ int r;
+
+ assert(name);
+ assert(ret);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ r = sd_json_buildo(
+ &v,
+ SD_JSON_BUILD_PAIR_STRING("name", name),
+ SD_JSON_BUILD_PAIR_UNSIGNED("priority", priority));
+ if (r < 0)
+ return r;
+
+ _cleanup_free_ char *data = NULL;
+ r = sd_json_variant_format(v, /* flags= */ 0, &data);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(data);
+ return 0;
+}
+
+static int tpm2_generate_report_try(
+ Tpm2Context *c,
+ const Tpm2Handle *key,
+ const TPML_PCR_SELECTION *pcrs,
+ NvPCRReportRequest *nv_pcrs,
+ size_t n_nv_pcrs,
+ const TPM2B_DATA *external_data,
+ sd_json_variant **ret_event_log,
+ Tpm2ReportComponent **ret_components,
+ size_t *ret_n_components) {
+
+ int r;
+
+ assert(c);
+ assert(pcrs);
+ assert(nv_pcrs);
+ assert(ret_event_log);
+ assert(ret_components);
+ assert(ret_n_components);
+
+ /* This generates a PCR quote and a NV certification per NvPCR index, all inside an audit session.
+ * Audit session exclusivity provides evidence that the sequence of attestations reflects a single
+ * and consistent snapshot of the machine's state. */
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *audit_session = NULL;
+ r = tpm2_make_exclusive_audit_session(c, &audit_session);
+ if (r < 0)
+ return r;
+
+ Tpm2ReportComponent *components = NULL;
+ size_t n_components = 0;
+ CLEANUP_ARRAY(components, n_components, tpm2_report_component_array_free);
+
+ /* First obtain a PCR quote. */
+ _cleanup_free_ TPMS_ATTEST *quoted = NULL;
+ _cleanup_(Esys_Freep) TPMT_SIGNATURE *quote_signature = NULL;
+ r = tpm2_quote(
+ c,
+ /* sign_session= */ NULL,
+ audit_session,
+ key,
+ /* qualifying_data= */ NULL,
+ pcrs,
+ "ed,
+ "e_signature);
+ if (r < 0)
+ return r;
+
+ if (!GREEDY_REALLOC(components, n_components + 1))
+ return log_oom_debug();
+ components[n_components++] = (Tpm2ReportComponent) {
+ .type = TPM2_REPORT_TYPE_PCR,
+ .attestation = TAKE_PTR(quoted),
+ .signature = TAKE_PTR(quote_signature),
+ };
+
+ /* Obtain NvPCR reports. */
+ FOREACH_ARRAY(n, nv_pcrs, n_nv_pcrs) {
+ log_debug("Fetching TPM attestation for NvPCR '%s'.", n->name);
+
+ TPM2B_DATA qualifying_data;
+ r = make_qualifying_data(n->public->nvPublic.nameAlg, n->authenticated_data, &qualifying_data);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to create qualifying data for NvPCR attestation");
+
+ _cleanup_free_ TPMS_ATTEST *certify_info = NULL;
+ _cleanup_(Esys_Freep) TPMT_SIGNATURE *signature = NULL;
+ r = tpm2_nv_certify(
+ c,
+ /* sign_session= */ NULL,
+ /* auth_session= */ NULL,
+ audit_session,
+ key,
+ &n->public->nvPublic,
+ n->handle,
+ &qualifying_data,
+ &certify_info,
+ &signature);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to fetch TPM attestation for NvPCR '%s'", n->name);
+
+ _cleanup_free_ char *name_dup = strdup(n->name);
+ if (!name_dup)
+ return log_oom_debug();
+
+ _cleanup_free_ TPMS_NV_PUBLIC *nv_public = new0(TPMS_NV_PUBLIC, 1);
+ if (!nv_public)
+ return log_oom_debug();
+ memcpy(nv_public, &n->public->nvPublic, sizeof(TPMS_NV_PUBLIC));
+
+ _cleanup_free_ char *authenticated_data = strdup(n->authenticated_data);
+ if (!authenticated_data)
+ return log_oom_debug();
+
+ if (!GREEDY_REALLOC(components, n_components + 1))
+ return log_oom_debug();
+ components[n_components++] = (Tpm2ReportComponent) {
+ .type = TPM2_REPORT_TYPE_NVPCR,
+ .nv_pcr_name = TAKE_PTR(name_dup),
+ .nv_public = TAKE_PTR(nv_public),
+ .authenticated_data = TAKE_PTR(authenticated_data),
+ .attestation = TAKE_PTR(certify_info),
+ .signature = TAKE_PTR(signature),
+ };
+ }
+
+ /* Read the event log before checking the audit session below. As tpm2_pcr_extend_bytes and
+ * tpm2_nvpcr_extend_bytes touch the log after performing a TPM measurement, this ensures that we
+ * get a log that is consistent with the previously obtained TPM quote and NV certifications. If
+ * the log is appended to before we read it here, the audit session exclusivity check will fail
+ * below because the TPM extend commands will have marked the session as no longer exclusive in
+ * the TPM. */
+ _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
+ r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.PCRLock");
+ if (r < 0)
+ return log_debug_errno(r, "Failed to connect to pcrlock: %m");
+
+ sd_json_variant *reply = NULL;
+ const char *error_id = NULL;
+ r = sd_varlink_collect(vl, "io.systemd.PCRLock.ReadEventLog", /* parameters= */ NULL, &reply, &error_id);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to issue io.systemd.PCRLock.ReadEventLog varlink call: %m");
+ if (error_id) {
+ r = sd_varlink_error_to_errno(error_id, reply);
+ if (r != -EBADR)
+ return log_debug_errno(r, "Failed to issue io.systemd.PCRLock.ReadEventLog varlink call: %m");
+
+ return log_debug_errno(r, "Failed to issue io.systemd.PCRLock.ReadEventLog varlink call: %s", error_id);
+ }
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *event_log = NULL;
+ sd_json_variant *i;
+ JSON_VARIANT_ARRAY_FOREACH(i, reply) {
+ sd_json_variant *record = sd_json_variant_by_key(i, "record");
+ if (!record)
+ return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Reply from io.systemd.PCRLock.ReadEventLog lacks 'record' field.");
+
+ r = sd_json_variant_append_array(&event_log, record);
+ if (r < 0)
+ return r;
+ }
+
+ if (!event_log) {
+ r = sd_json_variant_new_array(&event_log, /* array= */ NULL, /* n= */ 0);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to allocate empty event log array: %m");
+ }
+
+ _cleanup_free_ TPMS_ATTEST *audit_info = NULL;
+ _cleanup_(Esys_Freep) TPMT_SIGNATURE *audit_signature = NULL;
+ r = tpm2_get_session_audit_digest(
+ c,
+ /* eh_session= */ NULL,
+ /* sign_session= */ NULL,
+ audit_session,
+ key,
+ external_data,
+ &audit_info,
+ &audit_signature);
+ if (r < 0)
+ return r;
+
+ if (audit_info->type != TPM2_ST_ATTEST_SESSION_AUDIT)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Unexpected audit session attestation tag 0x%" PRIx16, audit_info->type);
+ if (audit_info->attested.sessionAudit.exclusiveSession == TPM2_NO)
+ return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Audit session did not remain exclusive");
+
+ if (!GREEDY_REALLOC(components, n_components + 1))
+ return log_oom_debug();
+ components[n_components++] = (Tpm2ReportComponent) {
+ .type = TPM2_REPORT_TYPE_SESSION_AUDIT,
+ .attestation = TAKE_PTR(audit_info),
+ .signature = TAKE_PTR(audit_signature),
+ };
+
+ *ret_event_log = TAKE_PTR(event_log);
+ *ret_components = TAKE_PTR(components);
+ *ret_n_components = n_components;
+
+ return 0;
+}
+
+#endif
+
+#define MAX_REPORT_GENERATE_RETRIES 10
+
+int tpm2_generate_report(
+ Tpm2Context *c,
+ const Tpm2ReportOptions *options,
+ const Tpm2Handle *key,
+ const TPM2B_DATA *external_data,
+ Tpm2Report **ret) {
+
+#if HAVE_OPENSSL
+ int r;
+
+ assert(c);
+ assert(options);
+
+ log_debug("Generating TPM report.");
+
+ r = dlopen_tpm2(LOG_DEBUG);
+ if (r < 0)
+ return r;
+
+ TPMI_ALG_HASH pcr_bank;
+ r = tpm2_get_best_pcr_bank(c, options->pcr_mask, &pcr_bank);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to select PCR bank");
+
+ TPML_PCR_SELECTION pcrs;
+ tpm2_tpml_pcr_selection_from_mask(options->pcr_mask, pcr_bank, &pcrs);
+
+ NvPCRReportRequest *nv_pcrs = NULL;
+ size_t n_nv_pcrs = 0;
+ CLEANUP_ARRAY(nv_pcrs, n_nv_pcrs, nvpcr_report_request_array_free);
+
+ nv_pcrs = new0(NvPCRReportRequest, strv_length(options->nv_pcrs));
+ if (!nv_pcrs)
+ return log_oom_debug();
+
+ STRV_FOREACH(s, options->nv_pcrs) {
+ const char *name = *s;
+
+ if (!tpm2_nvpcr_name_is_valid(name))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Attempt to report NvPCR with invalid name '%s', refusing", name);
+
+ uint32_t nv_index;
+ uint64_t priority;
+ r = tpm2_nvpcr_get_index(name, &nv_index, &priority);
+ if (r < 0)
+ return log_debug_errno(r, "Attempt to report invalid NvPCR '%s'", name);
+
+ _cleanup_(Esys_Freep) TPM2B_NV_PUBLIC *nv_public = NULL;
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL;
+ r = tpm2_nv_index_to_handle(c, nv_index, /* session= */ NULL, &nv_public, /* ret_name= */ NULL, &nv_handle);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to obtain handle and public area for NvPCR '%s'", name);
+
+ _cleanup_free_ char *authenticated_data = NULL;
+ r = make_nvpcr_report_authenticated_data(name, priority, &authenticated_data);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to make qualifying data for NvPCR '%s'", name);
+
+ _cleanup_free_ char *name_dup = strdup(name);
+ if (!name_dup)
+ return log_oom_debug();
+
+ log_debug("Created NvPCR report request for '%s', index 0x%" PRIx32 ".", name, nv_index);
+
+ nv_pcrs[n_nv_pcrs++] = (NvPCRReportRequest) {
+ .name = TAKE_PTR(name_dup),
+ .public = TAKE_PTR(nv_public),
+ .handle = TAKE_PTR(nv_handle),
+ .authenticated_data = TAKE_PTR(authenticated_data),
+ };
+ }
+
+ _cleanup_free_ TPMT_PUBLIC *pubkey = NULL;
+ if (key) {
+ _cleanup_(Esys_Freep) TPM2B_PUBLIC *public = NULL;
+ r = tpm2_read_public(c, /* session= */ NULL, key, &public, /* ret_name= */ NULL, /* ret_qname= */ NULL);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to read public area of signing key");
+
+ /* Ensure the supplied object is a restricted signing key. Restricted signing keys have to
+ * specify a signing scheme in order to be valid, which means we don't have to select a
+ * scheme when calling the TPM attestation functions. If we permit a normal signing key here,
+ * we would need to select a signing scheme when calling the TPM attestation functions in
+ * the scenario where the key hasn't specified one - they all just pass a NULL scheme now,
+ * so they'll fail with an unrestricted key if it's not explicit about its signing scheme. */
+ if ((public->publicArea.objectAttributes & (TPMA_OBJECT_RESTRICTED | TPMA_OBJECT_SIGN_ENCRYPT)) != (TPMA_OBJECT_RESTRICTED | TPMA_OBJECT_SIGN_ENCRYPT))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Supplied object is not a restricted signing key");
+
+ pubkey = new0(TPMT_PUBLIC, 1);
+ if (!pubkey)
+ return log_oom_debug();
+ memcpy(pubkey, &public->publicArea, sizeof(TPMT_PUBLIC));
+ }
+
+ if (external_data) {
+ r = tpm2_max_data_size(c);
+ if (r < 0)
+ return r;
+ if (external_data->size > r)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "External data size larger than supported by the TPM");
+ }
+
+ _cleanup_(tpm2_report_freep) Tpm2Report *report = NULL;
+ for (int i = MAX_REPORT_GENERATE_RETRIES;; i--) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *event_log = NULL;
+ Tpm2ReportComponent *components = NULL;
+ size_t n_components = 0;
+ CLEANUP_ARRAY(components, n_components, tpm2_report_component_array_free);
+
+ r = tpm2_generate_report_try(
+ c,
+ key,
+ &pcrs,
+ nv_pcrs, n_nv_pcrs,
+ external_data,
+ &event_log,
+ &components, &n_components);
+ if (r == -EBUSY && i > 0) {
+ log_debug("Audit session lost exclusivity, retrying.");
+ continue;
+ }
+ if (r < 0)
+ return log_debug_errno(r, "Failed to generate TPM report");
+
+ report = new(Tpm2Report, 1);
+ if (!report)
+ return log_oom_debug();
+
+ *report = (Tpm2Report) {
+ .event_log = TAKE_PTR(event_log),
+ .public_key = TAKE_PTR(pubkey),
+ .components = TAKE_PTR(components),
+ .n_components = n_components,
+ };
+ break;
+ }
+
+ if (ret)
+ *ret = TAKE_PTR(report);
+
+ return 0;
+#else
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled.");
+#endif
+}
diff --git a/src/shared/tpm2-report.h b/src/shared/tpm2-report.h
new file mode 100644
index 0000000000..c9ec9ff98c
--- /dev/null
+++ b/src/shared/tpm2-report.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include
+
+#include "forward.h"
+
+#if HAVE_TPM2
+
+#include
+
+typedef struct Tpm2ReportOptions {
+ uint32_t pcr_mask;
+ char **nv_pcrs;
+} Tpm2ReportOptions;
+
+void tpm2_report_options_done(Tpm2ReportOptions *opts);
+
+typedef enum Tpm2ReportComponentType {
+ TPM2_REPORT_TYPE_PCR,
+ TPM2_REPORT_TYPE_NVPCR,
+ TPM2_REPORT_TYPE_SESSION_AUDIT,
+
+ _TPM2_REPORT_TYPE_MAX,
+ _TPM2_REPORT_TYPE_INVALID = -EINVAL,
+} Tpm2ReportComponentType;
+
+DECLARE_STRING_TABLE_LOOKUP(tpm2_report_component_type, Tpm2ReportComponentType);
+
+typedef struct Tpm2ReportComponent {
+ Tpm2ReportComponentType type;
+
+ /* For NvPCRs only */
+ char *nv_pcr_name;
+ TPMS_NV_PUBLIC *nv_public;
+
+ /* Currently NvPCRs only but could be used in the future
+ * for other types, except session audit where the qualifying
+ * data is the report digest we get. */
+ char *authenticated_data;
+
+ /* Common fields */
+ TPMS_ATTEST *attestation;
+ TPMT_SIGNATURE *signature;
+} Tpm2ReportComponent;
+
+typedef struct Tpm2Report {
+ sd_json_variant *event_log;
+
+ TPMT_PUBLIC *public_key;
+
+ Tpm2ReportComponent *components;
+ size_t n_components;
+} Tpm2Report;
+
+Tpm2Report *tpm2_report_free(Tpm2Report *rp);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Tpm2Report*, tpm2_report_free);
+
+int tpm2_generate_report(
+ Tpm2Context *c,
+ const Tpm2ReportOptions *options,
+ const Tpm2Handle *key,
+ const TPM2B_DATA *external_data,
+ Tpm2Report **ret);
+
+#endif
diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c
index 336e7198d8..6e2865a3a5 100644
--- a/src/shared/tpm2-util.c
+++ b/src/shared/tpm2-util.c
@@ -8,6 +8,7 @@
#include "alloc-util.h"
#include "ansi-color.h"
#include "bitfield.h"
+#include "conf-files.h"
#include "constants.h"
#include "creds-util.h"
#include "crypto-util.h"
@@ -32,6 +33,7 @@
#include "log.h"
#include "logarithm.h"
#include "memory-util.h"
+#include "memstream-util.h"
#include "mkdir.h"
#include "ordered-set.h"
#include "random-util.h"
@@ -97,19 +99,23 @@ int tpm2_pcr_bank_from_efi_active_legacy(uint32_t active_banks, uint16_t *ret) {
}
#if HAVE_TPM2
+static DLSYM_PROTOTYPE(Esys_ContextLoad) = NULL;
+static DLSYM_PROTOTYPE(Esys_ContextSave) = NULL;
static DLSYM_PROTOTYPE(Esys_Create) = NULL;
static DLSYM_PROTOTYPE(Esys_CreateLoaded) = NULL;
static DLSYM_PROTOTYPE(Esys_CreatePrimary) = NULL;
static DLSYM_PROTOTYPE(Esys_EvictControl) = NULL;
static DLSYM_PROTOTYPE(Esys_Finalize) = NULL;
static DLSYM_PROTOTYPE(Esys_FlushContext) = NULL;
-static DLSYM_PROTOTYPE(Esys_Free) = NULL;
+DLSYM_PROTOTYPE(Esys_Free) = NULL;
static DLSYM_PROTOTYPE(Esys_GetCapability) = NULL;
static DLSYM_PROTOTYPE(Esys_GetRandom) = NULL;
+static DLSYM_PROTOTYPE(Esys_GetSessionAuditDigest) = NULL;
static DLSYM_PROTOTYPE(Esys_Import) = NULL;
static DLSYM_PROTOTYPE(Esys_Initialize) = NULL;
static DLSYM_PROTOTYPE(Esys_Load) = NULL;
static DLSYM_PROTOTYPE(Esys_LoadExternal) = NULL;
+static DLSYM_PROTOTYPE(Esys_NV_Certify) = NULL;
static DLSYM_PROTOTYPE(Esys_NV_DefineSpace) = NULL;
static DLSYM_PROTOTYPE(Esys_NV_Extend) = NULL;
static DLSYM_PROTOTYPE(Esys_NV_Read) = NULL;
@@ -125,7 +131,9 @@ static DLSYM_PROTOTYPE(Esys_PolicyGetDigest) = NULL;
static DLSYM_PROTOTYPE(Esys_PolicyNvWritten) = NULL;
static DLSYM_PROTOTYPE(Esys_PolicyOR) = NULL;
static DLSYM_PROTOTYPE(Esys_PolicyPCR) = NULL;
+static DLSYM_PROTOTYPE(Esys_PolicySecret) = NULL;
static DLSYM_PROTOTYPE(Esys_PolicySigned) = NULL;
+static DLSYM_PROTOTYPE(Esys_Quote) = NULL;
static DLSYM_PROTOTYPE(Esys_ReadPublic) = NULL;
static DLSYM_PROTOTYPE(Esys_StartAuthSession) = NULL;
static DLSYM_PROTOTYPE(Esys_Startup) = NULL;
@@ -155,6 +163,9 @@ static DLSYM_PROTOTYPE(Tss2_MU_TPM2B_PUBLIC_Marshal) = NULL;
static DLSYM_PROTOTYPE(Tss2_MU_TPM2B_PUBLIC_Unmarshal) = NULL;
static DLSYM_PROTOTYPE(Tss2_MU_TPM2B_SENSITIVE_Marshal) = NULL;
static DLSYM_PROTOTYPE(Tss2_MU_TPML_PCR_SELECTION_Marshal) = NULL;
+static DLSYM_PROTOTYPE(Tss2_MU_TPMS_ATTEST_Unmarshal) = NULL;
+static DLSYM_PROTOTYPE(Tss2_MU_TPMS_CONTEXT_Marshal) = NULL;
+static DLSYM_PROTOTYPE(Tss2_MU_TPMS_CONTEXT_Unmarshal) = NULL;
static DLSYM_PROTOTYPE(Tss2_MU_TPMS_NV_PUBLIC_Marshal) = NULL;
static DLSYM_PROTOTYPE(Tss2_MU_TPM2B_NV_PUBLIC_Marshal) = NULL;
static DLSYM_PROTOTYPE(Tss2_MU_TPM2B_NV_PUBLIC_Unmarshal) = NULL;
@@ -175,6 +186,8 @@ static int dlopen_tpm2_esys(int log_level) {
r = dlopen_many_sym_or_warn(
&libtss2_esys_dl, "libtss2-esys.so.0", log_level,
+ DLSYM_ARG(Esys_ContextLoad),
+ DLSYM_ARG(Esys_ContextSave),
DLSYM_ARG(Esys_Create),
DLSYM_ARG(Esys_CreateLoaded),
DLSYM_ARG(Esys_CreatePrimary),
@@ -184,10 +197,12 @@ static int dlopen_tpm2_esys(int log_level) {
DLSYM_ARG(Esys_Free),
DLSYM_ARG(Esys_GetCapability),
DLSYM_ARG(Esys_GetRandom),
+ DLSYM_ARG(Esys_GetSessionAuditDigest),
DLSYM_ARG(Esys_Import),
DLSYM_ARG(Esys_Initialize),
DLSYM_ARG(Esys_Load),
DLSYM_ARG(Esys_LoadExternal),
+ DLSYM_ARG(Esys_NV_Certify),
DLSYM_ARG(Esys_NV_DefineSpace),
DLSYM_ARG(Esys_NV_Extend),
DLSYM_ARG(Esys_NV_Read),
@@ -203,7 +218,9 @@ static int dlopen_tpm2_esys(int log_level) {
DLSYM_ARG(Esys_PolicyNvWritten),
DLSYM_ARG(Esys_PolicyOR),
DLSYM_ARG(Esys_PolicyPCR),
+ DLSYM_ARG(Esys_PolicySecret),
DLSYM_ARG(Esys_PolicySigned),
+ DLSYM_ARG(Esys_Quote),
DLSYM_ARG(Esys_ReadPublic),
DLSYM_ARG(Esys_StartAuthSession),
DLSYM_ARG(Esys_Startup),
@@ -262,6 +279,9 @@ static int dlopen_tpm2_mu(int log_level) {
DLSYM_ARG(Tss2_MU_TPM2B_PUBLIC_Unmarshal),
DLSYM_ARG(Tss2_MU_TPM2B_SENSITIVE_Marshal),
DLSYM_ARG(Tss2_MU_TPML_PCR_SELECTION_Marshal),
+ DLSYM_ARG(Tss2_MU_TPMS_ATTEST_Unmarshal),
+ DLSYM_ARG(Tss2_MU_TPMS_CONTEXT_Marshal),
+ DLSYM_ARG(Tss2_MU_TPMS_CONTEXT_Unmarshal),
DLSYM_ARG(Tss2_MU_TPMS_NV_PUBLIC_Marshal),
DLSYM_ARG(Tss2_MU_TPM2B_NV_PUBLIC_Marshal),
DLSYM_ARG(Tss2_MU_TPM2B_NV_PUBLIC_Unmarshal),
@@ -779,6 +799,26 @@ bool tpm2_supports_ecc_curve(Tpm2Context *c, TPM2_ECC_CURVE ecc_curve) {
return false;
}
+/* Return the maximum size of a TPM2B_DATA structure. This structure is intended to be able to
+ * store a TPMT_HA structure, so the maximum size is the value of TPM_PT_MAX_DIGEST (the maximum size
+ * of a TPMU_HA structure) + the size of TPM_ALG_ID. */
+int tpm2_max_data_size(Tpm2Context *c) {
+ int r;
+
+ assert(c);
+
+ uint32_t max_digest_size = 0;
+ r = tpm2_get_capability_property(c, TPM2_PT_MAX_DIGEST, &max_digest_size);
+ if (r < 0)
+ return r;
+
+ if (max_digest_size > (UINT16_MAX - sizeof(TPM2_ALG_ID)))
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "TPM bug: reported implausible value for TPM_PT_MAX_DIGEST");
+
+ return (int) max_digest_size + sizeof(TPM2_ALG_ID);
+}
+
/* Query the TPM for populated handles.
*
* This provides an array of handle indexes populated in the TPM, starting at the requested handle. The array will
@@ -940,6 +980,10 @@ static const TPMT_SYM_DEF SESSION_TEMPLATE_SYM_AES_128_CFB = {
.mode.aes = TPM2_ALG_CFB, /* The spec requires sessions to use CFB. */
};
+static const TPMT_SYM_DEF SESSION_TEMPLATE_SYM_NULL = {
+ .algorithm = TPM2_ALG_NULL,
+};
+
int tpm2_context_new(const char *device, Tpm2Context **ret_context) {
_cleanup_(tpm2_context_unrefp) Tpm2Context *context = NULL;
TSS2_RC rc;
@@ -1164,7 +1208,7 @@ int tpm2_handle_new(Tpm2Context *context, Tpm2Handle **ret_handle) {
return 0;
}
-static int tpm2_read_public(
+int tpm2_read_public(
Tpm2Context *c,
const Tpm2Handle *session,
const Tpm2Handle *handle,
@@ -1429,6 +1473,100 @@ int tpm2_index_from_handle(Tpm2Context *c, const Tpm2Handle *handle, TPM2_HANDLE
return 0;
}
+int tpm2_marshal_saved_handle_context(TPMS_CONTEXT *context, void **ret, size_t *ret_size) {
+ size_t max_size = SIZEOF(*context), blob_size = 0;
+ _cleanup_free_ void *blob = NULL;
+ TSS2_RC rc;
+
+ assert(context);
+ assert(ret);
+ assert(ret_size);
+
+ blob = malloc0(max_size);
+ if (!blob)
+ return log_oom_debug();
+
+ rc = sym_Tss2_MU_TPMS_CONTEXT_Marshal(context, blob, max_size, &blob_size);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to marshal saved context structure: %s", sym_Tss2_RC_Decode(rc));
+
+ *ret = TAKE_PTR(blob);
+ *ret_size = blob_size;
+ return 0;
+}
+
+int tpm2_unmarshal_saved_handle_context(const void *data, size_t size, TPMS_CONTEXT *ret) {
+ size_t offset = 0;
+ TPMS_CONTEXT context = {};
+ TSS2_RC rc;
+ int r;
+
+ assert(data);
+ assert(ret);
+
+ r = dlopen_tpm2(LOG_DEBUG);
+ if (r < 0)
+ return r;
+
+ rc = sym_Tss2_MU_TPMS_CONTEXT_Unmarshal(data, size, &offset, &context);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to unmarshal saved context structure: %s", sym_Tss2_RC_Decode(rc));
+ if (offset != size)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Garbage at end of saved context structure data.");
+
+ *ret = context;
+ return 0;
+}
+
+int tpm2_load_saved_handle_context(Tpm2Context *c, const TPMS_CONTEXT *context, TPM2B_NAME **ret_name, Tpm2Handle **ret_handle) {
+ TSS2_RC rc;
+ int r;
+
+ assert(c);
+ assert(context);
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+ r = tpm2_handle_new(c, &handle);
+ if (r < 0)
+ return r;
+
+ rc = sym_Esys_ContextLoad(c->esys_context, context, &handle->esys_handle);
+ if (rc != TPM2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to load handle context: %s", sym_Tss2_RC_Decode(rc));
+
+ if (ret_name) {
+ r = tpm2_get_name(c, handle, ret_name);
+ if (r < 0)
+ return r;
+ }
+
+ if (ret_handle)
+ *ret_handle = TAKE_PTR(handle);
+
+ return 0;
+}
+
+int tpm2_save_handle_context(Tpm2Context *c, const Tpm2Handle *handle, TPMS_CONTEXT **ret_context) {
+ TSS2_RC rc;
+
+ assert(c);
+ assert(handle);
+ assert(ret_context);
+
+ _cleanup_(Esys_Freep) TPMS_CONTEXT *context = NULL;
+ rc = sym_Esys_ContextSave(c->esys_context, handle->esys_handle, &context);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to save handle context: %s", sym_Tss2_RC_Decode(rc));
+
+ *ret_context = TAKE_PTR(context);
+ return 0;
+}
+
/* Copy an object in the TPM at a transient handle to a persistent handle.
*
* The provided transient handle must exist in the TPM in the transient range. The persistent handle may be 0
@@ -1626,6 +1764,118 @@ static int tpm2_get_legacy_template(TPMI_ALG_PUBLIC alg, TPMT_PUBLIC *ret_templa
return 0;
}
+int tpm2_get_best_attestation_key_template(Tpm2Context *c, TPMT_PUBLIC *ret) {
+ assert(c);
+ assert(ret);
+
+ static const struct {
+ TPMI_ALG_PUBLIC alg;
+ TPMI_ALG_HASH name_alg;
+ TPMT_ASYM_SCHEME asym_scheme;
+ union {
+ TPMI_RSA_KEY_BITS rsa_key_bits;
+ TPMI_ECC_CURVE ecc_curve_id;
+ } asym_params;
+ } template_params[] = {
+ {
+ .alg = TPM2_ALG_ECC,
+ .name_alg = TPM2_ALG_SHA384,
+ .asym_scheme = {
+ .scheme = TPM2_ALG_ECDSA,
+ .details.ecdsa.hashAlg = TPM2_ALG_SHA384,
+ },
+ .asym_params.ecc_curve_id = TPM2_ECC_NIST_P384,
+ },
+ {
+ .alg = TPM2_ALG_ECC,
+ .name_alg = TPM2_ALG_SHA256,
+ .asym_scheme = {
+ .scheme = TPM2_ALG_ECDSA,
+ .details.ecdsa.hashAlg = TPM2_ALG_SHA256,
+ },
+ .asym_params.ecc_curve_id = TPM2_ECC_NIST_P256,
+ },
+ {
+ .alg = TPM2_ALG_RSA,
+ .name_alg = TPM2_ALG_SHA384,
+ .asym_scheme = {
+ .scheme = TPM2_ALG_RSAPSS,
+ .details.rsapss.hashAlg = TPM2_ALG_SHA384,
+ },
+ .asym_params.rsa_key_bits = 3072,
+ },
+ {
+ .alg = TPM2_ALG_RSA,
+ .name_alg = TPM2_ALG_SHA384,
+ .asym_scheme = {
+ .scheme = TPM2_ALG_RSASSA,
+ .details.rsassa.hashAlg = TPM2_ALG_SHA384,
+ },
+ .asym_params.rsa_key_bits = 3072,
+ },
+ {
+ .alg = TPM2_ALG_RSA,
+ .name_alg = TPM2_ALG_SHA256,
+ .asym_scheme = {
+ .scheme = TPM2_ALG_RSAPSS,
+ .details.rsapss.hashAlg = TPM2_ALG_SHA256,
+ },
+ .asym_params.rsa_key_bits = 2048,
+ },
+ {
+ .alg = TPM2_ALG_RSA,
+ .name_alg = TPM2_ALG_SHA256,
+ .asym_scheme = {
+ .scheme = TPM2_ALG_RSASSA,
+ .details.rsassa.hashAlg = TPM2_ALG_SHA256,
+ },
+ .asym_params.rsa_key_bits = 2048,
+ },
+ };
+
+ FOREACH_ELEMENT(p, template_params) {
+ TPMT_PUBLIC template = {
+ .type = p->alg,
+ .nameAlg = p->name_alg,
+ .objectAttributes =
+ TPMA_OBJECT_FIXEDTPM |
+ TPMA_OBJECT_FIXEDPARENT |
+ TPMA_OBJECT_SENSITIVEDATAORIGIN |
+ TPMA_OBJECT_USERWITHAUTH |
+ TPMA_OBJECT_ADMINWITHPOLICY |
+ TPMA_OBJECT_RESTRICTED |
+ TPMA_OBJECT_SIGN_ENCRYPT,
+ .parameters.asymDetail = {
+ .symmetric.algorithm = TPM2_ALG_NULL,
+ .scheme = p->asym_scheme,
+ },
+ };
+ switch (template.type) {
+ case TPM2_ALG_RSA:
+ template.parameters.rsaDetail.keyBits = p->asym_params.rsa_key_bits;
+ break;
+ case TPM2_ALG_ECC:
+ template.parameters.eccDetail.curveID = p->asym_params.ecc_curve_id;
+ template.parameters.eccDetail.kdf.scheme = TPM2_ALG_NULL;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ if (!tpm2_supports_alg(c, template.type))
+ continue;
+ if (!tpm2_supports_tpmt_public(c, &template))
+ continue;
+ if (template.type == TPM2_ALG_ECC && !tpm2_supports_ecc_curve(c, template.parameters.eccDetail.curveID))
+ continue;
+
+ *ret = template;
+ return 0;
+ }
+
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "No supported attestation key template");
+}
+
/* Get a Storage Root Key (SRK) template.
*
* The SRK template values are recommended by the "TCG TPM v2.0 Provisioning Guidance" document in section
@@ -2399,6 +2649,72 @@ int tpm2_get_or_create_ek(
#endif
}
+/* Opens a policy session for the EK associated with the supplied handle and executes the
+ * TPM2_PolicySecret(TPM_RH_ENDORSEMENT) branch, leaving the session ready to use for the user auth role.
+ *
+ * This isn't very flexible - it doesn't support delegated policies and assumes that the endorsement
+ * hierarchy auth is empty (or at least known by the caller). If the object has the userWithAuth attribute
+ * set, it doesn't create a policy session, which assumes that the object's auth value is empty. The EK
+ * template is designed in such a way that the use of the key can be locked down via a delegated policy by
+ * setting the endorsement hierarchy auth and setting the persistent EKauth to a random value. In the future,
+ * if we adopt the JSON policy language which will make it nicer to execute arbitrary policies, we might
+ * want to support these use cases via systemd-tpm2-setup.
+ *
+ * Returns 1 if a policy session was opened, 0 if the object at the supplied handle doesn't need a policy for
+ * the user auth role (in which case, no policy session is returned), or < 0 on error. */
+int tpm2_open_ek_user_policy_session(
+ Tpm2Context *c,
+ const Tpm2Handle *session,
+ const Tpm2Handle *ek_handle,
+ const Tpm2Handle *tpm_key,
+ Tpm2Handle **ret_session) {
+ int r;
+
+ assert(c);
+ assert(ek_handle);
+
+ _cleanup_(Esys_Freep) TPM2B_PUBLIC *public = NULL;
+ r = tpm2_read_public(c, /* session= */ NULL, ek_handle, &public, /* ret_name= */ NULL, /* ret_qname= */ NULL);
+ if (r < 0)
+ return r;
+
+ if ((public->publicArea.objectAttributes & TPMA_OBJECT_USERWITHAUTH) != 0) {
+ /* Prefer HMAC or passphrase auth for this object. This catches
+ * the case where ek_handle is actually the SRK, and also all
+ * high-range EKs. */
+ if (ret_session)
+ *ret_session = NULL;
+ return 0;
+ }
+
+ /* We need a policy session. The only EKs we require this for today are low-range ones. */
+ if (public->publicArea.nameAlg != TPM2_ALG_SHA256)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to open EK user policy session: unsupported name algorithm");
+
+ TPM2B_DIGEST expected_policy = TPM2_EK_AUTH_POLICY_A;
+ if (memcmp_nn(expected_policy.buffer, expected_policy.size,
+ public->publicArea.authPolicy.buffer, public->publicArea.authPolicy.size) != 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to open EK user policy session: unrecognized policy digest");
+
+ /* Note that tpm2_make_policy_session is currently hardcoded to SHA256. That's ok for now because
+ * we only get this far if the supplied ek_handle has a name algorithm of SHA256. */
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *policy_session = NULL;
+ r = tpm2_make_policy_session(c, tpm_key, /* encryption_session= */ NULL, &policy_session);
+ if (r < 0)
+ return r;
+
+ r = tpm2_policy_secret(c, session, policy_session, &TPM2_HANDLE_RH_ENDORSEMENT, /* policy_ref= */ NULL, /* ret_policy_digest= */ NULL);
+ if (r < 0)
+ return r;
+
+ if (ret_session)
+ *ret_session = TAKE_PTR(policy_session);
+
+ return 1;
+}
+
/* Utility functions for TPMS_PCR_SELECTION. */
/* Convert a TPMS_PCR_SELECTION object to a mask. */
@@ -3294,7 +3610,7 @@ static int tpm2_load_external(
return 0;
}
-static int tpm2_marshal_private(const TPM2B_PRIVATE *private, void **ret, size_t *ret_size) {
+int tpm2_marshal_private(const TPM2B_PRIVATE *private, void **ret, size_t *ret_size) {
size_t max_size = SIZEOF(*private), blob_size = 0;
_cleanup_free_ void *blob = NULL;
TSS2_RC rc;
@@ -3317,7 +3633,7 @@ static int tpm2_marshal_private(const TPM2B_PRIVATE *private, void **ret, size_t
return 0;
}
-static int tpm2_unmarshal_private(const void *data, size_t size, TPM2B_PRIVATE *ret_private) {
+int tpm2_unmarshal_private(const void *data, size_t size, TPM2B_PRIVATE *ret_private) {
TPM2B_PRIVATE private = {};
size_t offset = 0;
TSS2_RC rc;
@@ -3360,7 +3676,7 @@ int tpm2_marshal_public(const TPM2B_PUBLIC *public, void **ret, size_t *ret_size
return 0;
}
-static int tpm2_unmarshal_public(const void *data, size_t size, TPM2B_PUBLIC *ret_public) {
+int tpm2_unmarshal_public(const void *data, size_t size, TPM2B_PUBLIC *ret_public) {
TPM2B_PUBLIC public = {};
size_t offset = 0;
TSS2_RC rc;
@@ -3929,6 +4245,51 @@ int tpm2_get_good_pcr_banks_strv(
#endif
}
+static int tpm2_tpmt_ha_to_data(TPMT_HA *ha, TPM2B_DATA *ret) {
+ TSS2_RC rc;
+ int r;
+
+ assert(ha);
+ assert(ret);
+
+ r = dlopen_tpm2(LOG_DEBUG);
+ if (r < 0)
+ return r;
+
+ TPM2B_DATA data = {};
+ size_t written = 0;
+ rc = sym_Tss2_MU_TPMT_HA_Marshal(ha, data.buffer, sizeof(data.buffer), &written);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to marshal digest.");
+ data.size = written;
+
+ *ret = data;
+ return 0;
+}
+
+int tpm2_digest_buf_to_data(TPMI_ALG_HASH alg, void *digest, size_t digest_sz, TPM2B_DATA *ret) {
+ assert(digest);
+ assert(ret);
+
+ if (digest_sz != (size_t) tpm2_hash_alg_to_size(alg))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid digest size");
+
+ TPMT_HA ha = {
+ .hashAlg = alg,
+ };
+ assert(digest_sz <= sizeof_field(TPMT_HA, digest));
+ memcpy_safe(&ha.digest, digest, digest_sz);
+
+ return tpm2_tpmt_ha_to_data(&ha, ret);
+}
+
+int tpm2_digest_iovec_to_data(TPMI_ALG_HASH alg, const struct iovec *digest, TPM2B_DATA *ret) {
+ assert(iovec_is_valid(digest));
+ assert(ret);
+
+ return tpm2_digest_buf_to_data(alg, digest->iov_base, digest->iov_len, ret);
+}
+
/* Hash data into the digest.
*
* If 'extend' is true, the hashing operation starts with the existing digest hash (and the digest is
@@ -4164,6 +4525,61 @@ int tpm2_make_encryption_session(
return 0;
}
+static bool tpm2_is_audit_session(Tpm2Context *c, const Tpm2Handle *session) {
+ TPMA_SESSION flags = 0;
+ TSS2_RC rc;
+
+ assert(c);
+ assert(session);
+
+ rc = sym_Esys_TRSess_GetAttributes(c->esys_context, session->esys_handle, &flags);
+ if (rc != TSS2_RC_SUCCESS)
+ return false;
+
+ return flags & TPMA_SESSION_AUDIT;
+}
+
+int tpm2_make_exclusive_audit_session(Tpm2Context *c, Tpm2Handle **ret_session) {
+ TSS2_RC rc;
+ int r;
+
+ assert(c);
+ assert(ret_session);
+
+ log_debug("Starting HMAC exclusive audit session.");
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *session = NULL;
+ r = tpm2_handle_new(c, &session);
+ if (r < 0)
+ return r;
+
+ rc = sym_Esys_StartAuthSession(
+ c->esys_context,
+ /* tpmKey= */ ESYS_TR_NONE,
+ /* bind= */ ESYS_TR_NONE,
+ /* shandle1= */ ESYS_TR_NONE,
+ /* shandle2= */ ESYS_TR_NONE,
+ /* shandle3= */ ESYS_TR_NONE,
+ /* nonceCaller= */ NULL,
+ TPM2_SE_HMAC,
+ &SESSION_TEMPLATE_SYM_NULL,
+ TPM2_ALG_SHA256,
+ &session->esys_handle);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to open session in TPM: %s", sym_Tss2_RC_Decode(rc));
+
+ const TPMA_SESSION sessionAttributes = TPMA_SESSION_AUDIT | TPMA_SESSION_AUDITEXCLUSIVE | TPMA_SESSION_CONTINUESESSION;
+ rc = sym_Esys_TRSess_SetAttributes(c->esys_context, session->esys_handle, sessionAttributes, 0xff);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to configure TPM session: %s", sym_Tss2_RC_Decode(rc));
+
+ *ret_session = TAKE_PTR(session);
+
+ return 0;
+}
+
int tpm2_make_policy_session(
Tpm2Context *c,
const Tpm2Handle *primary,
@@ -4746,6 +5162,43 @@ int tpm2_policy_signed_hmac_sha256(
#endif
}
+int tpm2_policy_secret(
+ Tpm2Context *c,
+ const Tpm2Handle *auth_session,
+ const Tpm2Handle *session,
+ const Tpm2Handle *handle,
+ const TPM2B_NONCE *policy_ref,
+ TPM2B_DIGEST **ret_policy_digest) {
+
+ TSS2_RC rc;
+
+ assert(c);
+ assert(session);
+ assert(handle);
+
+ log_debug("Submitting PolicySecret policy.");
+
+ rc = sym_Esys_PolicySecret(
+ c->esys_context,
+ handle->esys_handle,
+ session->esys_handle,
+ auth_session ? auth_session->esys_handle : ESYS_TR_PASSWORD,
+ /* shandle2= */ ESYS_TR_NONE,
+ /* shandle3= */ ESYS_TR_NONE,
+ /* nonceTPM= */ NULL,
+ /* cpHashA= */ NULL,
+ policy_ref,
+ /* expiration= */ 0,
+ /* timeout= */ NULL,
+ /* policyTicket= */ NULL);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to add PolicySecret policy to TPM: %s",
+ sym_Tss2_RC_Decode(rc));
+
+ return tpm2_get_policy_digest(c, session, ret_policy_digest);
+}
+
int tpm2_calculate_policy_authorize_nv(
const TPM2B_NV_PUBLIC *public_info,
TPM2B_DIGEST *digest) {
@@ -8345,6 +8798,39 @@ static int nvpcr_data_load(const char *name, NvPCRData *ret) {
return 0;
}
+int tpm2_nvpcr_all_names(char ***ret_names) {
+ int r;
+
+ _cleanup_strv_free_ char **paths = NULL;
+ r = conf_files_list_nulstr(&paths, ".nvpcr", /* root= */ NULL, CONF_FILES_WARN, CONF_PATHS_NULSTR("nvpcr"));
+ if (r < 0)
+ return r;
+
+ _cleanup_strv_free_ char **names = NULL;
+ STRV_FOREACH(p, paths) {
+ _cleanup_free_ char *fname = NULL;
+ r = path_extract_filename(*p, &fname);
+ if (r < 0)
+ return r;
+
+ char *e = endswith(fname, ".nvpcr");
+ if (!e)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unexpected nvpcr config file name '%s'", fname);
+ *e = 0;
+
+ if (!tpm2_nvpcr_name_is_valid(fname))
+ continue;
+
+ r = strv_consume(&names, TAKE_PTR(fname));
+ if (r < 0)
+ return r;
+ }
+
+ if (ret_names)
+ *ret_names = TAKE_PTR(names);
+ return 0;
+}
+
int tpm2_nvpcr_get_index(const char *name, uint32_t *ret_nv_index, uint64_t *ret_priority) {
int r;
@@ -9763,6 +10249,843 @@ int tpm2_hmac_key_from_pin(Tpm2Context *c, const Tpm2Handle *session, const TPM2
return 0;
}
+
+static int tpm2_unmarshal_attestation(const void *data, size_t size, TPMS_ATTEST *ret) {
+ size_t offset = 0;
+ TPMS_ATTEST attest = {};
+ TSS2_RC rc;
+ int r;
+
+ assert(data);
+ assert(ret);
+
+ r = dlopen_tpm2(LOG_DEBUG);
+ if (r < 0)
+ return r;
+
+ rc = sym_Tss2_MU_TPMS_ATTEST_Unmarshal(data, size, &offset, &attest);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to unmarshal attestation structure: %s", sym_Tss2_RC_Decode(rc));
+ if (offset != size)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Garbage at end of attestation structure data.");
+
+ *ret = attest;
+ return 0;
+}
+
+/* Convert a TPM signature to PEM. This only supports RSASSA, RSAPSS and ECDSA signatures. */
+int tpm2_tpmt_signature_to_pem(const TPMT_SIGNATURE *signature, char **ret) {
+#if HAVE_OPENSSL
+ int r;
+
+ assert(signature);
+ assert(ret);
+
+ const char *label = NULL;
+ _cleanup_free_ void *sig = NULL;
+ size_t sig_len = 0;
+ switch (signature->sigAlg) {
+ case TPM2_ALG_RSASSA:
+ label = "RSA SIGNATURE";
+ sig_len = signature->signature.rsassa.sig.size;
+ sig = memdup(signature->signature.rsassa.sig.buffer, sig_len);
+ if (!sig)
+ return log_oom_debug();
+ break;
+ case TPM2_ALG_RSAPSS:
+ label = "RSA SIGNATURE";
+ sig_len = signature->signature.rsapss.sig.size;
+ sig = memdup(signature->signature.rsapss.sig.buffer, sig_len);
+ if (!sig)
+ return log_oom_debug();
+ break;
+ case TPM2_ALG_ECDSA: {
+ label = "ECDSA SIGNATURE";
+ _cleanup_(BN_freep) BIGNUM *bn_r = sym_BN_bin2bn(
+ signature->signature.ecdsa.signatureR.buffer,
+ signature->signature.ecdsa.signatureR.size, NULL);
+ if (!bn_r)
+ return log_openssl_errors(LOG_DEBUG, "Failed to convert ECDSA signature r to BIGNUM");
+
+ _cleanup_(BN_freep) BIGNUM *bn_s = sym_BN_bin2bn(
+ signature->signature.ecdsa.signatureS.buffer,
+ signature->signature.ecdsa.signatureS.size, NULL);
+ if (!bn_s)
+ return log_openssl_errors(LOG_DEBUG, "Failed to convert ECDSA signature s to BIGNUM");
+
+ _cleanup_(ECDSA_SIG_freep) ECDSA_SIG *ecdsaSig = sym_ECDSA_SIG_new();
+ if (!ecdsaSig)
+ return log_oom_debug();
+
+ if (sym_ECDSA_SIG_set0(ecdsaSig, bn_r, bn_s) <= 0)
+ return log_openssl_errors(LOG_DEBUG, "Failed to set ECDSA signature");
+ TAKE_PTR(bn_r);
+ TAKE_PTR(bn_s);
+
+ /* We want to allocate our own buffer so we can have a common cleanup path for this
+ * and RSA signatures. */
+ r = sym_i2d_ECDSA_SIG(ecdsaSig, NULL);
+ if (r <= 0)
+ return log_openssl_errors(LOG_DEBUG, "Failed to determine ECDSA signature size");
+ sig_len = r;
+
+ sig = malloc(sig_len);
+ if (!sig)
+ return log_oom_debug();
+
+ unsigned char *p = sig;
+ r = sym_i2d_ECDSA_SIG(ecdsaSig, &p);
+ if (r <= 0)
+ return log_openssl_errors(LOG_DEBUG, "Failed to DER encode ECDSA signature");
+
+ break;
+ }
+ case TPM2_ALG_NULL:
+ *ret = NULL;
+ return 0;
+ default:
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown signature algorithm id 0x%" PRIx16, signature->sigAlg);
+ }
+
+ _cleanup_(memstream_done) MemStream m = {};
+ FILE *f = memstream_init(&m);
+ if (!f)
+ return -ENOMEM;
+
+ if (sym_PEM_write(f, label, /* header= */ NULL, sig, sig_len) <= 0)
+ return log_openssl_errors(LOG_DEBUG, "Failed to write signature in PEM format");
+
+ return memstream_finalize(&m, ret, /* ret_size= */ NULL);
+#else
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled.");
+#endif
+}
+
+/* Convert a TPM public key to a SubjectPublicKeyInfo PEM sequence. */
+int tpm2_tpmt_public_to_pem(const TPMT_PUBLIC *public, char **ret) {
+#if HAVE_OPENSSL
+ int r;
+
+ assert(public);
+ assert(ret);
+
+ TPM2B_PUBLIC public2b = {
+ .size = sizeof(TPMT_PUBLIC),
+ .publicArea = *public,
+ };
+
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
+ r = tpm2_tpm2b_public_to_openssl_pkey(&public2b, &pkey);
+ if (r < 0)
+ return r;
+
+ return openssl_pubkey_to_pem(pkey, ret);
+#else
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled.");
+#endif
+}
+
+static const char* tpm2_hash_alg_to_string_tss2(TPMI_ALG_HASH alg) {
+ /* Note that this is different to tpm2_hash_alg_to_string because it
+ * returns a string that aligns with those produced by TSS2 and compatible
+ * with the TCG TSS2 JSON format. The JSON format is actually not case
+ * sensitive, but this is here just to be consistent with the signature
+ * algorithm. */
+ switch (alg) {
+ case TPM2_ALG_SHA1:
+ return "SHA1";
+ case TPM2_ALG_SHA256:
+ return "SHA256";
+ case TPM2_ALG_SHA384:
+ return "SHA384";
+ case TPM2_ALG_SHA512:
+ return "SHA512";
+ default:
+ log_debug("Unknown hash algorithm id 0x%" PRIx16, alg);
+ return NULL;
+ }
+}
+
+static const char* tpm2_sig_alg_to_string(TPMI_ALG_SIG_SCHEME alg) {
+ switch (alg) {
+ case TPM2_ALG_RSASSA:
+ return "RSASSA";
+ case TPM2_ALG_RSAPSS:
+ return "RSAPSS";
+ case TPM2_ALG_ECDSA:
+ return "ECDSA";
+ case TPM2_ALG_NULL:
+ return "NULL";
+ default:
+ log_debug("Unknown signature algorithm id 0x%" PRIx16, alg);
+ return NULL;
+ }
+}
+
+/* Convert a TPM signature to JSON, in a format compatible with the TCG TSS2 JSON data format,
+ * and which is compatible with the TSS2 implementation. This only supports RSASSA, RSAPSS and ECDSA
+ * signatures.
+ *
+ * See https://trustedcomputinggroup.org/wp-content/uploads/TSS_JSON_Policy_v0p7_r08_pub.pdf */
+int tpm2_tpmt_signature_to_json(const TPMT_SIGNATURE *signature, sd_json_variant **ret) {
+ int r = 0;
+
+ assert(signature);
+ assert(ret);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *siginnerj = NULL;
+ switch (signature->sigAlg) {
+ case TPM2_ALG_RSASSA:
+ r = sd_json_buildo(
+ &siginnerj,
+ SD_JSON_BUILD_PAIR_STRING("hash", tpm2_hash_alg_to_string_tss2(signature->signature.rsassa.hash)),
+ SD_JSON_BUILD_PAIR_HEX("sig", signature->signature.rsassa.sig.buffer, signature->signature.rsassa.sig.size));
+ break;
+ case TPM2_ALG_RSAPSS:
+ r = sd_json_buildo(
+ &siginnerj,
+ SD_JSON_BUILD_PAIR_STRING("hash", tpm2_hash_alg_to_string_tss2(signature->signature.rsapss.hash)),
+ SD_JSON_BUILD_PAIR_HEX("sig", signature->signature.rsapss.sig.buffer, signature->signature.rsapss.sig.size));
+ break;
+ case TPM2_ALG_ECDSA:
+ r = sd_json_buildo(
+ &siginnerj,
+ SD_JSON_BUILD_PAIR_STRING("hash", tpm2_hash_alg_to_string_tss2(signature->signature.ecdsa.hash)),
+ SD_JSON_BUILD_PAIR_HEX("signatureR", signature->signature.ecdsa.signatureR.buffer, signature->signature.ecdsa.signatureR.size),
+ SD_JSON_BUILD_PAIR_HEX("signatureS", signature->signature.ecdsa.signatureS.buffer, signature->signature.ecdsa.signatureS.size));
+ break;
+ case TPM2_ALG_NULL:
+ break;
+ default:
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown signature algorithm id 0x%" PRIx16, signature->sigAlg);
+ }
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *sigj = NULL;
+ r = sd_json_buildo(
+ &sigj,
+ SD_JSON_BUILD_PAIR_STRING("sigAlg", tpm2_sig_alg_to_string(signature->sigAlg)),
+ JSON_BUILD_PAIR_VARIANT_NON_NULL("signature", siginnerj));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(sigj);
+ return 0;
+}
+
+static const char* tpm2_st_attest_to_string(TPMI_ST_ATTEST attest) {
+ switch (attest) {
+ case TPM2_ST_ATTEST_NV:
+ return "ATTEST_NV";
+ case TPM2_ST_ATTEST_SESSION_AUDIT:
+ return "ATTEST_SESSION_AUDIT";
+ case TPM2_ST_ATTEST_QUOTE:
+ return "ATTEST_QUOTE";
+ default:
+ log_debug("Unknown attestation type 0x%" PRIx16, attest);
+ return NULL;
+ }
+}
+
+/* Convert a TPMS_CLOCK_INFO to JSON, in a format compatible with the TCG TSS2 JSON data format,
+ * and which is compatible with the TSS2 implementation.
+ *
+ * See https://trustedcomputinggroup.org/wp-content/uploads/TSS_JSON_Policy_v0p7_r08_pub.pdf */
+static int tpm2_tpms_clock_info_to_json(const TPMS_CLOCK_INFO *clock_info, sd_json_variant **ret) {
+ int r;
+
+ assert(clock_info);
+ assert(ret);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ r = sd_json_buildo(
+ &v,
+ /* tss2-fapi serializes a uint64_t that doesn't fit into a signed 54-bits as an
+ * array of 2 integers, but the deserialize path handles decoding this just fine. */
+ SD_JSON_BUILD_PAIR_UNSIGNED("clock", clock_info->clock),
+ SD_JSON_BUILD_PAIR_UNSIGNED("resetCount", clock_info->resetCount),
+ SD_JSON_BUILD_PAIR_UNSIGNED("restartCount", clock_info->restartCount),
+ /* tss2-fapi doesn't handle this being a boolean when deserializing. */
+ SD_JSON_BUILD_PAIR_STRING("safe", clock_info->safe == TPM2_YES ? "YES" : "NO"));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+/* Convert TPML_PCR_SELECTION to JSON, in a format compatible with the TCG TSS2 JSON data format,
+ * and which is compatible with the TSS2 implementation.
+ *
+ * See https://trustedcomputinggroup.org/wp-content/uploads/TSS_JSON_Policy_v0p7_r08_pub.pdf */
+static int tpm2_tpml_pcr_selection_to_json(const TPML_PCR_SELECTION *pcrs, sd_json_variant **ret) {
+ int r;
+
+ assert(pcrs);
+ assert(ret);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ FOREACH_TPMS_PCR_SELECTION_IN_TPML_PCR_SELECTION(s, pcrs) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *sv = NULL;
+ FOREACH_PCR_IN_TPMS_PCR_SELECTION(pcr, s) {
+ r = sd_json_variant_append_arrayb(&sv, SD_JSON_BUILD_INTEGER(pcr));
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_json_variant_append_arraybo(
+ &v,
+ /* As documented in the TCG TSS2 JSON data format spec, the sizeOfSelect
+ * field isn't represented in the encoding. */
+ SD_JSON_BUILD_PAIR_STRING("hash", tpm2_hash_alg_to_string_tss2(s->hash)),
+ SD_JSON_BUILD_PAIR_VARIANT("pcrSelect", sv));
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+/* Convert TPMS_ATTEST to JSON, in a format compatible with the TCG TSS2 JSON data format, and which is
+ * compatible with the TSS2 implementation.
+ *
+ * See https://trustedcomputinggroup.org/wp-content/uploads/TSS_JSON_Policy_v0p7_r08_pub.pdf */
+static int tpm2_tpms_attest_to_json(const TPMS_ATTEST *attest, sd_json_variant **ret) {
+ int r;
+
+ assert(attest);
+ assert(ret);
+
+ if (attest->magic != TPM2_GENERATED_VALUE)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid magic value %" PRIu32, attest->magic);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *cv = NULL;
+ r = tpm2_tpms_clock_info_to_json(&attest->clockInfo, &cv);
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *av = NULL;
+ switch (attest->type) {
+ case TPM2_ST_ATTEST_NV:
+ r = sd_json_buildo(
+ &av,
+ SD_JSON_BUILD_PAIR_HEX("indexName", attest->attested.nv.indexName.name, attest->attested.nv.indexName.size),
+ SD_JSON_BUILD_PAIR_UNSIGNED("offset", attest->attested.nv.offset),
+ SD_JSON_BUILD_PAIR_HEX("nvContents", attest->attested.nv.nvContents.buffer, attest->attested.nv.nvContents.size));
+ break;
+ case TPM2_ST_ATTEST_SESSION_AUDIT:
+ r = sd_json_buildo(
+ &av,
+ /* tss2-fapi doesn't handle this being a boolean when deserializing. */
+ SD_JSON_BUILD_PAIR_STRING("exclusiveSession", attest->attested.sessionAudit.exclusiveSession == TPM2_YES ? "YES" : "NO"),
+ SD_JSON_BUILD_PAIR_HEX("sessionDigest", attest->attested.sessionAudit.sessionDigest.buffer, attest->attested.sessionAudit.sessionDigest.size));
+ break;
+ case TPM2_ST_ATTEST_QUOTE: {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *sv = NULL;
+ r = tpm2_tpml_pcr_selection_to_json(&attest->attested.quote.pcrSelect, &sv);
+ if (r < 0)
+ return r;
+ r = sd_json_buildo(
+ &av,
+ SD_JSON_BUILD_PAIR_VARIANT("pcrSelect", sv),
+ SD_JSON_BUILD_PAIR_HEX("pcrDigest", attest->attested.quote.pcrDigest.buffer, attest->attested.quote.pcrDigest.size));
+ break;
+ }
+ default:
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown attestation type id 0x%" PRIx16, attest->type);
+ }
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ r = sd_json_buildo(
+ &v,
+ SD_JSON_BUILD_PAIR_STRING("magic", "VALUE"),
+ SD_JSON_BUILD_PAIR_STRING("type", tpm2_st_attest_to_string(attest->type)),
+ SD_JSON_BUILD_PAIR_HEX("qualifiedSigner", attest->qualifiedSigner.name, attest->qualifiedSigner.size),
+ SD_JSON_BUILD_PAIR_HEX("extraData", attest->extraData.buffer, attest->extraData.size),
+ SD_JSON_BUILD_PAIR_VARIANT("clockInfo", cv),
+ /* tss2-fapi serializes a uint64_t that doesn't fit into a signed 54-bits as an
+ * array of 2 integers, but the deserialize path handles decoding this just fine. */
+ SD_JSON_BUILD_PAIR_UNSIGNED("firmwareVersion", attest->firmwareVersion),
+ SD_JSON_BUILD_PAIR_VARIANT("attested", av));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+/* Convert a signature scheme to JSON, in a format compatible with the TCG TSS2 JSON data format,
+ * and which is compatible with the TSS2 implementation. This only supports RSASSA, RSAPSS and ECDSA
+ * signatures.
+ *
+ * See https://trustedcomputinggroup.org/wp-content/uploads/TSS_JSON_Policy_v0p7_r08_pub.pdf */
+static int tpm2_tpmt_sig_scheme_to_json(const TPMT_SIG_SCHEME *scheme, sd_json_variant **ret) {
+ int r;
+
+ assert(scheme);
+ assert(ret);
+
+ TPMI_ALG_SIG_SCHEME alg = scheme->scheme;
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *dv = NULL;
+ if (IN_SET(alg, TPM2_ALG_RSASSA, TPM2_ALG_RSAPSS, TPM2_ALG_ECDSA)) {
+ r = sd_json_buildo(
+ &dv,
+ SD_JSON_BUILD_PAIR_STRING("hashAlg", tpm2_hash_alg_to_string_tss2(scheme->details.any.hashAlg)));
+ if (r < 0)
+ return r;
+ } else if (alg != TPM2_ALG_NULL)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown signature algorithm id 0x%" PRIx16, alg);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ r = sd_json_buildo(
+ &v,
+ SD_JSON_BUILD_PAIR_STRING("scheme", tpm2_sig_alg_to_string(alg)),
+ JSON_BUILD_PAIR_VARIANT_NON_NULL("details", dv));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+/* Convert a signature scheme and attestation structure to JSON in a format that is compatible with the
+ * "QuoteInfo encoding" described in the TCG FAPI spec and which matches the TSS2 FAPI implementation. Note
+ * that this is generic - supporting also the result from NV_Certify and GetSessionAuditDigest. Note that
+ * only RSASSA, RSAPSS and ECDSA signature schemes are supported.
+ *
+ * See https://trustedcomputinggroup.org/wp-content/uploads/TSS_JSON_Policy_v0p7_r08_pub.pdf and
+ * https://trustedcomputinggroup.org/wp-content/uploads/TSS_FAPI_v0p94_r09_pub.pdf */
+int tpm2_attest_info_to_json(const TPMT_SIG_SCHEME *scheme, const TPMS_ATTEST *attest, sd_json_variant **ret) {
+ int r;
+
+ assert(attest);
+ assert(ret);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *sv = NULL;
+ r = tpm2_tpmt_sig_scheme_to_json(scheme, &sv);
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *av = NULL;
+ r = tpm2_tpms_attest_to_json(attest, &av);
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ r = sd_json_buildo(
+ &v,
+ SD_JSON_BUILD_PAIR_VARIANT("sig_scheme", sv),
+ SD_JSON_BUILD_PAIR_VARIANT("attest", av));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+static const char* tpm2_public_alg_to_string(TPMI_ALG_PUBLIC alg) {
+ switch (alg) {
+ case TPM2_ALG_RSA:
+ return "RSA";
+ case TPM2_ALG_ECC:
+ return "ECC";
+ case TPM2_ALG_SYMCIPHER:
+ return "SYMCIPHER";
+ case TPM2_ALG_KEYEDHASH:
+ return "KEYEDHASH";
+ default:
+ log_debug("Unknown public object type 0x%" PRIx16, alg);
+ return NULL;
+ }
+}
+
+static const char* tpm2_ecc_curve_to_string(TPMI_ECC_CURVE curve) {
+ switch (curve) {
+ case TPM2_ECC_NIST_P192:
+ return "NIST_P192";
+ case TPM2_ECC_NIST_P224:
+ return "NIST_P224";
+ case TPM2_ECC_NIST_P256:
+ return "NIST_P256";
+ case TPM2_ECC_NIST_P384:
+ return "NIST_P384";
+ case TPM2_ECC_NIST_P521:
+ return "NIST_P521";
+ case TPM2_ECC_BN_P256:
+ return "BN_P256";
+ case TPM2_ECC_BN_P638:
+ return "BN_P638";
+ case TPM2_ECC_SM2_P256:
+ return "SM2_P256";
+ default:
+ log_debug("Unknown ECC curve ID 0x%" PRIx16, curve);
+ return NULL;
+ }
+}
+
+/* Convert an asymmetric scheme to JSON, in a format compatible with the TCG TSS2 JSON data format,
+ * and which is compatible with the TSS2 implementation. This only supports the signature schemes RSASSA,
+ * RSAPSS and ECDSA.
+ *
+ * See https://trustedcomputinggroup.org/wp-content/uploads/TSS_JSON_Policy_v0p7_r08_pub.pdf */
+static int tpm2_tpmt_asym_scheme_to_json(const TPMT_ASYM_SCHEME *scheme, sd_json_variant **ret) {
+ int r;
+
+ assert(scheme);
+ assert(ret);
+
+ TPMI_ALG_ASYM_SCHEME alg = scheme->scheme;
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *dv = NULL;
+ if (IN_SET(alg, TPM2_ALG_RSASSA, TPM2_ALG_RSAPSS, TPM2_ALG_ECDSA)) {
+ r = sd_json_buildo(
+ &dv,
+ SD_JSON_BUILD_PAIR_STRING("hashAlg", tpm2_hash_alg_to_string_tss2(scheme->details.anySig.hashAlg)));
+ if (r < 0)
+ return r;
+ } else if (alg != TPM2_ALG_NULL)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown asymmetric scheme id 0x%" PRIx16, alg);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ r = sd_json_buildo(
+ &v,
+ SD_JSON_BUILD_PAIR_STRING("scheme", tpm2_sig_alg_to_string(alg)),
+ SD_JSON_BUILD_PAIR_VARIANT("details", dv));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+/* Convert a TPM public key to JSON, in a format compatible with the TCG TSS2 JSON data format, and which
+ * is compatible with the TSS2 implementation. This only supports restricted asymmetric signing keys with
+ * the signing scheme RSASSA, RSAPSS or ECDSA.
+ *
+ * See https://trustedcomputinggroup.org/wp-content/uploads/TSS_JSON_Policy_v0p7_r08_pub.pdf */
+int tpm2_tpmt_public_to_json(const TPMT_PUBLIC *public, sd_json_variant **ret) {
+ int r;
+
+ assert(public);
+ assert(ret);
+
+ if (!IN_SET(public->type, TPM2_ALG_RSA, TPM2_ALG_ECC))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unsupported object type 0x%" PRIx16, public->type);
+
+ /* We're currently only serializing restricted signing keys which never have a symmetric algorithm,
+ * so to avoid implementing the JSON encoding for this, just reject anything with a symmetric
+ * algorithm. */
+ if (public->parameters.asymDetail.symmetric.algorithm != TPM2_ALG_NULL)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unsupported object symmetric algorithm 0x%" PRIx16,
+ public->parameters.asymDetail.symmetric.algorithm);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *sv = NULL;
+ r = tpm2_tpmt_asym_scheme_to_json(&public->parameters.asymDetail.scheme, &sv);
+ if (r < 0)
+ return r;
+
+ /* Marshal the public parameters. */
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *pv = NULL;
+ switch (public->type) {
+ case TPM2_ALG_RSA:
+ r = sd_json_buildo(
+ &pv,
+ SD_JSON_BUILD_PAIR_OBJECT("symmetric",
+ SD_JSON_BUILD_PAIR_STRING("algorithm", "NULL")),
+ SD_JSON_BUILD_PAIR_VARIANT("scheme", sv),
+ SD_JSON_BUILD_PAIR_UNSIGNED("keyBits", public->parameters.rsaDetail.keyBits),
+ SD_JSON_BUILD_PAIR_UNSIGNED("exponent", public->parameters.rsaDetail.exponent));
+ break;
+ case TPM2_ALG_ECC:
+ /* No valid key has anything other than NULL here, so reject anything else to avoid
+ * having to implement the JSON encoding. */
+ if (public->parameters.eccDetail.kdf.scheme != TPM2_ALG_NULL)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unsupported ECC object KDF algorithm 0x%" PRIx16,
+ public->parameters.eccDetail.kdf.scheme);
+
+ r = sd_json_buildo(
+ &pv,
+ SD_JSON_BUILD_PAIR_OBJECT("symmetric",
+ SD_JSON_BUILD_PAIR_STRING("algorithm", "NULL")),
+ SD_JSON_BUILD_PAIR_VARIANT("scheme", sv),
+ SD_JSON_BUILD_PAIR_STRING("curveID", tpm2_ecc_curve_to_string(public->parameters.eccDetail.curveID)),
+ SD_JSON_BUILD_PAIR_OBJECT("kdf",
+ SD_JSON_BUILD_PAIR_STRING("scheme", "NULL")));
+ break;
+ default:
+ /* We've already checked the type. */
+ assert_not_reached();
+ }
+ if (r < 0)
+ return r;
+
+ /* Marshal the public key. */
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *uv = NULL;
+ switch (public->type) {
+ case TPM2_ALG_RSA:
+ r = sd_json_variant_new_hex(&uv, public->unique.rsa.buffer, public->unique.rsa.size);
+ break;
+ case TPM2_ALG_ECC:
+ r = sd_json_buildo(
+ &uv,
+ SD_JSON_BUILD_PAIR_HEX("x", public->unique.ecc.x.buffer, public->unique.ecc.x.size),
+ SD_JSON_BUILD_PAIR_HEX("y", public->unique.ecc.y.buffer, public->unique.ecc.y.size));
+ break;
+ default:
+ /* We've already checked the type. */
+ assert_not_reached();
+ }
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ r = sd_json_buildo(
+ &v,
+ SD_JSON_BUILD_PAIR_STRING("type", tpm2_public_alg_to_string(public->type)),
+ SD_JSON_BUILD_PAIR_STRING("nameAlg", tpm2_hash_alg_to_string_tss2(public->nameAlg)),
+ /* tss2-fapi serializes these as an object with pretty names, but it can deserialize
+ * an integer here. */
+ SD_JSON_BUILD_PAIR_INTEGER("objectAttributes", public->objectAttributes),
+ SD_JSON_BUILD_PAIR_HEX("authPolicy", public->authPolicy.buffer, public->authPolicy.size),
+ SD_JSON_BUILD_PAIR_VARIANT("parameters", pv),
+ SD_JSON_BUILD_PAIR_VARIANT("unique", uv));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+/* Convert a NV public area to JSON, in a format compatible with the TCG TSS2 JSON data format, and which
+ * is compatible with the TSS2 implementation.
+ *
+ * See https://trustedcomputinggroup.org/wp-content/uploads/TSS_JSON_Policy_v0p7_r08_pub.pdf */
+int tpm2_tpms_nv_public_to_json(const TPMS_NV_PUBLIC *nv_public, sd_json_variant **ret) {
+ int r;
+
+ assert(nv_public);
+ assert(ret);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ r = sd_json_buildo(
+ &v,
+ SD_JSON_BUILD_PAIR_INTEGER("nvIndex", nv_public->nvIndex),
+ SD_JSON_BUILD_PAIR_STRING("nameAlg", tpm2_hash_alg_to_string_tss2(nv_public->nameAlg)),
+ /* tss2-fapi serializes these as an object with pretty names, but it can deserialize
+ * an integer here. */
+ SD_JSON_BUILD_PAIR_INTEGER("attributes", nv_public->attributes),
+ SD_JSON_BUILD_PAIR_HEX("authPolicy", nv_public->authPolicy.buffer, nv_public->authPolicy.size),
+ SD_JSON_BUILD_PAIR_INTEGER("dataSize", nv_public->dataSize));
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+static const TPMT_SIG_SCHEME SIG_SCHEME_TEMPLATE_NULL = {
+ .scheme = TPM2_ALG_NULL,
+};
+
+int tpm2_quote(
+ Tpm2Context *c,
+ const Tpm2Handle *sign_session,
+ const Tpm2Handle *audit_session,
+ const Tpm2Handle *sign_key,
+ const TPM2B_DATA *qualifying_data,
+ const TPML_PCR_SELECTION *pcr_select,
+ TPMS_ATTEST **ret_quoted,
+ TPMT_SIGNATURE **ret_signature) {
+
+ TSS2_RC rc;
+ int r;
+
+ assert(c);
+ assert(pcr_select);
+
+ tpm2_log_debug_tpml_pcr_selection(pcr_select, "Fetching TPM PCR quote for selection");
+
+ if (audit_session && !tpm2_is_audit_session(c, audit_session))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Supplied session handle is not an audit session.");
+
+ _cleanup_(Esys_Freep) TPM2B_ATTEST *quoted = NULL;
+ _cleanup_(Esys_Freep) TPMT_SIGNATURE *signature = NULL;
+ rc = sym_Esys_Quote(
+ c->esys_context,
+ sign_key ? sign_key->esys_handle : ESYS_TR_NONE,
+ sign_session ? sign_session->esys_handle : ESYS_TR_PASSWORD,
+ audit_session ? audit_session->esys_handle : ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ qualifying_data,
+ &SIG_SCHEME_TEMPLATE_NULL,
+ pcr_select,
+ "ed,
+ ret_signature ? &signature : NULL);
+ if (rc == TPM2_RC_EXCLUSIVE)
+ return log_debug_errno(SYNTHETIC_ERRNO(EBUSY),
+ "Failed to obtain TPM quote: audit session required exclusivity");
+ if (rc != TSS2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to obtain TPM quote: %s", sym_Tss2_RC_Decode(rc));
+
+ if (ret_quoted) {
+ _cleanup_free_ TPMS_ATTEST *quoted_decoded = new0(TPMS_ATTEST, 1);
+ if (!quoted_decoded)
+ return log_oom_debug();
+ r = tpm2_unmarshal_attestation(quoted->attestationData, quoted->size, quoted_decoded);
+ if (r < 0)
+ return r;
+
+ *ret_quoted = TAKE_PTR(quoted_decoded);
+ }
+ if (ret_signature)
+ *ret_signature = TAKE_PTR(signature);
+
+ return 0;
+}
+
+int tpm2_nv_certify(
+ Tpm2Context *c,
+ const Tpm2Handle *sign_session,
+ const Tpm2Handle *auth_session,
+ const Tpm2Handle *audit_session,
+ const Tpm2Handle *sign_key,
+ const TPMS_NV_PUBLIC *nv_public,
+ const Tpm2Handle *nv_handle,
+ const TPM2B_DATA *qualifying_data,
+ TPMS_ATTEST **ret_certify_info,
+ TPMT_SIGNATURE **ret_signature) {
+
+ TSS2_RC rc;
+ int r;
+
+ assert(c);
+ assert(nv_public);
+ assert(nv_handle);
+
+ log_debug("Fetching TPM attestation for NV index 0x%" PRIx32 ".", nv_public->nvIndex);
+
+ if (audit_session && !tpm2_is_audit_session(c, audit_session))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Supplied session handle is not an audit session.");
+
+ TPM2_HANDLE nv_index;
+ r = tpm2_index_from_handle(c, nv_handle, &nv_index);
+ if (r == -EOPNOTSUPP)
+ nv_index = nv_public->nvIndex;
+ else if (r < 0)
+ return log_debug_errno(r, "Failed to get index from TPM handle");
+
+ if (nv_index != nv_public->nvIndex)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Inconsistent TPMS_NV_PUBLIC and Tpm2Handle arguments supplied");
+
+ _cleanup_(Esys_Freep) TPM2B_ATTEST *certify_info = NULL;
+ _cleanup_(Esys_Freep) TPMT_SIGNATURE *signature = NULL;
+ rc = sym_Esys_NV_Certify(
+ c->esys_context,
+ sign_key ? sign_key->esys_handle : ESYS_TR_NONE,
+ nv_handle->esys_handle,
+ nv_handle->esys_handle,
+ sign_session ? sign_session->esys_handle : ESYS_TR_PASSWORD,
+ auth_session ? auth_session->esys_handle : ESYS_TR_PASSWORD,
+ audit_session ? audit_session->esys_handle : ESYS_TR_NONE,
+ qualifying_data,
+ &SIG_SCHEME_TEMPLATE_NULL,
+ nv_public->dataSize, 0,
+ &certify_info,
+ ret_signature ? &signature : NULL);
+ if (rc == TPM2_RC_EXCLUSIVE)
+ return log_debug_errno(SYNTHETIC_ERRNO(EBUSY),
+ "Failed to obtain TPM attestation for NV index: audit session required exclusivity");
+ if (rc != TSS2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to obtain TPM attestation for NV index: %s", sym_Tss2_RC_Decode(rc));
+
+ if (ret_certify_info) {
+ _cleanup_free_ TPMS_ATTEST *certify_info_decoded = new0(TPMS_ATTEST, 1);
+ if (!certify_info_decoded)
+ return log_oom_debug();
+ r = tpm2_unmarshal_attestation(certify_info->attestationData, certify_info->size, certify_info_decoded);
+ if (r < 0)
+ return r;
+
+ *ret_certify_info = TAKE_PTR(certify_info_decoded);
+ }
+ if (ret_signature)
+ *ret_signature = TAKE_PTR(signature);
+
+ return 0;
+}
+
+int tpm2_get_session_audit_digest(
+ Tpm2Context *c,
+ const Tpm2Handle *eh_session,
+ const Tpm2Handle *sign_session,
+ const Tpm2Handle *audit_session,
+ const Tpm2Handle *sign_key,
+ const TPM2B_DATA *qualifying_data,
+ TPMS_ATTEST **ret_audit_info,
+ TPMT_SIGNATURE **ret_signature) {
+
+ TSS2_RC rc;
+ int r;
+
+ assert(c);
+ assert(audit_session);
+
+ log_debug("Fetching TPM attestation for audit session.");
+
+ _cleanup_(Esys_Freep) TPM2B_ATTEST *audit_info = NULL;
+ _cleanup_(Esys_Freep) TPMT_SIGNATURE *signature = NULL;
+ rc = sym_Esys_GetSessionAuditDigest(
+ c->esys_context,
+ ESYS_TR_RH_ENDORSEMENT,
+ sign_key ? sign_key->esys_handle : ESYS_TR_NONE,
+ audit_session->esys_handle,
+ eh_session ? eh_session->esys_handle : ESYS_TR_PASSWORD,
+ sign_session ? sign_session->esys_handle : ESYS_TR_PASSWORD,
+ ESYS_TR_NONE,
+ qualifying_data,
+ &SIG_SCHEME_TEMPLATE_NULL,
+ &audit_info,
+ ret_signature ? &signature : NULL);
+ if (rc != TSS2_RC_SUCCESS)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to obtain TPM attestation for audit session: %s", sym_Tss2_RC_Decode(rc));
+
+ if (ret_audit_info) {
+ _cleanup_free_ TPMS_ATTEST *audit_info_decoded = new0(TPMS_ATTEST, 1);
+ if (!audit_info_decoded)
+ return log_oom_debug();
+ r = tpm2_unmarshal_attestation(audit_info->attestationData, audit_info->size, audit_info_decoded);
+ if (r < 0)
+ return r;
+
+ *ret_audit_info = TAKE_PTR(audit_info_decoded);
+ }
+ if (ret_signature)
+ *ret_signature = TAKE_PTR(signature);
+
+ return 0;
+}
+
#endif
int tpm2_argon2id_derive_split(
diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h
index b2f89f3a5d..3723281f42 100644
--- a/src/shared/tpm2-util.h
+++ b/src/shared/tpm2-util.h
@@ -68,6 +68,8 @@ int dlopen_tpm2(int log_level) _dlopen_loader_;
#include /* IWYU pragma: export */
#include /* IWYU pragma: export */
+#include "dlfcn-util.h"
+
typedef struct Tpm2Context {
unsigned n_ref;
@@ -102,7 +104,10 @@ typedef struct Tpm2Handle {
#define _tpm2_handle(c, h) { .tpm2_context = (c), .esys_handle = (h), }
static const Tpm2Handle TPM2_HANDLE_NONE = _tpm2_handle(NULL, ESYS_TR_NONE);
+static const Tpm2Handle TPM2_HANDLE_RH_OWNER = _tpm2_handle(NULL, ESYS_TR_RH_OWNER);
+static const Tpm2Handle TPM2_HANDLE_RH_ENDORSEMENT = _tpm2_handle(NULL, ESYS_TR_RH_ENDORSEMENT);
+extern DLSYM_PROTOTYPE(Esys_Free);
void Esys_Freep(void *p);
int tpm2_handle_new(Tpm2Context *context, Tpm2Handle **ret_handle);
@@ -136,11 +141,15 @@ int tpm2_pcr_values_hash_count(const Tpm2PCRValue *pcr_values, size_t n_pcr_valu
int tpm2_tpml_pcr_selection_from_pcr_values(const Tpm2PCRValue *pcr_values, size_t n_pcr_values, TPML_PCR_SELECTION *ret_selection, TPM2B_DIGEST **ret_values, size_t *ret_n_values);
int tpm2_make_encryption_session(Tpm2Context *c, const Tpm2Handle *primary, const Tpm2Handle *bind_key, Tpm2Handle **ret_session);
+int tpm2_make_exclusive_audit_session(Tpm2Context *c, Tpm2Handle **ret_session);
int tpm2_create_primary(Tpm2Context *c, const Tpm2Handle *session, ESYS_TR hierarchy, const TPM2B_PUBLIC *template, const TPM2B_SENSITIVE_CREATE *sensitive, TPM2B_PUBLIC **ret_public, Tpm2Handle **ret_handle);
int tpm2_create(Tpm2Context *c, const Tpm2Handle *parent, const Tpm2Handle *session, const TPMT_PUBLIC *template, const TPMS_SENSITIVE_CREATE *sensitive, TPM2B_PUBLIC **ret_public, TPM2B_PRIVATE **ret_private);
int tpm2_load(Tpm2Context *c, const Tpm2Handle *parent, const Tpm2Handle *session, const TPM2B_PUBLIC *public, const TPM2B_PRIVATE *private, Tpm2Handle **ret_handle);
+int tpm2_marshal_private(const TPM2B_PRIVATE *private, void **ret, size_t *ret_size);
+int tpm2_unmarshal_private(const void *data, size_t size, TPM2B_PRIVATE *ret_private);
int tpm2_marshal_public(const TPM2B_PUBLIC *public, void **ret, size_t *ret_size);
+int tpm2_unmarshal_public(const void *data, size_t size, TPM2B_PUBLIC *ret_public);
int tpm2_marshal_nv_public(const TPM2B_NV_PUBLIC *nv_public, void **ret, size_t *ret_size);
int tpm2_unmarshal_nv_public(const void *data, size_t size, TPM2B_NV_PUBLIC *ret_nv_public);
int tpm2_marshal_blob(const TPM2B_PUBLIC *public, const TPM2B_PRIVATE *private, const TPM2B_ENCRYPTED_SECRET *seed, void **ret_blob, size_t *ret_blob_size);
@@ -150,6 +159,7 @@ int tpm2_get_name(Tpm2Context *c, const Tpm2Handle *handle, TPM2B_NAME **ret_nam
bool tpm2_supports_alg(Tpm2Context *c, TPM2_ALG_ID alg);
bool tpm2_supports_command(Tpm2Context *c, TPM2_CC command);
bool tpm2_supports_ecc_curve(Tpm2Context *c, TPM2_ECC_CURVE ecc_curve);
+int tpm2_max_data_size(Tpm2Context *c);
bool tpm2_test_parms(Tpm2Context *c, TPMI_ALG_PUBLIC alg, const TPMU_PUBLIC_PARMS *parms);
@@ -187,6 +197,7 @@ int tpm2_pcr_extend_bytes(Tpm2Context *c, char **banks, unsigned pcr_index, cons
* important and are allocated first when the TPM's NV index space is constrained. */
#define TPM2_NVPCR_PRIORITY_DEFAULT UINT64_C(1000)
+int tpm2_nvpcr_all_names(char ***ret_names);
int tpm2_nvpcr_get_index(const char *name, uint32_t *ret_nv_index, uint64_t *ret_priority);
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_initialize(Tpm2Context *c, const Tpm2Handle *session, const char *name);
@@ -216,6 +227,9 @@ char* tpm2_tpml_pcr_selection_to_string(const TPML_PCR_SELECTION *l);
size_t tpm2_tpml_pcr_selection_weight(const TPML_PCR_SELECTION *l);
#define tpm2_tpml_pcr_selection_is_empty(l) (tpm2_tpml_pcr_selection_weight(l) == 0)
+int tpm2_digest_buf_to_data(TPMI_ALG_HASH alg, void *digest, size_t digest_sz, TPM2B_DATA *ret);
+int tpm2_digest_iovec_to_data(TPMI_ALG_HASH alg, const struct iovec *digest, TPM2B_DATA *ret);
+
int tpm2_digest_many(TPMI_ALG_HASH alg, TPM2B_DIGEST *digest, const struct iovec data[], size_t n_data, bool extend);
static inline int tpm2_digest_buffer(TPMI_ALG_HASH alg, TPM2B_DIGEST *digest, const void *data, size_t len, bool extend) {
return tpm2_digest_many(alg, digest, &IOVEC_MAKE((void*) data, len), 1, extend);
@@ -300,11 +314,18 @@ int tpm2_pcrlock_search_file(const char *path, FILE **ret_file, char **ret_path)
int tpm2_pcrlock_policy_load(const char *path, Tpm2PCRLockPolicy *ret_policy);
int tpm2_pcrlock_policy_from_credentials(const struct iovec *srk, const struct iovec *nv, Tpm2PCRLockPolicy *ret);
+int tpm2_read_public(Tpm2Context *c, const Tpm2Handle *session, const Tpm2Handle *handle, TPM2B_PUBLIC **ret_public, TPM2B_NAME **ret_name, TPM2B_NAME **ret_qname);
+
int tpm2_index_to_handle(Tpm2Context *c, TPM2_HANDLE index, const Tpm2Handle *session, TPM2B_NAME **ret_name, Tpm2Handle **ret_handle);
int tpm2_object_index_to_handle(Tpm2Context *c, TPM2_HANDLE index, const Tpm2Handle *session, TPM2B_PUBLIC **ret_public, TPM2B_NAME **ret_name, TPM2B_NAME **ret_qname, Tpm2Handle **ret_handle);
int tpm2_nv_index_to_handle(Tpm2Context *c, TPM2_HANDLE index, const Tpm2Handle *session, TPM2B_NV_PUBLIC **ret_nv_public, TPM2B_NAME **ret_name, Tpm2Handle **ret_handle);
int tpm2_index_from_handle(Tpm2Context *c, const Tpm2Handle *handle, TPM2_HANDLE *ret_index);
+int tpm2_marshal_saved_handle_context(TPMS_CONTEXT *context, void **ret, size_t *ret_size);
+int tpm2_unmarshal_saved_handle_context(const void *data, size_t size, TPMS_CONTEXT *ret);
+int tpm2_load_saved_handle_context(Tpm2Context *c, const TPMS_CONTEXT *context, TPM2B_NAME **ret_name, Tpm2Handle **ret_handle);
+int tpm2_save_handle_context(Tpm2Context *c, const Tpm2Handle *handle, TPMS_CONTEXT **ret_context);
+
int tpm2_pcr_read(Tpm2Context *c, const TPML_PCR_SELECTION *pcr_selection, Tpm2PCRValue **ret_pcr_values, size_t *ret_n_pcr_values);
int tpm2_pcr_read_missing_values(Tpm2Context *c, Tpm2PCRValue *pcr_values, size_t n_pcr_values);
@@ -321,6 +342,7 @@ int tpm2_policy_pcr(Tpm2Context *c, const Tpm2Handle *session, const TPML_PCR_SE
int tpm2_policy_or(Tpm2Context *c, const Tpm2Handle *session, const TPM2B_DIGEST *branches, size_t n_branches, TPM2B_DIGEST **ret_policy_digest);
int tpm2_policy_super_pcr(Tpm2Context *c, const Tpm2Handle *session, const Tpm2PCRPrediction *prediction, uint16_t algorithm);
int tpm2_policy_signed_hmac_sha256(Tpm2Context *c, const Tpm2Handle *session, const Tpm2Handle *hmac_key_handle, const struct iovec *hmac_key, TPM2B_DIGEST **ret_policy_digest);
+int tpm2_policy_secret(Tpm2Context *c, const Tpm2Handle *auth_session, const Tpm2Handle *session, const Tpm2Handle *handle, const TPM2B_NONCE *policy_ref, TPM2B_DIGEST **ret_policy_digest);
int tpm2_calculate_pubkey_name(const TPMT_PUBLIC *public, TPM2B_NAME *ret_name);
int tpm2_calculate_nv_index_name(const TPMS_NV_PUBLIC *nvpublic, TPM2B_NAME *ret_name);
@@ -339,6 +361,8 @@ int tpm2_calculate_seal(TPM2_HANDLE parent_handle, const TPM2B_PUBLIC *parent_pu
int tpm2_make_policy_authorize_tbs_data(const TPM2B_DIGEST *approved_digest, const char *policy_ref_data, struct iovec *ret_tbs_data);
+int tpm2_get_best_attestation_key_template(Tpm2Context *c, TPMT_PUBLIC *ret);
+
int tpm2_get_srk_template(TPMI_ALG_PUBLIC alg, TPMT_PUBLIC *ret_template);
int tpm2_get_best_srk_template(Tpm2Context *c, TPMT_PUBLIC *ret_template);
@@ -363,6 +387,8 @@ int tpm2_get_ek_template(Tpm2Context *c, const Tpm2Handle *session, Tpm2EKTempla
int tpm2_get_ek(Tpm2Context *c, const Tpm2Handle *session, TPM2B_PUBLIC **ret_public, TPM2B_NAME **ret_name, TPM2B_NAME **ret_qname, Tpm2Handle **ret_handle);
int tpm2_get_or_create_ek(Tpm2Context *c, const Tpm2Handle *session, TPM2B_PUBLIC **ret_public, TPM2B_NAME **ret_name, TPM2B_NAME **ret_qname, Tpm2Handle **ret_handle);
+int tpm2_open_ek_user_policy_session(Tpm2Context *c, const Tpm2Handle *session, const Tpm2Handle *ek_handle, const Tpm2Handle *tpm_key, Tpm2Handle **ret_session);
+
int tpm2_seal(Tpm2Context *c, uint32_t seal_key_handle, const TPM2B_DIGEST policy_hash[], size_t n_policy, const char *pin, struct iovec *ret_secret, struct iovec **ret_blobs, size_t *ret_n_blobs, uint16_t *ret_primary_alg, struct iovec *ret_srk);
int tpm2_unseal(Tpm2Context *c, uint32_t hash_pcr_mask, uint16_t pcr_bank, const struct iovec *pubkey, const char *pubkey_policy_ref, uint32_t pubkey_pcr_mask, sd_json_variant *signature, const char *pin, const Tpm2PCRLockPolicy *pcrlock_policy, uint16_t primary_alg, const struct iovec blobs[], size_t n_blobs, const struct iovec known_policy_hash[], size_t n_known_policy_hash, const struct iovec *srk, struct iovec *ret_secret);
@@ -394,6 +420,18 @@ int tpm2_load_public_key_file(const char *path, TPM2B_PUBLIC *ret);
int tpm2_hmac_key_from_pin(Tpm2Context *c, const Tpm2Handle *session, const TPM2B_AUTH *pin, Tpm2Handle **ret);
+int tpm2_tpmt_signature_to_pem(const TPMT_SIGNATURE *signature, char **ret);
+int tpm2_tpmt_public_to_pem(const TPMT_PUBLIC *public, char **ret);
+
+int tpm2_tpmt_signature_to_json(const TPMT_SIGNATURE *signature, sd_json_variant **ret);
+int tpm2_attest_info_to_json(const TPMT_SIG_SCHEME *scheme, const TPMS_ATTEST *attest, sd_json_variant **ret);
+int tpm2_tpmt_public_to_json(const TPMT_PUBLIC *public, sd_json_variant **ret);
+int tpm2_tpms_nv_public_to_json(const TPMS_NV_PUBLIC *nv_public, sd_json_variant **ret);
+
+int tpm2_quote(Tpm2Context *c, const Tpm2Handle *sign_session, const Tpm2Handle *audit_session, const Tpm2Handle *sign_key, const TPM2B_DATA *qualifying_data, const TPML_PCR_SELECTION *pcr_select, TPMS_ATTEST **ret_quoted, TPMT_SIGNATURE **ret_signature);
+int tpm2_nv_certify(Tpm2Context *c, const Tpm2Handle *sign_session, const Tpm2Handle *auth_session, const Tpm2Handle *audit_session, const Tpm2Handle *sign_key, const TPMS_NV_PUBLIC *nv_public, const Tpm2Handle *nv_handle, const TPM2B_DATA *qualifying_data, TPMS_ATTEST **ret_certify_info, TPMT_SIGNATURE **ret_signature);
+int tpm2_get_session_audit_digest(Tpm2Context *c, const Tpm2Handle *eh_session, const Tpm2Handle *sign_session, const Tpm2Handle *audit_session, const Tpm2Handle *sign_key, const TPM2B_DATA *qualifying_data, TPMS_ATTEST **ret_audit_info, TPMT_SIGNATURE **ret_signature);
+
/* The tpm2-tss library has many structs that are simply a combination of an array (or object) and
* size. These macros allow easily initializing or assigning instances of such structs from an existing
* buffer/object and size, while also checking the size for safety with the struct buffer/object size. If the
@@ -406,6 +444,7 @@ int tpm2_hmac_key_from_pin(Tpm2Context *c, const Tpm2Handle *session, const TPM2
#define TPM2B_ECC_PARAMETER_MAKE(b, s) TPM2B_BUF_SIZE_STRUCT_MAKE(b, s, TPM2B_ECC_PARAMETER, buffer, size)
#define TPM2B_ENCRYPTED_SECRET_MAKE(b, s) TPM2B_BUF_SIZE_STRUCT_MAKE(b, s, TPM2B_ENCRYPTED_SECRET, secret, size)
#define TPM2B_MAX_BUFFER_MAKE(b, s) TPM2B_BUF_SIZE_STRUCT_MAKE(b, s, TPM2B_MAX_BUFFER, buffer, size)
+#define TPM2B_MAX_NV_BUFFER_MAKE(b, s) TPM2B_BUF_SIZE_STRUCT_MAKE(b, s, TPM2B_MAX_NV_BUFFER, buffer, size)
#define TPM2B_NAME_MAKE(b, s) TPM2B_BUF_SIZE_STRUCT_MAKE(b, s, TPM2B_NAME, name, size)
#define TPM2B_NONCE_MAKE(b, s) TPM2B_BUF_SIZE_STRUCT_MAKE(b, s, TPM2B_NONCE, buffer, size)
#define TPM2B_PRIVATE_MAKE(b, s) TPM2B_BUF_SIZE_STRUCT_MAKE(b, s, TPM2B_PRIVATE, buffer, size)
@@ -433,6 +472,7 @@ int tpm2_hmac_key_from_pin(Tpm2Context *c, const Tpm2Handle *session, const TPM2
#define TPM2B_ECC_PARAMETER_CHECK_SIZE(s) TPM2B_BUF_SIZE_STRUCT_CHECK_SIZE(s, TPM2B_ECC_PARAMETER, buffer)
#define TPM2B_ENCRYPTED_SECRET_CHECK_SIZE(s) TPM2B_BUF_SIZE_STRUCT_CHECK_SIZE(s, TPM2B_ENCRYPTED_SECRET, buffer)
#define TPM2B_MAX_BUFFER_CHECK_SIZE(s) TPM2B_BUF_SIZE_STRUCT_CHECK_SIZE(s, TPM2B_MAX_BUFFER, buffer)
+#define TPM2B_MAX_NV_BUFFER_CHECK_SIZE(s) TPM2B_BUF_SIZE_STRUCT_CHECK_SIZE(s, TPM2B_MAX_NV_BUFFER, buffer)
#define TPM2B_NAME_CHECK_SIZE(s) TPM2B_BUF_SIZE_STRUCT_CHECK_SIZE(s, TPM2B_NAME, name)
#define TPM2B_NONCE_CHECK_SIZE(s) TPM2B_BUF_SIZE_STRUCT_CHECK_SIZE(s, TPM2B_NONCE, buffer)
#define TPM2B_PRIVATE_CHECK_SIZE(s) TPM2B_BUF_SIZE_STRUCT_CHECK_SIZE(s, TPM2B_PRIVATE, buffer)
diff --git a/src/test/test-tpm2.c b/src/test/test-tpm2.c
index 6fc94d4900..7ebcb0e30e 100644
--- a/src/test/test-tpm2.c
+++ b/src/test/test-tpm2.c
@@ -1,5 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#include "sd-json.h"
+
#include "crypto-util.h"
#include "hexdecoct.h"
#include "iovec-util.h"
@@ -1703,6 +1705,650 @@ static void check_get_or_create_ek(Tpm2Context *c) {
ASSERT_EQ(memcmp_nn(qname->name, qname->size, qname2->name, qname2->size), 0);
}
+static void check_max_data_size(Tpm2Context *c) {
+ int r;
+
+ assert(c);
+
+ TEST_LOG_FUNC();
+
+ r = tpm2_max_data_size(c);
+ ASSERT_OK_POSITIVE(r);
+ ASSERT_TRUE(IN_SET(r, 22, 34, 50, 66));
+}
+
+TEST(tpm2_digest_to_data) {
+ DEFINE_HEX_PTR(h, "b48a7bdf4214ed87d617690ff108e0089939a6d6754b2b6be324e2bfb2bbc54a");
+ DEFINE_HEX_PTR(expected, "000bb48a7bdf4214ed87d617690ff108e0089939a6d6754b2b6be324e2bfb2bbc54a");
+
+ TPM2B_DATA d;
+ ASSERT_OK(tpm2_digest_buf_to_data(TPM2_ALG_SHA256, h, h_len, &d));
+ ASSERT_EQ(memcmp_nn(d.buffer, d.size, expected, expected_len), 0);
+
+ memset(&d, 0, sizeof(d));
+ ASSERT_OK(tpm2_digest_iovec_to_data(TPM2_ALG_SHA256, &IOVEC_MAKE(h, h_len), &d));
+ ASSERT_EQ(memcmp_nn(d.buffer, d.size, expected, expected_len), 0);
+}
+
+static void check_context_saving(Tpm2Context *c) {
+ assert(c);
+
+ TEST_LOG_FUNC();
+
+ TPM2B_PUBLIC template = { .size = sizeof(TPMT_PUBLIC), };
+ ASSERT_OK(tpm2_get_srk_template(TPM2_ALG_ECC, &template.publicArea));
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+ ASSERT_OK(tpm2_create_primary(c, NULL, ESYS_TR_RH_OWNER, &template, NULL, NULL, &handle));
+
+ _cleanup_(Esys_Freep) TPMS_CONTEXT *context = NULL;
+ ASSERT_OK(tpm2_save_handle_context(c, handle, &context));
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle2 = NULL;
+ ASSERT_OK(tpm2_load_saved_handle_context(c, context, NULL, &handle2));
+
+ _cleanup_(Esys_Freep) TPM2B_NAME *name1 = NULL, *name2 = NULL;
+ ASSERT_OK(tpm2_get_name(c, handle, &name1));
+ ASSERT_OK(tpm2_get_name(c, handle2, &name2));
+
+ ASSERT_EQ(memcmp_nn(name1->name, name1->size, name2->name, name2->size), 0);
+}
+
+static void check_saved_context_marshaling(Tpm2Context *c) {
+ assert(c);
+
+ TEST_LOG_FUNC();
+
+ TPM2B_PUBLIC template = { .size = sizeof(TPMT_PUBLIC), };
+ ASSERT_OK(tpm2_get_srk_template(TPM2_ALG_ECC, &template.publicArea));
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+ ASSERT_OK(tpm2_create_primary(c, NULL, ESYS_TR_RH_OWNER, &template, NULL, NULL, &handle));
+
+ _cleanup_(Esys_Freep) TPMS_CONTEXT *context = NULL;
+ ASSERT_OK(tpm2_save_handle_context(c, handle, &context));
+
+ _cleanup_free_ void *buf = NULL;
+ size_t sz;
+ ASSERT_OK(tpm2_marshal_saved_handle_context(context, &buf, &sz));
+
+ TPMS_CONTEXT context2;
+ ASSERT_OK(tpm2_unmarshal_saved_handle_context(buf, sz, &context2));
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle2 = NULL;
+ ASSERT_OK(tpm2_load_saved_handle_context(c, &context2, NULL, &handle2));
+
+ _cleanup_(Esys_Freep) TPM2B_NAME *name1 = NULL, *name2 = NULL;
+ ASSERT_OK(tpm2_get_name(c, handle, &name1));
+ ASSERT_OK(tpm2_get_name(c, handle2, &name2));
+
+ ASSERT_EQ(memcmp_nn(name1->name, name1->size, name2->name, name2->size), 0);
+}
+
+static void check_policy_secret(Tpm2Context *c) {
+ assert(c);
+
+ TEST_LOG_FUNC();
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *session = NULL;
+ ASSERT_OK(tpm2_make_policy_session(c, NULL, NULL, &session));
+
+ _cleanup_(Esys_Freep) TPM2B_DIGEST *digest = NULL;
+ ASSERT_OK(tpm2_policy_secret(c, NULL, session, &TPM2_HANDLE_RH_ENDORSEMENT, NULL, &digest));
+ ASSERT_TRUE(digest_check(digest, "837197674484b3f81a90cc8d46a5d724fd52d76e06520b64f2a1da1b331469aa"));
+
+ session = tpm2_handle_free(session);
+ ASSERT_OK(tpm2_make_policy_session(c, NULL, NULL, &session));
+
+ const char *s = "foo";
+
+ _cleanup_(Esys_Freep) TPM2B_DIGEST *digest2 = NULL;
+ TPM2B_NONCE ref = TPM2B_NONCE_MAKE(s, strlen(s));
+ ASSERT_OK(tpm2_policy_secret(c, NULL, session, &TPM2_HANDLE_RH_OWNER, &ref, &digest2));
+ ASSERT_TRUE(digest_check(digest2, "62fd94980db2a746545cab626e9df21a1d0f00472f637d4bf567026e40a6ebed"));
+}
+
+static void check_best_attestation_key_template(Tpm2Context *c) {
+ assert(c);
+
+ TEST_LOG_FUNC();
+
+ TPMT_PUBLIC template;
+ ASSERT_OK(tpm2_get_best_attestation_key_template(c, &template));
+
+ ASSERT_TRUE(IN_SET(template.type, TPM2_ALG_RSA, TPM2_ALG_ECC));
+ ASSERT_TRUE(IN_SET(template.nameAlg, TPM2_ALG_SHA256, TPM2_ALG_SHA384));
+ ASSERT_EQ(template.objectAttributes, TPMA_OBJECT_FIXEDTPM | TPMA_OBJECT_FIXEDPARENT | TPMA_OBJECT_SENSITIVEDATAORIGIN | TPMA_OBJECT_USERWITHAUTH | TPMA_OBJECT_ADMINWITHPOLICY | TPMA_OBJECT_RESTRICTED | TPMA_OBJECT_SIGN_ENCRYPT);
+ ASSERT_EQ(template.parameters.asymDetail.symmetric.algorithm, TPM2_ALG_NULL);
+ ASSERT_NE(template.parameters.asymDetail.scheme.scheme, TPM2_ALG_NULL);
+ ASSERT_TRUE(IN_SET(template.parameters.asymDetail.scheme.details.anySig.hashAlg, TPM2_ALG_SHA256, TPM2_ALG_SHA384));
+
+ if (template.type == TPM2_ALG_RSA) {
+ ASSERT_TRUE(IN_SET(template.parameters.rsaDetail.scheme.scheme, TPM2_ALG_RSASSA, TPM2_ALG_RSAPSS));
+ ASSERT_TRUE(IN_SET(template.parameters.rsaDetail.keyBits, 2048, 3072));
+ } else {
+ ASSERT_EQ(template.parameters.eccDetail.scheme.scheme, TPM2_ALG_ECDSA);
+ ASSERT_TRUE(IN_SET(template.parameters.eccDetail.curveID, TPM2_ECC_NIST_P256, TPM2_ECC_NIST_P384));
+ }
+}
+
+TEST(tpm2_tpmt_signature_to_pem) {
+ DEFINE_HEX_PTR(rsa,
+ "dc3d4338b3a20f081f2204c54c0ddfd633fba8e49c2029d6612d1412e3204344fc64ee4b9ce9f4d37d0efe2637291643bbedea8e3af30f9396db7c33d02f7cf6"
+ "ab8d2a668b6610092507370b9eebf4f044e248b89af6474085250d47de3cb40825f49d85a829df7915e6956bb48ad92a5765423698a13ee847c0edc1a25675bf"
+ "e058c04992bc662c8734fc8a4dfc8276593cf3538e123a06e41b74ee012f7f7bca5d672cf3183d08e17eae0f9798455682902122bbbee3d76c89a851485c3461"
+ "1158ec1a3abfc088753896d6739758ac99548df1b53ed0552789c5a007d75917aa680f2610117479544d13ae9234f14349d37a833999a43c9db4b16c26d0fb31");
+
+ TPMT_SIGNATURE rsa_sig = {
+ .sigAlg = TPM2_ALG_RSAPSS,
+ .signature.rsapss = {
+ .hash = TPM2_ALG_SHA256,
+ .sig = TPM2B_PUBLIC_KEY_RSA_MAKE(rsa, rsa_len),
+ },
+ };
+
+ _cleanup_free_ char *rsa_pem = NULL;
+ ASSERT_OK(tpm2_tpmt_signature_to_pem(&rsa_sig, &rsa_pem));
+ ASSERT_STREQ(rsa_pem,
+ "-----BEGIN RSA SIGNATURE-----\n"
+ "3D1DOLOiDwgfIgTFTA3f1jP7qOScICnWYS0UEuMgQ0T8ZO5LnOn0030O/iY3KRZD\n"
+ "u+3qjjrzD5OW23wz0C989quNKmaLZhAJJQc3C57r9PBE4ki4mvZHQIUlDUfePLQI\n"
+ "JfSdhagp33kV5pVrtIrZKldlQjaYoT7oR8DtwaJWdb/gWMBJkrxmLIc0/IpN/IJ2\n"
+ "WTzzU44SOgbkG3TuAS9/e8pdZyzzGD0I4X6uD5eYRVaCkCEiu77j12yJqFFIXDRh\n"
+ "EVjsGjq/wIh1OJbWc5dYrJlUjfG1PtBVJ4nFoAfXWReqaA8mEBF0eVRNE66SNPFD\n"
+ "SdN6gzmZpDydtLFsJtD7MQ==\n"
+ "-----END RSA SIGNATURE-----\n");
+
+ DEFINE_HEX_PTR(ecc_r, "d9eb686422a6fb9a64a5cf9806495d7e787f11b77f5f5928680c02558a2467ec526f04a9745dc4f196248dd2198a17d4");
+ DEFINE_HEX_PTR(ecc_s, "8e9f92622c4cd4c00ae4c551feecbc4e0cc5b321e023acf6f8b67f9075ecac5c9cea3cd1b6d76055a46c20ecd080d2cf");
+
+ TPMT_SIGNATURE ecc_sig = {
+ .sigAlg = TPM2_ALG_ECDSA,
+ .signature.ecdsa = {
+ .hash = TPM2_ALG_SHA384,
+ .signatureR = TPM2B_ECC_PARAMETER_MAKE(ecc_r, ecc_r_len),
+ .signatureS = TPM2B_ECC_PARAMETER_MAKE(ecc_s, ecc_s_len),
+ },
+ };
+
+ _cleanup_free_ char *ecc_pem = NULL;
+ ASSERT_OK(tpm2_tpmt_signature_to_pem(&ecc_sig, &ecc_pem));
+ ASSERT_STREQ(ecc_pem,
+ "-----BEGIN ECDSA SIGNATURE-----\n"
+ "MGYCMQDZ62hkIqb7mmSlz5gGSV1+eH8Rt39fWShoDAJViiRn7FJvBKl0XcTxliSN\n"
+ "0hmKF9QCMQCOn5JiLEzUwArkxVH+7LxODMWzIeAjrPb4tn+QdeysXJzqPNG212BV\n"
+ "pGwg7NCA0s8=\n"
+ "-----END ECDSA SIGNATURE-----\n");
+}
+
+TEST(tpm2_tpmt_public_to_pem) {
+ DEFINE_HEX_PTR(rsa,
+ "615fc18fec08de00721ae55823d2e2c631fe3d7cb2bd7117a40d9be3cd7623c386db5c60ebcdccca39443c1203297f91b59945544efc7977e16e202e5938a37b"
+ "ae31ee0b7e5249fe7f76f36c94428b3e0f0d53b730270dbb44b3c007a0b45733018f1d8feba462a5e67c7b87a5b913e4a606e105f97828732491686be253d0d7"
+ "f20ad3450ae7b86fe36a7163013487c2659fe56420623241edbd1ecc9c6d2443143a4db68f6c449008e8b4fec5ad5b56598ffd22f67d317e46e4c48e693c38ee"
+ "389886b1084e51cfe56408e7e8eee1ab65615d36aff585e25fb198df519b961054b6cfae85717ba387c597146f4d36e548101409a1ceddf321571d3364c968ef");
+
+ TPMT_PUBLIC rsa_public = {
+ .type = TPM2_ALG_RSA,
+ .nameAlg = TPM2_ALG_SHA256,
+ .objectAttributes =
+ TPMA_OBJECT_FIXEDTPM |
+ TPMA_OBJECT_FIXEDPARENT |
+ TPMA_OBJECT_SENSITIVEDATAORIGIN |
+ TPMA_OBJECT_USERWITHAUTH |
+ TPMA_OBJECT_ADMINWITHPOLICY |
+ TPMA_OBJECT_RESTRICTED |
+ TPMA_OBJECT_SIGN_ENCRYPT,
+ .parameters.rsaDetail = {
+ .symmetric.algorithm = TPM2_ALG_NULL,
+ .scheme = {
+ .scheme = TPM2_ALG_RSAPSS,
+ .details.rsapss.hashAlg = TPM2_ALG_SHA256,
+ },
+ .keyBits = 2048,
+ .exponent = 0,
+ },
+ .unique.rsa = TPM2B_PUBLIC_KEY_RSA_MAKE(rsa, rsa_len),
+ };
+
+ _cleanup_free_ char *rsa_pem = NULL;
+ ASSERT_OK(tpm2_tpmt_public_to_pem(&rsa_public, &rsa_pem));
+ ASSERT_STREQ(rsa_pem,
+ "-----BEGIN PUBLIC KEY-----\n"
+ "MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQBhX8GP7AjeAHIa5Vgj0uLG\n"
+ "Mf49fLK9cRekDZvjzXYjw4bbXGDrzczKOUQ8EgMpf5G1mUVUTvx5d+FuIC5ZOKN7\n"
+ "rjHuC35SSf5/dvNslEKLPg8NU7cwJw27RLPAB6C0VzMBjx2P66RipeZ8e4eluRPk\n"
+ "pgbhBfl4KHMkkWhr4lPQ1/IK00UK57hv42pxYwE0h8Jln+VkIGIyQe29HsycbSRD\n"
+ "FDpNto9sRJAI6LT+xa1bVlmP/SL2fTF+RuTEjmk8OO44mIaxCE5Rz+VkCOfo7uGr\n"
+ "ZWFdNq/1heJfsZjfUZuWEFS2z66FcXujh8WXFG9NNuVIEBQJoc7d8yFXHTNkyWjv\n"
+ "AgMBAAE=\n"
+ "-----END PUBLIC KEY-----\n");
+
+ DEFINE_HEX_PTR(ecc_x, "6381d4a6aebcc46d5968efa80665820ed8b2ea8069e62ddfa28130f7a823620bf44e0779e2b9fe18c9f8b783800e7c2c");
+ DEFINE_HEX_PTR(ecc_y, "473fcbe01831c3be463dcc0093a34eb8196e095671bc10e38e0c8fb3ae459c50a408dfe45142fada5fc29bee6580c51e");
+
+ TPMT_PUBLIC ecc_public = {
+ .type = TPM2_ALG_ECC,
+ .nameAlg = TPM2_ALG_SHA384,
+ .objectAttributes =
+ TPMA_OBJECT_FIXEDTPM |
+ TPMA_OBJECT_FIXEDPARENT |
+ TPMA_OBJECT_SENSITIVEDATAORIGIN |
+ TPMA_OBJECT_USERWITHAUTH |
+ TPMA_OBJECT_ADMINWITHPOLICY |
+ TPMA_OBJECT_RESTRICTED |
+ TPMA_OBJECT_SIGN_ENCRYPT,
+ .parameters.eccDetail = {
+ .symmetric.algorithm = TPM2_ALG_NULL,
+ .scheme = {
+ .scheme = TPM2_ALG_ECDSA,
+ .details.ecdsa.hashAlg = TPM2_ALG_SHA384,
+ },
+ .curveID = TPM2_ECC_NIST_P384,
+ .kdf.scheme = TPM2_ALG_NULL,
+ },
+ .unique.ecc = {
+ .x = TPM2B_ECC_PARAMETER_MAKE(ecc_x, ecc_x_len),
+ .y = TPM2B_ECC_PARAMETER_MAKE(ecc_y, ecc_y_len),
+ },
+ };
+
+ _cleanup_free_ char *ecc_pem = NULL;
+ ASSERT_OK(tpm2_tpmt_public_to_pem(&ecc_public, &ecc_pem));
+ ASSERT_STREQ(ecc_pem,
+ "-----BEGIN PUBLIC KEY-----\n"
+ "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEY4HUpq68xG1ZaO+oBmWCDtiy6oBp5i3f\n"
+ "ooEw96gjYgv0Tgd54rn+GMn4t4OADnwsRz/L4Bgxw75GPcwAk6NOuBluCVZxvBDj\n"
+ "jgyPs65FnFCkCN/kUUL62l/Cm+5lgMUe\n"
+ "-----END PUBLIC KEY-----\n");
+}
+
+TEST(tpm2_tpmt_signature_to_json) {
+ TPMT_SIGNATURE rsa_sig = {
+ .sigAlg = TPM2_ALG_RSAPSS,
+ .signature.rsapss = {
+ .hash = TPM2_ALG_SHA256,
+ .sig = {
+ .size = 256,
+ },
+ },
+ };
+ assert(sizeof(rsa_sig.signature.rsapss.sig.buffer) >= 256);
+ random_bytes(rsa_sig.signature.rsapss.sig.buffer, 256);
+
+ _cleanup_free_ char *h_rsa = hexmem(rsa_sig.signature.rsapss.sig.buffer, 256);
+ ASSERT_NOT_NULL(h_rsa);
+
+ _cleanup_free_ char *rsa_expected = NULL;
+ ASSERT_OK(asprintf(&rsa_expected, "{\"sigAlg\":\"RSAPSS\",\"signature\":{\"hash\":\"SHA256\",\"sig\":\"%s\"}}", h_rsa));
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *rsav = NULL;
+ ASSERT_OK(tpm2_tpmt_signature_to_json(&rsa_sig, &rsav));
+
+ _cleanup_free_ char *rsa_json = NULL;
+ ASSERT_OK(sd_json_variant_format(rsav, 0, &rsa_json));
+ ASSERT_STREQ(rsa_json, rsa_expected);
+
+ DEFINE_HEX_PTR(ecc_r, "d9eb686422a6fb9a64a5cf9806495d7e787f11b77f5f5928680c02558a2467ec526f04a9745dc4f196248dd2198a17d4");
+ DEFINE_HEX_PTR(ecc_s, "8e9f92622c4cd4c00ae4c551feecbc4e0cc5b321e023acf6f8b67f9075ecac5c9cea3cd1b6d76055a46c20ecd080d2cf");
+
+ TPMT_SIGNATURE ecc_sig = {
+ .sigAlg = TPM2_ALG_ECDSA,
+ .signature.ecdsa = {
+ .hash = TPM2_ALG_SHA384,
+ .signatureR = TPM2B_ECC_PARAMETER_MAKE(ecc_r, ecc_r_len),
+ .signatureS = TPM2B_ECC_PARAMETER_MAKE(ecc_s, ecc_s_len),
+ },
+ };
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *eccv = NULL;
+ ASSERT_OK(tpm2_tpmt_signature_to_json(&ecc_sig, &eccv));
+
+ _cleanup_free_ char *ecc_json = NULL;
+ ASSERT_OK(sd_json_variant_format(eccv, 0, &ecc_json));
+ ASSERT_STREQ(ecc_json, "{\"sigAlg\":\"ECDSA\",\"signature\":{\"hash\":\"SHA384\",\"signatureR\":\"d9eb686422a6fb9a64a5cf9806495d7e787f11b77f5f5928680c02558a2467ec526f04a9745dc4f196248dd2198a17d4\",\"signatureS\":\"8e9f92622c4cd4c00ae4c551feecbc4e0cc5b321e023acf6f8b67f9075ecac5c9cea3cd1b6d76055a46c20ecd080d2cf\"}}");
+}
+
+TEST(tpm2_attest_info_to_json) {
+ TPMT_SIG_SCHEME scheme1 = {
+ .scheme = TPM2_ALG_RSAPSS,
+ .details.rsapss.hashAlg = TPM2_ALG_SHA256,
+ };
+
+ DEFINE_HEX_PTR(signer1, "000b8f80817492905f8b4014186c828a5e0191d5146c70e644af0605e2cdd2093bfd");
+ TPML_PCR_SELECTION pcrs;
+ tpm2_tpml_pcr_selection_from_mask(64191, TPM2_ALG_SHA256, &pcrs);
+ DEFINE_HEX_PTR(pcr_digest, "4cdecd069d7522065dfa70e6d31292fe87ee99d0053d5582abddb2a6b5c2640c");
+
+ TPMS_ATTEST attest1 = {
+ .magic = TPM2_GENERATED_VALUE,
+ .type = TPM2_ST_ATTEST_QUOTE,
+ .qualifiedSigner = TPM2B_NAME_MAKE(signer1, signer1_len),
+ .clockInfo = {
+ .clock = 8726451,
+ .resetCount = 72,
+ .restartCount = 0,
+ .safe = TPM2_YES,
+ },
+ .firmwareVersion = 4294967300,
+ .attested.quote = {
+ .pcrSelect = pcrs,
+ .pcrDigest = TPM2B_DIGEST_MAKE(pcr_digest, pcr_digest_len),
+ },
+ };
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v1 = NULL;
+ ASSERT_OK(tpm2_attest_info_to_json(&scheme1, &attest1, &v1));
+
+ _cleanup_free_ char *json1 = NULL;
+ ASSERT_OK(sd_json_variant_format(v1, 0, &json1));
+ ASSERT_STREQ(json1, "{\"sig_scheme\":{\"scheme\":\"RSAPSS\",\"details\":{\"hashAlg\":\"SHA256\"}},\"attest\":{\"magic\":\"VALUE\",\"type\":\"ATTEST_QUOTE\",\"qualifiedSigner\":\"000b8f80817492905f8b4014186c828a5e0191d5146c70e644af0605e2cdd2093bfd\",\"extraData\":\"\",\"clockInfo\":{\"clock\":8726451,\"resetCount\":72,\"restartCount\":0,\"safe\":\"YES\"},\"firmwareVersion\":4294967300,\"attested\":{\"pcrSelect\":[{\"hash\":\"SHA256\",\"pcrSelect\":[0,1,2,3,4,5,7,9,11,12,13,14,15]}],\"pcrDigest\":\"4cdecd069d7522065dfa70e6d31292fe87ee99d0053d5582abddb2a6b5c2640c\"}}}");
+
+ TPMT_SIG_SCHEME scheme2 = {
+ .scheme = TPM2_ALG_ECDSA,
+ .details.ecdsa.hashAlg = TPM2_ALG_SHA384,
+ };
+
+ DEFINE_HEX_PTR(signer2, "000cf8d4b1e869e68f96f37b3cbe1106fd5566fa2de9ffbe3ab5a7b9a3193e10e35e7072bd7c3d3c4d081c931511e7aa5166");
+ DEFINE_HEX_PTR(extra_data, "000b7c88777e5165ac16f59fb7f74c6d54a2f77a2266974d6f811f2d4ee575203667");
+ DEFINE_HEX_PTR(nv_name, "000b743f1f9cf4b7e7f0e4e5d234d72310b4661c2b30d51801c8096e104325ccce9d");
+ DEFINE_HEX_PTR(nv_contents, "aefb5cd55ce0546baacb0ed96440eb796a0f10091f5c22b3c3b1d207ed338c7e");
+
+ TPMS_ATTEST attest2 = {
+ .magic = TPM2_GENERATED_VALUE,
+ .type = TPM2_ST_ATTEST_NV,
+ .qualifiedSigner = TPM2B_NAME_MAKE(signer2, signer2_len),
+ .extraData = TPM2B_DATA_MAKE(extra_data, extra_data_len),
+ .clockInfo = {
+ .clock = 25924398,
+ .resetCount = 151,
+ .restartCount = 1,
+ .safe = TPM2_YES,
+ },
+ .firmwareVersion = 8589934602,
+ .attested.nv = {
+ .indexName = TPM2B_NAME_MAKE(nv_name, nv_name_len),
+ .offset = 0,
+ .nvContents = TPM2B_MAX_NV_BUFFER_MAKE(nv_contents, nv_contents_len),
+ },
+ };
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v2 = NULL;
+ ASSERT_OK(tpm2_attest_info_to_json(&scheme2, &attest2, &v2));
+
+ _cleanup_free_ char *json2 = NULL;
+ ASSERT_OK(sd_json_variant_format(v2, 0, &json2));
+ ASSERT_STREQ(json2, "{\"sig_scheme\":{\"scheme\":\"ECDSA\",\"details\":{\"hashAlg\":\"SHA384\"}},\"attest\":{\"magic\":\"VALUE\",\"type\":\"ATTEST_NV\",\"qualifiedSigner\":\"000cf8d4b1e869e68f96f37b3cbe1106fd5566fa2de9ffbe3ab5a7b9a3193e10e35e7072bd7c3d3c4d081c931511e7aa5166\",\"extraData\":\"000b7c88777e5165ac16f59fb7f74c6d54a2f77a2266974d6f811f2d4ee575203667\",\"clockInfo\":{\"clock\":25924398,\"resetCount\":151,\"restartCount\":1,\"safe\":\"YES\"},\"firmwareVersion\":8589934602,\"attested\":{\"indexName\":\"000b743f1f9cf4b7e7f0e4e5d234d72310b4661c2b30d51801c8096e104325ccce9d\",\"offset\":0,\"nvContents\":\"aefb5cd55ce0546baacb0ed96440eb796a0f10091f5c22b3c3b1d207ed338c7e\"}}}");
+}
+
+TEST(tpm2_tpmt_public_to_json) {
+ const char *rsa_h =
+ "615fc18fec08de00721ae55823d2e2c631fe3d7cb2bd7117a40d9be3cd7623c386db5c60ebcdccca39443c1203297f91b59945544efc7977e16e202e5938a37b"
+ "ae31ee0b7e5249fe7f76f36c94428b3e0f0d53b730270dbb44b3c007a0b45733018f1d8feba462a5e67c7b87a5b913e4a606e105f97828732491686be253d0d7"
+ "f20ad3450ae7b86fe36a7163013487c2659fe56420623241edbd1ecc9c6d2443143a4db68f6c449008e8b4fec5ad5b56598ffd22f67d317e46e4c48e693c38ee"
+ "389886b1084e51cfe56408e7e8eee1ab65615d36aff585e25fb198df519b961054b6cfae85717ba387c597146f4d36e548101409a1ceddf321571d3364c968ef";
+ DEFINE_HEX_PTR(rsa, rsa_h);
+
+ TPMT_PUBLIC rsa_public = {
+ .type = TPM2_ALG_RSA,
+ .nameAlg = TPM2_ALG_SHA256,
+ .objectAttributes =
+ TPMA_OBJECT_FIXEDTPM |
+ TPMA_OBJECT_FIXEDPARENT |
+ TPMA_OBJECT_SENSITIVEDATAORIGIN |
+ TPMA_OBJECT_USERWITHAUTH |
+ TPMA_OBJECT_ADMINWITHPOLICY |
+ TPMA_OBJECT_RESTRICTED |
+ TPMA_OBJECT_SIGN_ENCRYPT,
+ .parameters.rsaDetail = {
+ .symmetric.algorithm = TPM2_ALG_NULL,
+ .scheme = {
+ .scheme = TPM2_ALG_RSAPSS,
+ .details.rsapss.hashAlg = TPM2_ALG_SHA256,
+ },
+ .keyBits = 2048,
+ .exponent = 0,
+ },
+ .unique.rsa = TPM2B_PUBLIC_KEY_RSA_MAKE(rsa, rsa_len),
+ };
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *rsav = NULL;
+ ASSERT_OK(tpm2_tpmt_public_to_json(&rsa_public, &rsav));
+
+ _cleanup_free_ char *rsa_json = NULL;
+ ASSERT_OK(sd_json_variant_format(rsav, 0, &rsa_json));
+
+ _cleanup_free_ char *rsa_expected = NULL;
+ ASSERT_OK(asprintf(&rsa_expected, "{\"type\":\"RSA\",\"nameAlg\":\"SHA256\",\"objectAttributes\":327922,\"authPolicy\":\"\",\"parameters\":{\"symmetric\":{\"algorithm\":\"NULL\"},\"scheme\":{\"scheme\":\"RSAPSS\",\"details\":{\"hashAlg\":\"SHA256\"}},\"keyBits\":2048,\"exponent\":0},\"unique\":\"%s\"}", rsa_h));
+
+ ASSERT_STREQ(rsa_json, rsa_expected);
+
+ DEFINE_HEX_PTR(ecc_x, "6381d4a6aebcc46d5968efa80665820ed8b2ea8069e62ddfa28130f7a823620bf44e0779e2b9fe18c9f8b783800e7c2c");
+ DEFINE_HEX_PTR(ecc_y, "473fcbe01831c3be463dcc0093a34eb8196e095671bc10e38e0c8fb3ae459c50a408dfe45142fada5fc29bee6580c51e");
+
+ TPMT_PUBLIC ecc_public = {
+ .type = TPM2_ALG_ECC,
+ .nameAlg = TPM2_ALG_SHA384,
+ .objectAttributes =
+ TPMA_OBJECT_FIXEDTPM |
+ TPMA_OBJECT_FIXEDPARENT |
+ TPMA_OBJECT_SENSITIVEDATAORIGIN |
+ TPMA_OBJECT_USERWITHAUTH |
+ TPMA_OBJECT_ADMINWITHPOLICY |
+ TPMA_OBJECT_RESTRICTED |
+ TPMA_OBJECT_SIGN_ENCRYPT,
+ .parameters.eccDetail = {
+ .symmetric.algorithm = TPM2_ALG_NULL,
+ .scheme = {
+ .scheme = TPM2_ALG_ECDSA,
+ .details.ecdsa.hashAlg = TPM2_ALG_SHA384,
+ },
+ .curveID = TPM2_ECC_NIST_P384,
+ .kdf.scheme = TPM2_ALG_NULL,
+ },
+ .unique.ecc = {
+ .x = TPM2B_ECC_PARAMETER_MAKE(ecc_x, ecc_x_len),
+ .y = TPM2B_ECC_PARAMETER_MAKE(ecc_y, ecc_y_len),
+ },
+ };
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *eccv = NULL;
+ ASSERT_OK(tpm2_tpmt_public_to_json(&ecc_public, &eccv));
+
+ _cleanup_free_ char *ecc_json = NULL;
+ ASSERT_OK(sd_json_variant_format(eccv, 0, &ecc_json));
+ ASSERT_STREQ(ecc_json, "{\"type\":\"ECC\",\"nameAlg\":\"SHA384\",\"objectAttributes\":327922,\"authPolicy\":\"\",\"parameters\":{\"symmetric\":{\"algorithm\":\"NULL\"},\"scheme\":{\"scheme\":\"ECDSA\",\"details\":{\"hashAlg\":\"SHA384\"}},\"curveID\":\"NIST_P384\",\"kdf\":{\"scheme\":\"NULL\"}},\"unique\":{\"x\":\"6381d4a6aebcc46d5968efa80665820ed8b2ea8069e62ddfa28130f7a823620bf44e0779e2b9fe18c9f8b783800e7c2c\",\"y\":\"473fcbe01831c3be463dcc0093a34eb8196e095671bc10e38e0c8fb3ae459c50a408dfe45142fada5fc29bee6580c51e\"}}");
+}
+
+TEST(tpm2_tpms_nv_public_to_json) {
+ DEFINE_HEX_PTR(policy, "c0f52d0be7f6c1666d90a181a99a74b99c5e0bfd00bc52cc27ae0e66d89afcf5");
+
+ TPMS_NV_PUBLIC nv_public = {
+ .nvIndex = 0x01d10202,
+ .nameAlg = TPM2_ALG_SHA256,
+ .attributes =
+ TPMA_NV_CLEAR_STCLEAR |
+ TPMA_NV_ORDERLY |
+ TPMA_NV_POLICYWRITE |
+ TPMA_NV_OWNERREAD |
+ TPMA_NV_AUTHREAD |
+ TPMA_NV_WRITTEN |
+ (TPM2_NT_EXTEND << TPMA_NV_TPM2_NT_SHIFT),
+ .authPolicy = TPM2B_DIGEST_MAKE(policy, policy_len),
+ .dataSize = 32,
+ };
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ ASSERT_OK(tpm2_tpms_nv_public_to_json(&nv_public, &v));
+
+ _cleanup_free_ char *json = NULL;
+ ASSERT_OK(sd_json_variant_format(v, 0, &json));
+ ASSERT_STREQ(json, "{\"nvIndex\":30474754,\"nameAlg\":\"SHA256\",\"attributes\":738590792,\"authPolicy\":\"c0f52d0be7f6c1666d90a181a99a74b99c5e0bfd00bc52cc27ae0e66d89afcf5\",\"dataSize\":32}");
+}
+
+static void check_attest_common(const TPMS_ATTEST *attest, TPMI_ST_ATTEST type, const TPM2B_DATA *extra_data) {
+ ASSERT_EQ(attest->magic, TPM2_GENERATED_VALUE);
+ ASSERT_EQ(attest->type, type);
+ ASSERT_EQ(memcmp_nn(attest->extraData.buffer, attest->extraData.size, extra_data->buffer, extra_data->size), 0);
+}
+
+static void check_attest_signature(const TPM2B_PUBLIC *public, const TPMT_SIGNATURE *sig) {
+ ASSERT_NOT_NULL(sig);
+ ASSERT_EQ(sig->sigAlg, public->publicArea.parameters.asymDetail.scheme.scheme);
+ ASSERT_EQ(sig->signature.any.hashAlg, public->publicArea.parameters.asymDetail.scheme.details.anySig.hashAlg);
+ switch (public->publicArea.parameters.asymDetail.scheme.scheme) {
+ case TPM2_ALG_RSAPSS:
+ ASSERT_EQ(sig->signature.rsapss.sig.size, public->publicArea.parameters.rsaDetail.keyBits / 8);
+ break;
+ case TPM2_ALG_RSASSA:
+ ASSERT_EQ(sig->signature.rsapss.sig.size, public->publicArea.parameters.rsaDetail.keyBits / 8);
+ break;
+ case TPM2_ALG_ECDSA: {
+ size_t expected_sz;
+ switch (public->publicArea.parameters.eccDetail.curveID) {
+ case TPM2_ECC_NIST_P256:
+ expected_sz = 32;
+ break;
+ case TPM2_ECC_NIST_P384:
+ expected_sz = 48;
+ break;
+ default:
+ assert_not_reached();
+ }
+ ASSERT_EQ(sig->signature.ecdsa.signatureR.size, expected_sz);
+ ASSERT_EQ(sig->signature.ecdsa.signatureS.size, expected_sz);
+ break;
+ }
+ default:
+ assert_not_reached();
+ }
+
+ /* XXX: Probably would be good to verify the actual signature here. We could do that with
+ * TPM2_VerifySignature, we would just need to implement that in tpm2-util.c. */
+}
+
+static void check_quote(Tpm2Context *c) {
+ assert(c);
+
+ TEST_LOG_FUNC();
+
+ TPM2B_PUBLIC template = {
+ .size = sizeof(TPMT_PUBLIC),
+ };
+ ASSERT_OK(tpm2_get_best_attestation_key_template(c, &template.publicArea));
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *key = NULL;
+ ASSERT_OK(tpm2_create_primary(c, NULL, ESYS_TR_RH_OWNER, &template, NULL, NULL, &key));
+
+ const char *s = "foo";
+ TPM2B_DATA data = TPM2B_DATA_MAKE(s, strlen(s));
+
+ TPML_PCR_SELECTION pcrs;
+ tpm2_tpml_pcr_selection_from_mask(191, TPM2_ALG_SHA256, &pcrs);
+
+ _cleanup_(Esys_Freep) TPMS_ATTEST *quote = NULL;
+ _cleanup_(Esys_Freep) TPMT_SIGNATURE *sig = NULL;
+ ASSERT_OK(tpm2_quote(c, NULL, NULL, key, &data, &pcrs, "e, &sig));
+
+ check_attest_common(quote, TPM2_ST_ATTEST_QUOTE, &data);
+ ASSERT_EQ(memcmp("e->attested.quote.pcrSelect, &pcrs, sizeof(pcrs)), 0);
+
+ check_attest_signature(&template, sig);
+}
+
+static void check_nv_certify(Tpm2Context *c) {
+ int r;
+
+ assert(c);
+
+ TEST_LOG_FUNC();
+
+ char payload[16];
+ random_bytes(payload, sizeof(payload));
+ struct iovec nv_data = IOVEC_MAKE(payload, sizeof(payload));
+
+ TPM2_HANDLE nv_index = 0;
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL;
+ r = tpm2_define_data_nv_index(c, /* session= */ NULL, /* requested_nv_index= */ 0, &nv_data, &nv_index, &nv_handle);
+ if (r < 0) {
+ /* Could fail because the index size is greater than the value of TPM2_PT_NV_INDEX_MAX, or
+ * there isn't enough space available. */
+ log_notice_errno(r, "Could not allocate NV index, skipping NV certify test: %m");
+ return;
+ }
+ ASSERT_NE(nv_index, 0U);
+ ASSERT_NOT_NULL(nv_handle);
+
+ TPMS_NV_PUBLIC nv_public = {
+ .nvIndex = nv_index,
+ .nameAlg = TPM2_ALG_SHA256,
+ .attributes = TPM2_NT_ORDINARY | TPMA_NV_AUTHWRITE | TPMA_NV_AUTHREAD | TPMA_NV_NO_DA,
+ .dataSize = nv_data.iov_len,
+ };
+
+ _cleanup_(Esys_Freep) TPM2B_NAME *nv_name = NULL;
+ ASSERT_OK(tpm2_get_name(c, nv_handle, &nv_name));
+
+ TPM2B_PUBLIC template = {
+ .size = sizeof(TPMT_PUBLIC),
+ };
+ ASSERT_OK(tpm2_get_best_attestation_key_template(c, &template.publicArea));
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *key = NULL;
+ ASSERT_OK(tpm2_create_primary(c, NULL, ESYS_TR_RH_OWNER, &template, NULL, NULL, &key));
+
+ const char *s = "bar";
+ TPM2B_DATA data = TPM2B_DATA_MAKE(s, strlen(s));
+
+ _cleanup_(Esys_Freep) TPMS_ATTEST *certify_info = NULL;
+ _cleanup_(Esys_Freep) TPMT_SIGNATURE *sig = NULL;
+ ASSERT_OK(tpm2_nv_certify(c, NULL, NULL, NULL, key, &nv_public, nv_handle, &data, &certify_info, &sig));
+
+ ASSERT_OK(tpm2_undefine_nv_index(c, NULL, nv_index, nv_handle));
+
+ check_attest_common(certify_info, TPM2_ST_ATTEST_NV, &data);
+ ASSERT_EQ(memcmp_nn(certify_info->attested.nv.indexName.name, certify_info->attested.nv.indexName.size, nv_name->name, nv_name->size), 0);
+ ASSERT_EQ(certify_info->attested.nv.offset, 0);
+ ASSERT_EQ(memcmp_nn(certify_info->attested.nv.nvContents.buffer, certify_info->attested.nv.nvContents.size, nv_data.iov_base, nv_data.iov_len), 0);
+
+ check_attest_signature(&template, sig);
+}
+
+static void check_get_session_audit_digest(Tpm2Context *c) {
+ assert(c);
+
+ TEST_LOG_FUNC();
+
+ TPM2B_PUBLIC template = {
+ .size = sizeof(TPMT_PUBLIC),
+ };
+ ASSERT_OK(tpm2_get_best_attestation_key_template(c, &template.publicArea));
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *key = NULL;
+ ASSERT_OK(tpm2_create_primary(c, NULL, ESYS_TR_RH_OWNER, &template, NULL, NULL, &key));
+
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *session = NULL;
+ ASSERT_OK(tpm2_make_exclusive_audit_session(c, &session));
+
+ /* Use the session */
+ TPML_PCR_SELECTION pcrs;
+ tpm2_tpml_pcr_selection_from_mask(191, TPM2_ALG_SHA256, &pcrs);
+ ASSERT_OK(tpm2_quote(c, NULL, session, key, NULL, &pcrs, NULL, NULL));
+
+ const char *s = "foo";
+ TPM2B_DATA data = TPM2B_DATA_MAKE(s, strlen(s));
+
+ _cleanup_(Esys_Freep) TPMS_ATTEST *audit_info = NULL;
+ _cleanup_(Esys_Freep) TPMT_SIGNATURE *sig = NULL;
+ ASSERT_OK(tpm2_get_session_audit_digest(c, NULL, NULL, session, key, &data, &audit_info, &sig));
+
+ check_attest_common(audit_info, TPM2_ST_ATTEST_SESSION_AUDIT, &data);
+ ASSERT_EQ(audit_info->attested.sessionAudit.exclusiveSession, TPM2_YES);
+ ASSERT_EQ(audit_info->attested.sessionAudit.sessionDigest.size, 32);
+
+ check_attest_signature(&template, sig);
+}
+
TEST_RET(tests_which_require_tpm) {
_cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL;
int r = 0;
@@ -1719,6 +2365,14 @@ TEST_RET(tests_which_require_tpm) {
check_nv_index_read(c);
check_get_ek_template(c);
check_get_or_create_ek(c);
+ check_max_data_size(c);
+ check_context_saving(c);
+ check_saved_context_marshaling(c);
+ check_policy_secret(c);
+ check_best_attestation_key_template(c);
+ check_quote(c);
+ check_nv_certify(c);
+ check_get_session_audit_digest(c);
#if HAVE_OPENSSL
r = check_calculate_seal(c);
diff --git a/test/units/TEST-70-TPM2.report-sign.sh b/test/units/TEST-70-TPM2.report-sign.sh
new file mode 100755
index 0000000000..24ca9b9aff
--- /dev/null
+++ b/test/units/TEST-70-TPM2.report-sign.sh
@@ -0,0 +1,532 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eux
+set -o pipefail
+
+# Test report signing through the TPM2 backend (systemd-report-sign-tpm2), driven
+# via the io.systemd.Report Varlink interface (the GenerateSigned method). This is
+# the TPM2 counterpart to the plain software backend test in
+# TEST-74-AUX-UTILS.report.sh; it lives here because it needs a real TPM, which
+# only the TPM2 integration test provides.
+#
+# The TPM2 backend returns a set of signed TPM attestations (a PCR quote, one
+# NV certification per NvPCR, and a session audit digest), together with the
+# signing key's public area and the pcrlock event log. The public area, the
+# attestations and the signatures are all serialized as TCG TSS2 JSON. For each
+# attestation we rebuild the public key, re-marshal the TPMS_ATTEST that was
+# signed, and verify the signature using the embedded Python helper below. The
+# helper also cross-checks the parallel PEM encodings (publicKeyPEM and signaturePEM)
+# against the JSON encodings. We also confirm the report digest is carried in the
+# extraData field of the session audit attestation.
+#
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+export SYSTEMD_LOG_LEVEL=debug
+# Unset $PAGER so we don't have to use --no-pager everywhere
+export PAGER=
+
+# The TPM2 backend is only built/installed with OpenSSL and TPM2 support, so
+# skip if the socket isn't present.
+if ! systemctl cat systemd-report-sign-tpm2.socket &>/dev/null; then
+ echo "systemd-report-sign-tpm2.socket is not installed, skipping TPM2 report signing test."
+ exit 0
+fi
+
+# We create the EK with tpm2-tools.
+if ! command -v tpm2_createek >/dev/null; then
+ echo "tpm2-tools not installed, skipping TPM2 report signing test."
+ exit 0
+fi
+
+# The attestation signatures are verified with the embedded Python helper below,
+# which uses the cryptography module.
+if ! python3 -c "import cryptography" >/dev/null 2>&1; then
+ echo "python3 cryptography module not available, skipping TPM2 report signing test."
+ exit 0
+fi
+
+WORK="$(mktemp -d)"
+
+at_exit() {
+ set +e
+ systemctl stop systemd-report.socket systemd-report-sign-tpm2.socket
+ rm -rf "$WORK"
+}
+trap at_exit EXIT
+
+# The TPM2 backend creates its signing key as a child of the TPM's endorsement
+# key. In a QEMU/swtpm guest there is no EK certificate, and the backend only
+# provisions an EK when a matching certificate is present. Create and persist
+# an EK directly. This fails if one already is already present, so ignore that.
+if ! tpm2_createek -c 0x81010001 -G ecc; then
+ echo "tpm2_createek failed, assuming an EK is already present."
+fi
+
+# The TPM2 backend reads the event log from pcrlock's Varlink interface, so make
+# sure its socket is up.
+systemctl start systemd-pcrlock.socket
+
+systemctl start systemd-report.socket
+systemctl start systemd-report-sign-tpm2.socket
+
+# Ask systemd-report to generate a *signed* report over Varlink. The reply
+# carries the signed report as base64-encoded JSON-SEQ data.
+varlinkctl call /run/systemd/io.systemd.Report io.systemd.Report.GenerateSigned \
+ '{"matches":["io.systemd.Manager.UnitsTotal"]}' | jq -r .reportData | base64 -d >"$WORK/report.seq"
+
+# The first JSON-SEQ record is the report itself. This is exactly the byte
+# sequence that got signed, including the leading record separator (0x1e) and
+# the trailing newline, so 'head -n1' reproduces it verbatim.
+head -n1 "$WORK/report.seq" >"$WORK/message.bin"
+tr -d '\036' <"$WORK/message.bin" | jq -e '.mediaType == "application/vnd.io.systemd.report"' >/dev/null
+
+# The remaining record(s) are signature objects, one per enabled backend. Pick
+# out the one produced by the TPM2 backend (the plain backend may be enabled too).
+sig_json=""
+while IFS= read -r line; do
+ rec="$(echo "$line" | tr -d '\036')"
+ [[ -n "$rec" ]] || continue
+ if [[ "$(echo "$rec" | jq -r '.mechanism // empty')" == "tpm2" ]]; then
+ sig_json="$rec"
+ fi
+done < <(tail -n +2 "$WORK/report.seq")
+test -n "$sig_json"
+
+[ "$(echo "$sig_json" | jq -r .mediaType)" = "application/vnd.io.systemd.report.signature" ]
+
+# The sha256 recorded in the signature must match the digest of the report bytes.
+report_digest="$(sha256sum "$WORK/message.bin" | cut -d' ' -f1)"
+[ "$(echo "$sig_json" | jq -r .sha256)" = "$report_digest" ]
+
+# Use a python script for verifying the report component signatures because we
+# need to reconstruct the TPM2B_ATTEST bytes from the provided TPMS_ATTEST JSON
+# encoding, and construct a public key from the provided TPMT_PUBLIC JSON encoding.
+VERIFY="$WORK/verify-report-sig.py"
+cat >"$VERIFY" <<'EOF'
+#!/usr/bin/env python3
+"""Verify systemd TPM report signatures."""
+
+import base64
+import json
+import struct
+import sys
+
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
+from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
+
+HASH_ALG_ID = {"SHA1": 0x0004, "SHA256": 0x000b, "SHA384": 0x000c, "SHA512": 0x000d}
+SIG_ALG_ID = {"RSASSA": 0x0014, "RSAPSS": 0x0016, "ECDSA": 0x0018, "NULL": 0x0010}
+PUB_ALG_ID = {"RSA": 0x0001, "ECC": 0x0023}
+CURVE_ID = {"NIST_P192": 0x0001, "NIST_P224": 0x0002, "NIST_P256": 0x0003,
+ "NIST_P384": 0x0004, "NIST_P521": 0x0005, "BN_P256": 0x0010,
+ "BN_P638": 0x0011, "SM2_P256": 0x0020}
+ST_ATTEST = {"ATTEST_NV": 0x8014, "ATTEST_SESSION_AUDIT": 0x8016, "ATTEST_QUOTE": 0x8018}
+TPM_GENERATED = 0xff544347
+ALG_NULL = 0x0010
+CC_QUOTE = 0x00000158
+CC_NV_CERTIFY = 0x00000184
+
+HASHES = {"SHA1": hashes.SHA1, "SHA256": hashes.SHA256,
+ "SHA384": hashes.SHA384, "SHA512": hashes.SHA512}
+CURVES = {"NIST_P192": ec.SECP192R1, "NIST_P224": ec.SECP224R1, "NIST_P256": ec.SECP256R1,
+ "NIST_P384": ec.SECP384R1, "NIST_P521": ec.SECP521R1}
+
+
+def hash(alg, data):
+ """Digest the supplied data using the specific TPM2 digest algorithm."""
+ h = hashes.Hash(HASHES[alg]())
+ h.update(data)
+ return h.finalize()
+
+
+def sha256(data):
+ """Digest the supplied data using SHA-256."""
+ h = hashes.Hash(hashes.SHA256())
+ h.update(data)
+ return h.finalize()
+
+
+# Helpers to serialize JSON encodings to the TPM wire format.
+
+def marshal_u8(v):
+ return struct.pack(">B", v)
+
+
+def marshal_u16(v):
+ return struct.pack(">H", v)
+
+
+def marshal_u32(v):
+ return struct.pack(">I", v)
+
+
+def marshal_u64(v):
+ return struct.pack(">Q", v)
+
+
+def marshal_hex_tpm2b(h):
+ """Serialize the supplied hex string as a TPM2B_ type."""
+ b = bytes.fromhex(h) if h else b""
+ return marshal_u16(len(b)) + b
+
+
+def marshal_bytes_tpm2b(b):
+ """Serialize the supplied raw bytes as a TPM2B_ type."""
+ return marshal_u16(len(b)) + b
+
+
+def marshal_tpml_pcr_selection(sel_list):
+ """Serialize the supplied JSON encoded TPML_PCR_SELECTION to its wire form."""
+ out = marshal_u32(len(sel_list))
+ for s in sel_list:
+ pcrs = s["pcrSelect"]
+ # The TSS2 JSON format doesn't include the sizeOfSelect field, so we
+ # reconstruct it here based on the maximum PCR, and assuming that the
+ # value of TPM_PT_PCR_SELECT_MIN is 3.
+ size = max(3, (max(pcrs) // 8 + 1) if pcrs else 0)
+ bm = bytearray(size)
+ for p in pcrs:
+ bm[p // 8] |= 1 << (p % 8)
+ out += marshal_u16(HASH_ALG_ID[s["hash"]]) + marshal_u8(size) + bytes(bm)
+ return out
+
+
+def marshal_tpms_attest(att):
+ """Serialize the supplied JSON encoded TPMS_ATTEST structure to its wire form."""
+ out = marshal_u32(TPM_GENERATED) # .magic
+ out += marshal_u16(ST_ATTEST[att["type"]])
+ out += marshal_hex_tpm2b(att.get("qualifiedSigner", ""))
+ out += marshal_hex_tpm2b(att.get("extraData", ""))
+
+ # .clockInfo
+ ci = att["clockInfo"]
+ out += marshal_u64(ci["clock"])
+ out += marshal_u32(ci["resetCount"] & 0xffffffff)
+ out += marshal_u32(ci["restartCount"] & 0xffffffff)
+ out += marshal_u8(1 if ci["safe"] == "YES" else 0)
+
+ out += marshal_u64(att["firmwareVersion"])
+
+ # .attested
+ a, t = att["attested"], att["type"]
+ if t == "ATTEST_QUOTE":
+ out += marshal_tpml_pcr_selection(a["pcrSelect"])
+ out += marshal_hex_tpm2b(a["pcrDigest"])
+ elif t == "ATTEST_NV":
+ out += marshal_hex_tpm2b(a["indexName"])
+ out += marshal_u16(a["offset"] & 0xffff)
+ out += marshal_hex_tpm2b(a["nvContents"])
+ elif t == "ATTEST_SESSION_AUDIT":
+ out += marshal_u8(1 if a["exclusiveSession"] == "YES" else 0)
+ out += marshal_hex_tpm2b(a["sessionDigest"])
+ else:
+ sys.exit(f"unsupported attest type {t}")
+ return out
+
+
+def marshal_tpms_nv_public(nv):
+ """Serialize the supplied JSON encoded TPMS_NV_PUBLIC to its wire form."""
+ out = marshal_u32(nv["nvIndex"] & 0xffffffff)
+ out += marshal_u16(HASH_ALG_ID[nv["nameAlg"]])
+ out += marshal_u32(nv["attributes"] & 0xffffffff)
+ out += marshal_hex_tpm2b(nv.get("authPolicy", ""))
+ out += marshal_u16(nv["dataSize"] & 0xffff)
+ return out
+
+
+def digest_bytes_and_marshal_tpmt_ha(alg, data):
+ """Digest the suppied data using the specified algorithm and serialize it
+ as a TPMT_HA structure in its wire form."""
+ return marshal_u16(HASH_ALG_ID[alg]) + hash(alg, data)
+
+
+def marshal_tpmt_public(pub):
+ """Serialize the supplied JSON encoded TPMT_PUBLIC structure to its wire form."""
+ typ = pub["type"]
+
+ out = marshal_u16(PUB_ALG_ID[typ])
+ out += marshal_u16(HASH_ALG_ID[pub["nameAlg"]])
+ out += marshal_u32(pub["objectAttributes"] & 0xffffffff)
+ out += marshal_hex_tpm2b(pub.get("authPolicy", ""))
+
+ # .parameters
+ parms = pub["parameters"]
+ out += marshal_u16(ALG_NULL) # .parameters.symmetric.algorithm = TPM_ALG_NULL
+ out += marshal_u16(SIG_ALG_ID[parms["scheme"]["scheme"]])
+ if parms["scheme"]["scheme"] != "NULL":
+ out += marshal_u16(HASH_ALG_ID[parms["scheme"]["details"]["hashAlg"]])
+ if typ == "RSA":
+ out += marshal_u16(parms["keyBits"] & 0xffff)
+ out += marshal_u32(parms["exponent"] & 0xffffffff)
+ elif typ == "ECC":
+ out += marshal_u16(CURVE_ID[parms["curveID"]])
+ out += marshal_u16(ALG_NULL) # .parameters.eccDetail.kdf.scheme = TPM_ALG_NULL
+ else:
+ sys.exit(f"unsupported key type {typ}")
+
+ # .unique
+ if typ == "RSA":
+ out += marshal_hex_tpm2b(pub["unique"])
+ elif typ == "ECC":
+ out += marshal_hex_tpm2b(pub["unique"]["x"])
+ out += marshal_hex_tpm2b(pub["unique"]["y"])
+ else:
+ sys.exit(f"unsupported key type {typ}")
+
+ return out
+
+
+def marshal_tpmt_signature(sig):
+ """Serialize the supplied JSON encoded TPMT_SIGNATURE structure to its wire form."""
+ alg, s = sig["sigAlg"], sig["signature"]
+
+ out = marshal_u16(SIG_ALG_ID[alg]) # .sigAlg
+ out += marshal_u16(HASH_ALG_ID[s["hash"]]) # .siganture.any.hashAlg
+ if alg in ("RSASSA", "RSAPSS"):
+ return out + marshal_hex_tpm2b(s["sig"])
+ if alg == "ECDSA":
+ return out + marshal_hex_tpm2b(s["signatureR"]) + marshal_hex_tpm2b(s["signatureS"])
+ sys.exit(f"unsupported sigAlg {alg}")
+
+
+def build_pubkey(pub):
+ """Build a public key from the supplied JSON encoded TPMT_PUBLIC structure."""
+ if pub["type"] == "RSA":
+ e = pub["parameters"]["exponent"] or 65537
+ return rsa.RSAPublicNumbers(e, int(pub["unique"], 16)).public_key()
+ if pub["type"] == "ECC":
+ curve = CURVES[pub["parameters"]["curveID"]]()
+ return ec.EllipticCurvePublicNumbers(
+ int(pub["unique"]["x"], 16), int(pub["unique"]["y"], 16), curve).public_key()
+ sys.exit(f"unsupported key type {pub['type']}")
+
+
+def json_signature_bytes(sig):
+ """Serialize a signature from the supplied JSON encoded TPMT_SIGNATURE structure."""
+ alg, s = sig["sigAlg"], sig["signature"]
+
+ if alg in ("RSASSA", "RSAPSS"):
+ return bytes.fromhex(s["sig"]), "RSA SIGNATURE"
+ if alg == "ECDSA":
+ sr, ss = int(s["signatureR"], 16), int(s["signatureS"], 16)
+ return encode_dss_signature(sr, ss), "ECDSA SIGNATURE"
+ sys.exit(f"unsupported sigAlg {alg}")
+
+
+def pem_to_der(pem, label):
+ """Convert the supplied PEM structure to DER."""
+ begin, end = f"-----BEGIN {label}-----", f"-----END {label}-----"
+ lines = [ln for ln in pem.strip().splitlines() if ln]
+ if not lines or lines[0] != begin or lines[-1] != end:
+ sys.exit(f"unexpected PEM envelope, wanted {label!r}")
+ return base64.b64decode("".join(lines[1:-1]))
+
+
+def verify(key, scheme, sig_bytes, message):
+ alg = scheme["scheme"]
+ h = HASHES[scheme["details"]["hashAlg"]]()
+ if alg == "RSASSA":
+ key.verify(sig_bytes, message, padding.PKCS1v15(), h)
+ elif alg == "RSAPSS":
+ key.verify(sig_bytes, message,
+ padding.PSS(mgf=padding.MGF1(h), salt_length=padding.PSS.AUTO), h)
+ elif alg == "ECDSA":
+ key.verify(sig_bytes, message, ec.ECDSA(h))
+ else:
+ sys.exit(f"unsupported scheme {alg}")
+
+
+def check_nvpcr(i, comp):
+ """Check the properties of the nvpcr component."""
+ nv = comp["nvPublic"]
+ att = comp["attestInfo"]["attest"]
+
+ # Make sure that nvPublic is consistent with the indexName in the attestation.
+ name = digest_bytes_and_marshal_tpmt_ha(nv["nameAlg"], marshal_tpms_nv_public(nv))
+ if name != bytes.fromhex(att["attested"]["indexName"]):
+ sys.exit(f"component {i}: nvPublic Name does not match attested indexName")
+
+ # Make sure that the authenticatedData is consistent with the extraData in
+ # the attestation.
+ extra = digest_bytes_and_marshal_tpmt_ha(nv["nameAlg"], comp["authenticatedData"].encode())
+ if extra != bytes.fromhex(att["extraData"]):
+ sys.exit(f"component {i}: authenticatedData digest does not match attested extraData")
+
+
+def check_session_audit(doc, key_name):
+ """Check the properties of the session-audit component."""
+ digest = b"\x00" * 32 # Starting audit digest
+
+ reported = None
+ for comp in doc["components"]:
+ # Determine the command code, command handle names and cpBytes.
+ att = comp["attestInfo"]["attest"]
+ t = comp["type"]
+ if t == "pcr":
+ cc = CC_QUOTE
+ names = key_name # signHandle only
+ cp = marshal_hex_tpm2b("") # qualifyingData
+ cp += marshal_u16(ALG_NULL) # inScheme.scheme
+ cp += marshal_tpml_pcr_selection(att["attested"]["pcrSelect"]) # pcrSelect
+ elif t == "nvpcr":
+ cc = CC_NV_CERTIFY
+ nv_name = digest_bytes_and_marshal_tpmt_ha(comp["nvPublic"]["nameAlg"], marshal_tpms_nv_public(comp["nvPublic"]))
+ # Handle area is signHandle, authHandle, nvIndex - we pass the NV index for both
+ # the authHandle and nvIndex.
+ names = key_name + nv_name + nv_name
+ cp = marshal_hex_tpm2b(att["extraData"]) # qualifyingData
+ cp += marshal_u16(ALG_NULL) # inScheme.scheme
+ cp += marshal_u16(comp["nvPublic"]["dataSize"]) # size
+ cp += marshal_u16(0) # offset
+ elif t == "session-audit":
+ reported = att["attested"]["sessionDigest"]
+ continue # not audited itself
+ else:
+ sys.exit(f"unsupported component type {t}")
+
+ # Calculate rpBytes.
+ rp = marshal_bytes_tpm2b(marshal_tpms_attest(att)) # TPM2B_ATTEST out
+ rp += marshal_tpmt_signature(comp["signature"]) # signature
+
+ # Calculate cpHash and rpHash.
+ cp_hash = sha256(marshal_u32(cc) + names + cp)
+ rp_hash = sha256(marshal_u32(0) + marshal_u32(cc) + rp)
+
+ # Extend the audit digest.
+ digest = sha256(digest + cp_hash + rp_hash)
+
+ if reported is None:
+ sys.exit("no session audit component present")
+ if digest.hex() != reported:
+ sys.exit("session audit digest does not match the audited command sequence")
+
+
+def main():
+ report_digest = sys.argv[1]
+
+ full = json.load(sys.stdin)
+ data = full["data"]
+
+ # Rebuild the key from the JSON public area.
+ key_json = build_pubkey(data["publicKey"])
+
+ # Also load the PEM encoded public key, and make sure they're the same.
+ key_pem = serialization.load_pem_public_key(data["publicKeyPEM"].encode())
+ if key_json.public_numbers() != key_pem.public_numbers():
+ sys.exit("publicKeyPEM does not match the JSON public key")
+
+ saw_report_binding = False
+ for i, comp in enumerate(data["components"]):
+ scheme = comp["attestInfo"]["sig_scheme"]
+ message = marshal_tpms_attest(comp["attestInfo"]["attest"])
+
+ sig = comp["signature"]
+
+ # Make sure that the signature scheme in the attestInfo matches the
+ # information in the JSON encoded signature.
+ if scheme["scheme"] != sig["sigAlg"] or scheme["details"]["hashAlg"] != sig["signature"]["hash"]:
+ sys.exit(f"component {i}: signature scheme inconsistent with signature")
+
+ # Reconstruct the signature from the JSON fields and decode the parallel
+ # signaturePEM blob. They should be the same.
+ sig_json, label = json_signature_bytes(sig)
+ sig_pem = pem_to_der(comp["signaturePEM"], label)
+ if sig_pem != sig_json:
+ sys.exit(f"component {i}: signaturePEM does not match the JSON signature")
+
+ # Verify using the PEM-loaded key and PEM-decoded signature.
+ try:
+ verify(key_pem, scheme, sig_pem, message)
+ except InvalidSignature:
+ sys.exit(f"component {i} ({comp['type']}): signature verification FAILED")
+
+ if comp["type"] == "nvpcr":
+ check_nvpcr(i, comp)
+ elif comp["type"] == "session-audit":
+ # The report digest passed to the signer is the session audit's
+ # qualifying data, so it appears as extraData prefixed with the SHA256
+ # algorithm id.
+ extra = bytes.fromhex(comp["attestInfo"]["attest"]["extraData"])
+ if extra != marshal_u16(HASH_ALG_ID["SHA256"]) + bytes.fromhex(report_digest):
+ sys.exit(f"component {i}: session audit extraData does not match the report digest")
+ saw_report_binding = True
+
+ print(comp["type"])
+
+ if not saw_report_binding:
+ sys.exit("no session audit component bound the report digest")
+
+ # The session audit digest must reproduce the audited command sequence that
+ # produced the other components.
+ key_name = digest_bytes_and_marshal_tpmt_ha(data["publicKey"]["nameAlg"], marshal_tpmt_public(data["publicKey"]))
+ check_session_audit(data, key_name)
+
+
+if __name__ == "__main__":
+ main()
+EOF
+
+n_components="$(echo "$sig_json" | jq '.data.components | length')"
+[ "$n_components" -gt 0 ]
+
+# The backend attests every defined NvPCR. Fetch them via systemd-analyze so we
+# know how many components to expect and can cross-check each one's name, index
+# and priority below. Columns: name, nvindex (an unsigned integer), priority.
+nvpcrs_json="$(systemd-analyze nvpcrs --json=short)"
+expected_nvpcrs="$(echo "$nvpcrs_json" | jq 'length')"
+[ "$expected_nvpcrs" -gt 0 ]
+
+# Verify every component signature and collect the component types.
+echo "$sig_json" | python3 "$VERIFY" "$report_digest" >"$WORK/component-types"
+mapfile -t comp_types <"$WORK/component-types"
+[ "${#comp_types[@]}" -eq "$n_components" ]
+
+saw_pcr=0
+saw_audit=0
+n_nvpcr=0
+
+for i in "${!comp_types[@]}"; do
+ type="${comp_types[$i]}"
+ comp="$(echo "$sig_json" | jq -c ".data.components[$i]")"
+
+ case "$type" in
+ pcr)
+ saw_pcr=1
+ ;;
+ nvpcr)
+ n_nvpcr=$((n_nvpcr + 1))
+
+ # NvPCR components carry the readable name, the serialized NV public
+ # area, and the authenticated data digested into the attestation's
+ # qualifying data. Ensure they're populated.
+ name="$(echo "$comp" | jq -r '.["nvpcrName"] // empty')"
+ [ -n "$name" ]
+ [ -n "$(echo "$comp" | jq -r '.["nvPublic"] // empty')" ]
+ [ -n "$(echo "$comp" | jq -r '.["authenticatedData"] // empty')" ]
+
+ # The name must be one systemd-analyze knows about.
+ expected_nvpcr="$(echo "$nvpcrs_json" | jq -c --arg n "$name" '.[] | select(.name == $n)')"
+ [ -n "$expected_nvpcr" ]
+
+ # The certified NV index must match the one systemd-analyze reports.
+ [ "$(echo "$comp" | jq -r '.nvPublic.nvIndex')" = "$(echo "$expected_nvpcr" | jq -r '.nvindex')" ]
+
+ # authenticatedData is a JSON string carrying the NvPCR name and
+ # priority; both must match this component and systemd-analyze.
+ auth="$(echo "$comp" | jq -r '.authenticatedData')"
+ [ "$(echo "$auth" | jq -r '.name')" = "$name" ]
+ [ "$(echo "$auth" | jq -r '.priority')" = "$(echo "$expected_nvpcr" | jq -r '.priority')" ]
+ ;;
+ session-audit)
+ saw_audit=1
+ ;;
+ esac
+done
+
+# Make sure we saw the expected components.
+[ "$saw_pcr" -eq 1 ]
+[ "$saw_audit" -eq 1 ]
+[ "$n_nvpcr" -eq "$expected_nvpcrs" ]
diff --git a/units/meson.build b/units/meson.build
index 8e1f3c2aa8..6e0c474830 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -796,6 +796,14 @@ units = [
'file' : 'systemd-report-sign-plain@.service.in',
'conditions' : ['HAVE_OPENSSL'],
},
+ {
+ 'file' : 'systemd-report-sign-tpm2.socket',
+ 'conditions' : ['HAVE_OPENSSL','HAVE_TPM2'],
+ },
+ {
+ 'file' : 'systemd-report-sign-tpm2@.service.in',
+ 'conditions' : ['HAVE_OPENSSL', 'HAVE_TPM2'],
+ },
{ 'file' : 'systemd-report-sign-tsm.socket' },
{ 'file' : 'systemd-report-sign-tsm@.service.in' },
{
diff --git a/units/systemd-report-sign-tpm2.socket b/units/systemd-report-sign-tpm2.socket
new file mode 100644
index 0000000000..f75a3c1c55
--- /dev/null
+++ b/units/systemd-report-sign-tpm2.socket
@@ -0,0 +1,31 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Report Signing Socket (TPM2)
+Documentation=man:systemd-report-sign-tpm2@.service(8)
+DefaultDependencies=no
+Before=sockets.target shutdown.target
+Conflicts=shutdown.target
+WantsMountsFor=/var/lib/systemd/report.sign.tpm2
+ConditionSecurity=measured-uki
+
+[Socket]
+ListenStream=/run/systemd/report.sign/tpm2
+FileDescriptorName=varlink
+SocketMode=0644
+Accept=yes
+MaxConnectionsPerSource=16
+RemoveOnStop=yes
+XAttrEntryPoint=user.varlink=entrypoint
+XAttrListen=user.varlink=listen
+XAttrAccept=user.varlink=server
+
+[Install]
+WantedBy=sockets.target
diff --git a/units/systemd-report-sign-tpm2@.service.in b/units/systemd-report-sign-tpm2@.service.in
new file mode 100644
index 0000000000..8ceb08ebde
--- /dev/null
+++ b/units/systemd-report-sign-tpm2@.service.in
@@ -0,0 +1,53 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Report Signing Service (TPM2)
+Documentation=man:systemd-report-sign-tpm2@.service(8)
+DefaultDependencies=no
+Conflicts=shutdown.target
+Before=shutdown.target
+WantsMountsFor=/var/lib/systemd/report.sign.tpm2
+
+[Service]
+StateDirectory=systemd/report.sign.tpm2
+StateDirectoryMode=0700
+RuntimeDirectory=systemd/report.sign.tpm2
+RuntimeDirectoryMode=0700
+RuntimeDirectoryPreserve=yes
+ExecStart={{LIBEXECDIR}}/systemd-report-sign-tpm2
+CapabilityBoundingSet=CAP_DAC_OVERRIDE
+DeviceAllow=/dev/tpmrm0 rw
+DevicePolicy=closed
+IPAddressDeny=any
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+PrivateIPC=yes
+PrivateNetwork=yes
+PrivateTmp=disconnected
+ProtectClock=yes
+ProtectControlGroups=yes
+ProtectHome=yes
+ProtectHostname=yes
+ProtectKernelLogs=yes
+ProtectKernelModules=yes
+ProtectKernelTunables=yes
+ProtectProc=invisible
+ProtectSystem=strict
+RestrictAddressFamilies=AF_UNIX
+RestrictNamespaces=yes
+RestrictRealtime=yes
+RestrictSUIDSGID=yes
+RuntimeMaxSec=5min
+SystemCallArchitectures=native
+SystemCallErrorNumber=EPERM
+SystemCallFilter=@system-service
+SystemCallFilter=~@privileged @resources
+UMask=0077