/*	$OpenBSD: rsc.c,v 1.47 2026/07/09 12:13:56 tb Exp $ */
/*
 * Copyright (c) 2022 Theo Buehler <tb@openbsd.org>
 * Copyright (c) 2022 Job Snijders <job@fastly.com>
 * 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 <stdlib.h>
#include <string.h>
#include <unistd.h>

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

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

/*
 * RSC eContent definition in RFC 9323, section 4.
 */

ASN1_ITEM_EXP RpkiSignedChecklist_it;
ASN1_ITEM_EXP FileNameAndHash_it;
ASN1_ITEM_EXP ResourceBlock_it;
ASN1_ITEM_EXP ConstrainedIPAddrBlocks_it;
ASN1_ITEM_EXP ConstrainedIPAddressFamily_it;
ASN1_ITEM_EXP ConstrainedASIdentifiers_it;

ASN1_SEQUENCE(RpkiSignedChecklist) = {
	ASN1_EXP_OPT(RpkiSignedChecklist, version, ASN1_INTEGER, 0),
	ASN1_SIMPLE(RpkiSignedChecklist, resources, ResourceBlock),
	ASN1_SIMPLE(RpkiSignedChecklist, digestAlgorithm, X509_ALGOR),
	ASN1_SEQUENCE_OF(RpkiSignedChecklist, checkList, FileNameAndHash),
} ASN1_SEQUENCE_END(RpkiSignedChecklist);

IMPLEMENT_ASN1_FUNCTIONS(RpkiSignedChecklist);

ASN1_SEQUENCE(FileNameAndHash) = {
	ASN1_OPT(FileNameAndHash, fileName, ASN1_IA5STRING),
	ASN1_SIMPLE(FileNameAndHash, hash, ASN1_OCTET_STRING),
} ASN1_SEQUENCE_END(FileNameAndHash);

ASN1_SEQUENCE(ResourceBlock) = {
	ASN1_EXP_OPT(ResourceBlock, asID, ConstrainedASIdentifiers, 0),
	ASN1_EXP_SEQUENCE_OF_OPT(ResourceBlock, ipAddrBlocks,
	    ConstrainedIPAddressFamily, 1)
} ASN1_SEQUENCE_END(ResourceBlock);

ASN1_ITEM_TEMPLATE(ConstrainedIPAddrBlocks) =
	ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_SEQUENCE_OF, 0, ConstrainedIPAddrBlocks,
	    ConstrainedIPAddressFamily)
ASN1_ITEM_TEMPLATE_END(ConstrainedIPAddrBlocks);

ASN1_SEQUENCE(ConstrainedIPAddressFamily) = {
	ASN1_SIMPLE(ConstrainedIPAddressFamily, addressFamily,
	    ASN1_OCTET_STRING),
	ASN1_SEQUENCE_OF(ConstrainedIPAddressFamily, addressesOrRanges,
	    IPAddressOrRange),
} ASN1_SEQUENCE_END(ConstrainedIPAddressFamily);

ASN1_SEQUENCE(ConstrainedASIdentifiers) = {
	ASN1_EXP_SEQUENCE_OF(ConstrainedASIdentifiers, asnum, ASIdOrRange, 0),
} ASN1_SEQUENCE_END(ConstrainedASIdentifiers);

/*
 * Parse asID (inside ResourceBlock)
 * Return 0 on failure.
 */
static int
rsc_parse_aslist(const char *fn, struct rsc *rsc,
    const ConstrainedASIdentifiers *asids)
{
	int	 i, num_ases;

	if (asids == NULL)
		return 1;

	if ((num_ases = sk_ASIdOrRange_num(asids->asnum)) == 0) {
		warnx("%s: RSC asID empty", fn);
		return 0;
	}

	if (num_ases >= MAX_AS_SIZE) {
		warnx("%s: too many AS number entries: limit %d",
		    fn, MAX_AS_SIZE);
		return 0;
	}

	if ((rsc->ases = calloc(num_ases, sizeof(struct cert_as))) == NULL)
		err(1, NULL);

	for (i = 0; i < num_ases; i++) {
		const ASIdOrRange *aor;

		aor = sk_ASIdOrRange_value(asids->asnum, i);

		switch (aor->type) {
		case ASIdOrRange_id:
			if (!sbgp_as_id(fn, rsc->ases, &rsc->num_ases,
			    aor->u.id))
				return 0;
			break;
		case ASIdOrRange_range:
			if (!sbgp_as_range(fn, rsc->ases, &rsc->num_ases,
			    aor->u.range))
				return 0;
			break;
		default:
			warnx("%s: RSC AsList: unknown type %d", fn, aor->type);
			return 0;
		}
	}

	return 1;
}

static int
rsc_parse_iplist(const char *fn, struct rsc *rsc,
    const ConstrainedIPAddrBlocks *ipAddrBlocks)
{
	const ConstrainedIPAddressFamily	*af;
	const IPAddressOrRanges			*aors;
	const IPAddressOrRange			*aor;
	size_t					 num_ips;
	enum afi				 afi;
	int					 i, j;

	if (ipAddrBlocks == NULL)
		return 1;

	if (sk_ConstrainedIPAddressFamily_num(ipAddrBlocks) == 0) {
		warnx("%s: RSC ipAddrBlocks empty", fn);
		return 0;
	}

	for (i = 0; i < sk_ConstrainedIPAddressFamily_num(ipAddrBlocks); i++) {
		af = sk_ConstrainedIPAddressFamily_value(ipAddrBlocks, i);
		aors = af->addressesOrRanges;

		num_ips = rsc->num_ips + sk_IPAddressOrRange_num(aors);
		if (num_ips >= MAX_IP_SIZE) {
			warnx("%s: too many IP address entries: limit %d",
			    fn, MAX_IP_SIZE);
			return 0;
		}

		rsc->ips = recallocarray(rsc->ips, rsc->num_ips, num_ips,
		    sizeof(struct cert_ip));
		if (rsc->ips == NULL)
			err(1, NULL);

		if (!ip_addr_afi_parse(fn, af->addressFamily, &afi)) {
			warnx("%s: RSC: invalid AFI", fn);
			return 0;
		}

		for (j = 0; j < sk_IPAddressOrRange_num(aors); j++) {
			aor = sk_IPAddressOrRange_value(aors, j);
			switch (aor->type) {
			case IPAddressOrRange_addressPrefix:
				if (!sbgp_addr(fn, rsc->ips,
				    &rsc->num_ips, afi, aor->u.addressPrefix))
					return 0;
				break;
			case IPAddressOrRange_addressRange:
				if (!sbgp_addr_range(fn, rsc->ips,
				    &rsc->num_ips, afi, aor->u.addressRange))
					return 0;
				break;
			default:
				warnx("%s: RFC 3779: IPAddressOrRange: "
				    "unknown type %d", fn, aor->type);
				return 0;
			}
		}
	}

	return 1;
}

static int
rsc_check_digesttype(const char *fn, struct rsc *rsc, const X509_ALGOR *alg)
{
	const ASN1_OBJECT	*obj;
	int			 type, nid;

	X509_ALGOR_get0(&obj, &type, NULL, alg);

	if (type != V_ASN1_UNDEF) {
		warnx("%s: RSC DigestAlgorithmIdentifier unexpected parameters:"
		    " %d", fn, type);
		return 0;
	}

	if ((nid = OBJ_obj2nid(obj)) != NID_sha256) {
		warnx("%s: RSC DigestAlgorithmIdentifier: want SHA256, have %s",
		    fn, nid2str(nid));
		return 0;
	}

	return 1;
}

/*
 * Parse the FileNameAndHash sequence, RFC 9323, section 4.4.
 * Return zero on failure, non-zero on success.
 */
static int
rsc_parse_checklist(const char *fn, struct rsc *rsc,
    const STACK_OF(FileNameAndHash) *checkList)
{
	FileNameAndHash		*fh;
	struct rscfile		*file;
	size_t			 num_files, i;

	if ((num_files = sk_FileNameAndHash_num(checkList)) == 0) {
		warnx("%s: RSC checkList needs at least one entry", fn);
		return 0;
	}

	if (num_files >= MAX_CHECKLIST_ENTRIES) {
		warnx("%s: %zu exceeds checklist entry limit (%d)", fn,
		    num_files, MAX_CHECKLIST_ENTRIES);
		return 0;
	}

	rsc->files = calloc(num_files, sizeof(struct rscfile));
	if (rsc->files == NULL)
		err(1, NULL);
	rsc->num_files = num_files;

	for (i = 0; i < num_files; i++) {
		const unsigned char *data;
		int length;

		fh = sk_FileNameAndHash_value(checkList, i);

		file = &rsc->files[i];

		data = ASN1_STRING_get0_data(fh->hash);
		length = ASN1_STRING_length(fh->hash);
		if (length != SHA256_DIGEST_LENGTH) {
			warnx("%s: RSC Digest: invalid SHA256 length", fn);
			return 0;
		}
		memcpy(file->hash, data, length);

		if (fh->fileName == NULL)
			continue;

		data = ASN1_STRING_get0_data(fh->fileName);
		length = ASN1_STRING_length(fh->fileName);

		if (!valid_filename(data, length)) {
			warnx("%s: RSC FileNameAndHash: invalid filename", fn);
			return 0;
		}

		file->filename = strndup(data, length);
		if (file->filename == NULL)
			err(1, NULL);
	}

	return 1;
}

/*
 * Parses the eContent segment of an RSC file
 * RFC 9323, section 4
 * Returns zero on failure, non-zero on success.
 */
static int
rsc_parse_econtent(const char *fn, void *obj, const unsigned char *d,
    size_t dsz)
{
	struct rsc		*rsc = obj;
	const unsigned char	*oder;
	RpkiSignedChecklist	*rsc_asn1;
	ResourceBlock		*resources;
	int			 rc = 0;

	/*
	 * RFC 9323 section 4
	 */

	oder = d;
	if ((rsc_asn1 = d2i_RpkiSignedChecklist(NULL, &d, dsz)) == NULL) {
		warnx("%s: RSC: failed to parse RpkiSignedChecklist", 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, rsc_asn1->version, 0))
		goto out;

	resources = rsc_asn1->resources;
	if (resources->asID == NULL && resources->ipAddrBlocks == NULL) {
		warnx("%s: RSC: one of asID or ipAddrBlocks must be present",
		    fn);
		goto out;
	}

	if (!rsc_parse_aslist(fn, rsc, resources->asID))
		goto out;

	if (!rsc_parse_iplist(fn, rsc, resources->ipAddrBlocks))
		goto out;

	if (!rsc_check_digesttype(fn, rsc, rsc_asn1->digestAlgorithm))
		goto out;

	if (!rsc_parse_checklist(fn, rsc, rsc_asn1->checkList))
		goto out;

	rc = 1;
 out:
	RpkiSignedChecklist_free(rsc_asn1);
	return rc;
}

static int
rsc_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->signedobj != NULL) {
		warnx("%s: RSC: EE cert must not have an SIA extension", fn);
		return 0;
	}

	return 1;
}

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

	rsc->valid = valid_rsc(fn, cert, rsc);

	return 1; /* XXX */
}

static void *
rsc_obj_new(size_t der_len, time_t signtime)
{
	struct rsc *rsc;

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

	return rsc;
}

static void
rsc_obj_free(void *obj)
{
	rsc_free(obj);
}

static const struct signed_obj rsc_signed_obj = {
	.rtype = RTYPE_RSC,
	.new = rsc_obj_new,
	.free = rsc_obj_free,
	.cert_info = rsc_cert_info,
	.parse_econtent = rsc_parse_econtent,
	.validate = rsc_validate,
};

const struct signed_obj *
rsc_obj(void)
{
	return &rsc_signed_obj;
}

/*
 * Parse a full RFC 9323 file.
 * Returns the RSC or NULL if the object was malformed.
 */
struct rsc *
rsc_parse(struct cert **out_cert, const char *fn, int talid,
    const unsigned char *der, size_t len)
{
	struct rsc		*rsc;
	struct cert		*cert = NULL;
	unsigned char		*cms;
	size_t			 cmsz;
	time_t			 signtime = 0;
	int			 rc = 0;

	assert(*out_cert == NULL);

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

	rsc = rsc_obj_new(len, signtime);
	if (!rsc_cert_info(fn, rsc, cert))
		goto out;
	if (!rsc_parse_econtent(fn, rsc, cms, cmsz))
		goto out;
	(void)rsc_validate(fn, rsc, cert);

	*out_cert = cert;
	cert = NULL;

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

/*
 * Free an RSC pointer.
 * Safe to call with NULL.
 */
void
rsc_free(struct rsc *p)
{
	size_t	i;

	if (p == NULL)
		return;

	for (i = 0; i < p->num_files; i++)
		free(p->files[i].filename);

	free(p->ips);
	free(p->ases);
	free(p->files);
	free(p);
}
