From 768b6f21ec950bbfff345c54a189050698dbb3ee Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Mon, 29 Jun 2026 22:17:56 +0100 Subject: [PATCH] report: Add a TPM2 signing backend This adds an initial TPM2 implementation of the io.systemd.Report.Signer interface, which returns a set of signed TPM attestations that also includes the supplied report digest. The returned report consists of multiple components: - A signed PCR quote generated with TPM2_Quote. - Signed NvPCR attestations generated with TPM2_NV_Certify. - A signed audit session attestation generated with TPM2_GetSessionAuditDigest. Each signed report component includes the JSON encoded TPMS_ATTEST attestation structure and corresponding signature, in both PEM encoded form and as a JSON encoded TPMT_SIGNATURE structure. NvPCR report components also include the readable name of the NvPCR, the NV index public area as the JSON encoded TPMS_NV_PUBLIC structure, and some additional authenticated data that is digested and supplied as the qualifying data to the TPM2_NV_Certify command. This authenticated data includes the NvPCR name and priority. The report digest that is supplied via varlink is supplied as the qualifying data to the audit session attestation. The returned report also includes the event log which is retrieved from pcrlock's varlink interface, and the public area of the signing key in both PEM encoded form and as a JSON encoded TPMT_PUBLIC structure. The audit session attestation is always the last component. The preceding components are all executed with the audit session, and the signer requires the audit session to remain exclusive whilst generating the report. What this does is provide evidence to a verifier that the overall report is a single, consistent snapshot of the system (ie, no other TPM commands executed whilst generating the report) because the audit session attestation contains a field that indicates that the session was exclusive at the time it was attested. The set of attested PCRs and NvPCRs are currently hardcoded. It includes all firmware PCRs with the exception of PCR6, and all other PCRs with the exception of the IMA one (which isn't useful on its own without the IMA log). All defined NvPCRs are included. A verifier can verify the signed attestation components individually by verifying their signatures against the supplied attestation data, using the supplied public key. For the PCR report component, a verifier can replay the measurements in the log in order to reconstruct the PCR values, and then use this and the PCR selection from the attestation in order to calculate the PCR digest, and then check that this matches the one in the attestation. For the NvPCR report components, a verifier checks that the NV index name in the attestation matches one that is anchored in PCR9 to be sure that the attestation is for a valid NvPCR. A verifier can then replay the measurements in the log to ensure that the resulting NvPCR value matches the one supplied in the attestation. The authenticated data is a JSON blob containing the NvPCR name and priority. A verifier can digest this and ensure it matches the qualifying data in the attestation. The audit session attestation includes the audit session digest and whether the audit session was still exclusive when the attestation was generated. A verifier should confirm exclusivity. The digest includes a record of all TPM commands executed with the audit session, as each command results in it being extended with the command parameter hash and the result parameter hash. A verifier should replay the preceding report components to compute an expected audit digest and make sure that this digest matches the one in the attestation in order to be sure that the overall report represents a single, consistent snapshot. The other report components provide sufficient information for a verifier to reconstruct the cpHash and rpHash. When reconstructing the cpHash for the PCR and NvPCR components, the signing scheme is the NULL scheme (they use the attestation key's signing scheme). The required signing key public area is supplied in the report. The supplied qualifying data is in the extra data field of the attestation. The PCR selection is also required to reconstruct the cpHash for the PCR component. The PCR selection is available in the attestation. The name of the NV index is also required to reconstruct the cpHash for the NvPCR components. The name is available in the attestation. The rpHash for each component can be reconstructed from the supplied attestation blob and signature. In the future, we might want to add more report component types. Eg, we could add report components for ordinary NV indexes. Using the audit session means that we could also add additional components that the TPM can't attest directly, such as the values of interesting TPM properties (as the TPM2_GetCapability calls will appear in the audit session digest). The attestation signing key is stored as a JSON file in the report signer's state directory. Different types of keys are supported: - Ordinary keys that are protected by an existing persistent TPM key such as the SRK or EK. - Persistent TPM keys. - Primary keys, which are recreated using a template on use. When an ordinary or primary key is used, the report signer saves the object context in its runtime directory. This context is used on future invocations. This has the benefit that where a primary key is used, it doesn't need to be recreated on each use. When an ordinary key is used, we don't need to run TPM2_Load, which requires authorization with the parent key, on each This authorization currently requires a policy session for low-range EKs. Regardless of the key type, it must be a restricted signing key. If no key exists, an ordinary key is generated automatically. The attestation key can also have an associated credential which is attached to the signed report. The credential can be issued by a CA or some other party during onboarding, and is presented to a verifier as proof that the attestation key is authentic. It is considered an opaque blob in order to not require it to be a specific format, such as a X.509 certificate. I've called this a "voucher" though because "cred" or "credential" has other meanings in systemd. As a follow-up to this, I plan to add a varlink interface for creating attestation keys that can be used for report signing. --- man/rules/meson.build | 4 + man/systemd-report-sign-tpm2@.service.xml | 112 ++ presets/90-systemd.preset | 1 + src/report/meson.build | 14 + src/report/report-sign-tpm2.c | 784 ++++++++++++ src/report/report-sign.c | 4 - src/shared/crypto-util.c | 2 + src/shared/crypto-util.h | 1 + src/shared/meson.build | 4 + src/shared/tpm2-report.c | 472 +++++++ src/shared/tpm2-report.h | 66 + src/shared/tpm2-util.c | 1333 +++++++++++++++++++- src/shared/tpm2-util.h | 40 + src/test/test-tpm2.c | 654 ++++++++++ test/units/TEST-70-TPM2.report-sign.sh | 532 ++++++++ units/meson.build | 8 + units/systemd-report-sign-tpm2.socket | 31 + units/systemd-report-sign-tpm2@.service.in | 53 + 18 files changed, 4106 insertions(+), 9 deletions(-) create mode 100644 man/systemd-report-sign-tpm2@.service.xml create mode 100644 src/report/report-sign-tpm2.c create mode 100644 src/shared/tpm2-report.c create mode 100644 src/shared/tpm2-report.h create mode 100755 test/units/TEST-70-TPM2.report-sign.sh create mode 100644 units/systemd-report-sign-tpm2.socket create mode 100644 units/systemd-report-sign-tpm2@.service.in 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