mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-02 05:47:46 +00:00
1086 lines
30 KiB
C
1086 lines
30 KiB
C
/*
|
|
* Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.1+
|
|
*/
|
|
|
|
#define G_LOG_DOMAIN "FuCabinet"
|
|
|
|
#include "config.h"
|
|
|
|
#include <gio/gio.h>
|
|
#include <libgcab.h>
|
|
|
|
#include "fwupd-common.h"
|
|
#include "fwupd-enums.h"
|
|
#include "fwupd-error.h"
|
|
|
|
#include "fu-cabinet.h"
|
|
#include "fu-common.h"
|
|
|
|
/**
|
|
* FuCabinet:
|
|
*
|
|
* Cabinet archive parser and writer.
|
|
*
|
|
* See also: [class@FuArchive]
|
|
*/
|
|
|
|
struct _FuCabinet {
|
|
GObject parent_instance;
|
|
guint64 size_max;
|
|
GCabCabinet *gcab_cabinet;
|
|
gchar *container_checksum;
|
|
XbBuilder *builder;
|
|
XbSilo *silo;
|
|
JcatContext *jcat_context;
|
|
JcatFile *jcat_file;
|
|
};
|
|
|
|
G_DEFINE_TYPE(FuCabinet, fu_cabinet, G_TYPE_OBJECT)
|
|
|
|
static void
|
|
fu_cabinet_finalize(GObject *obj)
|
|
{
|
|
FuCabinet *self = FU_CABINET(obj);
|
|
if (self->silo != NULL)
|
|
g_object_unref(self->silo);
|
|
if (self->builder != NULL)
|
|
g_object_unref(self->builder);
|
|
g_free(self->container_checksum);
|
|
g_object_unref(self->gcab_cabinet);
|
|
g_object_unref(self->jcat_context);
|
|
g_object_unref(self->jcat_file);
|
|
G_OBJECT_CLASS(fu_cabinet_parent_class)->finalize(obj);
|
|
}
|
|
|
|
static void
|
|
fu_cabinet_class_init(FuCabinetClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS(klass);
|
|
object_class->finalize = fu_cabinet_finalize;
|
|
}
|
|
|
|
static void
|
|
fu_cabinet_init(FuCabinet *self)
|
|
{
|
|
self->size_max = 1024 * 1024 * 100;
|
|
self->gcab_cabinet = gcab_cabinet_new();
|
|
self->builder = xb_builder_new();
|
|
self->jcat_file = jcat_file_new();
|
|
self->jcat_context = jcat_context_new();
|
|
}
|
|
|
|
/**
|
|
* fu_cabinet_set_size_max:
|
|
* @self: a #FuCabinet
|
|
* @size_max: size in bytes
|
|
*
|
|
* Sets the maximum size of the decompressed cabinet file.
|
|
*
|
|
* Since: 1.4.0
|
|
**/
|
|
void
|
|
fu_cabinet_set_size_max(FuCabinet *self, guint64 size_max)
|
|
{
|
|
g_return_if_fail(FU_IS_CABINET(self));
|
|
self->size_max = size_max;
|
|
}
|
|
|
|
/**
|
|
* fu_cabinet_set_jcat_context: (skip):
|
|
* @self: a #FuCabinet
|
|
* @jcat_context: (nullable): a Jcat context
|
|
*
|
|
* Sets the Jcat context, which is used for setting the trust flags on the
|
|
* each release in the archive.
|
|
*
|
|
* Since: 1.4.0
|
|
**/
|
|
void
|
|
fu_cabinet_set_jcat_context(FuCabinet *self, JcatContext *jcat_context)
|
|
{
|
|
g_return_if_fail(FU_IS_CABINET(self));
|
|
g_return_if_fail(JCAT_IS_CONTEXT(jcat_context));
|
|
g_set_object(&self->jcat_context, jcat_context);
|
|
}
|
|
|
|
/**
|
|
* fu_cabinet_get_silo: (skip):
|
|
* @self: a #FuCabinet
|
|
*
|
|
* Gets the silo that represents the superset metadata of all the metainfo files
|
|
* found in the archive.
|
|
*
|
|
* Returns: (transfer full): a #XbSilo, or %NULL if the archive has not been parsed
|
|
*
|
|
* Since: 1.4.0
|
|
**/
|
|
XbSilo *
|
|
fu_cabinet_get_silo(FuCabinet *self)
|
|
{
|
|
g_return_val_if_fail(FU_IS_CABINET(self), NULL);
|
|
if (self->silo == NULL)
|
|
return NULL;
|
|
return g_object_ref(self->silo);
|
|
}
|
|
|
|
static GCabFile *
|
|
fu_cabinet_get_file_by_name(FuCabinet *self, const gchar *basename)
|
|
{
|
|
GPtrArray *folders = gcab_cabinet_get_folders(self->gcab_cabinet);
|
|
for (guint i = 0; i < folders->len; i++) {
|
|
GCabFolder *cabfolder = GCAB_FOLDER(g_ptr_array_index(folders, i));
|
|
GCabFile *cabfile = gcab_folder_get_file_by_name(cabfolder, basename);
|
|
if (cabfile != NULL)
|
|
return cabfile;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* fu_cabinet_add_file:
|
|
* @self: a #FuCabinet
|
|
* @basename: filename
|
|
* @data: file data
|
|
*
|
|
* Adds a file to the silo.
|
|
*
|
|
* Since: 1.6.0
|
|
**/
|
|
void
|
|
fu_cabinet_add_file(FuCabinet *self, const gchar *basename, GBytes *data)
|
|
{
|
|
GPtrArray *folders;
|
|
GCabFile *gcab_file_old;
|
|
g_autoptr(GCabFolder) gcab_folder = NULL;
|
|
g_autoptr(GCabFile) gcab_file = NULL;
|
|
|
|
g_return_if_fail(FU_IS_CABINET(self));
|
|
g_return_if_fail(basename != NULL);
|
|
g_return_if_fail(data != NULL);
|
|
|
|
/* existing file? */
|
|
gcab_file_old = fu_cabinet_get_file_by_name(self, basename);
|
|
if (gcab_file_old != NULL) {
|
|
g_object_set(gcab_file_old, "bytes", data, NULL);
|
|
return;
|
|
}
|
|
|
|
/* new file, in a possibly new folder */
|
|
folders = gcab_cabinet_get_folders(self->gcab_cabinet);
|
|
if (folders->len == 0) {
|
|
gcab_folder = gcab_folder_new(GCAB_COMPRESSION_NONE);
|
|
gcab_cabinet_add_folder(self->gcab_cabinet, gcab_folder, NULL);
|
|
} else {
|
|
gcab_folder = g_object_ref(GCAB_FOLDER(g_ptr_array_index(folders, 0)));
|
|
}
|
|
gcab_file = gcab_file_new_with_bytes(basename, data);
|
|
gcab_folder_add_file(gcab_folder, gcab_file, FALSE, NULL, NULL);
|
|
}
|
|
|
|
/**
|
|
* fu_cabinet_get_file:
|
|
* @self: a #FuCabinet
|
|
* @basename: filename
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Gets a file from the archive.
|
|
*
|
|
* Returns: (transfer full): a #GBytes, or %NULL if the file does not exist
|
|
*
|
|
* Since: 1.6.0
|
|
**/
|
|
GBytes *
|
|
fu_cabinet_get_file(FuCabinet *self, const gchar *basename, GError **error)
|
|
{
|
|
GCabFile *cabfile;
|
|
GBytes *blob;
|
|
|
|
g_return_val_if_fail(FU_IS_CABINET(self), NULL);
|
|
g_return_val_if_fail(basename != NULL, NULL);
|
|
|
|
cabfile = fu_cabinet_get_file_by_name(self, basename);
|
|
if (cabfile == NULL) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"cannot find %s in archive",
|
|
basename);
|
|
return NULL;
|
|
}
|
|
blob = gcab_file_get_bytes(cabfile);
|
|
if (blob == NULL) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"no GBytes from GCabFile firmware");
|
|
return NULL;
|
|
}
|
|
return g_bytes_ref(blob);
|
|
}
|
|
|
|
/* sets the firmware and signature blobs on XbNode */
|
|
static gboolean
|
|
fu_cabinet_parse_release(FuCabinet *self, XbNode *release, GError **error)
|
|
{
|
|
GCabFile *cabfile;
|
|
GBytes *blob;
|
|
const gchar *csum_filename = NULL;
|
|
g_autofree gchar *basename = NULL;
|
|
g_autoptr(XbNode) artifact = NULL;
|
|
g_autoptr(XbNode) csum_tmp = NULL;
|
|
g_autoptr(XbNode) metadata_trust = NULL;
|
|
g_autoptr(XbNode) nsize = NULL;
|
|
g_autoptr(JcatItem) item = NULL;
|
|
g_autoptr(GBytes) release_flags_blob = NULL;
|
|
FwupdReleaseFlags release_flags = FWUPD_RELEASE_FLAG_NONE;
|
|
|
|
/* we set this with XbBuilderSource before the silo was created */
|
|
metadata_trust = xb_node_query_first(release, "../../info/metadata_trust", NULL);
|
|
if (metadata_trust != NULL)
|
|
release_flags |= FWUPD_RELEASE_FLAG_TRUSTED_METADATA;
|
|
|
|
/* look for source artifact first */
|
|
artifact = xb_node_query_first(release, "artifacts/artifact[@type='binary']", NULL);
|
|
if (artifact != NULL) {
|
|
csum_filename = xb_node_query_text(artifact, "filename", NULL);
|
|
csum_tmp = xb_node_query_first(artifact, "checksum[@type='sha256']", NULL);
|
|
if (csum_tmp == NULL)
|
|
csum_tmp = xb_node_query_first(artifact, "checksum", NULL);
|
|
} else {
|
|
csum_tmp = xb_node_query_first(release, "checksum[@target='content']", NULL);
|
|
if (csum_tmp != NULL)
|
|
csum_filename = xb_node_get_attr(csum_tmp, "filename");
|
|
}
|
|
|
|
/* if this isn't true, a firmware needs to set in the metainfo.xml file
|
|
* something like: <checksum target="content" filename="FLASH.ROM"/> */
|
|
if (csum_filename == NULL)
|
|
csum_filename = "firmware.bin";
|
|
|
|
/* get the main firmware file */
|
|
basename = g_path_get_basename(csum_filename);
|
|
cabfile = fu_cabinet_get_file_by_name(self, basename);
|
|
if (cabfile == NULL) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"cannot find %s in archive",
|
|
basename);
|
|
return FALSE;
|
|
}
|
|
blob = gcab_file_get_bytes(cabfile);
|
|
if (blob == NULL) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"no GBytes from GCabFile firmware");
|
|
return FALSE;
|
|
}
|
|
|
|
/* set the blob */
|
|
xb_node_set_data(release, "fwupd::FirmwareBlob", blob);
|
|
|
|
/* set as metadata if unset, but error if specified and incorrect */
|
|
nsize = xb_node_query_first(release, "size[@type='installed']", NULL);
|
|
if (nsize != NULL) {
|
|
guint64 size = fu_common_strtoull(xb_node_get_text(nsize));
|
|
if (size != g_bytes_get_size(blob)) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"contents size invalid, expected "
|
|
"%" G_GSIZE_FORMAT ", got %" G_GUINT64_FORMAT,
|
|
g_bytes_get_size(blob),
|
|
size);
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
guint64 size = g_bytes_get_size(blob);
|
|
g_autoptr(GBytes) blob_sz = g_bytes_new(&size, sizeof(guint64));
|
|
xb_node_set_data(release, "fwupd::ReleaseSize", blob_sz);
|
|
}
|
|
|
|
/* set if unspecified, but error out if specified and incorrect */
|
|
if (csum_tmp != NULL && xb_node_get_text(csum_tmp) != NULL) {
|
|
const gchar *checksum_old = xb_node_get_text(csum_tmp);
|
|
GChecksumType checksum_type = fwupd_checksum_guess_kind(checksum_old);
|
|
g_autofree gchar *checksum = NULL;
|
|
checksum = g_compute_checksum_for_bytes(checksum_type, blob);
|
|
if (g_strcmp0(checksum, checksum_old) != 0) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"contents checksum invalid, expected %s, got %s",
|
|
checksum,
|
|
xb_node_get_text(csum_tmp));
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* find out if the payload is signed, falling back to detached */
|
|
item = jcat_file_get_item_by_id(self->jcat_file, basename, NULL);
|
|
if (item != NULL) {
|
|
g_autoptr(GError) error_local = NULL;
|
|
g_autoptr(GPtrArray) results = NULL;
|
|
results = jcat_context_verify_item(self->jcat_context,
|
|
blob,
|
|
item,
|
|
JCAT_VERIFY_FLAG_REQUIRE_CHECKSUM |
|
|
JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE,
|
|
&error_local);
|
|
if (results == NULL) {
|
|
g_debug("failed to verify payload %s: %s", basename, error_local->message);
|
|
} else {
|
|
g_debug("verified payload %s: %u", basename, results->len);
|
|
release_flags |= FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD;
|
|
}
|
|
|
|
/* legacy GPG detached signature */
|
|
} else {
|
|
g_autofree gchar *basename_sig = NULL;
|
|
basename_sig = g_strdup_printf("%s.asc", basename);
|
|
cabfile = fu_cabinet_get_file_by_name(self, basename_sig);
|
|
if (cabfile != NULL) {
|
|
GBytes *data_sig;
|
|
g_autoptr(JcatResult) jcat_result = NULL;
|
|
g_autoptr(JcatBlob) jcat_blob = NULL;
|
|
g_autoptr(GError) error_local = NULL;
|
|
|
|
data_sig = gcab_file_get_bytes(cabfile);
|
|
if (data_sig == NULL) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"no GBytes from GCabFile %s",
|
|
basename_sig);
|
|
return FALSE;
|
|
}
|
|
jcat_blob = jcat_blob_new(JCAT_BLOB_KIND_GPG, data_sig);
|
|
jcat_result = jcat_context_verify_blob(self->jcat_context,
|
|
blob,
|
|
jcat_blob,
|
|
JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE,
|
|
&error_local);
|
|
if (jcat_result == NULL) {
|
|
g_debug("failed to verify payload %s using detached: %s",
|
|
basename,
|
|
error_local->message);
|
|
} else {
|
|
g_debug("verified payload %s using detached", basename);
|
|
release_flags |= FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* this means we can get the data from fu_keyring_get_release_flags */
|
|
release_flags_blob = g_bytes_new(&release_flags, sizeof(release_flags));
|
|
xb_node_set_data(release, "fwupd::ReleaseFlags", release_flags_blob);
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gint
|
|
fu_cabinet_sort_cb(XbBuilderNode *bn1, XbBuilderNode *bn2, gpointer user_data)
|
|
{
|
|
guint64 prio1 = xb_builder_node_get_attr_as_uint(bn1, "priority");
|
|
guint64 prio2 = xb_builder_node_get_attr_as_uint(bn2, "priority");
|
|
if (prio1 > prio2)
|
|
return -1;
|
|
if (prio1 < prio2)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cabinet_sort_priority_cb(XbBuilderFixup *self,
|
|
XbBuilderNode *bn,
|
|
gpointer user_data,
|
|
GError **error)
|
|
{
|
|
xb_builder_node_sort_children(bn, fu_cabinet_sort_cb, user_data);
|
|
return TRUE;
|
|
}
|
|
|
|
static XbBuilderNode *
|
|
_xb_builder_node_get_child_by_element_attr(XbBuilderNode *bn,
|
|
const gchar *element,
|
|
const gchar *attr_name,
|
|
const gchar *attr_value)
|
|
{
|
|
GPtrArray *bcs = xb_builder_node_get_children(bn);
|
|
for (guint i = 0; i < bcs->len; i++) {
|
|
XbBuilderNode *bc = g_ptr_array_index(bcs, i);
|
|
if (g_strcmp0(xb_builder_node_get_element(bc), element) != 0)
|
|
continue;
|
|
if (g_strcmp0(xb_builder_node_get_attr(bc, attr_name), attr_value) == 0)
|
|
return g_object_ref(bc);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cabinet_set_container_checksum_cb(XbBuilderFixup *builder_fixup,
|
|
XbBuilderNode *bn,
|
|
gpointer user_data,
|
|
GError **error)
|
|
{
|
|
FuCabinet *self = FU_CABINET(user_data);
|
|
g_autoptr(XbBuilderNode) csum = NULL;
|
|
|
|
/* not us */
|
|
if (g_strcmp0(xb_builder_node_get_element(bn), "release") != 0)
|
|
return TRUE;
|
|
|
|
/* verify it exists */
|
|
csum = _xb_builder_node_get_child_by_element_attr(bn, "checksum", "type", "container");
|
|
if (csum == NULL) {
|
|
csum = xb_builder_node_insert(bn, "checksum", "target", "container", NULL);
|
|
}
|
|
|
|
/* verify it is correct */
|
|
if (g_strcmp0(xb_builder_node_get_text(csum), self->container_checksum) != 0) {
|
|
if (xb_builder_node_get_text(csum) != NULL) {
|
|
g_warning("invalid container checksum %s, fixing up to %s",
|
|
xb_builder_node_get_text(csum),
|
|
self->container_checksum);
|
|
}
|
|
xb_builder_node_set_text(csum, self->container_checksum, -1);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
fu_cabinet_fixup_checksum_children(XbBuilderNode *bn,
|
|
const gchar *element,
|
|
const gchar *attr_name,
|
|
const gchar *attr_value)
|
|
{
|
|
GPtrArray *bcs = xb_builder_node_get_children(bn);
|
|
for (guint i = 0; i < bcs->len; i++) {
|
|
XbBuilderNode *bc = g_ptr_array_index(bcs, i);
|
|
if (g_strcmp0(xb_builder_node_get_element(bc), element) != 0)
|
|
continue;
|
|
if (attr_value == NULL ||
|
|
g_strcmp0(xb_builder_node_get_attr(bc, attr_name), attr_value) == 0) {
|
|
const gchar *tmp = xb_builder_node_get_text(bc);
|
|
if (tmp != NULL) {
|
|
g_autofree gchar *lowercase = g_ascii_strdown(tmp, -1);
|
|
xb_builder_node_set_text(bc, lowercase, -1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
fu_cabinet_set_lowercase_checksum_cb(XbBuilderFixup *builder_fixup,
|
|
XbBuilderNode *bn,
|
|
gpointer user_data,
|
|
GError **error)
|
|
{
|
|
if (g_strcmp0(xb_builder_node_get_element(bn), "artifact") == 0)
|
|
/* don't care whether it's sha256, sha1 or something else so don't check for
|
|
* specific value */
|
|
fu_cabinet_fixup_checksum_children(bn, "checksum", "type", NULL);
|
|
else if (g_strcmp0(xb_builder_node_get_element(bn), "release") == 0)
|
|
fu_cabinet_fixup_checksum_children(bn, "checksum", "target", "content");
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* adds each GCabFile to the silo */
|
|
static gboolean
|
|
fu_cabinet_build_silo_file(FuCabinet *self,
|
|
GCabFile *cabfile,
|
|
FwupdReleaseFlags release_flags,
|
|
GError **error)
|
|
{
|
|
GBytes *blob;
|
|
g_autoptr(GError) error_local = NULL;
|
|
g_autoptr(XbBuilderSource) source = xb_builder_source_new();
|
|
g_autoptr(XbBuilderNode) bn_info = xb_builder_node_new("info");
|
|
|
|
/* indicate the metainfo file was signed */
|
|
if (release_flags & FWUPD_RELEASE_FLAG_TRUSTED_METADATA)
|
|
xb_builder_node_insert(bn_info, "metadata_trust", NULL);
|
|
xb_builder_node_insert_text(bn_info, "filename", gcab_file_get_name(cabfile), NULL);
|
|
xb_builder_source_set_info(source, bn_info);
|
|
|
|
/* rewrite to be under a components root */
|
|
xb_builder_source_set_prefix(source, "components");
|
|
|
|
/* parse file */
|
|
blob = gcab_file_get_bytes(cabfile);
|
|
if (blob == NULL) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"no GBytes from GCabFile");
|
|
return FALSE;
|
|
}
|
|
if (!xb_builder_source_load_bytes(source,
|
|
blob,
|
|
XB_BUILDER_SOURCE_FLAG_NONE,
|
|
&error_local)) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"could not parse MetaInfo XML: %s",
|
|
error_local->message);
|
|
return FALSE;
|
|
}
|
|
xb_builder_import_source(self->builder, source);
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cabinet_build_silo_metainfo(FuCabinet *self, GCabFile *cabfile, GError **error)
|
|
{
|
|
FwupdReleaseFlags release_flags = FWUPD_RELEASE_FLAG_NONE;
|
|
const gchar *fn = gcab_file_get_extract_name(cabfile);
|
|
g_autoptr(JcatItem) item = NULL;
|
|
|
|
/* validate against the Jcat file */
|
|
item = jcat_file_get_item_by_id(self->jcat_file, fn, NULL);
|
|
if (item == NULL) {
|
|
g_debug("failed to verify %s: no JcatItem", fn);
|
|
} else {
|
|
g_autoptr(GError) error_local = NULL;
|
|
g_autoptr(GPtrArray) results = NULL;
|
|
results = jcat_context_verify_item(self->jcat_context,
|
|
gcab_file_get_bytes(cabfile),
|
|
item,
|
|
JCAT_VERIFY_FLAG_REQUIRE_CHECKSUM |
|
|
JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE,
|
|
&error_local);
|
|
if (results == NULL) {
|
|
g_debug("failed to verify %s: %s", fn, error_local->message);
|
|
} else {
|
|
g_debug("verified metadata %s: %u", fn, results->len);
|
|
release_flags |= FWUPD_RELEASE_FLAG_TRUSTED_METADATA;
|
|
}
|
|
}
|
|
|
|
/* actually parse the XML now */
|
|
g_debug("processing file: %s", fn);
|
|
if (!fu_cabinet_build_silo_file(self, cabfile, release_flags, error)) {
|
|
g_prefix_error(error,
|
|
"%s could not be loaded: ",
|
|
gcab_file_get_extract_name(cabfile));
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
/* load the firmware.jcat files if included */
|
|
static gboolean
|
|
fu_cabinet_build_jcat_folder(FuCabinet *self, GCabFolder *cabfolder, GError **error)
|
|
{
|
|
g_autoptr(GSList) cabfiles = gcab_folder_get_files(cabfolder);
|
|
for (GSList *l = cabfiles; l != NULL; l = l->next) {
|
|
GCabFile *cabfile = GCAB_FILE(l->data);
|
|
const gchar *fn = gcab_file_get_extract_name(cabfile);
|
|
if (g_str_has_suffix(fn, ".jcat")) {
|
|
GBytes *data_jcat = gcab_file_get_bytes(cabfile);
|
|
g_autoptr(GInputStream) istream = NULL;
|
|
istream = g_memory_input_stream_new_from_bytes(data_jcat);
|
|
if (!jcat_file_import_stream(self->jcat_file,
|
|
istream,
|
|
JCAT_IMPORT_FLAG_NONE,
|
|
NULL,
|
|
error))
|
|
return FALSE;
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/* adds each GCabFolder to the silo */
|
|
static gboolean
|
|
fu_cabinet_build_silo_folder(FuCabinet *self, GCabFolder *cabfolder, GError **error)
|
|
{
|
|
g_autoptr(GSList) cabfiles = gcab_folder_get_files(cabfolder);
|
|
for (GSList *l = cabfiles; l != NULL; l = l->next) {
|
|
GCabFile *cabfile = GCAB_FILE(l->data);
|
|
const gchar *fn = gcab_file_get_extract_name(cabfile);
|
|
if (!g_str_has_suffix(fn, ".metainfo.xml"))
|
|
continue;
|
|
if (!fu_cabinet_build_silo_metainfo(self, cabfile, error))
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cabinet_build_silo(FuCabinet *self, GBytes *data, GError **error)
|
|
{
|
|
GPtrArray *folders;
|
|
g_autoptr(XbBuilderFixup) fixup1 = NULL;
|
|
g_autoptr(XbBuilderFixup) fixup2 = NULL;
|
|
g_autoptr(XbBuilderFixup) fixup3 = NULL;
|
|
|
|
/* verbose profiling */
|
|
if (g_getenv("FWUPD_XMLB_VERBOSE") != NULL) {
|
|
xb_builder_set_profile_flags(self->builder,
|
|
XB_SILO_PROFILE_FLAG_XPATH |
|
|
XB_SILO_PROFILE_FLAG_DEBUG);
|
|
}
|
|
|
|
/* load Jcat */
|
|
folders = gcab_cabinet_get_folders(self->gcab_cabinet);
|
|
if (self->jcat_context != NULL) {
|
|
for (guint i = 0; i < folders->len; i++) {
|
|
GCabFolder *cabfolder = GCAB_FOLDER(g_ptr_array_index(folders, i));
|
|
if (!fu_cabinet_build_jcat_folder(self, cabfolder, error))
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* adds each metainfo file to the silo */
|
|
for (guint i = 0; i < folders->len; i++) {
|
|
GCabFolder *cabfolder = GCAB_FOLDER(g_ptr_array_index(folders, i));
|
|
if (!fu_cabinet_build_silo_folder(self, cabfolder, error))
|
|
return FALSE;
|
|
}
|
|
|
|
/* sort the components by priority */
|
|
fixup1 = xb_builder_fixup_new("OrderByPriority", fu_cabinet_sort_priority_cb, NULL, NULL);
|
|
xb_builder_fixup_set_max_depth(fixup1, 0);
|
|
xb_builder_add_fixup(self->builder, fixup1);
|
|
|
|
/* ensure the container checksum is always set */
|
|
fixup2 = xb_builder_fixup_new("SetContainerChecksum",
|
|
fu_cabinet_set_container_checksum_cb,
|
|
self,
|
|
NULL);
|
|
xb_builder_add_fixup(self->builder, fixup2);
|
|
|
|
fixup3 = xb_builder_fixup_new("LowerCaseCheckSum",
|
|
fu_cabinet_set_lowercase_checksum_cb,
|
|
self,
|
|
NULL);
|
|
xb_builder_add_fixup(self->builder, fixup3);
|
|
|
|
/* did we get any valid files */
|
|
self->silo = xb_builder_compile(self->builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error);
|
|
if (self->silo == NULL)
|
|
return FALSE;
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
typedef struct {
|
|
FuCabinet *self;
|
|
guint64 size_total;
|
|
GError *error;
|
|
} FuCabinetDecompressHelper;
|
|
|
|
static gboolean
|
|
fu_cabinet_decompress_file_cb(GCabFile *file, gpointer user_data)
|
|
{
|
|
FuCabinetDecompressHelper *helper = (FuCabinetDecompressHelper *)user_data;
|
|
FuCabinet *self = FU_CABINET(helper->self);
|
|
g_autofree gchar *basename = NULL;
|
|
g_autofree gchar *name = NULL;
|
|
|
|
/* already failed */
|
|
if (helper->error != NULL)
|
|
return FALSE;
|
|
|
|
/* check the size of the compressed file */
|
|
if (gcab_file_get_size(file) > self->size_max) {
|
|
g_autofree gchar *sz_val = g_format_size(gcab_file_get_size(file));
|
|
g_autofree gchar *sz_max = g_format_size(self->size_max);
|
|
g_set_error(&helper->error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"file %s was too large (%s, limit %s)",
|
|
gcab_file_get_name(file),
|
|
sz_val,
|
|
sz_max);
|
|
return FALSE;
|
|
}
|
|
|
|
/* check the total size of all the compressed files */
|
|
helper->size_total += gcab_file_get_size(file);
|
|
if (helper->size_total > self->size_max) {
|
|
g_autofree gchar *sz_val = g_format_size(helper->size_total);
|
|
g_autofree gchar *sz_max = g_format_size(self->size_max);
|
|
g_set_error(&helper->error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"uncompressed data too large (%s, limit %s)",
|
|
sz_val,
|
|
sz_max);
|
|
return FALSE;
|
|
}
|
|
|
|
/* convert to UNIX paths */
|
|
name = g_strdup(gcab_file_get_name(file));
|
|
g_strdelimit(name, "\\", '/');
|
|
|
|
/* ignore the dirname completely */
|
|
basename = g_path_get_basename(name);
|
|
gcab_file_set_extract_name(file, basename);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cabinet_decompress(FuCabinet *self, GBytes *data, GError **error)
|
|
{
|
|
FuCabinetDecompressHelper helper = {
|
|
.self = self,
|
|
.size_total = 0,
|
|
.error = NULL,
|
|
};
|
|
g_autoptr(GError) error_local = NULL;
|
|
g_autoptr(GInputStream) istream = NULL;
|
|
|
|
/* load from a seekable stream */
|
|
istream = g_memory_input_stream_new_from_bytes(data);
|
|
if (!gcab_cabinet_load(self->gcab_cabinet, istream, NULL, error))
|
|
return FALSE;
|
|
|
|
/* check the size is sane */
|
|
if (gcab_cabinet_get_size(self->gcab_cabinet) > self->size_max) {
|
|
g_autofree gchar *sz_val = g_format_size(gcab_cabinet_get_size(self->gcab_cabinet));
|
|
g_autofree gchar *sz_max = g_format_size(self->size_max);
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"archive too large (%s, limit %s)",
|
|
sz_val,
|
|
sz_max);
|
|
return FALSE;
|
|
}
|
|
|
|
/* decompress the file to memory */
|
|
if (!gcab_cabinet_extract_simple(self->gcab_cabinet,
|
|
NULL,
|
|
fu_cabinet_decompress_file_cb,
|
|
&helper,
|
|
NULL,
|
|
&error_local)) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
error_local->message);
|
|
return FALSE;
|
|
}
|
|
|
|
/* the file callback set an error */
|
|
if (helper.error != NULL) {
|
|
g_propagate_error(error, helper.error);
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* fu_cabinet_export:
|
|
* @self: a #FuCabinet
|
|
* @flags: export flags, e.g. %FU_CABINET_EXPORT_FLAG_NONE
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Exports the cabinet archive.
|
|
*
|
|
* Returns: (transfer full): a data blob
|
|
*
|
|
* Since: 1.6.0
|
|
**/
|
|
GBytes *
|
|
fu_cabinet_export(FuCabinet *self, FuCabinetExportFlags flags, GError **error)
|
|
{
|
|
g_autoptr(GOutputStream) op = NULL;
|
|
op = g_memory_output_stream_new_resizable();
|
|
if (!gcab_cabinet_write_simple(self->gcab_cabinet,
|
|
op,
|
|
NULL,
|
|
NULL, /* progress */
|
|
NULL,
|
|
error))
|
|
return NULL;
|
|
if (!g_output_stream_close(op, NULL, error))
|
|
return NULL;
|
|
return g_memory_output_stream_steal_as_bytes(G_MEMORY_OUTPUT_STREAM(op));
|
|
}
|
|
|
|
static gboolean
|
|
fu_cabinet_sign_filename(FuCabinet *self,
|
|
const gchar *filename,
|
|
JcatEngine *jcat_engine,
|
|
JcatFile *jcat_file,
|
|
GBytes *cert,
|
|
GBytes *privkey,
|
|
GError **error)
|
|
{
|
|
g_autoptr(GBytes) source_blob = NULL;
|
|
g_autoptr(JcatBlob) jcat_blob = NULL;
|
|
g_autoptr(JcatItem) jcat_item = NULL;
|
|
|
|
/* sign the file using the engine */
|
|
source_blob = fu_cabinet_get_file(self, filename, error);
|
|
if (source_blob == NULL)
|
|
return FALSE;
|
|
jcat_item = jcat_file_get_item_by_id(jcat_file, filename, NULL);
|
|
if (jcat_item == NULL) {
|
|
jcat_item = jcat_item_new(filename);
|
|
jcat_file_add_item(jcat_file, jcat_item);
|
|
}
|
|
jcat_blob = jcat_engine_pubkey_sign(jcat_engine,
|
|
source_blob,
|
|
cert,
|
|
privkey,
|
|
JCAT_SIGN_FLAG_ADD_TIMESTAMP | JCAT_SIGN_FLAG_ADD_CERT,
|
|
error);
|
|
if (jcat_blob == NULL)
|
|
return FALSE;
|
|
jcat_item_add_blob(jcat_item, jcat_blob);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cabinet_sign_enumerate_metainfo(FuCabinet *self, GPtrArray *files, GError **error)
|
|
{
|
|
g_autoptr(GError) error_local = NULL;
|
|
g_autoptr(GPtrArray) nodes = NULL;
|
|
g_autoptr(XbSilo) silo = fu_cabinet_get_silo(self);
|
|
|
|
/* get all the firmware referenced by the metainfo files */
|
|
nodes = xb_silo_query(silo,
|
|
"components/component[@type='firmware']/info/filename",
|
|
0,
|
|
&error_local);
|
|
if (nodes == NULL) {
|
|
if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT) ||
|
|
g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
|
|
g_debug("ignoring: %s", error_local->message);
|
|
g_ptr_array_add(files, g_strdup("firmware.metainfo.xml"));
|
|
} else {
|
|
g_propagate_error(error, g_steal_pointer(&error_local));
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
for (guint i = 0; i < nodes->len; i++) {
|
|
XbNode *n = g_ptr_array_index(nodes, i);
|
|
g_debug("adding: %s", xb_node_get_text(n));
|
|
g_ptr_array_add(files, g_strdup(xb_node_get_text(n)));
|
|
}
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cabinet_sign_enumerate_firmware(FuCabinet *self, GPtrArray *files, GError **error)
|
|
{
|
|
g_autoptr(GError) error_local = NULL;
|
|
g_autoptr(GPtrArray) nodes = NULL;
|
|
g_autoptr(XbSilo) silo = fu_cabinet_get_silo(self);
|
|
|
|
nodes = xb_silo_query(silo,
|
|
"components/component[@type='firmware']/releases/"
|
|
"release/checksum[@target='content']",
|
|
0,
|
|
&error_local);
|
|
if (nodes == NULL) {
|
|
if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT) ||
|
|
g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
|
|
g_debug("ignoring: %s", error_local->message);
|
|
g_ptr_array_add(files, g_strdup("firmware.bin"));
|
|
} else {
|
|
g_propagate_error(error, g_steal_pointer(&error_local));
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
for (guint i = 0; i < nodes->len; i++) {
|
|
XbNode *n = g_ptr_array_index(nodes, i);
|
|
g_debug("adding: %s", xb_node_get_attr(n, "filename"));
|
|
g_ptr_array_add(files, g_strdup(xb_node_get_attr(n, "filename")));
|
|
}
|
|
}
|
|
g_error(
|
|
"%s",
|
|
xb_silo_export(silo,
|
|
XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE | XB_NODE_EXPORT_FLAG_FORMAT_INDENT,
|
|
NULL));
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* fu_cabinet_sign:
|
|
* @self: a #FuCabinet
|
|
* @cert: a PCKS#7 certificate
|
|
* @privkey: a private key
|
|
* @flags: signing flags, e.g. %FU_CABINET_SIGN_FLAG_NONE
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Sign the cabinet archive using JCat.
|
|
*
|
|
* Returns: %TRUE for success
|
|
*
|
|
* Since: 1.6.0
|
|
**/
|
|
gboolean
|
|
fu_cabinet_sign(FuCabinet *self,
|
|
GBytes *cert,
|
|
GBytes *privkey,
|
|
FuCabinetSignFlags flags,
|
|
GError **error)
|
|
{
|
|
g_autoptr(GBytes) new_bytes = NULL;
|
|
g_autoptr(GBytes) old_bytes = NULL;
|
|
g_autoptr(GOutputStream) ostr = NULL;
|
|
g_autoptr(GPtrArray) filenames = g_ptr_array_new_with_free_func(g_free);
|
|
g_autoptr(JcatContext) jcat_context = jcat_context_new();
|
|
g_autoptr(JcatEngine) jcat_engine = NULL;
|
|
g_autoptr(JcatFile) jcat_file = jcat_file_new();
|
|
|
|
/* load existing .jcat file if it exists */
|
|
old_bytes = fu_cabinet_get_file(self, "firmware.jcat", NULL);
|
|
if (old_bytes != NULL) {
|
|
g_autoptr(GInputStream) istr = NULL;
|
|
istr = g_memory_input_stream_new_from_bytes(old_bytes);
|
|
if (!jcat_file_import_stream(jcat_file, istr, JCAT_IMPORT_FLAG_NONE, NULL, error))
|
|
return FALSE;
|
|
}
|
|
|
|
/* get all the metainfo.xml and firmware.bin files */
|
|
if (!fu_cabinet_sign_enumerate_metainfo(self, filenames, error))
|
|
return FALSE;
|
|
if (!fu_cabinet_sign_enumerate_firmware(self, filenames, error))
|
|
return FALSE;
|
|
|
|
/* sign all the files */
|
|
jcat_engine = jcat_context_get_engine(jcat_context, JCAT_BLOB_KIND_PKCS7, error);
|
|
if (jcat_engine == NULL)
|
|
return FALSE;
|
|
for (guint i = 0; i < filenames->len; i++) {
|
|
const gchar *filename = g_ptr_array_index(filenames, i);
|
|
if (!fu_cabinet_sign_filename(self,
|
|
filename,
|
|
jcat_engine,
|
|
jcat_file,
|
|
cert,
|
|
privkey,
|
|
error))
|
|
return FALSE;
|
|
}
|
|
|
|
/* export new JCat file and add it to the archive */
|
|
ostr = g_memory_output_stream_new_resizable();
|
|
if (!jcat_file_export_stream(jcat_file, ostr, JCAT_EXPORT_FLAG_NONE, NULL, error))
|
|
return FALSE;
|
|
new_bytes = g_memory_output_stream_steal_as_bytes(G_MEMORY_OUTPUT_STREAM(ostr));
|
|
fu_cabinet_add_file(self, "firmware.jcat", new_bytes);
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* fu_cabinet_parse:
|
|
* @self: a #FuCabinet
|
|
* @data: cabinet archive
|
|
* @flags: parse flags, e.g. %FU_CABINET_PARSE_FLAG_NONE
|
|
* @error: (nullable): optional return location for an error
|
|
*
|
|
* Parses the cabinet archive.
|
|
*
|
|
* Returns: %TRUE for success
|
|
*
|
|
* Since: 1.4.0
|
|
**/
|
|
gboolean
|
|
fu_cabinet_parse(FuCabinet *self, GBytes *data, FuCabinetParseFlags flags, GError **error)
|
|
{
|
|
g_autoptr(GError) error_local = NULL;
|
|
g_autoptr(GPtrArray) components = NULL;
|
|
g_autoptr(XbQuery) query = NULL;
|
|
|
|
g_return_val_if_fail(FU_IS_CABINET(self), FALSE);
|
|
g_return_val_if_fail(data != NULL, FALSE);
|
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
|
g_return_val_if_fail(self->silo == NULL, FALSE);
|
|
|
|
/* decompress */
|
|
if (!fu_cabinet_decompress(self, data, error))
|
|
return FALSE;
|
|
|
|
/* build xmlb silo */
|
|
self->container_checksum = g_compute_checksum_for_bytes(G_CHECKSUM_SHA1, data);
|
|
if (!fu_cabinet_build_silo(self, data, error))
|
|
return FALSE;
|
|
|
|
/* sanity check */
|
|
components =
|
|
xb_silo_query(self->silo, "components/component[@type='firmware']", 0, &error_local);
|
|
if (components == NULL) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"archive contained no valid metadata: %s",
|
|
error_local->message);
|
|
return FALSE;
|
|
}
|
|
|
|
/* prepare query */
|
|
query = xb_query_new_full(self->silo,
|
|
"releases/release",
|
|
#if LIBXMLB_CHECK_VERSION(0, 2, 0)
|
|
XB_QUERY_FLAG_FORCE_NODE_CACHE,
|
|
#else
|
|
XB_QUERY_FLAG_NONE,
|
|
#endif
|
|
error);
|
|
if (query == NULL)
|
|
return FALSE;
|
|
|
|
/* process each listed release */
|
|
for (guint i = 0; i < components->len; i++) {
|
|
XbNode *component = g_ptr_array_index(components, i);
|
|
g_autoptr(GPtrArray) releases = NULL;
|
|
releases = xb_node_query_full(component, query, &error_local);
|
|
if (releases == NULL) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"no releases in metainfo file: %s",
|
|
error_local->message);
|
|
return FALSE;
|
|
}
|
|
for (guint j = 0; j < releases->len; j++) {
|
|
XbNode *rel = g_ptr_array_index(releases, j);
|
|
g_debug("processing release: %s", xb_node_get_attr(rel, "version"));
|
|
if (!fu_cabinet_parse_release(self, rel, error))
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* fu_cabinet_new:
|
|
*
|
|
* Returns: a #FuCabinet
|
|
*
|
|
* Since: 1.4.0
|
|
**/
|
|
FuCabinet *
|
|
fu_cabinet_new(void)
|
|
{
|
|
return g_object_new(FU_TYPE_CABINET, NULL);
|
|
}
|