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