Files
systemd/src/report/report-sign-tpm2.c
T
Chris Coulson 768b6f21ec 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.
2026-08-20 15:30:20 +02:00

785 lines
32 KiB
C

/* 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);