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.
This commit is contained in:
committed by
Lennart Poettering
parent
ac80a8f4f3
commit
768b6f21ec
@@ -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'],
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0"?> <!--*-nxml-*-->
|
||||
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
|
||||
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
|
||||
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
|
||||
|
||||
<refentry id="systemd-report-sign-tpm2_.service" conditional='HAVE_OPENSSL HAVE_TPM2'
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
|
||||
<refentryinfo>
|
||||
<title>systemd-report-sign-tpm2@.service</title>
|
||||
<productname>systemd</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>systemd-report-sign-tpm2@.service</refentrytitle>
|
||||
<manvolnum>8</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname>systemd-report-sign-tpm2@.service</refname>
|
||||
<refname>systemd-report-sign-tpm2.socket</refname>
|
||||
<refname>systemd-report-sign-tpm2</refname>
|
||||
<refpurpose>Sign system reports with a set of signed TPM2 attestations</refpurpose>
|
||||
</refnamediv>
|
||||
|
||||
<refsynopsisdiv>
|
||||
<para><filename>systemd-report-sign-tpm2@.service</filename></para>
|
||||
<para><filename>systemd-report-sign-tpm2.socket</filename></para>
|
||||
<para><filename>/usr/lib/systemd/systemd-report-sign-tpm2</filename></para>
|
||||
</refsynopsisdiv>
|
||||
|
||||
<refsect1>
|
||||
<title>Description</title>
|
||||
|
||||
<para><command>systemd-report-sign-tpm2@.service</command> is a system service that signs system reports
|
||||
generated by
|
||||
<citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry>. It is
|
||||
a signing backend for the <option>--sign=</option> logic of that tool: it implements the
|
||||
<function>io.systemd.Report.Signer.Sign()</function> Varlink method and is reached via a socket linked into
|
||||
the <filename>/run/systemd/report.sign/</filename> directory, named <filename>tpm2</filename>.</para>
|
||||
|
||||
<para>The service is socket-activated (one instance per connection) via
|
||||
<filename>systemd-report-sign-tpm2.socket</filename>. 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.</para>
|
||||
|
||||
<para>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
|
||||
<constant>TPMT_SIGNATURE</constant> structure and in PEM encoded form. The signed component types are:</para>
|
||||
|
||||
<itemizedlist>
|
||||
<listitem><para>A PCR quote, generated with <constant>TPM2_Quote</constant></para></listitem>
|
||||
|
||||
<listitem><para>One attestation per NvPCR, generated with <constant>TPM2_NV_Certify</constant>. Each
|
||||
component also includes the readable name of the NvPCR, the NV index public area (as a JSON encoded
|
||||
<constant>TPMS_NV_PUBLIC</constant> 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.</para></listitem>
|
||||
|
||||
<listitem><para>An audit session attestation, generated with
|
||||
<constant>TPM2_GetSessionAuditDigest</constant>. This is always the last component. The report digest
|
||||
passed in via Varlink is supplied as its qualifying data.</para></listitem>
|
||||
</itemizedlist>
|
||||
|
||||
<para>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 <constant>TPMT_PUBLIC</constant> structure, and in PEM
|
||||
encooded form.</para>
|
||||
|
||||
<para>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.</para>
|
||||
|
||||
<para>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.</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>Signing key</title>
|
||||
|
||||
<para>The attestation signing key is stored as a JSON file named <filename>default.key</filename> in
|
||||
<filename>/var/lib/systemd/report.sign.tpm2/</filename>. 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.</para>
|
||||
|
||||
<para>For ordinary and primary keys, the loaded object context is cached in the service's runtime
|
||||
directory as <filename>/run/systemd/report.sign.tpm2/default.context</filename> and reused on subsequent
|
||||
invocations, avoiding a repeated <constant>TPM2_Load</constant> (which requires authorization with the
|
||||
parent key) or primary key recreation.</para>
|
||||
|
||||
<para>The key may optionally have an associated <emphasis>voucher</emphasis>, stored alongside it as
|
||||
<filename>default.voucher</filename>. 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).</para>
|
||||
</refsect1>
|
||||
|
||||
<!-- Note: we do not document the command line switches here. The systemd-report-sign-tpm2 binary itself
|
||||
should not be invoked by users, but only as a service. Use systemd-report(1) to generate signed
|
||||
reports. -->
|
||||
|
||||
<refsect1>
|
||||
<title>See Also</title>
|
||||
<para><simplelist type="inline">
|
||||
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
</simplelist></para>
|
||||
</refsect1>
|
||||
|
||||
</refentry>
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,784 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#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);
|
||||
@@ -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");
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#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
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include <sys/uio.h>
|
||||
|
||||
#include "forward.h"
|
||||
|
||||
#if HAVE_TPM2
|
||||
|
||||
#include <tss2/tss2_tpm2_types.h>
|
||||
|
||||
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
|
||||
+1328
-5
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,8 @@ int dlopen_tpm2(int log_level) _dlopen_loader_;
|
||||
#include <tss2/tss2_mu.h> /* IWYU pragma: export */
|
||||
#include <tss2/tss2_rc.h> /* 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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Executable
+532
@@ -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" ]
|
||||
@@ -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' },
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user