fwupd/libfwupdplugin/fu-coswid-firmware.c
Richard Hughes 5a2ef166ee Support loading COSWID when only one role has been set
CBOR supports removing the array for only one base element, which the python
uSWID tools now support; allow parsing this in fwupd.
2022-10-24 16:12:00 +01:00

636 lines
19 KiB
C

/*
* Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#define G_LOG_DOMAIN "FuFirmware"
#include "config.h"
#ifdef HAVE_CBOR
#include <cbor.h>
#endif
#include "fu-common.h"
#include "fu-coswid-common.h"
#include "fu-coswid-firmware.h"
/**
* FuCoswidFirmware:
*
* A coSWID SWID section.
*
* See also: [class@FuCoswidFirmware]
*/
typedef struct {
gchar *product;
gchar *summary;
gchar *colloquial_version;
FuCoswidVersionScheme version_scheme;
GPtrArray *links; /* of FuCoswidFirmwareLink */
GPtrArray *entities; /* of FuCoswidFirmwareEntity */
} FuCoswidFirmwarePrivate;
G_DEFINE_TYPE_WITH_PRIVATE(FuCoswidFirmware, fu_coswid_firmware, FU_TYPE_FIRMWARE)
#define GET_PRIVATE(o) (fu_coswid_firmware_get_instance_private(o))
typedef struct {
gchar *name;
gchar *regid;
FuCoswidEntityRole roles[6];
} FuCoswidFirmwareEntity;
typedef struct {
gchar *href;
FuCoswidLinkRel rel;
} FuCoswidFirmwareLink;
static void
fu_coswid_firmware_entity_free(FuCoswidFirmwareEntity *entity)
{
g_free(entity->name);
g_free(entity->regid);
g_free(entity);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCoswidFirmwareEntity, fu_coswid_firmware_entity_free)
static void
fu_coswid_firmware_link_free(FuCoswidFirmwareLink *link)
{
g_free(link->href);
g_free(link);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCoswidFirmwareLink, fu_coswid_firmware_link_free)
#ifdef HAVE_CBOR
G_DEFINE_AUTOPTR_CLEANUP_FUNC(cbor_item_t, cbor_intermediate_decref)
static gchar *
fu_coswid_firmware_strndup(cbor_item_t *item)
{
if (!cbor_string_is_definite(item))
return NULL;
return g_strndup((const gchar *)cbor_string_handle(item), cbor_string_length(item));
}
static gboolean
fu_coswid_firmware_parse_meta(FuCoswidFirmware *self, cbor_item_t *item, GError **error)
{
FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self);
struct cbor_pair *pairs = cbor_map_handle(item);
for (gsize i = 0; i < cbor_map_size(item); i++) {
FuCoswidTag tag_id = cbor_get_uint8(pairs[i].key);
if (tag_id == FU_COSWID_TAG_SUMMARY) {
priv->summary = fu_coswid_firmware_strndup(pairs[i].value);
} else if (tag_id == FU_COSWID_TAG_COLLOQUIAL_VERSION) {
priv->colloquial_version = fu_coswid_firmware_strndup(pairs[i].value);
}
}
/* success */
return TRUE;
}
static gboolean
fu_coswid_firmware_parse_link(FuCoswidFirmware *self, cbor_item_t *item, GError **error)
{
FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self);
struct cbor_pair *pairs = cbor_map_handle(item);
g_autoptr(FuCoswidFirmwareLink) link = g_new0(FuCoswidFirmwareLink, 1);
for (gsize i = 0; i < cbor_map_size(item); i++) {
FuCoswidTag tag_id = cbor_get_uint8(pairs[i].key);
if (tag_id == FU_COSWID_TAG_HREF) {
link->href = fu_coswid_firmware_strndup(pairs[i].value);
} else if (tag_id == FU_COSWID_TAG_REL) {
if (cbor_isa_negint(pairs[i].value))
link->rel = (-1) - cbor_get_uint8(pairs[i].value);
else
link->rel = cbor_get_uint8(pairs[i].value);
}
}
/* success */
g_ptr_array_add(priv->links, g_steal_pointer(&link));
return TRUE;
}
static gboolean
fu_coswid_firmware_parse_entity(FuCoswidFirmware *self, cbor_item_t *item, GError **error)
{
FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self);
struct cbor_pair *pairs = cbor_map_handle(item);
guint entity_role_cnt = 0;
g_autoptr(FuCoswidFirmwareEntity) entity = g_new0(FuCoswidFirmwareEntity, 1);
for (gsize i = 0; i < cbor_map_size(item); i++) {
FuCoswidTag tag_id = cbor_get_uint8(pairs[i].key);
if (tag_id == FU_COSWID_TAG_ENTITY_NAME) {
entity->name = fu_coswid_firmware_strndup(pairs[i].value);
} else if (tag_id == FU_COSWID_TAG_REG_ID) {
entity->regid = fu_coswid_firmware_strndup(pairs[i].value);
} else if (tag_id == FU_COSWID_TAG_ROLE) {
if (cbor_isa_uint(pairs[i].value)) {
FuCoswidEntityRole role = cbor_get_uint8(pairs[i].value);
entity->roles[entity_role_cnt++] = role;
}
for (guint j = 0; j < cbor_array_size(pairs[i].value); j++) {
g_autoptr(cbor_item_t) value = cbor_array_get(pairs[i].value, j);
FuCoswidEntityRole role = cbor_get_uint8(value);
if (entity_role_cnt >= G_N_ELEMENTS(entity->roles)) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"too many roles");
return FALSE;
}
entity->roles[entity_role_cnt++] = role;
}
}
}
/* success */
g_ptr_array_add(priv->entities, g_steal_pointer(&entity));
return TRUE;
}
#endif
static gboolean
fu_coswid_firmware_parse(FuFirmware *firmware,
GBytes *fw,
gsize offset,
FwupdInstallFlags flags,
GError **error)
{
#ifdef HAVE_CBOR
FuCoswidFirmware *self = FU_COSWID_FIRMWARE(firmware);
FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self);
struct cbor_load_result result = {0x0};
struct cbor_pair *pairs = NULL;
g_autoptr(cbor_item_t) item = NULL;
item = cbor_load(g_bytes_get_data(fw, NULL), g_bytes_get_size(fw), &result);
if (item == NULL) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"failed to parse CBOR at offset 0x%x: 0x%x",
(guint)result.error.position,
result.error.code);
return FALSE;
}
fu_firmware_set_size(firmware, result.read);
/* pretty-print the result */
if (g_getenv("FWUPD_CBOR_VERBOSE") != NULL) {
cbor_describe(item, stdout);
fflush(stdout);
}
/* parse out anything interesting */
pairs = cbor_map_handle(item);
for (gsize i = 0; i < cbor_map_size(item); i++) {
FuCoswidTag tag_id = cbor_get_uint8(pairs[i].key);
/* identity can be specified as a string or in binary */
if (tag_id == FU_COSWID_TAG_TAG_ID) {
g_autofree gchar *str = NULL;
if (cbor_isa_string(pairs[i].value)) {
str = fu_coswid_firmware_strndup(pairs[i].value);
} else if (cbor_isa_bytestring(pairs[i].value) &&
cbor_bytestring_length(pairs[i].value) == 16) {
str = fwupd_guid_to_string(
(const fwupd_guid_t *)cbor_bytestring_handle(pairs[i].value),
FWUPD_GUID_FLAG_NONE);
}
if (str != NULL)
fu_firmware_set_id(firmware, str);
} else if (tag_id == FU_COSWID_TAG_SOFTWARE_NAME) {
priv->product = fu_coswid_firmware_strndup(pairs[i].value);
} else if (tag_id == FU_COSWID_TAG_SOFTWARE_VERSION) {
g_autofree gchar *str = fu_coswid_firmware_strndup(pairs[i].value);
fu_firmware_set_version(firmware, str);
} else if (tag_id == FU_COSWID_TAG_VERSION_SCHEME) {
priv->version_scheme = cbor_get_uint16(pairs[i].value);
} else if (tag_id == FU_COSWID_TAG_SOFTWARE_META) {
if (!fu_coswid_firmware_parse_meta(self, pairs[i].value, error))
return FALSE;
} else if (tag_id == FU_COSWID_TAG_LINK) {
for (guint j = 0; j < cbor_array_size(pairs[i].value); j++) {
g_autoptr(cbor_item_t) value = cbor_array_get(pairs[i].value, j);
if (!fu_coswid_firmware_parse_link(self, value, error))
return FALSE;
}
} else if (tag_id == FU_COSWID_TAG_ENTITY) {
for (guint j = 0; j < cbor_array_size(pairs[i].value); j++) {
g_autoptr(cbor_item_t) value = cbor_array_get(pairs[i].value, j);
if (!fu_coswid_firmware_parse_entity(self, value, error))
return FALSE;
}
}
}
/* success */
return TRUE;
#else
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"not compiled with CBOR support");
return FALSE;
#endif
}
#ifdef HAVE_CBOR
static void
fu_coswid_firmware_write_tag_string(cbor_item_t *root, FuCoswidTag tag, const gchar *item)
{
g_autoptr(cbor_item_t) key = cbor_build_uint8(tag);
g_autoptr(cbor_item_t) val = cbor_build_string(item);
cbor_map_add(root, (struct cbor_pair){.key = key, .value = val});
}
static void
fu_coswid_firmware_write_tag_bool(cbor_item_t *root, FuCoswidTag tag, gboolean item)
{
g_autoptr(cbor_item_t) key = cbor_build_uint8(tag);
g_autoptr(cbor_item_t) val = cbor_build_bool(item);
cbor_map_add(root, (struct cbor_pair){.key = key, .value = val});
}
static void
fu_coswid_firmware_write_tag_uint16(cbor_item_t *root, FuCoswidTag tag, guint16 item)
{
g_autoptr(cbor_item_t) key = cbor_build_uint8(tag);
g_autoptr(cbor_item_t) val = cbor_build_uint16(item);
cbor_map_add(root, (struct cbor_pair){.key = key, .value = val});
}
static void
fu_coswid_firmware_write_tag_int8(cbor_item_t *root, FuCoswidTag tag, gint8 item)
{
g_autoptr(cbor_item_t) key = cbor_build_uint8(tag);
g_autoptr(cbor_item_t) val = cbor_new_int8();
if (item >= 0) {
cbor_set_uint8(val, item);
} else {
cbor_set_uint8(val, 0xFF - item);
cbor_mark_negint(val);
}
cbor_map_add(root, (struct cbor_pair){.key = key, .value = val});
}
static void
fu_coswid_firmware_write_tag_item(cbor_item_t *root, FuCoswidTag tag, cbor_item_t *item)
{
g_autoptr(cbor_item_t) key = cbor_build_uint8(tag);
cbor_map_add(root, (struct cbor_pair){.key = key, .value = item});
}
#endif
static GBytes *
fu_coswid_firmware_write(FuFirmware *firmware, GError **error)
{
#ifdef HAVE_CBOR
FuCoswidFirmware *self = FU_COSWID_FIRMWARE(firmware);
FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self);
gsize buflen;
gsize bufsz = 0;
g_autofree guchar *buf = NULL;
g_autoptr(cbor_item_t) root = cbor_new_indefinite_map();
g_autoptr(cbor_item_t) item_meta = cbor_new_indefinite_map();
/* preallocate the map structure */
fu_coswid_firmware_write_tag_string(root, FU_COSWID_TAG_LANG, "en-US");
if (fu_firmware_get_id(firmware) != NULL) {
fu_coswid_firmware_write_tag_string(root,
FU_COSWID_TAG_TAG_ID,
fu_firmware_get_id(firmware));
}
fu_coswid_firmware_write_tag_bool(root, FU_COSWID_TAG_CORPUS, TRUE);
if (priv->product != NULL) {
fu_coswid_firmware_write_tag_string(root,
FU_COSWID_TAG_SOFTWARE_NAME,
priv->product);
}
if (fu_firmware_get_version(firmware) != NULL) {
fu_coswid_firmware_write_tag_string(root,
FU_COSWID_TAG_SOFTWARE_VERSION,
fu_firmware_get_version(firmware));
}
if (priv->version_scheme != FU_COSWID_VERSION_SCHEME_UNKNOWN) {
fu_coswid_firmware_write_tag_uint16(root,
FU_COSWID_TAG_VERSION_SCHEME,
priv->version_scheme);
}
fu_coswid_firmware_write_tag_item(root, FU_COSWID_TAG_SOFTWARE_META, item_meta);
fu_coswid_firmware_write_tag_string(item_meta, FU_COSWID_TAG_GENERATOR, PACKAGE_NAME);
if (priv->summary != NULL) {
fu_coswid_firmware_write_tag_string(item_meta,
FU_COSWID_TAG_SUMMARY,
priv->summary);
}
if (priv->colloquial_version != NULL) {
fu_coswid_firmware_write_tag_string(item_meta,
FU_COSWID_TAG_COLLOQUIAL_VERSION,
priv->colloquial_version);
}
/* add entities */
if (priv->entities->len > 0) {
g_autoptr(cbor_item_t) item_entities = cbor_new_indefinite_array();
for (guint i = 0; i < priv->entities->len; i++) {
FuCoswidFirmwareEntity *entity = g_ptr_array_index(priv->entities, i);
g_autoptr(cbor_item_t) item_entity = cbor_new_indefinite_map();
g_autoptr(cbor_item_t) item_roles = cbor_new_indefinite_array();
if (entity->name != NULL) {
fu_coswid_firmware_write_tag_string(item_entity,
FU_COSWID_TAG_ENTITY_NAME,
entity->name);
}
if (entity->regid != NULL) {
fu_coswid_firmware_write_tag_string(item_entity,
FU_COSWID_TAG_REG_ID,
entity->regid);
}
for (guint j = 0; entity->roles[j] != FU_COSWID_ENTITY_ROLE_UNKNOWN; j++) {
g_autoptr(cbor_item_t) item_role =
cbor_build_uint8(entity->roles[j]);
cbor_array_push(item_roles, item_role);
}
fu_coswid_firmware_write_tag_item(item_entity,
FU_COSWID_TAG_ROLE,
item_roles);
cbor_array_push(item_entities, item_entity);
}
fu_coswid_firmware_write_tag_item(root, FU_COSWID_TAG_ENTITY, item_entities);
}
/* add links */
if (priv->links->len > 0) {
g_autoptr(cbor_item_t) item_links = cbor_new_indefinite_array();
for (guint i = 0; i < priv->links->len; i++) {
FuCoswidFirmwareLink *link = g_ptr_array_index(priv->links, i);
g_autoptr(cbor_item_t) item_link = cbor_new_indefinite_map();
if (link->href != NULL) {
fu_coswid_firmware_write_tag_string(item_link,
FU_COSWID_TAG_HREF,
link->href);
}
fu_coswid_firmware_write_tag_int8(item_link, FU_COSWID_TAG_REL, link->rel);
cbor_array_push(item_links, item_link);
}
fu_coswid_firmware_write_tag_item(root, FU_COSWID_TAG_LINK, item_links);
}
/* serialize */
buflen = cbor_serialize_alloc(root, &buf, &bufsz);
if (buflen > bufsz) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"CBOR allocation failure");
return NULL;
}
return g_bytes_new(buf, buflen);
#else
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"not compiled with CBOR support");
return NULL;
#endif
}
static gboolean
fu_coswid_firmware_build_entity(FuCoswidFirmware *self, XbNode *n, GError **error)
{
FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self);
const gchar *tmp;
guint entity_role_cnt = 0;
FuCoswidEntityRole role;
g_autoptr(GPtrArray) roles = NULL;
g_autoptr(FuCoswidFirmwareEntity) entity = g_new0(FuCoswidFirmwareEntity, 1);
/* these are required */
tmp = xb_node_query_text(n, "name", error);
if (tmp == NULL)
return FALSE;
entity->name = g_strdup(tmp);
tmp = xb_node_query_text(n, "regid", error);
if (tmp == NULL)
return FALSE;
entity->regid = g_strdup(tmp);
/* optional */
roles = xb_node_query(n, "role", 0, NULL);
if (roles != NULL) {
for (guint i = 0; i < roles->len; i++) {
XbNode *c = g_ptr_array_index(roles, i);
tmp = xb_node_get_text(c);
role = fu_coswid_entity_role_from_string(tmp);
if (role == FU_COSWID_ENTITY_ROLE_UNKNOWN) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"failed to parse entity role %s",
tmp);
return FALSE;
}
if (entity_role_cnt >= G_N_ELEMENTS(entity->roles)) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"too many roles");
return FALSE;
}
entity->roles[entity_role_cnt++] = role;
}
}
/* success */
g_ptr_array_add(priv->entities, g_steal_pointer(&entity));
return TRUE;
}
static gboolean
fu_coswid_firmware_build_link(FuCoswidFirmware *self, XbNode *n, GError **error)
{
FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self);
const gchar *tmp;
g_autoptr(FuCoswidFirmwareLink) link = g_new0(FuCoswidFirmwareLink, 1);
/* required */
tmp = xb_node_query_text(n, "href", error);
if (tmp == NULL)
return FALSE;
link->href = g_strdup(tmp);
/* optional */
tmp = xb_node_query_text(n, "rel", NULL);
if (tmp != NULL) {
link->rel = fu_coswid_link_rel_from_string(tmp);
if (link->rel == FU_COSWID_LINK_REL_UNKNOWN) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"failed to parse link rel %s",
tmp);
return FALSE;
}
}
/* success */
g_ptr_array_add(priv->links, g_steal_pointer(&link));
return TRUE;
}
static gboolean
fu_coswid_firmware_build(FuFirmware *firmware, XbNode *n, GError **error)
{
FuCoswidFirmware *self = FU_COSWID_FIRMWARE(firmware);
FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self);
const gchar *tmp;
g_autoptr(GPtrArray) links = NULL;
g_autoptr(GPtrArray) entities = NULL;
/* simple properties */
tmp = xb_node_query_text(n, "product", NULL);
if (tmp != NULL)
priv->product = g_strdup(tmp);
tmp = xb_node_query_text(n, "summary", NULL);
if (tmp != NULL)
priv->summary = g_strdup(tmp);
tmp = xb_node_query_text(n, "colloquial_version", NULL);
if (tmp != NULL)
priv->colloquial_version = g_strdup(tmp);
tmp = xb_node_query_text(n, "version_scheme", NULL);
if (tmp != NULL) {
priv->version_scheme = fu_coswid_version_scheme_from_string(tmp);
if (priv->version_scheme == FU_COSWID_VERSION_SCHEME_UNKNOWN) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"failed to parse version_scheme %s",
tmp);
return FALSE;
}
}
/* multiple links allowed */
links = xb_node_query(n, "link", 0, NULL);
if (links != NULL) {
for (guint i = 0; i < links->len; i++) {
XbNode *c = g_ptr_array_index(links, i);
if (!fu_coswid_firmware_build_link(self, c, error))
return FALSE;
}
}
/* multiple entities allowed */
entities = xb_node_query(n, "entity", 0, NULL);
if (entities != NULL) {
for (guint i = 0; i < entities->len; i++) {
XbNode *c = g_ptr_array_index(entities, i);
if (!fu_coswid_firmware_build_entity(self, c, error))
return FALSE;
}
}
/* success */
return TRUE;
}
static void
fu_coswid_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn)
{
FuCoswidFirmware *self = FU_COSWID_FIRMWARE(firmware);
FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self);
if (priv->version_scheme != FU_COSWID_VERSION_SCHEME_UNKNOWN) {
fu_xmlb_builder_insert_kv(bn,
"version_scheme",
fu_coswid_version_scheme_to_string(priv->version_scheme));
}
fu_xmlb_builder_insert_kv(bn, "product", priv->product);
fu_xmlb_builder_insert_kv(bn, "summary", priv->summary);
fu_xmlb_builder_insert_kv(bn, "colloquial_version", priv->colloquial_version);
for (guint i = 0; i < priv->links->len; i++) {
FuCoswidFirmwareLink *link = g_ptr_array_index(priv->links, i);
g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, "link", NULL);
fu_xmlb_builder_insert_kv(bc, "href", link->href);
if (link->rel != FU_COSWID_LINK_REL_UNKNOWN) {
fu_xmlb_builder_insert_kv(bc,
"rel",
fu_coswid_link_rel_to_string(link->rel));
}
}
for (guint i = 0; i < priv->entities->len; i++) {
FuCoswidFirmwareEntity *entity = g_ptr_array_index(priv->entities, i);
g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, "entity", NULL);
fu_xmlb_builder_insert_kv(bc, "name", entity->name);
fu_xmlb_builder_insert_kv(bc, "regid", entity->regid);
for (guint j = 0; entity->roles[j] != FU_COSWID_ENTITY_ROLE_UNKNOWN; j++) {
fu_xmlb_builder_insert_kv(
bc,
"role",
fu_coswid_entity_role_to_string(entity->roles[j]));
}
}
}
static void
fu_coswid_firmware_init(FuCoswidFirmware *self)
{
FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self);
priv->version_scheme = FU_COSWID_VERSION_SCHEME_SEMVER;
priv->links = g_ptr_array_new_with_free_func((GDestroyNotify)fu_coswid_firmware_link_free);
priv->entities =
g_ptr_array_new_with_free_func((GDestroyNotify)fu_coswid_firmware_entity_free);
}
static void
fu_coswid_firmware_finalize(GObject *object)
{
FuCoswidFirmware *self = FU_COSWID_FIRMWARE(object);
FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self);
g_free(priv->product);
g_free(priv->summary);
g_free(priv->colloquial_version);
g_ptr_array_unref(priv->links);
g_ptr_array_unref(priv->entities);
G_OBJECT_CLASS(fu_coswid_firmware_parent_class)->finalize(object);
}
static void
fu_coswid_firmware_class_init(FuCoswidFirmwareClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass);
object_class->finalize = fu_coswid_firmware_finalize;
klass_firmware->parse = fu_coswid_firmware_parse;
klass_firmware->write = fu_coswid_firmware_write;
klass_firmware->build = fu_coswid_firmware_build;
klass_firmware->export = fu_coswid_firmware_export;
}
/**
* fu_coswid_firmware_new:
*
* Creates a new #FuFirmware of sub type coSWID
*
* Since: 1.8.0
**/
FuFirmware *
fu_coswid_firmware_new(void)
{
return FU_FIRMWARE(g_object_new(FU_TYPE_COSWID_FIRMWARE, NULL));
}