| // SPDX-License-Identifier: GPL-2.0-only | |
| /* Copyright(c) 2023 Intel Corporation. All rights reserved. */ | |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt | |
| #include <linux/tsm.h> | |
| #include <linux/err.h> | |
| #include <linux/slab.h> | |
| #include <linux/rwsem.h> | |
| #include <linux/string.h> | |
| #include <linux/module.h> | |
| #include <linux/cleanup.h> | |
| #include <linux/configfs.h> | |
| static struct tsm_provider { | |
| const struct tsm_ops *ops; | |
| void *data; | |
| } provider; | |
| static DECLARE_RWSEM(tsm_rwsem); | |
| /** | |
| * DOC: Trusted Security Module (TSM) Attestation Report Interface | |
| * | |
| * The TSM report interface is a common provider of blobs that facilitate | |
| * attestation of a TVM (confidential computing guest) by an attestation | |
| * service. A TSM report combines a user-defined blob (likely a public-key with | |
| * a nonce for a key-exchange protocol) with a signed attestation report. That | |
| * combined blob is then used to obtain secrets provided by an agent that can | |
| * validate the attestation report. The expectation is that this interface is | |
| * invoked infrequently, however configfs allows for multiple agents to | |
| * own their own report generation instances to generate reports as | |
| * often as needed. | |
| * | |
| * The attestation report format is TSM provider specific, when / if a standard | |
| * materializes that can be published instead of the vendor layout. Until then | |
| * the 'provider' attribute indicates the format of 'outblob', and optionally | |
| * 'auxblob' and 'manifestblob'. | |
| */ | |
| struct tsm_report_state { | |
| struct tsm_report report; | |
| unsigned long write_generation; | |
| unsigned long read_generation; | |
| struct config_item cfg; | |
| }; | |
| enum tsm_data_select { | |
| TSM_REPORT, | |
| TSM_CERTS, | |
| TSM_MANIFEST, | |
| }; | |
| static struct tsm_report *to_tsm_report(struct config_item *cfg) | |
| { | |
| struct tsm_report_state *state = | |
| container_of(cfg, struct tsm_report_state, cfg); | |
| return &state->report; | |
| } | |
| static struct tsm_report_state *to_state(struct tsm_report *report) | |
| { | |
| return container_of(report, struct tsm_report_state, report); | |
| } | |
| static int try_advance_write_generation(struct tsm_report *report) | |
| { | |
| struct tsm_report_state *state = to_state(report); | |
| lockdep_assert_held_write(&tsm_rwsem); | |
| /* | |
| * Malicious or broken userspace has written enough times for | |
| * read_generation == write_generation by modular arithmetic without an | |
| * interim read. Stop accepting updates until the current report | |
| * configuration is read. | |
| */ | |
| if (state->write_generation == state->read_generation - 1) | |
| return -EBUSY; | |
| state->write_generation++; | |
| return 0; | |
| } | |
| static ssize_t tsm_report_privlevel_store(struct config_item *cfg, | |
| const char *buf, size_t len) | |
| { | |
| struct tsm_report *report = to_tsm_report(cfg); | |
| unsigned int val; | |
| int rc; | |
| rc = kstrtouint(buf, 0, &val); | |
| if (rc) | |
| return rc; | |
| /* | |
| * The valid privilege levels that a TSM might accept, if it accepts a | |
| * privilege level setting at all, are a max of TSM_PRIVLEVEL_MAX (see | |
| * SEV-SNP GHCB) and a minimum of a TSM selected floor value no less | |
| * than 0. | |
| */ | |
| if (provider.ops->privlevel_floor > val || val > TSM_PRIVLEVEL_MAX) | |
| return -EINVAL; | |
| guard(rwsem_write)(&tsm_rwsem); | |
| rc = try_advance_write_generation(report); | |
| if (rc) | |
| return rc; | |
| report->desc.privlevel = val; | |
| return len; | |
| } | |
| CONFIGFS_ATTR_WO(tsm_report_, privlevel); | |
| static ssize_t tsm_report_privlevel_floor_show(struct config_item *cfg, | |
| char *buf) | |
| { | |
| guard(rwsem_read)(&tsm_rwsem); | |
| return sysfs_emit(buf, "%u\n", provider.ops->privlevel_floor); | |
| } | |
| CONFIGFS_ATTR_RO(tsm_report_, privlevel_floor); | |
| static ssize_t tsm_report_service_provider_store(struct config_item *cfg, | |
| const char *buf, size_t len) | |
| { | |
| struct tsm_report *report = to_tsm_report(cfg); | |
| size_t sp_len; | |
| char *sp; | |
| int rc; | |
| guard(rwsem_write)(&tsm_rwsem); | |
| rc = try_advance_write_generation(report); | |
| if (rc) | |
| return rc; | |
| sp_len = (buf[len - 1] != '\n') ? len : len - 1; | |
| sp = kstrndup(buf, sp_len, GFP_KERNEL); | |
| if (!sp) | |
| return -ENOMEM; | |
| kfree(report->desc.service_provider); | |
| report->desc.service_provider = sp; | |
| return len; | |
| } | |
| CONFIGFS_ATTR_WO(tsm_report_, service_provider); | |
| static ssize_t tsm_report_service_guid_store(struct config_item *cfg, | |
| const char *buf, size_t len) | |
| { | |
| struct tsm_report *report = to_tsm_report(cfg); | |
| int rc; | |
| guard(rwsem_write)(&tsm_rwsem); | |
| rc = try_advance_write_generation(report); | |
| if (rc) | |
| return rc; | |
| report->desc.service_guid = guid_null; | |
| rc = guid_parse(buf, &report->desc.service_guid); | |
| if (rc) | |
| return rc; | |
| return len; | |
| } | |
| CONFIGFS_ATTR_WO(tsm_report_, service_guid); | |
| static ssize_t tsm_report_service_manifest_version_store(struct config_item *cfg, | |
| const char *buf, size_t len) | |
| { | |
| struct tsm_report *report = to_tsm_report(cfg); | |
| unsigned int val; | |
| int rc; | |
| rc = kstrtouint(buf, 0, &val); | |
| if (rc) | |
| return rc; | |
| guard(rwsem_write)(&tsm_rwsem); | |
| rc = try_advance_write_generation(report); | |
| if (rc) | |
| return rc; | |
| report->desc.service_manifest_version = val; | |
| return len; | |
| } | |
| CONFIGFS_ATTR_WO(tsm_report_, service_manifest_version); | |
| static ssize_t tsm_report_inblob_write(struct config_item *cfg, | |
| const void *buf, size_t count) | |
| { | |
| struct tsm_report *report = to_tsm_report(cfg); | |
| int rc; | |
| guard(rwsem_write)(&tsm_rwsem); | |
| rc = try_advance_write_generation(report); | |
| if (rc) | |
| return rc; | |
| report->desc.inblob_len = count; | |
| memcpy(report->desc.inblob, buf, count); | |
| return count; | |
| } | |
| CONFIGFS_BIN_ATTR_WO(tsm_report_, inblob, NULL, TSM_INBLOB_MAX); | |
| static ssize_t tsm_report_generation_show(struct config_item *cfg, char *buf) | |
| { | |
| struct tsm_report *report = to_tsm_report(cfg); | |
| struct tsm_report_state *state = to_state(report); | |
| guard(rwsem_read)(&tsm_rwsem); | |
| return sysfs_emit(buf, "%lu\n", state->write_generation); | |
| } | |
| CONFIGFS_ATTR_RO(tsm_report_, generation); | |
| static ssize_t tsm_report_provider_show(struct config_item *cfg, char *buf) | |
| { | |
| guard(rwsem_read)(&tsm_rwsem); | |
| return sysfs_emit(buf, "%s\n", provider.ops->name); | |
| } | |
| CONFIGFS_ATTR_RO(tsm_report_, provider); | |
| static ssize_t __read_report(struct tsm_report *report, void *buf, size_t count, | |
| enum tsm_data_select select) | |
| { | |
| loff_t offset = 0; | |
| ssize_t len; | |
| u8 *out; | |
| if (select == TSM_REPORT) { | |
| out = report->outblob; | |
| len = report->outblob_len; | |
| } else if (select == TSM_MANIFEST) { | |
| out = report->manifestblob; | |
| len = report->manifestblob_len; | |
| } else { | |
| out = report->auxblob; | |
| len = report->auxblob_len; | |
| } | |
| /* | |
| * Recall that a NULL @buf is configfs requesting the size of | |
| * the buffer. | |
| */ | |
| if (!buf) | |
| return len; | |
| return memory_read_from_buffer(buf, count, &offset, out, len); | |
| } | |
| static ssize_t read_cached_report(struct tsm_report *report, void *buf, | |
| size_t count, enum tsm_data_select select) | |
| { | |
| struct tsm_report_state *state = to_state(report); | |
| guard(rwsem_read)(&tsm_rwsem); | |
| if (!report->desc.inblob_len) | |
| return -EINVAL; | |
| /* | |
| * A given TSM backend always fills in ->outblob regardless of | |
| * whether the report includes an auxblob/manifestblob or not. | |
| */ | |
| if (!report->outblob || | |
| state->read_generation != state->write_generation) | |
| return -EWOULDBLOCK; | |
| return __read_report(report, buf, count, select); | |
| } | |
| static ssize_t tsm_report_read(struct tsm_report *report, void *buf, | |
| size_t count, enum tsm_data_select select) | |
| { | |
| struct tsm_report_state *state = to_state(report); | |
| const struct tsm_ops *ops; | |
| ssize_t rc; | |
| /* try to read from the existing report if present and valid... */ | |
| rc = read_cached_report(report, buf, count, select); | |
| if (rc >= 0 || rc != -EWOULDBLOCK) | |
| return rc; | |
| /* slow path, report may need to be regenerated... */ | |
| guard(rwsem_write)(&tsm_rwsem); | |
| ops = provider.ops; | |
| if (!ops) | |
| return -ENOTTY; | |
| if (!report->desc.inblob_len) | |
| return -EINVAL; | |
| /* did another thread already generate this report? */ | |
| if (report->outblob && | |
| state->read_generation == state->write_generation) | |
| goto out; | |
| kvfree(report->outblob); | |
| kvfree(report->auxblob); | |
| kvfree(report->manifestblob); | |
| report->outblob = NULL; | |
| report->auxblob = NULL; | |
| report->manifestblob = NULL; | |
| rc = ops->report_new(report, provider.data); | |
| if (rc < 0) | |
| return rc; | |
| state->read_generation = state->write_generation; | |
| out: | |
| return __read_report(report, buf, count, select); | |
| } | |
| static ssize_t tsm_report_outblob_read(struct config_item *cfg, void *buf, | |
| size_t count) | |
| { | |
| struct tsm_report *report = to_tsm_report(cfg); | |
| return tsm_report_read(report, buf, count, TSM_REPORT); | |
| } | |
| CONFIGFS_BIN_ATTR_RO(tsm_report_, outblob, NULL, TSM_OUTBLOB_MAX); | |
| static ssize_t tsm_report_auxblob_read(struct config_item *cfg, void *buf, | |
| size_t count) | |
| { | |
| struct tsm_report *report = to_tsm_report(cfg); | |
| return tsm_report_read(report, buf, count, TSM_CERTS); | |
| } | |
| CONFIGFS_BIN_ATTR_RO(tsm_report_, auxblob, NULL, TSM_OUTBLOB_MAX); | |
| static ssize_t tsm_report_manifestblob_read(struct config_item *cfg, void *buf, | |
| size_t count) | |
| { | |
| struct tsm_report *report = to_tsm_report(cfg); | |
| return tsm_report_read(report, buf, count, TSM_MANIFEST); | |
| } | |
| CONFIGFS_BIN_ATTR_RO(tsm_report_, manifestblob, NULL, TSM_OUTBLOB_MAX); | |
| static struct configfs_attribute *tsm_report_attrs[] = { | |
| [TSM_REPORT_GENERATION] = &tsm_report_attr_generation, | |
| [TSM_REPORT_PROVIDER] = &tsm_report_attr_provider, | |
| [TSM_REPORT_PRIVLEVEL] = &tsm_report_attr_privlevel, | |
| [TSM_REPORT_PRIVLEVEL_FLOOR] = &tsm_report_attr_privlevel_floor, | |
| [TSM_REPORT_SERVICE_PROVIDER] = &tsm_report_attr_service_provider, | |
| [TSM_REPORT_SERVICE_GUID] = &tsm_report_attr_service_guid, | |
| [TSM_REPORT_SERVICE_MANIFEST_VER] = &tsm_report_attr_service_manifest_version, | |
| NULL, | |
| }; | |
| static struct configfs_bin_attribute *tsm_report_bin_attrs[] = { | |
| [TSM_REPORT_INBLOB] = &tsm_report_attr_inblob, | |
| [TSM_REPORT_OUTBLOB] = &tsm_report_attr_outblob, | |
| [TSM_REPORT_AUXBLOB] = &tsm_report_attr_auxblob, | |
| [TSM_REPORT_MANIFESTBLOB] = &tsm_report_attr_manifestblob, | |
| NULL, | |
| }; | |
| static void tsm_report_item_release(struct config_item *cfg) | |
| { | |
| struct tsm_report *report = to_tsm_report(cfg); | |
| struct tsm_report_state *state = to_state(report); | |
| kvfree(report->manifestblob); | |
| kvfree(report->auxblob); | |
| kvfree(report->outblob); | |
| kfree(report->desc.service_provider); | |
| kfree(state); | |
| } | |
| static struct configfs_item_operations tsm_report_item_ops = { | |
| .release = tsm_report_item_release, | |
| }; | |
| static bool tsm_report_is_visible(struct config_item *item, | |
| struct configfs_attribute *attr, int n) | |
| { | |
| guard(rwsem_read)(&tsm_rwsem); | |
| if (!provider.ops) | |
| return false; | |
| if (!provider.ops->report_attr_visible) | |
| return true; | |
| return provider.ops->report_attr_visible(n); | |
| } | |
| static bool tsm_report_is_bin_visible(struct config_item *item, | |
| struct configfs_bin_attribute *attr, int n) | |
| { | |
| guard(rwsem_read)(&tsm_rwsem); | |
| if (!provider.ops) | |
| return false; | |
| if (!provider.ops->report_bin_attr_visible) | |
| return true; | |
| return provider.ops->report_bin_attr_visible(n); | |
| } | |
| static struct configfs_group_operations tsm_report_attr_group_ops = { | |
| .is_visible = tsm_report_is_visible, | |
| .is_bin_visible = tsm_report_is_bin_visible, | |
| }; | |
| static const struct config_item_type tsm_report_type = { | |
| .ct_owner = THIS_MODULE, | |
| .ct_bin_attrs = tsm_report_bin_attrs, | |
| .ct_attrs = tsm_report_attrs, | |
| .ct_item_ops = &tsm_report_item_ops, | |
| .ct_group_ops = &tsm_report_attr_group_ops, | |
| }; | |
| static struct config_item *tsm_report_make_item(struct config_group *group, | |
| const char *name) | |
| { | |
| struct tsm_report_state *state; | |
| guard(rwsem_read)(&tsm_rwsem); | |
| if (!provider.ops) | |
| return ERR_PTR(-ENXIO); | |
| state = kzalloc(sizeof(*state), GFP_KERNEL); | |
| if (!state) | |
| return ERR_PTR(-ENOMEM); | |
| config_item_init_type_name(&state->cfg, name, &tsm_report_type); | |
| return &state->cfg; | |
| } | |
| static struct configfs_group_operations tsm_report_group_ops = { | |
| .make_item = tsm_report_make_item, | |
| }; | |
| static const struct config_item_type tsm_reports_type = { | |
| .ct_owner = THIS_MODULE, | |
| .ct_group_ops = &tsm_report_group_ops, | |
| }; | |
| static const struct config_item_type tsm_root_group_type = { | |
| .ct_owner = THIS_MODULE, | |
| }; | |
| static struct configfs_subsystem tsm_configfs = { | |
| .su_group = { | |
| .cg_item = { | |
| .ci_namebuf = "tsm", | |
| .ci_type = &tsm_root_group_type, | |
| }, | |
| }, | |
| .su_mutex = __MUTEX_INITIALIZER(tsm_configfs.su_mutex), | |
| }; | |
| int tsm_register(const struct tsm_ops *ops, void *priv) | |
| { | |
| const struct tsm_ops *conflict; | |
| guard(rwsem_write)(&tsm_rwsem); | |
| conflict = provider.ops; | |
| if (conflict) { | |
| pr_err("\"%s\" ops already registered\n", conflict->name); | |
| return -EBUSY; | |
| } | |
| provider.ops = ops; | |
| provider.data = priv; | |
| return 0; | |
| } | |
| EXPORT_SYMBOL_GPL(tsm_register); | |
| int tsm_unregister(const struct tsm_ops *ops) | |
| { | |
| guard(rwsem_write)(&tsm_rwsem); | |
| if (ops != provider.ops) | |
| return -EBUSY; | |
| provider.ops = NULL; | |
| provider.data = NULL; | |
| return 0; | |
| } | |
| EXPORT_SYMBOL_GPL(tsm_unregister); | |
| static struct config_group *tsm_report_group; | |
| static int __init tsm_init(void) | |
| { | |
| struct config_group *root = &tsm_configfs.su_group; | |
| struct config_group *tsm; | |
| int rc; | |
| config_group_init(root); | |
| rc = configfs_register_subsystem(&tsm_configfs); | |
| if (rc) | |
| return rc; | |
| tsm = configfs_register_default_group(root, "report", | |
| &tsm_reports_type); | |
| if (IS_ERR(tsm)) { | |
| configfs_unregister_subsystem(&tsm_configfs); | |
| return PTR_ERR(tsm); | |
| } | |
| tsm_report_group = tsm; | |
| return 0; | |
| } | |
| module_init(tsm_init); | |
| static void __exit tsm_exit(void) | |
| { | |
| configfs_unregister_default_group(tsm_report_group); | |
| configfs_unregister_subsystem(&tsm_configfs); | |
| } | |
| module_exit(tsm_exit); | |
| MODULE_LICENSE("GPL"); | |
| MODULE_DESCRIPTION("Provide Trusted Security Module attestation reports via configfs"); |