/*
 * Copyright (C) 2026 iopsys Software Solutions AB
 *
 * See LICENSE file for license related information.
 */

#include <libbbfdm-api/dmapi.h>
#include <libbbfdm-api/dmjson.h>

#include <openssl/pem.h>
#include <openssl/bio.h>

#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

#include "common.h"
#include "certificate.h"
#include "cabundle.h"

/*************************************************************
 * SECURITY-LEVEL PARAMS (TR-181 2.21)
 *************************************************************/
static int get_Security_SupportedBundleFormats(char *refparam, struct dmctx *ctx, void *data, char *instance, char **value)
{
	(void)refparam; (void)ctx; (void)data; (void)instance;
	/* Only format accepted by AddCertificateBundle(); carries a cert + key. */
	*value = dmstrdup("PEM");
	return 0;
}

/* AddCertificateBundle(): install a cert/key from a hex-encoded blob. */
static operation_args addcertificatebundle_args = {
	.in  = (const char *[]) { "Alias", "Name", "BundleFormat", "Bundle", NULL },
	.out = (const char *[]) { "Certificate", NULL }
};

static int get_operate_args_Security_AddCertificateBundle(char *refparam, struct dmctx *ctx, void *data, char *instance, char **value)
{
	*value = (char *)&addcertificatebundle_args;
	return 0;
}

/* AddCertificateBundle(): install a cert+key from a PEM bundle.
 *
 * Input args (spec order): Alias (optional), Name (mandatory),
 * BundleFormat (mandatory, must be "PEM" — the only supported format),
 * Bundle (mandatory).
 *
 * The password is read from `secmngr.@globals[0].pkcs12_password`; it is
 * used to decrypt an encrypted PEM private key, when present. Any format
 * other than "PEM" returns the spec-defined 7260 fault.
 *
 * Files are written to /etc/secmngr/{certs,private}/ — never to
 * /etc/ssl/certs, since those system files don't belong to us. On success
 * the new cert is discoverable by the next browse and the output
 * `Certificate` parameter points at its instance path.
 *
 * Fault 7270 (Certificate Identity Conflict) / replace-in-place: identity is
 * SerialNumber+Issuer (Certificate.{i}.'s functional unique key), not path —
 * two different issuers can legitimately share a serial number, so path
 * alone was never a safe identity check (see
 * secmngr_find_certificate_dmmap_by_cert()). If a certificate with the same
 * SerialNumber+Issuer already exists:
 *   - same Name too       -> replace it in place (reuse its existing path
 *                             and tracked key path); Certificate output
 *                             references that same, now-updated entry.
 *   - a *different* Name  -> reject with 7270.
 * If no existing certificate shares that identity, Name must still be
 * globally unique (Certificate.{i}.Name is DM_FLAG_UNIQUE) — reject with
 * 7270 if it already belongs to a different certificate.
 */
static int operate_Security_AddCertificateBundle(char *refparam, struct dmctx *ctx, void *data, char *instance, char *value, int action)
{
	json_object *jin = (json_object *)value;
	char *alias = dmjson_get_value(jin, 1, "Alias");
	char *name = dmjson_get_value(jin, 1, "Name");
	char *format = dmjson_get_value(jin, 1, "BundleFormat");
	char *bundle_hex = dmjson_get_value(jin, 1, "Bundle");
	unsigned char *blob = NULL;
	int blen, ret = 7262;
	BIO *bio = NULL;
	EVP_PKEY *pkey = NULL;
	X509 *cert = NULL;
	char *password = NULL;
	char stem[96] = {0};
	char cert_path[SECMNGR_CERT_PATH_LEN];
	char key_path[SECMNGR_CERT_PATH_LEN];
	char dm_path[96];
	FILE *fp = NULL;
	int fd = -1;
	struct uci_section *s;
	struct uci_section *existing;
	bool replacing = false;
	char *existing_key_path = NULL;

	(void)refparam; (void)data; (void)instance; (void)action;

	/* Name is mandatory per TR-181 2.21. */
	if (DM_STRLEN(name) == 0) {
		bbfdm_set_fault_message(ctx, "AddCertificateBundle(): Name is a mandatory input argument");
		return USP_FAULT_INVALID_ARGUMENT;
	}

	if (DM_STRLEN(format) == 0 || DM_STRLEN(bundle_hex) == 0) {
		bbfdm_set_fault_message(ctx, "AddCertificateBundle(): missing input arguments");
		return USP_FAULT_INVALID_ARGUMENT;
	}

	if (DM_STRCMP(format, "PEM") != 0) {
		bbfdm_set_fault_message(ctx, "AddCertificateBundle(): unsupported BundleFormat %s", format);
		return 7260; /* Bundle Format Not Supported */
	}

	blen = secmngr_decode_bundle_input(bundle_hex, &blob);
	if (blen <= 0) {
		bbfdm_set_fault_message(ctx, "AddCertificateBundle(): Bundle is not valid hexBinary or plain-text PEM data");
		return USP_FAULT_INVALID_TYPE;
	}

	bio = BIO_new_mem_buf(blob, blen);
	if (!bio)
		goto out;

	/* Password is used to decrypt an encrypted PEM private key, when present. */
	dmuci_get_option_value_string(SECMNGR_UCI_PACKAGE, "globals", "pkcs12_password", &password);

	/* The blob concatenates a certificate and a private key in an
	 * unspecified order. Read each from its own BIO over the same blob so
	 * the cert search can't consume a key that appears first. */
	BIO *kbio = BIO_new_mem_buf(blob, blen);

	cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
	if (kbio) {
		pkey = PEM_read_bio_PrivateKey(kbio, NULL, NULL, password ? password : NULL);
		BIO_free(kbio);
	}

	if (!cert) {
		bbfdm_set_fault_message(ctx, "AddCertificateBundle(): no certificate found in PEM bundle");
		ret = 7263; /* Failed To Import Certificate */
		goto out;
	}
	if (!pkey) {
		bbfdm_set_fault_message(ctx, "AddCertificateBundle(): no private key found in PEM bundle");
		ret = 7264; /* Failed To Import Private Key */
		goto out;
	}

	secmngr_cert_file_stem(cert, stem, sizeof(stem));

	/* Fault 7270 / replace-in-place: same SerialNumber+Issuer as an
	 * existing certificate. */
	existing = secmngr_find_certificate_dmmap_by_cert(cert);
	if (existing) {
		char *exist_name = NULL, *exist_path = NULL;

		dmuci_get_value_by_section_string(existing, "Name", &exist_name);
		if (DM_STRCMP(exist_name, name) != 0) {
			bbfdm_set_fault_message(ctx, "AddCertificateBundle(): certificate identity conflict — SerialNumber/Issuer match existing cert with Name '%s'", exist_name);
			ret = 7270; /* Certificate Identity Conflict */
			goto out;
		}

		dmuci_get_value_by_section_string(existing, "__path__", &exist_path);

		/* Same identity AND same Name: normally "replace in place". But if
		 * the existing entry is a system-store certificate (not
		 * secmngr-owned), we can neither safely overwrite /etc/ssl/certs
		 * (see the file header comment above) nor create a second row
		 * with the same SerialNumber+Issuer — that pair is itself a
		 * functional unique key per spec, so a fresh row here would just
		 * trade one spec violation for another. Reject as an identity
		 * conflict instead. */
		struct uci_section *existing_cfg;

		if (DM_STRLEN(exist_path) == 0 || DM_STRLEN(exist_path) >= sizeof(cert_path) ||
		    strncmp(exist_path, SECMNGR_OWN_CERT_DIR, strlen(SECMNGR_OWN_CERT_DIR)) != 0) {
			bbfdm_set_fault_message(ctx, "AddCertificateBundle(): certificate identity conflict — SerialNumber/Issuer match an existing system-store certificate");
			ret = 7270; /* Certificate Identity Conflict */
			goto out;
		}

		DM_STRNCPY(cert_path, exist_path, sizeof(cert_path));
		replacing = true;

		/* Reuse the tracked private key path, if any (there may be
		 * none if this row was only ever installed without a key,
		 * e.g. via AddCABundle()). */
		existing_cfg = secmngr_find_cert_section(exist_path);
		if (existing_cfg)
			dmuci_get_value_by_section_string(existing_cfg, "key_path", &existing_key_path);
	}

	if (!replacing) {
		mkdir(SECMNGR_OWN_CERT_DIR, 0755);
		if (snprintf(cert_path, sizeof(cert_path), "%s/%s.pem", SECMNGR_OWN_CERT_DIR, stem) >= (int)sizeof(cert_path)) {
			bbfdm_set_fault_message(ctx, "AddCertificateBundle(): certificate path too long");
			ret = 7263; /* Failed To Import Certificate */
			goto out;
		}

		/* Guard the extremely unlikely case where this stem-derived path
		 * is already occupied by a file belonging to a *different*
		 * certificate (a true hash collision on the combined
		 * Serial+Issuer stem — the identity lookup above already ruled
		 * out "it's this same certificate"). Overwriting it would
		 * silently destroy an unrelated tracked certificate. */
		if (file_exists(cert_path)) {
			bbfdm_set_fault_message(ctx, "AddCertificateBundle(): certificate path collision");
			ret = 7263; /* Failed To Import Certificate */
			goto out;
		}
	}

	if (DM_STRLEN(existing_key_path)) {
		DM_STRNCPY(key_path, existing_key_path, sizeof(key_path));
	} else {
		mkdir(SECMNGR_OWN_KEY_DIR, 0700);
		if (snprintf(key_path, sizeof(key_path), "%s/%s.key", SECMNGR_OWN_KEY_DIR, stem) >= (int)sizeof(key_path)) {
			bbfdm_set_fault_message(ctx, "AddCertificateBundle(): private key path too long");
			ret = 7264; /* Failed To Import Private Key */
			goto out;
		}
	}

	/* Name is a Certificate.{i}. unique key: if no identity match was found
	 * above, the submitted Name must still not already belong to a
	 * different certificate. */
	struct uci_section *cs = NULL;

	uci_path_foreach_sections(bbfdm, "Security", "Certificate", cs) {
		char *cur_name = NULL, *cur_path = NULL;

		dmuci_get_value_by_section_string(cs, "Name", &cur_name);
		if (DM_STRLEN(cur_name) == 0 || DM_STRCMP(cur_name, name) != 0)
			continue;

		dmuci_get_value_by_section_string(cs, "__path__", &cur_path);
		if (DM_STRCMP(cur_path, cert_path) != 0) {
			bbfdm_set_fault_message(ctx, "AddCertificateBundle(): Name '%s' is already used by another certificate", name);
			ret = 7270; /* Certificate Identity Conflict */
			goto out;
		}
	}

	/* On a write failure below, only unlink what this call itself just
	 * created — never a pre-existing file we're replacing in place (that
	 * would turn a failed replace into unintended data loss). */
	bool reused_key_path = DM_STRLEN(existing_key_path) > 0;

	// cppcheck-suppress cert-MSC24-C
	fp = fopen(cert_path, "w");
	if (!fp || PEM_write_X509(fp, cert) != 1) {
		bbfdm_set_fault_message(ctx, "AddCertificateBundle(): unable to write certificate to disk");
		ret = 7263; /* Failed To Import Certificate */
		if (fp) fclose(fp);
		if (!replacing) unlink(cert_path);
		goto out;
	}
	fclose(fp);
	fp = NULL;

	fd = open(key_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
	if (fd < 0) {
		bbfdm_set_fault_message(ctx, "AddCertificateBundle(): unable to open key file");
		ret = 7264; /* Failed To Import Private Key */
		if (!replacing) unlink(cert_path);
		goto out;
	}
	fp = fdopen(fd, "w");
	if (!fp || PEM_write_PKCS8PrivateKey(fp, pkey, NULL, NULL, 0, NULL, NULL) != 1) {
		bbfdm_set_fault_message(ctx, "AddCertificateBundle(): unable to write private key to disk");
		ret = 7264;
		if (fp) fclose(fp); else close(fd);
		if (!replacing) unlink(cert_path);
		if (!reused_key_path) unlink(key_path);
		goto out;
	}
	fclose(fp);
	fp = NULL;

	s = secmngr_ensure_cert_section(cert_path, name);
	if (s) {
		dmuci_set_value_by_section(s, "key_path", key_path);
	}

	char *inst = NULL;
	struct uci_section *dm = secmngr_sync_certificate_dmmap_row(ctx, cert_path, &inst);
	if (dm != NULL) {
		/* Alias is stored on the dmmap row itself (see
		 * get_SecurityCertificate_Alias()/set_SecurityCertificate_Alias() in
		 * certificate.c), not the config section — set it here if the
		 * optional Alias input was provided. */
		if (DM_STRLEN(alias))
			dmuci_set_value_by_section(dm, "Alias", alias);

		dmuci_commit_package_bbfdm("Security");
		snprintf(dm_path, sizeof(dm_path), "Device.Security.Certificate.%s", inst);
	}

	fill_blob_param(&ctx->bb, "Certificate", dm_path, DMT_TYPE[DMT_STRING], 0);
	ret = 0;

out:
	if (cert) X509_free(cert);
	if (pkey) EVP_PKEY_free(pkey);
	if (bio)  BIO_free(bio);
	FREE(blob); /* BIO_new_mem_buf does not take ownership of the blob. */
	return ret;
}

/* AddCABundle(): install a CA bundle from a hex-encoded PEM blob. */
static operation_args addcabundle_args = {
	.in  = (const char *[]) { "Name", "Bundle", NULL },
	.out = (const char *[]) { "CABundle", "NumberOfCertificates", NULL }
};

static int get_operate_args_Security_AddCABundle(char *refparam, struct dmctx *ctx, void *data, char *instance, char **value)
{
	*value = (char *)&addcabundle_args;
	return 0;
}

/* Map secmngr_install_cabundle_certs's negative return into the right USP fault. */
static int install_fault_to_usp(int rc)
{
	switch (rc) {
	case -7265: return 7265; /* Invalid PEM Format */
	case -7266: return 7266; /* Insufficient Disk Space */
	case -7263: return 7263; /* Failed To Import Certificate */
	default:    return 7262; /* Failed To Import Bundle */
	}
}

/* AddCABundle(): install one or more PEM-encoded CA certs as a new bundle.
 *
 * Each cert in the blob becomes its own Device.Security.Certificate.{i}.
 * row (file under SECMNGR_OWN_CERT_DIR, deduped by Issuer+SerialNumber
 * identity against every existing Certificate row — see
 * secmngr_install_cabundle_certs()); the bundle's own UCI section records
 * membership as a `cert` list of those certs' own cached Name values (this
 * table's DM_FLAG_LINKER parameter, not a raw UCI section name). The Name
 * input is the UCI section name (the unique key for CABundle.{i}). A
 * pre-existing section with the same Name yields the spec's 7267 fault
 * rather than silently replacing it.
 *
 * The original submitted blob is additionally persisted verbatim as
 * SECMNGR_OWN_CABUNDLE_DIR/<name>.pem — a single combined-bundle record,
 * independent of the individual certificate files, removed by Delete().
 */
static int operate_Security_AddCABundle(char *refparam, struct dmctx *ctx, void *data, char *instance, char *value, int action)
{
	json_object *jin = (json_object *)value;
	char *name = dmjson_get_value(jin, 1, "Name");
	char *bundle_hex = dmjson_get_value(jin, 1, "Bundle");
	unsigned char *blob = NULL;
	int blen, n;
	struct uci_section *s = NULL;
	char bundle_path[SECMNGR_CERT_PATH_LEN];
	char count_str[16];
	char dm_path[96];
	FILE *fp;

	(void)refparam; (void)data; (void)instance; (void)action;

	if (DM_STRLEN(name) == 0 || DM_STRLEN(bundle_hex) == 0) {
		bbfdm_set_fault_message(ctx, "AddCABundle(): missing input arguments");
		return USP_FAULT_INVALID_ARGUMENT;
	}

	/* Duplicate-Name check — spec 7267. Checked against every "Security"/
	 * CABundle dmmap row's cached Name (this table's DM_FLAG_UNIQUE
	 * parameter), not just real `config cabundle` sections — the read-only
	 * system bundle has a cached Name too (its file's basename, e.g.
	 * "cert") and must not be collidable with either. */
	struct uci_section *cs = NULL;

	uci_path_foreach_sections(bbfdm, "Security", "CABundle", cs) {
		char *cur_name = NULL;

		dmuci_get_value_by_section_string(cs, "Name", &cur_name);
		if (DM_STRLEN(cur_name) && DM_STRCMP(cur_name, name) == 0) {
			bbfdm_set_fault_message(ctx, "AddCABundle(): bundle '%s' already exists", name);
			return 7267; /* Duplicate CA Bundle Name */
		}
	}

	blen = secmngr_decode_bundle_input(bundle_hex, &blob);
	if (blen <= 0) {
		bbfdm_set_fault_message(ctx, "AddCABundle(): Bundle is not valid hexBinary or plain-text PEM data");
		return USP_FAULT_INVALID_TYPE;
	}

	/* Create the `cabundle` UCI section up front so secmngr_install_cabundle_certs()
	 * can append each installed cert's Name directly to its `cert` list as it
	 * goes, with no fixed cap on the number of certs in a bundle. Any failure
	 * from here through the combined-bundle-file write below deletes this
	 * section again — since no dmmap row exists for it yet at this point,
	 * that keeps UCI and dmmap in sync (no orphaned config section without a
	 * matching dmmap row, and vice versa). */
	char ca_section_name[SECMNGR_MAX_SECTION_NAME_LEN + sizeof(SECMNGR_CABUNDLE_SECTION_PREFIX)];

	snprintf(ca_section_name, sizeof(ca_section_name), "%s%s", SECMNGR_CABUNDLE_SECTION_PREFIX, name);
	replace_special_char(ca_section_name, '_');
	dmuci_add_named_section(SECMNGR_UCI_PACKAGE, "cabundle", ca_section_name, &s);
	if (!s) {
		bbfdm_set_fault_message(ctx, "AddCABundle(): unable to persist bundle '%s'", name);
		FREE(blob);
		return 7262;
	}
	dmuci_set_value_by_section(s, "name", name);

	n = secmngr_install_cabundle_certs(ctx, blob, blen, s);
	if (n < 0) {
		int fault = install_fault_to_usp(n);
		bbfdm_set_fault_message(ctx, "AddCABundle(): PEM install failed (%d)", fault);
		dmuci_delete_by_section(s, NULL, NULL);
		FREE(blob);
		return fault;
	}

	if (snprintf(bundle_path, sizeof(bundle_path), "%s/%s.pem", SECMNGR_OWN_CABUNDLE_DIR, name) >= (int)sizeof(bundle_path)) {
		bbfdm_set_fault_message(ctx, "AddCABundle(): bundle path too long");
		dmuci_delete_by_section(s, NULL, NULL);
		FREE(blob);
		return 7266; /* Insufficient Disk Space */
	}
	// cppcheck-suppress cert-MSC24-C
	fp = fopen(bundle_path, "w");
	if (!fp || fwrite(blob, 1, blen, fp) != (size_t)blen) {
		bbfdm_set_fault_message(ctx, "AddCABundle(): unable to write combined bundle file");
		if (fp) fclose(fp);
		dmuci_delete_by_section(s, NULL, NULL);
		FREE(blob);
		return 7266; /* Insufficient Disk Space */
	}
	fclose(fp);
	FREE(blob);

	char *inst = NULL;
	struct uci_section *dm = create_dmmap_obj(ctx, 0, "Security", "CABundle", s, &inst);
	if (dm != NULL) {
		/* Name is this table's DM_FLAG_LINKER parameter (see
		 * tSecurityCABundleParams) — cache it on the dmmap row, same as
		 * sync_cabundle_dmmap() does for existing bundles at rebuild time.
		 * path is the persisted combined-bundle file just written above */
		dmuci_set_value_by_section(dm, "Name", name);
		dmuci_set_value_by_section(dm, "__path__", bundle_path);
		dmuci_commit_package_bbfdm("Security");
		snprintf(dm_path, sizeof(dm_path), "Device.Security.CABundle.%s", inst);
	}

	snprintf(count_str, sizeof(count_str), "%d", n);
	fill_blob_param(&ctx->bb, "CABundle", dm_path, DMT_TYPE[DMT_STRING], 0);
	fill_blob_param(&ctx->bb, "NumberOfCertificates", count_str, DMT_TYPE[DMT_UNINT], 0);
	return 0;
}

/**********************************************************************************************************************************
 *                                            OBJ & PARAM DEFINITION
 **********************************************************************************************************************************/
/* Device.Security. */
static DMOBJ tSecurityObj[] = {
/* OBJ, permission, addobj, delobj, checkdep, browseinstobj, nextdynamicobj, dynamicleaf, nextobj, leaf, linker, bbfdm_type, uniqueKeys */
{"Certificate", &DMREAD, NULL, NULL, NULL, browseSecurityCertificateInst, NULL, NULL, NULL, tSecurityCertificateParams, NULL, BBFDM_BOTH},
{"CABundle", &DMREAD, NULL, NULL, NULL, browseSecurityCABundleInst, NULL, NULL, NULL, tSecurityCABundleParams, NULL, BBFDM_BOTH},
{0}
};

static DMLEAF tSecurityParams[] = {
/* PARAM, permission, type, getvalue, setvalue, bbfdm_type */
{"CertificateNumberOfEntries", &DMREAD, DMT_UNINT, get_Security_CertificateNumberOfEntries, NULL, BBFDM_BOTH},
{"CABundleNumberOfEntries", &DMREAD, DMT_UNINT, get_Security_CABundleNumberOfEntries, NULL, BBFDM_BOTH},
{"SupportedBundleFormats", &DMREAD, DMT_STRING, get_Security_SupportedBundleFormats, NULL, BBFDM_USP},
{"AddCertificateBundle()", &DMSYNC, DMT_COMMAND, get_operate_args_Security_AddCertificateBundle, operate_Security_AddCertificateBundle, BBFDM_USP},
{"AddCABundle()", &DMSYNC, DMT_COMMAND, get_operate_args_Security_AddCABundle, operate_Security_AddCABundle, BBFDM_USP},
{0}
};

static DMOBJ tDeviceSecurityObj[] = {
{"Security", &DMREAD, NULL, NULL, NULL, NULL, NULL, NULL, tSecurityObj, tSecurityParams, NULL, BBFDM_BOTH},
{0}
};

/* Plugin manifest read by libbbfdm-api/legacy/plugin/dotso_plugin.c via dlsym. */
DM_MAP_OBJ tDynamicObj[] = {
/* parentobj, nextobject, parameter, init_module, clean_module, uci_sync_handler */
{"Device.", tDeviceSecurityObj, NULL, secmngr_init, secmngr_clean, secmngr_sync},
{0}
};
