/*
 * Copyright (C) 2020 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <fwupd.h>
#include <string.h>

#include "fu-common.h"
#include "fu-efi-signature-private.h"
#include "fu-efi-signature-list.h"

/**
 * SECTION:fu-efi-signature-list
 * @short_description: Parser for EFI_SIGNATURE_LIST
 *
 * An object that represents a UEFI SignatureList.
 *
 * See also: #FuFirmware
 */

struct _FuEfiSignatureList {
	FuFirmware		 parent_instance;
};

G_DEFINE_TYPE (FuEfiSignatureList, fu_efi_signature_list, FU_TYPE_FIRMWARE)

static gboolean
fu_efi_signature_list_parse_item (FuEfiSignatureList *self,
				  FuEfiSignatureKind sig_kind,
				  const guint8 *buf,
				  gsize bufsz,
				  gsize offset,
				  guint32 sig_size,
				  GError **error)
{
	fwupd_guid_t guid;
	gsize sig_datasz;
	g_autofree gchar *sig_owner = NULL;
	g_autofree guint8 *sig_data = NULL;
	g_autoptr(FuEfiSignature) sig = NULL;
	g_autoptr(GBytes) data = NULL;

	/* allocate data buf */
	if (sig_size <= sizeof(fwupd_guid_t)) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "SignatureSize invalid: 0x%x",
			     (guint) sig_size);
		return FALSE;
	}
	sig_datasz = sig_size - sizeof(fwupd_guid_t);
	sig_data = g_malloc0 (sig_datasz);

	/* read both blocks of data */
	if (!fu_memcpy_safe ((guint8 *) &guid, sizeof(guid), 0x0,	/* dst */
			     buf, bufsz, offset,			/* src */
			     sizeof(guid), error)) {
		g_prefix_error (error, "failed to read signature GUID: ");
		return FALSE;
	}
	if (!fu_memcpy_safe (sig_data, sig_datasz, 0x0,			/* dst */
			     buf, bufsz, offset + sizeof(fwupd_guid_t),	/* src */
			     sig_datasz, error)) {
		g_prefix_error (error, "failed to read signature data: ");
		return FALSE;
	}

	/* create item */
	sig_owner = fwupd_guid_to_string (&guid, FWUPD_GUID_FLAG_MIXED_ENDIAN);
	data = g_bytes_new (sig_data, sig_datasz);
	sig = fu_efi_signature_new (sig_kind, sig_owner);
	fu_firmware_image_set_bytes (FU_FIRMWARE_IMAGE (sig), data);
	fu_firmware_add_image (FU_FIRMWARE (self), FU_FIRMWARE_IMAGE (sig));
	return TRUE;
}

static gboolean
fu_efi_signature_list_parse_list (FuEfiSignatureList *self,
				  const guint8 *buf,
				  gsize bufsz,
				  gsize *offset,
				  GError **error)
{
	FuEfiSignatureKind sig_kind = FU_EFI_SIGNATURE_KIND_UNKNOWN;
	fwupd_guid_t guid;
	gsize offset_tmp;
	guint32 sig_header_size = 0;
	guint32 sig_list_size = 0;
	guint32 sig_size = 0;
	g_autofree gchar *sig_type = NULL;

	/* read EFI_SIGNATURE_LIST */
	if (!fu_memcpy_safe ((guint8 *) &guid, sizeof(guid), 0x0,	/* dst */
			     buf, bufsz, *offset,			/* src */
			     sizeof(guid), error)) {
		g_prefix_error (error, "failed to read GUID header: ");
		return FALSE;
	}
	sig_type = fwupd_guid_to_string (&guid, FWUPD_GUID_FLAG_MIXED_ENDIAN);
	if (g_strcmp0 (sig_type, "c1c41626-504c-4092-aca9-41f936934328") == 0) {
		sig_kind = FU_EFI_SIGNATURE_KIND_SHA256;
	} else if (g_strcmp0 (sig_type, "a5c059a1-94e4-4aa7-87b5-ab155c2bf072") == 0) {
		sig_kind = FU_EFI_SIGNATURE_KIND_X509;
	}
	if (!fu_common_read_uint32_safe (buf, bufsz, *offset + 0x10,
					 &sig_list_size, G_LITTLE_ENDIAN, error))
		return FALSE;
	if (sig_list_size < 0x1c || sig_list_size > 1024 * 1024) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "SignatureListSize invalid: 0x%x", sig_list_size);
		return FALSE;
	}
	if (!fu_common_read_uint32_safe (buf, bufsz, *offset + 0x14,
					 &sig_header_size, G_LITTLE_ENDIAN, error))
		return FALSE;
	if (sig_header_size > 1024 * 1024) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "SignatureHeaderSize invalid: 0x%x", sig_size);
		return FALSE;
	}
	if (!fu_common_read_uint32_safe (buf, bufsz, *offset + 0x18,
					 &sig_size, G_LITTLE_ENDIAN, error))
		return FALSE;
	if (sig_size < sizeof(fwupd_guid_t) || sig_size > 1024 * 1024) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "SignatureSize invalid: 0x%x", sig_size);
		return FALSE;
	}

	/* header is typically unused */
	offset_tmp = *offset + 0x1c + sig_header_size;
	for (guint i = 0; i < (sig_list_size - 0x1c) / sig_size; i++) {
		if (!fu_efi_signature_list_parse_item (self, sig_kind, buf, bufsz,
						       offset_tmp, sig_size,
						       error))
			return FALSE;
		offset_tmp += sig_size;
	}
	*offset += sig_list_size;
	return TRUE;
}

static gchar *
fu_efi_signature_list_get_version (FuEfiSignatureList *self)
{
	guint csum_cnt = 0;
	const gchar *ignored_guids[] = {
		FU_EFI_SIGNATURE_GUID_OVMF,
		FU_EFI_SIGNATURE_GUID_OVMF_LEGACY,
		NULL };
	g_autoptr(GPtrArray) sigs = NULL;
	sigs = fu_firmware_get_images (FU_FIRMWARE (self));
	for (guint i = 0; i < sigs->len; i++) {
		FuEfiSignature *sig = g_ptr_array_index (sigs, i);
		if (fu_efi_signature_get_kind (sig) != FU_EFI_SIGNATURE_KIND_SHA256)
			continue;
		if (g_strv_contains (ignored_guids, fu_efi_signature_get_owner (sig)))
			continue;
		csum_cnt++;
	}
	return g_strdup_printf ("%u", csum_cnt);
}

static gboolean
fu_efi_signature_list_parse (FuFirmware *firmware,
			     GBytes *fw,
			     guint64 addr_start,
			     guint64 addr_end,
			     FwupdInstallFlags flags,
			     GError **error)
{
	FuEfiSignatureList *self = FU_EFI_SIGNATURE_LIST (firmware);
	gsize bufsz = 0;
	gsize offset_fs = 0;
	const guint8 *buf = g_bytes_get_data (fw, &bufsz);
	g_autofree gchar *version_str = NULL;

	/* this allows us to skip the efi permissions uint32_t or even the
	 * Microsoft PKCS-7 signature */
	if ((flags & FWUPD_INSTALL_FLAG_NO_SEARCH) == 0) {
		if (bufsz < 5) {
			g_set_error (error,
				     G_IO_ERROR,
				     G_IO_ERROR_FAILED,
				     "signature invalid: 0x%x",
				     (guint) bufsz);
			return FALSE;
		}
		for (gsize i = 0; i < bufsz - 5; i++) {
			if (memcmp (buf + i, "\x26\x16\xc4\xc1\x4c", 5) == 0) {
				g_debug ("found EFI_SIGNATURE_LIST @0x%x", (guint) i);
				offset_fs = i;
				break;
			}
		}
	}

	/* parse each EFI_SIGNATURE_LIST */
	for (gsize offset = offset_fs; offset < bufsz;) {
		if (!fu_efi_signature_list_parse_list (self, buf, bufsz, &offset, error))
			return FALSE;
	}

	/* set version */
	version_str = fu_efi_signature_list_get_version (self);
	if (version_str != NULL)
		fu_firmware_set_version (firmware, version_str);

	/* success */
	return TRUE;
}

static GBytes *
fu_efi_signature_list_write (FuFirmware *firmware, GError **error)
{
	GByteArray *buf = g_byte_array_new ();

	/* SignatureType */
	for (guint i = 0; i < 16; i++)
		fu_byte_array_append_uint8 (buf, 0x0);

	/* SignatureListSize */
	fu_byte_array_append_uint32 (buf, 16 + 4 + 4 + 4 + 16 + 32, G_LITTLE_ENDIAN);

	/* SignatureHeaderSize */
	fu_byte_array_append_uint32 (buf, 0, G_LITTLE_ENDIAN);

	/* SignatureSize */
	fu_byte_array_append_uint32 (buf, 16 + 32, G_LITTLE_ENDIAN);

	/* SignatureOwner */
	for (guint i = 0; i < 16; i++)
		fu_byte_array_append_uint8 (buf, '1');

	/* SignatureData */
	for (guint i = 0; i < 16; i++)
		fu_byte_array_append_uint8 (buf, '2');

	return g_byte_array_free_to_bytes (buf);
}

/**
 * fu_efi_signature_list_new:
 *
 * Creates a new #FuFirmware that can parse an EFI_SIGNATURE_LIST
 *
 * Since: 1.5.5
 **/
FuFirmware *
fu_efi_signature_list_new (void)
{
	return g_object_new (FU_TYPE_EFI_SIGNATURE_LIST, NULL);
}

static void
fu_efi_signature_list_class_init (FuEfiSignatureListClass *klass)
{
	FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS (klass);
	klass_firmware->parse = fu_efi_signature_list_parse;
	klass_firmware->write = fu_efi_signature_list_write;
}

static void
fu_efi_signature_list_init (FuEfiSignatureList *self)
{
}
