/*
 * Copyright 2025 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#ifdef HAVE_GNUTLS
#include <gnutls/abstract.h>
#include <gnutls/crypto.h>
#include <gnutls/x509.h>
#endif

#include "fu-common.h"
#include "fu-input-stream.h"
#include "fu-string.h"
#include "fu-x509-certificate.h"

#ifdef HAVE_GNUTLS
static void
fu_x509_certificate_gnutls_datum_deinit(gnutls_datum_t *d)
{
	gnutls_free(d->data);
	gnutls_free(d);
}

static void
fu_x509_certificate_gnutls_datum_clear(gnutls_datum_t *d)
{
	gnutls_free(d->data);
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
G_DEFINE_AUTOPTR_CLEANUP_FUNC(gnutls_datum_t, fu_x509_certificate_gnutls_datum_deinit)
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(gnutls_datum_t, fu_x509_certificate_gnutls_datum_clear)
G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_x509_crt_t, gnutls_x509_crt_deinit, NULL)
G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_x509_privkey_t, gnutls_x509_privkey_deinit, NULL)
#pragma clang diagnostic pop
#endif

/**
 * FuX509Certificate:
 *
 * An X.509 certificate.
 *
 * See also: [class@FuFirmware]
 */

struct _FuX509Certificate {
	FuFirmware parent_instance;
	gchar *issuer;
	gchar *subject;
	GDateTime *activation_time;
};

G_DEFINE_TYPE(FuX509Certificate, fu_x509_certificate, FU_TYPE_FIRMWARE)

static void
fu_x509_certificate_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn)
{
	FuX509Certificate *self = FU_X509_CERTIFICATE(firmware);
	fu_xmlb_builder_insert_kv(bn, "issuer", self->issuer);
	fu_xmlb_builder_insert_kv(bn, "subject", self->subject);
}

/**
 * fu_x509_certificate_get_issuer:
 * @self: A #FuX509Certificate
 *
 * Returns the certificate issuer.
 *
 * Returns: string, or %NULL for unset
 *
 * Since: 2.0.9
 **/
const gchar *
fu_x509_certificate_get_issuer(FuX509Certificate *self)
{
	g_return_val_if_fail(FU_IS_X509_CERTIFICATE(self), NULL);
	return self->issuer;
}

/**
 * fu_x509_certificate_set_issuer:
 * @self: A #FuX509Certificate
 * @issuer: the certificate issuer
 *
 * Sets the certificate issuer.
 *
 * Since: 2.1.5
 **/
void
fu_x509_certificate_set_issuer(FuX509Certificate *self, const gchar *issuer)
{
	g_return_if_fail(FU_IS_X509_CERTIFICATE(self));
	if (g_strcmp0(issuer, self->issuer) == 0)
		return;
	g_free(self->issuer);
	self->issuer = g_strdup(issuer);
}

/**
 * fu_x509_certificate_set_subject:
 * @self: A #FuX509Certificate
 * @subject: the certificate subject
 *
 * Sets the certificate subject.
 *
 * Since: 2.0.13
 **/
void
fu_x509_certificate_set_subject(FuX509Certificate *self, const gchar *subject)
{
	g_return_if_fail(FU_IS_X509_CERTIFICATE(self));
	if (g_strcmp0(subject, self->subject) == 0)
		return;
	g_free(self->subject);
	self->subject = g_strdup(subject);
}

/**
 * fu_x509_certificate_set_activation_time:
 * @self: A #FuX509Certificate
 * @activation_time: the activation time as a UNIX epoch
 *
 * Sets the certificate activation time.
 *
 * Since: 2.1.5
 **/
void
fu_x509_certificate_set_activation_time(FuX509Certificate *self, gint64 activation_time)
{
	g_return_if_fail(FU_IS_X509_CERTIFICATE(self));
	if (self->activation_time != NULL)
		g_date_time_unref(self->activation_time);
	self->activation_time = g_date_time_new_from_unix_utc(activation_time);
}

/**
 * fu_x509_certificate_get_subject:
 * @self: A #FuX509Certificate
 *
 * Returns the certificate subject.
 *
 * Returns: string, or %NULL for unset
 *
 * Since: 2.0.9
 **/
const gchar *
fu_x509_certificate_get_subject(FuX509Certificate *self)
{
	g_return_val_if_fail(FU_IS_X509_CERTIFICATE(self), NULL);
	return self->subject;
}

/**
 * fu_x509_certificate_get_activation_time:
 * @self: A #FuX509Certificate
 *
 * Returns the certificate activation time.
 *
 * Returns: (transfer full): a #GDateTime, or %NULL for unset
 *
 * Since: 2.0.11
 **/
GDateTime *
fu_x509_certificate_get_activation_time(FuX509Certificate *self)
{
	g_return_val_if_fail(FU_IS_X509_CERTIFICATE(self), NULL);
	if (self->activation_time == NULL)
		return NULL;
	return g_date_time_ref(self->activation_time);
}

static gboolean
fu_x509_certificate_parse(FuFirmware *firmware,
			  GInputStream *stream,
			  FuFirmwareParseFlags flags,
			  GError **error)
{
#ifdef HAVE_GNUTLS
	FuX509Certificate *self = FU_X509_CERTIFICATE(firmware);
	gchar buf[1024] = {'\0'};
	guchar key_id[32] = {'\0'};
	gsize key_idsz = sizeof(key_id);
	gnutls_datum_t d = {0};
	gnutls_x509_dn_t dn = {0x0};
	gint64 ts;
	gsize bufsz = sizeof(buf);
	int rc;
	g_auto(gnutls_x509_crt_t) crt = NULL;
	g_autoptr(gnutls_datum_t) subject = NULL;
	g_autoptr(GBytes) blob = NULL;
	g_autoptr(GString) key_idstr = g_string_new(NULL);

	/* parse certificate */
	blob = fu_input_stream_read_bytes(stream, 0x0, G_MAXSIZE, NULL, error);
	if (blob == NULL)
		return FALSE;
	d.size = g_bytes_get_size(blob);
	d.data = (unsigned char *)g_bytes_get_data(blob, NULL);

	rc = gnutls_x509_crt_init(&crt);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "crt_init: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return FALSE;
	}
	rc = gnutls_x509_crt_import(crt, &d, GNUTLS_X509_FMT_DER);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "crt_import: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return FALSE;
	}

	/* issuer */
	if (gnutls_x509_crt_get_issuer_dn(crt, buf, &bufsz) == GNUTLS_E_SUCCESS) {
		g_autofree gchar *str = fu_strsafe((const gchar *)buf, bufsz);
		fu_x509_certificate_set_issuer(self, str);
	}

	/* subject */
	subject = (gnutls_datum_t *)gnutls_calloc(1, sizeof(gnutls_datum_t));
	if (gnutls_x509_crt_get_subject(crt, &dn) == GNUTLS_E_SUCCESS) {
		g_autofree gchar *str = NULL;
		if (gnutls_x509_dn_get_str(dn, subject) != GNUTLS_E_SUCCESS) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_DATA,
					    "failed to get subject string");
			return FALSE;
		}
		str = fu_strsafe((const gchar *)subject->data, subject->size);
		fu_x509_certificate_set_subject(self, str);
	}

	/* activation_time */
	ts = (gint64)gnutls_x509_crt_get_activation_time(crt);
	if (ts == -1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_DATA,
				    "failed to get activation time");
		return FALSE;
	}
	fu_x509_certificate_set_activation_time(self, ts);

	/* key ID */
	rc = gnutls_x509_crt_get_key_id(crt, 0, key_id, &key_idsz);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "failed to get key ID: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return FALSE;
	}
	for (guint i = 0; i < MIN(sizeof(key_id), key_idsz); i++)
		g_string_append_printf(key_idstr, "%02x", key_id[i]);
	fu_firmware_set_id(firmware, key_idstr->str);

	/* success */
	return TRUE;
#else
	g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no GnuTLS support");
	return FALSE;
#endif
}

static GByteArray *
fu_x509_certificate_write(FuFirmware *firmware, GError **error)
{
#ifdef HAVE_GNUTLS
	FuX509Certificate *self = FU_X509_CERTIFICATE(firmware);
	int rc;
	g_auto(gnutls_datum_t) d = {0};
	g_auto(gnutls_x509_crt_t) crt = NULL;
	g_auto(gnutls_x509_privkey_t) key = NULL;
	g_autoptr(GByteArray) buf = NULL;

	/* generate a small private key */
	rc = gnutls_x509_privkey_init(&key);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "privkey_init: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return NULL;
	}
	rc = gnutls_x509_privkey_generate(key,
					  GNUTLS_PK_ECDSA,
					  GNUTLS_CURVE_TO_BITS(GNUTLS_ECC_CURVE_SECP256R1),
					  0);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "privkey_generate: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return NULL;
	}

	/* create a self-signed certificate */
	rc = gnutls_x509_crt_init(&crt);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "crt_init: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return NULL;
	}
	rc = gnutls_x509_crt_set_version(crt, 3);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "crt_set_version: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return NULL;
	}
	if (self->subject != NULL) {
		rc = gnutls_x509_crt_set_dn(crt, self->subject, NULL);
		if (rc < 0) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "crt_set_dn: %s [%i]",
				    gnutls_strerror(rc),
				    rc);
			return NULL;
		}
	}
	rc = gnutls_x509_crt_set_serial(crt, "\x01", 1);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "crt_set_serial: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return NULL;
	}
	rc = gnutls_x509_crt_set_activation_time(
	    crt,
	    self->activation_time != NULL ? g_date_time_to_unix(self->activation_time) : 0);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "crt_set_activation_time: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return NULL;
	}
	rc = gnutls_x509_crt_set_expiration_time(crt, (time_t)-1);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "crt_set_expiration_time: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return NULL;
	}
	rc = gnutls_x509_crt_set_key(crt, key);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "crt_set_key: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return NULL;
	}
	rc = gnutls_x509_crt_sign2(crt, crt, key, GNUTLS_DIG_SHA256, 0);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "crt_sign2: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return NULL;
	}

	/* export as DER */
	rc = gnutls_x509_crt_export2(crt, GNUTLS_X509_FMT_DER, &d);
	if (rc < 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "crt_export2: %s [%i]",
			    gnutls_strerror(rc),
			    rc);
		return NULL;
	}
	buf = g_byte_array_sized_new(d.size);
	g_byte_array_append(buf, d.data, d.size);
	return g_steal_pointer(&buf);
#else
	g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no GnuTLS support");
	return NULL;
#endif
}

static void
fu_x509_certificate_init(FuX509Certificate *self)
{
	fu_firmware_set_size_max(FU_FIRMWARE(self), 1 * FU_MB);
}

static void
fu_x509_certificate_finalize(GObject *obj)
{
	FuX509Certificate *self = FU_X509_CERTIFICATE(obj);
	g_free(self->issuer);
	g_free(self->subject);
	if (self->activation_time != NULL)
		g_date_time_unref(self->activation_time);
	G_OBJECT_CLASS(fu_x509_certificate_parent_class)->finalize(obj);
}

static void
fu_x509_certificate_class_init(FuX509CertificateClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass);
	object_class->finalize = fu_x509_certificate_finalize;
	firmware_class->export = fu_x509_certificate_export;
	firmware_class->parse = fu_x509_certificate_parse;
	firmware_class->write = fu_x509_certificate_write;
}

/**
 * fu_x509_certificate_new:
 *
 * Creates a new #FuX509Certificate.
 *
 * Returns: (transfer full): object
 *
 * Since: 2.0.9
 **/
FuX509Certificate *
fu_x509_certificate_new(void)
{
	return g_object_new(FU_TYPE_X509_CERTIFICATE, NULL);
}
