/*	$OpenBSD: spl.c,v 1.18 2026/06/25 07:51:58 tb Exp $ */
/*
 * Copyright (c) 2024 Job Snijders <job@fastly.com>
 * Copyright (c) 2022 Theo Buehler <tb@openbsd.org>
 * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <assert.h>
#include <err.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <openssl/asn1.h>
#include <openssl/asn1t.h>
#include <openssl/stack.h>
#include <openssl/safestack.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>

#include "extern.h"
#include "rpki-asn1.h"

/*
 * SPL eContent definition in draft-ietf-sidrops-rpki-prefixlist-04, section 3.
 */

ASN1_ITEM_EXP SignedPrefixList_it;
ASN1_ITEM_EXP AddressFamilyPrefixes_it;

ASN1_SEQUENCE(SignedPrefixList) = {
	ASN1_EXP_OPT(SignedPrefixList, version, ASN1_INTEGER, 0),
	ASN1_SIMPLE(SignedPrefixList, asid, ASN1_INTEGER),
	ASN1_SEQUENCE_OF(SignedPrefixList, prefixBlocks, AddressFamilyPrefixes)
} ASN1_SEQUENCE_END(SignedPrefixList);

IMPLEMENT_ASN1_FUNCTIONS(SignedPrefixList);

ASN1_SEQUENCE(AddressFamilyPrefixes) = {
	ASN1_SIMPLE(AddressFamilyPrefixes, addressFamily, ASN1_OCTET_STRING),
	ASN1_SEQUENCE_OF(AddressFamilyPrefixes, addressPrefixes,
	    ASN1_BIT_STRING),
} ASN1_SEQUENCE_END(AddressFamilyPrefixes);

/*
 * Comparator to help sorting elements in SPL prefixBlocks and VSPs.
 * Returns -1 if 'a' should precede 'b', 1 if 'b' should precede 'a',
 * or '0' if a and b are equal.
 */
static int
prefix_cmp(enum afi afi, const struct ip_addr *a, const struct ip_addr *b)
{
	int cmp;

	switch (afi) {
	case AFI_IPV4:
		cmp = memcmp(&a->addr, &b->addr, 4);
		if (cmp < 0)
			return -1;
		if (cmp > 0)
			return 1;
		break;
	case AFI_IPV6:
		cmp = memcmp(&a->addr, &b->addr, 16);
		if (cmp < 0)
			return -1;
		if (cmp > 0)
			return 1;
		break;
	default:
		break;
	}

	if (a->prefixlen < b->prefixlen)
		return -1;
	if (a->prefixlen > b->prefixlen)
		return 1;

	return 0;
}

/*
 * Parses the eContent section of a SPL file,
 * draft-ietf-sidrops-rpki-prefixlist-02 section 3.
 * Returns zero on failure, non-zero on success.
 */
static int
spl_parse_econtent(const char *fn, void *obj, const unsigned char *d,
    size_t dsz)
{
	struct spl			*spl = obj;
	const unsigned char		*oder;
	SignedPrefixList		*spl_asn1;
	const AddressFamilyPrefixes	*afp;
	const STACK_OF(ASN1_BIT_STRING)	*prefixes;
	const ASN1_BIT_STRING		*prefix_asn1;
	int				 num_afps, num_prefixes;
	enum afi			 afi;
	struct ip_addr			 ip_addr;
	struct spl_pfx			*prefix;
	int				 ipv4_seen = 0, ipv6_seen = 0;
	int				 i, j, rc = 0;

	oder = d;
	if ((spl_asn1 = d2i_SignedPrefixList(NULL, &d, dsz)) == NULL) {
		warnx("%s: failed to parse SignedPrefixList", fn);
		goto out;
	}
	if (d != oder + dsz) {
		warnx("%s: %td bytes trailing garbage in eContent", fn,
		    oder + dsz - d);
		goto out;
	}

	if (!valid_econtent_version(fn, spl_asn1->version, 0))
		goto out;

	if (!as_id_parse(spl_asn1->asid, &spl->asid)) {
		warnx("%s: asid: malformed AS identifier", fn);
		goto out;
	}

	num_afps = sk_AddressFamilyPrefixes_num(spl_asn1->prefixBlocks);
	if (num_afps < 0 || num_afps > 2) {
		warnx("%s: unexpected number of AddressFamilyAddressPrefixes"
		    "(got %d, expected 0, 1, or 2)", fn, num_afps);
		goto out;
	}

	for (i = 0; i < num_afps; i++) {
		struct ip_addr *prev_ip_addr = NULL;

		afp = sk_AddressFamilyPrefixes_value(spl_asn1->prefixBlocks, i);
		prefixes = afp->addressPrefixes;
		num_prefixes = sk_ASN1_BIT_STRING_num(afp->addressPrefixes);

		if (num_prefixes == 0) {
			warnx("%s: empty AddressFamilyAddressPrefixes", fn);
			goto out;
		}
		if (spl->num_prefixes + num_prefixes >= MAX_IP_SIZE) {
			warnx("%s: too many addressPrefixes entries", fn);
			goto out;
		}

		if (!ip_addr_afi_parse(fn, afp->addressFamily, &afi))
			goto out;

		switch (afi) {
		case AFI_IPV4:
			if (ipv4_seen++ > 0) {
				warnx("%s: addressFamilyIPv4 appeared twice",
				    fn);
				goto out;
			}
			if (ipv6_seen > 0) {
				warnx("%s: invalid sorting, IPv6 before IPv4",
				    fn);
				goto out;
			}
			break;
		case AFI_IPV6:
			if (ipv6_seen++ > 0) {
				warnx("%s: addressFamilyIPv6 appeared twice",
				    fn);
				goto out;
			}
		}

		spl->prefixes = recallocarray(spl->prefixes, spl->num_prefixes,
		    spl->num_prefixes + num_prefixes, sizeof(spl->prefixes[0]));
		if (spl->prefixes == NULL)
			err(1, NULL);

		for (j = 0; j < num_prefixes; j++) {
			prefix_asn1 = sk_ASN1_BIT_STRING_value(prefixes, j);

			if (!ip_addr_parse(prefix_asn1, afi, fn, &ip_addr))
				goto out;

			if (j > 0 &&
			    prefix_cmp(afi, prev_ip_addr, &ip_addr) != -1) {
				warnx("%s: invalid addressPrefixes sorting", fn);
				goto out;
			}

			prefix = &spl->prefixes[spl->num_prefixes++];
			prefix->prefix = ip_addr;
			prefix->afi = afi;
			prev_ip_addr = &prefix->prefix;
		}
	}

	rc = 1;
 out:
	SignedPrefixList_free(spl_asn1);
	return rc;
}

static int
spl_cert_info(const char *fn, void *obj, const struct cert *cert)
{
	if (x509_any_inherits(cert->x509)) {
		warnx("%s: inherit elements not allowed in EE cert", fn);
		return 0;
	}

	if (cert->num_ases == 0) {
		warnx("%s: AS Resources extension missing", fn);
		return 0;
	}

	if (cert->num_ips > 0) {
		warnx("%s: superfluous IP Resources extension present", fn);
		return 0;
	}

	return 1;
}

static int
spl_validate(const char *fn, void *obj, struct cert *cert)
{
	struct spl *spl = obj;

	spl->valid = valid_spl(fn, cert, spl);

	return 1; /* XXX */
}

static void *
spl_obj_new(size_t der_len, time_t signtime)
{
	struct spl *spl;

	if ((spl = calloc(1, sizeof(*spl))) == NULL)
		err(1, NULL);
	spl->signtime = signtime;

	return spl;
}

static void
spl_obj_free(void *obj)
{
	spl_free(obj);
}

static const struct signed_obj spl_signed_obj = {
	.rtype = RTYPE_SPL,
	.new = spl_obj_new,
	.free = spl_obj_free,
	.cert_info = spl_cert_info,
	.parse_econtent = spl_parse_econtent,
	.validate = spl_validate,
};

const struct signed_obj *
spl_obj(void)
{
	return &spl_signed_obj;
}

/*
 * Parse a full Signed Prefix List file.
 * Returns the SPL, or NULL if the object was malformed.
 */
struct spl *
spl_parse(struct cert **out_cert, const char *fn, int talid,
    const unsigned char *der, size_t len)
{
	struct spl	*spl;
	struct cert	*cert = NULL;
	size_t		 cmsz;
	unsigned char	*cms;
	time_t		 signtime = 0;
	int		 rc = 0;

	assert(*out_cert == NULL);

	cms = cms_parse_validate(&cert, fn, talid, der, len, spl_oid, &cmsz,
	    &signtime);
	if (cms == NULL)
		return NULL;

	spl = spl_obj_new(len, signtime);
	if (!spl_cert_info(fn, spl, cert))
		goto out;
	if (!spl_parse_econtent(fn, spl, cms, cmsz))
		goto out;
	(void)spl_validate(fn, spl, cert);

	*out_cert = cert;
	cert = NULL;

	rc = 1;
 out:
	if (rc == 0) {
		spl_free(spl);
		spl = NULL;
	}
	cert_free(cert);
	free(cms);
	return spl;
}

void
spl_free(struct spl *s)
{
	if (s == NULL)
		return;

	free(s->prefixes);
	free(s);
}

/*
 * Serialize parsed SPL content.
 * See spl_read() for reader.
 */
void
spl_buffer(struct ibuf *b, const struct spl *s)
{
	io_simple_buffer(b, &s->valid, sizeof(s->valid));
	io_simple_buffer(b, &s->asid, sizeof(s->asid));
	io_simple_buffer(b, &s->talid, sizeof(s->talid));
	io_simple_buffer(b, &s->num_prefixes, sizeof(s->num_prefixes));
	io_simple_buffer(b, &s->expires, sizeof(s->expires));

	io_simple_buffer(b, s->prefixes,
	    s->num_prefixes * sizeof(s->prefixes[0]));
}

/*
 * Read parsed SPL content from descriptor.
 * See spl_buffer() for writer.
 * Result must be passed to spl_free().
 */
struct spl *
spl_read(struct ibuf *b)
{
	struct spl *s;

	if ((s = calloc(1, sizeof(struct spl))) == NULL)
		err(1, NULL);

	io_read_buf(b, &s->valid, sizeof(s->valid));
	io_read_buf(b, &s->asid, sizeof(s->asid));
	io_read_buf(b, &s->talid, sizeof(s->talid));
	io_read_buf(b, &s->num_prefixes, sizeof(s->num_prefixes));
	io_read_buf(b, &s->expires, sizeof(s->expires));

	if (s->num_prefixes > 0) {
		if ((s->prefixes = calloc(s->num_prefixes,
		    sizeof(s->prefixes[0]))) == NULL)
			err(1, NULL);
		io_read_buf(b, s->prefixes,
		    s->num_prefixes * sizeof(s->prefixes[0]));
	}

	return s;
}

static int
spl_pfx_cmp(const struct spl_pfx *a, const struct spl_pfx *b)
{
	if (a->afi > b->afi)
		return 1;
	if (a->afi < b->afi)
		return -1;

	return prefix_cmp(a->afi, &a->prefix, &b->prefix);
}

static void
insert_vsp(struct vsp *vsp, size_t idx, struct spl_pfx *pfx)
{
	if (idx < vsp->num_prefixes)
		memmove(vsp->prefixes + idx + 1, vsp->prefixes + idx,
		    (vsp->num_prefixes - idx) * sizeof(vsp->prefixes[0]));
	vsp->prefixes[idx] = *pfx;
	vsp->num_prefixes++;
}

/*
 * Add each prefix in the SPL into the VSP tree.
 * Updates "vsps" to be the number of VSPs and "uniqs" to be the unique
 * number of prefixes.
 */
void
spl_insert_vsps(struct vsp_tree *tree, struct spl *spl, struct repo *rp)
{
	struct vsp	*vsp, *found;
	size_t		 i, j;
	int		 cmp;

	if ((vsp = calloc(1, sizeof(*vsp))) == NULL)
		err(1, NULL);

	vsp->asid = spl->asid;
	vsp->talid = spl->talid;
	vsp->expires = spl->expires;
	vsp->repoid = repo_id(rp);

	if ((found = RB_INSERT(vsp_tree, tree, vsp)) != NULL) {
		/* already exists */
		if (found->expires < vsp->expires) {
			/* adjust unique count */
			repo_stat_inc(repo_byid(found->repoid),
			    found->talid, RTYPE_SPL, STYPE_DEC_UNIQUE);
			found->expires = vsp->expires;
			found->talid = vsp->talid;
			found->repoid = vsp->repoid;
			repo_stat_inc(rp, vsp->talid, RTYPE_SPL,
			    STYPE_UNIQUE);
		}
		free(vsp);
		vsp = found;
	} else
		repo_stat_inc(rp, vsp->talid, RTYPE_SPL, STYPE_UNIQUE);
	repo_stat_inc(rp, spl->talid, RTYPE_SPL, STYPE_TOTAL);

	/* merge content of multiple SPLs */
	vsp->prefixes = reallocarray(vsp->prefixes,
	    vsp->num_prefixes + spl->num_prefixes, sizeof(vsp->prefixes[0]));
	if (vsp->prefixes == NULL)
		err(1, NULL);

	/*
	 * Merge all data from the new SPL at hand into 'vsp': loop over
	 * all SPL->pfxs, and insert them in the right place in
	 * vsp->prefixes while keeping the order of the array.
	 */
	for (i = 0, j = 0; i < spl->num_prefixes; ) {
		cmp = -1;
		if (j == vsp->num_prefixes ||
		    (cmp = spl_pfx_cmp(&spl->prefixes[i],
		     &vsp->prefixes[j])) < 0) {
			insert_vsp(vsp, j, &spl->prefixes[i]);
			i++;
		} else if (cmp == 0)
			i++;

		if (j < vsp->num_prefixes)
			j++;
	}
}

/*
 * Comparison function for the RB tree
 */
static inline int
vspcmp(const struct vsp *a, const struct vsp *b)
{
	if (a->asid > b->asid)
		return 1;
	if (a->asid < b->asid)
		return -1;

	return 0;
}

RB_GENERATE(vsp_tree, vsp, entry, vspcmp);
