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

#include "common.h"

#include <libbbfdm-ubus/bbfdm-ubus.h>

#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <ctype.h>
#include <time.h>
#include <stdlib.h>
#include <limits.h>

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

#define DATE_LEN 128

#ifndef SECMNGR_SKIP_SYSTEM_CERTS
/* /etc/ssl/certs holds the real cert files (e.g. GlobalSign_Root_R46.crt)
 * alongside c_rehash symlinks named <hash>.<N> (.0, .1, .2, ...). Enumerate the
 * real files and skip the hash symlinks: reject dotfiles and any entry whose
 * final extension is all digits (.0, .1, ...). The named cert files (e.g.
 * Foo.crt / Foo.pem) are kept; the aggregated ca-certificates.crt bundle is
 * excluded separately in scan_dir. Only used (and only defined) when the
 * system trust store is actually scanned — see secmngr_discover_certificates(). */
static int filter_system_cert(const struct dirent *entry)
{
	const char *dot;

	if (entry->d_name[0] == '.')
		return 0;

	dot = strrchr(entry->d_name, '.');
	if (dot && dot[1] != '\0') {
		const char *p = dot + 1;
		while (*p && isdigit((unsigned char)*p))
			p++;
		if (*p == '\0')
			return 0;  /* extension is all digits -> c_rehash link */
	}
	return 1;
}
#endif

/* The secmngr-owned cert dir uses .pem suffixes. */
static int filter_pem(const struct dirent *entry)
{
	if (entry->d_name[0] == '.')
		return 0;
	const char *dot = strrchr(entry->d_name, '.');
	return dot && strcasecmp(dot, ".pem") == 0;
}

static int compare(const struct dirent **a, const struct dirent **b)
{
	return strcasecmp((*a)->d_name, (*b)->d_name);
}

static int scan_dir(const char *dir_path, int (*flt)(const struct dirent *),
                    const char *exclude, void (*cb)(const char *cert_path, void *arg), void *arg)
{
	struct dirent **namelist = NULL;

	int num_files = scandir(dir_path, &namelist, flt, compare);
	if (num_files < 0)
		return -1;

	for (int i = 0; i < num_files; i++) {
		char cert_path[SECMNGR_CERT_PATH_LEN];

		snprintf(cert_path, sizeof(cert_path), "%s/%s", dir_path, namelist[i]->d_name);

		/* Only enumerate real cert files; the c_rehash <hash>.N symlinks are
		 * already dropped by filter_system_cert. Also skip the aggregated CA
		 * bundle (exposed as a CABundle, not an individual Certificate). */
		if (!file_exists(cert_path) || !is_regular_file(cert_path) ||
		    (DM_STRLEN(exclude) && DM_STRCMP(cert_path, exclude) == 0)) {
			FREE(namelist[i]);
			continue;
		}

		cb(cert_path, arg);

		FREE(namelist[i]);
	}

	FREE(namelist);
	return 0;
}

void secmngr_discover_certificates(void (*cb)(const char *cert_path, void *arg), void *arg)
{
#ifndef SECMNGR_SKIP_SYSTEM_CERTS
	char bundle_real[SECMNGR_CERT_PATH_LEN] = {0};
	char resolved[PATH_MAX];

	/* Resolve the system CA bundle (/etc/ssl/cert.pem -> ca-certificates.crt)
	 * so the system scan can skip it: it lives in /etc/ssl/certs as a
	 * regular file but is surfaced as a CABundle instance, and
	 * PEM_read_X509 would otherwise add a bogus Certificate carrying only
	 * the bundle's first cert. */
	if (realpath(SECMNGR_SYSTEM_CABUNDLE_PATH, resolved))
		DM_STRNCPY(bundle_real, resolved, sizeof(bundle_real));

	/* System trust store (real cert files; c_rehash symlinks and the bundle skipped). */
	scan_dir(SECMNGR_SYSTEM_CERT_PATH, filter_system_cert, bundle_real, cb, arg);
#endif

	/* Certs added via the data model, stored under SECMNGR_OWN_CERT_DIR. */
	scan_dir(SECMNGR_OWN_CERT_DIR, filter_pem, NULL, cb, arg);
}

/*************************************************************
 * Cached readonly certificate fields
 *
 * The X509 is parsed once here (during dmmap sync) and its derived readonly
 * fields are stored as options on the dmmap Certificate section, so the GET
 * handlers in certificate.c serve them straight from dmmap without re-parsing
 * the file on every request. A stored `mtime` drives invalidation.
 *************************************************************/
static char *get_certificate_sig_alg(int sig_nid)
{
	switch (sig_nid) {
	case NID_sha256WithRSAEncryption:  return "sha256WithRSAEncryption";
	case NID_sha384WithRSAEncryption:  return "sha384WithRSAEncryption";
	case NID_sha512WithRSAEncryption:  return "sha512WithRSAEncryption";
	case NID_sha224WithRSAEncryption:  return "sha224WithRSAEncryption";
	case NID_md5WithRSAEncryption:     return "md5WithRSAEncryption";
	case NID_sha1WithRSAEncryption:    return "sha1WithRSAEncryption";
	default:                           return "";
	}
}

static char *generate_serial_number(const char *text, int length)
{
	unsigned pos = 0;
	char *hex = (char *)dmcalloc(100, sizeof(char));
	if (!hex)
		return dmstrdup("");

	for (int i = 0; i < length; i++)
		pos += snprintf(&hex[pos], 100 - pos, "%02x:", text[i] & 0xff);

	if (pos)
		hex[pos - 1] = 0;

	return hex;
}

/* X509_NAME as a TR-181 string: drop the leading '/' and turn the OpenSSL
 * '/'-separated oneline form into a space-separated one. */
static char *format_x509_name(X509_NAME *name)
{
	char buf[256] = {0};
	char *p = buf;

	X509_NAME_oneline(name, buf, sizeof(buf));
	if (*p == '/')
		p++;
	return replace_char(dmstrdup(p), '/', ' ');
}

/* Small, non-cryptographic string hash (FNV-1a), folded into
 * secmngr_cert_file_stem()'s output purely to disambiguate filenames —
 * never a security boundary. */
static unsigned long fnv1a_hash(const char *s)
{
	unsigned long h = 2166136261UL;

	for (; s && *s; s++) {
		h ^= (unsigned char)*s;
		h *= 16777619UL;
	}
	return h;
}

/* Derive a filesystem-safe, UCI-section-name-safe stem for a certificate's
 * on-disk filename (and, absent an explicit Name, its Certificate.{i}.Name
 * fallback, and the "cert_<stem>" section name secmngr_ensure_cert_section()
 * derives from it) from its SerialNumber *and* Issuer together — the same
 * pair secmngr_find_certificate_dmmap_by_cert() treats as the certificate's
 * functional identity. SerialNumber alone is not a safe filename key: the
 * spec only requires it to be unique *within* a single issuing CA, so two
 * different, legitimate CAs can plausibly reuse the same serial (e.g. both
 * starting at "01") — which would otherwise make two unrelated
 * certificates collide on the same file. Format: "<serial-hex>_<issuer-hash>",
 * 8 lowercase hex digits for the hash. The separator is '_', not '-': UCI
 * section names only allow [A-Za-z0-9_] (see uci_validate_str() in
 * libuci's util.c) — a '-' here would make dmuci_add_named_section() in
 * secmngr_ensure_cert_section() silently fail (uci_set() rejects the name,
 * *s is never set), so the cert's file gets written but the UCI/dmmap row
 * and its bundle `cert` list entry never do. */
void secmngr_cert_file_stem(X509 *cert, char *out, size_t outlen)
{
	ASN1_INTEGER *serial = X509_get_serialNumber(cert);
	char *issuer = format_x509_name(X509_get_issuer_name(cert));
	char serial_hex[128] = {0};
	size_t pos = 0;

	if (serial && serial->length > 0) {
		for (int i = 0; i < serial->length && pos + 2 < sizeof(serial_hex); i++)
			pos += snprintf(serial_hex + pos, sizeof(serial_hex) - pos, "%02x", serial->data[i] & 0xff);
	} else {
		DM_STRNCPY(serial_hex, "unknown", sizeof(serial_hex));
	}

	snprintf(out, outlen, "%s_%08lx", serial_hex, fnv1a_hash(issuer) & 0xffffffffUL);
}

static char *format_asn1_time(const ASN1_TIME *t)
{
	char buf[DATE_LEN] = {0};
	struct tm tm;

	if (t && ASN1_TIME_to_tm(t, &tm))
		strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
	else
		DM_STRNCPY(buf, "0001-01-01T00:00:00Z", sizeof(buf));

	return dmstrdup(buf);
}

/* Render the X509v3 Subject Alternative Names extension as a TR-181 <<list>>
 * (comma-separated), each entry prefixed with its type ("DNS:", "IP:", "URI:",
 * "email:") matching `openssl x509 -text`. */
static char *format_subject_alt(X509 *cert)
{
	GENERAL_NAMES *names = NULL;
	char buf[2048] = {0};
	size_t pos = 0;

	names = (GENERAL_NAMES *)X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
	if (!names)
		return dmstrdup("");

	int n = sk_GENERAL_NAME_num(names);
	for (int i = 0; i < n && pos < sizeof(buf) - 1; i++) {
		GENERAL_NAME *gn = sk_GENERAL_NAME_value(names, i);
		char entry[300] = {0};

		switch (gn->type) {
		case GEN_DNS:
			snprintf(entry, sizeof(entry), "DNS:%.*s",
				ASN1_STRING_length(gn->d.dNSName),
				ASN1_STRING_get0_data(gn->d.dNSName));
			break;
		case GEN_URI:
			snprintf(entry, sizeof(entry), "URI:%.*s",
				ASN1_STRING_length(gn->d.uniformResourceIdentifier),
				ASN1_STRING_get0_data(gn->d.uniformResourceIdentifier));
			break;
		case GEN_EMAIL:
			snprintf(entry, sizeof(entry), "email:%.*s",
				ASN1_STRING_length(gn->d.rfc822Name),
				ASN1_STRING_get0_data(gn->d.rfc822Name));
			break;
		case GEN_IPADD: {
			const unsigned char *ip = ASN1_STRING_get0_data(gn->d.iPAddress);
			int iplen = ASN1_STRING_length(gn->d.iPAddress);
			if (iplen == 4)
				snprintf(entry, sizeof(entry), "IP:%u.%u.%u.%u",
					ip[0], ip[1], ip[2], ip[3]);
			else if (iplen == 16)
				snprintf(entry, sizeof(entry),
					"IP:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
					ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7],
					ip[8], ip[9], ip[10], ip[11], ip[12], ip[13], ip[14], ip[15]);
			break;
		}
		default:
			continue;
		}

		if (entry[0] == 0)
			continue;

		pos += snprintf(buf + pos, sizeof(buf) - pos, "%s%s", pos ? "," : "", entry);
	}

	GENERAL_NAMES_free(names);
	return dmstrdup(buf);
}

/* Parse the cert file and (re)populate the cached readonly fields + mtime on the
 * dmmap section. On parse failure the row keeps whatever it had (browse still
 * lists it; the getters return "" for missing options). */
static void store_cert_fields(struct uci_section *dm, const char *path, time_t mtime)
{
	char mtime_str[32];
	ASN1_INTEGER *serial;
	FILE *fp;
	X509 *cert;

	// cppcheck-suppress cert-MSC24-C
	fp = fopen(path, "r");
	if (!fp)
		return;
	cert = PEM_read_X509(fp, NULL, NULL, NULL);
	fclose(fp);
	if (!cert)
		return;

	serial = X509_get_serialNumber(cert);
	dmuci_set_value_by_section(dm, "SerialNumber",
		generate_serial_number((char *)serial->data, serial->length));
	dmuci_set_value_by_section(dm, "Issuer", format_x509_name(X509_get_issuer_name(cert)));
	dmuci_set_value_by_section(dm, "Subject", format_x509_name(X509_get_subject_name(cert)));
	dmuci_set_value_by_section(dm, "NotBefore", format_asn1_time(X509_get0_notBefore(cert)));
	dmuci_set_value_by_section(dm, "NotAfter", format_asn1_time(X509_get0_notAfter(cert)));
	dmuci_set_value_by_section(dm, "SubjectAlt", format_subject_alt(cert));
	dmuci_set_value_by_section(dm, "SignatureAlgorithm", get_certificate_sig_alg(X509_get_signature_nid(cert)));

	snprintf(mtime_str, sizeof(mtime_str), "%lld", (long long)mtime);
	dmuci_set_value_by_section(dm, "__mtime__", mtime_str);

	X509_free(cert);
}

/* Return the basename of path with its final extension stripped (e.g.
 * "/etc/ssl/cert.pem" -> "cert", "/etc/ssl/certs/abc.crt" -> "abc"). Writes
 * into out (caller-allocated, at least outlen bytes); always non-empty for
 * any non-empty path. */
void secmngr_basename_no_ext(const char *path, char *out, size_t outlen)
{
	const char *base;
	const char *dot;
	size_t len;

	base = path ? strrchr(path, '/') : NULL;
	base = base ? base + 1 : path;
	if (!base) {
		out[0] = '\0';
		return;
	}

	dot = strrchr(base, '.');
	len = dot ? (size_t)(dot - base) : strlen(base);
	if (len >= outlen)
		len = outlen - 1;

	memcpy(out, base, len);
	out[len] = '\0';
}

struct uci_section *secmngr_sync_certificate_dmmap_row(struct dmctx *dmctx,
		const char *cert_path, char **instance)
{
	struct uci_section *dm = is_dmmap_section_exist_eq("Security", "Certificate", "__path__", cert_path);
	struct uci_section *cfg = secmngr_find_cert_section(cert_path);
	struct stat st;
	time_t mtime = (stat(cert_path, &st) == 0) ? st.st_mtime : 0;
	char *explicit_name = NULL;

	if (dm == NULL) {
		dm = create_dmmap_obj(dmctx, 0, "Security", "Certificate", cfg, instance);
		if (!dm)
			return NULL;
		dmuci_set_value_by_section(dm, "__path__", cert_path);
		store_cert_fields(dm, cert_path, mtime);
	} else {
		char *cached = NULL;
		char mtime_str[32];

		if (instance)
			dmuci_get_value_by_section_string(dm, "__instance__", instance);

		/* Refresh cached fields only when the file changed on disk
		 * (or the row predates caching and has no stored mtime). */
		snprintf(mtime_str, sizeof(mtime_str), "%lld", (long long)mtime);
		dmuci_get_value_by_section_string(dm, "__mtime__", &cached);
		if (DM_STRCMP(cached, mtime_str) != 0)
			store_cert_fields(dm, cert_path, mtime);
	}

	/* Name: the config section's explicit `name` option if present;
	 * otherwise the file's own basename with its extension stripped. Both
	 * AddCertificateBundle() (the caller-supplied Name) and
	 * install_certs_from_bio()/AddCABundle() (the serial-derived stem,
	 * set via secmngr_ensure_cert_section() so the row has a real backing
	 * section) now populate the `name` option explicitly; only rows with
	 * no config section at all — every system-store cert, and the rare
	 * case where secmngr_ensure_cert_section() itself failed — fall
	 * through to the basename here. Name is this table's DM_FLAG_LINKER
	 * parameter (see tSecurityCertificateParams) so it must never be
	 * empty; the basename fallback guarantees that. Recomputed every
	 * call; both inputs are stable once set, so this is idempotent. */
	if (cfg)
		dmuci_get_value_by_section_string(cfg, "name", &explicit_name);

	if (DM_STRLEN(explicit_name)) {
		dmuci_set_value_by_section(dm, "Name", explicit_name);
	} else {
		char base[SECMNGR_MAX_SECTION_NAME_LEN];

		secmngr_basename_no_ext(cert_path, base, sizeof(base));
		dmuci_set_value_by_section(dm, "Name", DM_STRLEN(base) ? base : cert_path);
	}

	return dm;
}

/* Create dmmap Certificate sections for newly discovered cert files and prune
 * ones whose file has gone. Link to the secmngr.certificate metadata section
 * via __section_name__ when one exists (cert get/set handlers resolve writable
 * metadata by path regardless, so a stale/empty link is benign). */
static void sync_one_certificate_dmmap_row(const char *cert_path, void *arg)
{
	struct dmctx *dmctx = arg;
	char *instance = NULL;

	secmngr_sync_certificate_dmmap_row(dmctx, cert_path, &instance);
}

static void sync_certificate_dmmap(struct dmctx *dmctx)
{
	struct uci_section *s = NULL, *stmp = NULL;

	secmngr_discover_certificates(sync_one_certificate_dmmap_row, dmctx);

	uci_path_foreach_sections_safe(bbfdm, "Security", "Certificate", stmp, s) {
		char *p = NULL;

		dmuci_get_value_by_section_string(s, "__path__", &p);
		if (DM_STRLEN(p) == 0 || !file_exists(p)) {
			dmuci_delete_by_section(s, NULL, NULL);
			continue;
		}

#ifdef SECMNGR_SKIP_SYSTEM_CERTS
		/* The file itself hasn't gone anywhere — SECMNGR_SKIP_SYSTEM_CERTS
		 * just means a build should no longer expose it. Prune any row a
		 * previous build (without the flag) left behind, so toggling the
		 * flag doesn't leave stale rows visible on the data model. */
		if (strncmp(p, SECMNGR_SYSTEM_CERT_PATH "/", strlen(SECMNGR_SYSTEM_CERT_PATH) + 1) == 0)
			dmuci_delete_by_section(s, NULL, NULL);
#endif
	}
}

/* Create dmmap CABundle sections and prune ones whose backing config section
 * (or, for the system bundle, whose file) has gone. The system bundle
 * (/etc/ssl/cert.pem) is a read-only synthesized row with no backing
 * `config cabundle` section — it carries no persisted UCI state of its own
 * at all, so toggling SECMNGR_SKIP_SYSTEM_CERTS on a later build leaves
 * nothing behind to clean up. */
static void sync_cabundle_dmmap(struct dmctx *dmctx)
{
	struct uci_section *s = NULL, *stmp = NULL;
	char *instance = NULL;

#ifndef SECMNGR_SKIP_SYSTEM_CERTS
	/* System trust store bundle: /etc/ssl/cert.pem. It has no backing
	 * config section (read-only), so de-dup by path like the system certs
	 * do. Name (this table's DM_FLAG_LINKER parameter, see
	 * tSecurityCABundleParams) is the file's own basename with its
	 * extension stripped — needs no persisted state either. */
	if (file_exists(SECMNGR_SYSTEM_CABUNDLE_PATH) &&
	    is_dmmap_section_exist_eq("Security", "CABundle", "__path__", SECMNGR_SYSTEM_CABUNDLE_PATH) == NULL) {
		struct uci_section *dm = create_dmmap_obj(dmctx, 0, "Security", "CABundle", NULL, &instance);
		if (dm) {
			char base[SECMNGR_MAX_SECTION_NAME_LEN];

			secmngr_basename_no_ext(SECMNGR_SYSTEM_CABUNDLE_PATH, base, sizeof(base));
			dmuci_set_value_by_section(dm, "__path__", SECMNGR_SYSTEM_CABUNDLE_PATH);
			dmuci_set_value_by_section(dm, "Name", DM_STRLEN(base) ? base : SECMNGR_SYSTEM_CABUNDLE_PATH);
		}
	}
#endif

	uci_foreach_sections(SECMNGR_UCI_PACKAGE, "cabundle", s) {
		struct uci_section *dm = create_dmmap_obj(dmctx, 0, "Security", "CABundle", s, &instance);
		if (dm) {
			char *cs_name = NULL;
			char bundle_path[SECMNGR_CERT_PATH_LEN];

			dmuci_get_value_by_section_string(s, "name", &cs_name);
			dmuci_set_value_by_section(dm, "Name", DM_STRLEN(cs_name) ? cs_name : "");

			/* The persisted combined-bundle file AddCABundle() writes
			 * (see sec_plugin.c) — cached here so a fresh dmctx doesn't
			 * need to re-derive it. Never equal to
			 * SECMNGR_SYSTEM_CABUNDLE_PATH, so this can't be confused
			 * with the system row's own `path` (see the exact-match
			 * check in the prune loop below). */
			if (DM_STRLEN(cs_name) &&
			    snprintf(bundle_path, sizeof(bundle_path), "%s/%s.pem", SECMNGR_OWN_CABUNDLE_DIR, cs_name) < (int)sizeof(bundle_path))
				dmuci_set_value_by_section(dm, "__path__", bundle_path);
		}
	}

	uci_path_foreach_sections_safe(bbfdm, "Security", "CABundle", stmp, s) {
		char *path = NULL, *sec_name = NULL;

		/* System bundle row: identified by exact path match (not mere
		 * presence — user-added rows carry a `path` too, see below), so
		 * it's pruned when the file is gone, not by the config back-link.
		 * SECMNGR_SKIP_SYSTEM_CERTS additionally prunes it unconditionally —
		 * the file itself hasn't gone anywhere, but a build with the flag
		 * set should never expose a row a previous build left behind. */
		dmuci_get_value_by_section_string(s, "__path__", &path);
		if (DM_STRCMP(path, SECMNGR_SYSTEM_CABUNDLE_PATH) == 0) {
#ifdef SECMNGR_SKIP_SYSTEM_CERTS
			dmuci_delete_by_section(s, NULL, NULL);
#else
			if (!file_exists(path))
				dmuci_delete_by_section(s, NULL, NULL);
#endif
			continue;
		}

		dmuci_get_value_by_section_string(s, "__section_name__", &sec_name);
		if (get_config_section_from_dmmap_section_name(sec_name) == NULL)
			dmuci_delete_by_section(s, NULL, NULL);
	}
}

void secmngr_dmmap_rebuild(void)
{
	struct dmctx bbf_ctx = {0};

	bbf_ctx_init(&bbf_ctx, NULL);
	sync_cabundle_dmmap(&bbf_ctx);
	sync_certificate_dmmap(&bbf_ctx);
	dmuci_commit_package_bbfdm("Security");
	bbf_ctx_clean(&bbf_ctx);
}

struct uci_section *secmngr_find_cert_section(const char *cert_path)
{
	struct uci_section *s = NULL;

	if (DM_STRLEN(cert_path) == 0)
		return NULL;

	uci_foreach_option_eq(SECMNGR_UCI_PACKAGE, "certificate", "path", cert_path, s) {
		return s;
	}

	return NULL;
}

/* Public wrapper: looks up an existing "Security"/Certificate dmmap row by
 * its cached SerialNumber+Issuer (the certificate's functional unique key
 * per spec) — via the framework's own get_section_in_dmmap_with_options_eq()
 * (dmcommon.h), which is exactly this two-option AND-match lookup already.
 * Computes the identity strings directly from an X509, using the exact same
 * formatting store_cert_fields() caches them with, so
 * operate_Security_AddCertificateBundle() (sec_plugin.c) can find "is this
 * exact certificate already tracked" without duplicating that formatting
 * logic itself. */
struct uci_section *secmngr_find_certificate_dmmap_by_cert(X509 *cert)
{
	ASN1_INTEGER *serial_asn1;
	char *serial_str, *issuer_str;

	if (!cert)
		return NULL;

	serial_asn1 = X509_get_serialNumber(cert);
	serial_str = generate_serial_number((char *)serial_asn1->data, serial_asn1->length);
	issuer_str = format_x509_name(X509_get_issuer_name(cert));

	return get_section_in_dmmap_with_options_eq("Security", "Certificate",
			"SerialNumber", serial_str, "Issuer", issuer_str);
}

struct uci_section *secmngr_ensure_cert_section(const char *cert_path, const char *name)
{
	struct uci_section *s = NULL;
	char sec_name[SECMNGR_MAX_SECTION_NAME_LEN + sizeof(SECMNGR_CERT_SECTION_PREFIX)];

	if (DM_STRLEN(cert_path) == 0 || DM_STRLEN(name) == 0)
		return NULL;

	snprintf(sec_name, sizeof(sec_name), "%s%s", SECMNGR_CERT_SECTION_PREFIX, name);
	replace_special_char(sec_name, '_');

	s = secmngr_find_cert_section(cert_path);
	if (s) {
		/* Keep the section's UCI identity in sync with a changed Name
		 * (e.g. AddCertificateBundle() replacing this entry in place
		 * under the same identity but, in principle, a fresh Name). */
		if (DM_STRCMP(section_name(s), sec_name) != 0)
			dmuci_rename_section_by_section(s, sec_name);
	} else {
		dmuci_add_named_section(SECMNGR_UCI_PACKAGE, "certificate", sec_name, &s);
		if (!s)
			return NULL;

		dmuci_set_value_by_section(s, "path", cert_path);
	}

	dmuci_set_value_by_section(s, "name", name);

	return s;
}

int secmngr_init(void *data)
{
	(void)data;

	mkdir(SECMNGR_DEFAULT_BASE_PATH, 0755);
	mkdir(SECMNGR_OWN_CERT_DIR, 0755);
	mkdir(SECMNGR_OWN_KEY_DIR, 0700);
	mkdir(SECMNGR_OWN_CABUNDLE_DIR, 0755);

	/* Generate the "Security" dmmap so browse/GET, cross-service reference
	 * resolution and the v2 AddObject path (which requires the dmmap file to
	 * already exist, see dm_entry_add_object) all work from first load. */
	secmngr_dmmap_rebuild();

	return 0;
}

int secmngr_hex_decode(const char *hex, unsigned char **out)
{
	size_t len, blen;

	*out = NULL;
	if (!hex)
		return -1;
	len = strlen(hex);
	if (len == 0 || (len & 1))
		return -1;

	blen = len / 2;
	*out = (unsigned char *)calloc(blen, 1);
	if (!*out)
		return -1;

	for (size_t i = 0; i < blen; i++) {
		unsigned int b;
		if (sscanf(hex + 2 * i, "%2x", &b) != 1) {
			FREE(*out);
			return -1;
		}
		(*out)[i] = (unsigned char)b;
	}
	return (int)blen;
}

/*************************************************************
 * Bundle input decoding
 *
 * Each handler pairs a detector with a decoder for one accepted encoding
 * of the Bundle input. secmngr_decode_bundle_input() tries them in array
 * order and uses the first match; an input matching none of them is
 * rejected outright (-1) rather than falling through to some default —
 * every handler's detector must earn its match.
 *
 * A handler's detector is only a cheap format signature check (e.g. "is
 * this a well-formed hex string", "does this start with PEM armor"), not
 * full content validation — actual certificate/key well-formedness is
 * still solely OpenSSL's job (PEM_read_bio_X509()/PEM_read_bio_PrivateKey()
 * downstream in sec_plugin.c/common.c), which already has its own fault
 * codes (7263/7265/etc.) for genuinely malformed-but-right-shaped input.
 * The point of a detector is only to keep obviously-wrong input (garbage
 * that is neither valid hex nor even PEM-shaped) from being treated as
 * "successfully decoded" in the first place.
 *************************************************************/
struct bundle_format_handler {
	const char *name;
	bool (*detect)(const char *input);
	int (*decode)(const char *input, unsigned char **out);
};

/* hexBinary — the spec-mandated encoding: an even-length string of only
 * hex digits. Validated fully here (not just "looks hexy") so it never
 * misclassifies plain text that happens to start with hex-looking
 * characters — real PEM text always contains '-', spaces, or newlines
 * from its "-----BEGIN ...-----" armor, none of which are hex digits. */
static bool bundle_input_is_hexbinary(const char *input)
{
	size_t len = strlen(input);

	if (len == 0 || (len & 1))
		return false;

	for (size_t i = 0; i < len; i++) {
		if (!isxdigit((unsigned char)input[i]))
			return false;
	}
	return true;
}

static int bundle_input_decode_hexbinary(const char *input, unsigned char **out)
{
	return secmngr_hex_decode(input, out);
}

/* Plain text — a literal PEM blob. Signature check only: does it contain
 * PEM armor ("-----BEGIN ")? This rejects unrelated garbage (random
 * strings, truncated/binary noise) at the decode stage instead of
 * silently accepting it as "decoded" and only failing much later inside
 * PEM_read_bio_X509(). It intentionally does not validate the full PEM
 * structure (matching "-----END ", correct base64 body, etc.) — that
 * remains OpenSSL's job downstream, which already has its own fault
 * codes for that. */
static bool bundle_input_is_pem_text(const char *input)
{
	return strstr(input, "-----BEGIN ") != NULL;
}

/* The payload is taken verbatim, no decoding — it's already the raw bytes
 * PEM_read_bio_X509()/PEM_read_bio_PrivateKey() expect. */
static int bundle_input_decode_plain_text(const char *input, unsigned char **out)
{
	size_t len = strlen(input);

	*out = (unsigned char *)calloc(len ? len : 1, 1);
	if (!*out)
		return -1;

	memcpy(*out, input, len);
	return (int)len;
}

static const struct bundle_format_handler bundle_format_handlers[] = {
	{ "hexBinary",  bundle_input_is_hexbinary, bundle_input_decode_hexbinary },
	{ "plain-text", bundle_input_is_pem_text,  bundle_input_decode_plain_text },
};

#define BUNDLE_FORMAT_HANDLER_COUNT \
	(sizeof(bundle_format_handlers) / sizeof(bundle_format_handlers[0]))

int secmngr_decode_bundle_input(const char *input, unsigned char **out)
{
	*out = NULL;
	if (DM_STRLEN(input) == 0)
		return -1;

	for (size_t i = 0; i < BUNDLE_FORMAT_HANDLER_COUNT; i++) {
		const struct bundle_format_handler *h = &bundle_format_handlers[i];

		if (h->detect != NULL && !h->detect(input))
			continue;

		return h->decode(input, out);
	}

	return -1;
}

/* Core of secmngr_install_cabundle_certs(): parse every PEM cert readable
 * from an already-open BIO. For each: if a Certificate row with the same
 * Issuer+SerialNumber already exists (this bundle, another bundle, or one
 * discovered independently), reuse its Name — no new file or section is
 * created. Otherwise write it to SECMNGR_OWN_CERT_DIR/<stem>.pem (stem
 * from secmngr_cert_file_stem() — the same location and naming
 * AddCertificateBundle() uses) and give it a real `config certificate`
 * section too (named "cert_<stem>", via
 * secmngr_ensure_cert_section()) — so a cert installed only through a
 * CABundle is just as visible in `uci show secmngr` for debugging as one
 * added directly via AddCertificateBundle(), not a dmmap-only row. If that
 * section can't be created, the dmmap row is skipped too rather than left
 * unbacked (the file itself still gets picked up, and given a proper row,
 * the next time secmngr_sync() rescans SECMNGR_OWN_CERT_DIR). Otherwise the
 * row's cached Name (its DM_FLAG_LINKER parameter — see
 * secmngr_sync_certificate_dmmap_row()) is appended directly to
 * bundle_section's `cert` list — the value CACertificates resolves back
 * to a pathRef via the framework's own bbfdm_get_references()/
 * bbfdm_get_reference_linker() (see cabundle.c), not hand-rolled lookups.
 * No fixed cap on the number of certs in a bundle. Does not take ownership
 * of bio. Returns the number of certs processed, or negative on failure:
 *   -7265 if the BIO isn't recognisable PEM
 *   -7266 on disk-write failure
 * A cert whose dmmap bookkeeping couldn't be created, or that collided on
 * path with an unrelated file, still counts towards the returned total but
 * contributes no `cert` list entry. */
static int install_certs_from_bio(struct dmctx *dmctx, BIO *bio,
                                  struct uci_section *bundle_section)
{
	X509 *cert = NULL;
	int count = 0;

	/* Probe: must contain at least one PEM cert. */
	cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
	if (!cert)
		return -7265;

	mkdir(SECMNGR_OWN_CERT_DIR, 0755);

	do {
		struct uci_section *existing;

		existing = secmngr_find_certificate_dmmap_by_cert(cert);
		if (existing) {
			char *existing_name = NULL;

			dmuci_get_value_by_section_string(existing, "Name", &existing_name);
			if (DM_STRLEN(existing_name))
				dmuci_add_list_value_by_section(bundle_section, "cert", existing_name);
			count++;
			X509_free(cert);
			continue;
		}

		char stem[96] = {0};
		char cert_path[SECMNGR_CERT_PATH_LEN];
		struct uci_section *dm;
		char *inst = NULL, *new_name = NULL;
		FILE *fp;

		secmngr_cert_file_stem(cert, stem, sizeof(stem));
		if (snprintf(cert_path, sizeof(cert_path), "%s/%s.pem", SECMNGR_OWN_CERT_DIR, stem) >= (int)sizeof(cert_path)) {
			X509_free(cert);
			return -7266;
		}

		/* Same identity was already ruled out above — this is a
		 * genuine path collision with an unrelated certificate (a true
		 * hash collision on the combined Serial+Issuer stem — see
		 * secmngr_cert_file_stem()). Never overwrite it; skip this one
		 * cert rather than aborting the whole bundle. */
		if (file_exists(cert_path)) {
			count++;
			X509_free(cert);
			continue;
		}

		// cppcheck-suppress cert-MSC24-C
		fp = fopen(cert_path, "w");
		if (!fp || PEM_write_X509(fp, cert) != 1) {
			if (fp) fclose(fp);
			X509_free(cert);
			return -7266;
		}
		fclose(fp);

		/* Give this cert a real `config certificate` section too (named
		 * "cert_<stem>", path+name both set to stem — the same value
		 * secmngr_basename_no_ext() would derive from cert_path anyway,
		 * so the resulting Name is unchanged) rather than leaving it as a
		 * dmmap-only row. A cert installed only via AddCABundle() used to
		 * have no persisted UCI record at all — invisible to `uci show
		 * secmngr`, which made it needlessly hard to inspect/debug.
		 *
		 * If this fails, skip the dmmap row too rather than creating one
		 * with no backing UCI section — keeps the two in sync (a dmmap
		 * row implies a real config section behind it) instead of leaving
		 * an orphan a developer would have to explain. The file already
		 * on disk isn't tracked this run; it will still be picked up (and
		 * given a proper row then) the next time secmngr_sync() rescans
		 * SECMNGR_OWN_CERT_DIR. */
		struct uci_section *cfg = secmngr_ensure_cert_section(cert_path, stem);
		if (!cfg) {
			count++;
			X509_free(cert);
			continue;
		}

		dm = secmngr_sync_certificate_dmmap_row(dmctx, cert_path, &inst);

		if (dm)
			dmuci_get_value_by_section_string(dm, "Name", &new_name);

		if (DM_STRLEN(new_name))
			dmuci_add_list_value_by_section(bundle_section, "cert", new_name);

		count++;
		X509_free(cert);
	} while ((cert = PEM_read_bio_X509(bio, NULL, NULL, NULL)) != NULL);

	return count;
}

int secmngr_install_cabundle_certs(struct dmctx *dmctx, const unsigned char *blob, int blen,
                                   struct uci_section *bundle_section)
{
	BIO *bio;
	int n;

	if (!blob || blen <= 0)
		return -7265;

	bio = BIO_new_mem_buf(blob, blen);
	if (!bio)
		return -7262;

	n = install_certs_from_bio(dmctx, bio, bundle_section);
	BIO_free(bio);
	return n;
}

int secmngr_clean(void *data)
{
	(void)data;
	return 0;
}

int secmngr_sync(void *data)
{
	struct bbfdm_context *bbfdm_ctx = (struct bbfdm_context *)data;

	if (bbfdm_ctx == NULL)
		return 0;

	/* uci_sync_handler: our own AddCertificateBundle()/AddCABundle()/Delete()
	 * operate handlers already keep the dmmap fully in sync as they run (see
	 * secmngr_sync_certificate_dmmap_row() and the CABundle/Certificate
	 * Delete() handlers), so a full rebuild here is only needed to catch
	 * /etc/config/secmngr being modified from outside the data model (e.g. a
	 * raw `uci set`). This avoids the expensive /etc/ssl/certs rescan + X.509
	 * reparse on every framework-wide apply event, most of which have nothing
	 * to do with secmngr's own config. */
	if (DM_STRCMP(bbfdm_ctx->uci_change_proto, "external") == 0 && !list_empty(&bbfdm_ctx->changed_uci)) {
		struct apply_handler_node *node = NULL;

		list_for_each_entry(node, &bbfdm_ctx->changed_uci, list) {
			if (DM_STRCMP(node->file_path, "/etc/config/" SECMNGR_UCI_PACKAGE) == 0) {
				secmngr_dmmap_rebuild();
				break;
			}
		}
	}

	return 0;
}
