/*	$OpenBSD: x509.c,v 1.133 2026/06/21 19:23:56 tb Exp $ */
/*
 * Copyright (c) 2022 Theo Buehler <tb@openbsd.org>
 * Copyright (c) 2021 Claudio Jeker <claudio@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 <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <openssl/evp.h>
#include <openssl/x509v3.h>

#include "extern.h"

#define GENTIME_LENGTH 15

ASN1_OBJECT	*certpol_oid;	/* id-cp-ipAddr-asNumber cert policy */
ASN1_OBJECT	*caissuers_oid;	/* 1.3.6.1.5.5.7.48.2 (caIssuers) */
ASN1_OBJECT	*carepo_oid;	/* 1.3.6.1.5.5.7.48.5 (caRepository) */
ASN1_OBJECT	*manifest_oid;	/* 1.3.6.1.5.5.7.48.10 (rpkiManifest) */
ASN1_OBJECT	*signedobj_oid;	/* 1.3.6.1.5.5.7.48.11 (signedObject) */
ASN1_OBJECT	*notify_oid;	/* 1.3.6.1.5.5.7.48.13 (rpkiNotify) */
ASN1_OBJECT	*roa_oid;	/* id-ct-routeOriginAuthz CMS content type */
ASN1_OBJECT	*mft_oid;	/* id-ct-rpkiManifest CMS content type */
ASN1_OBJECT	*bgpsec_oid;	/* id-kp-bgpsec-router Key Purpose */
ASN1_OBJECT	*cnt_type_oid;	/* pkcs-9 id-contentType */
ASN1_OBJECT	*msg_dgst_oid;	/* pkcs-9 id-messageDigest */
ASN1_OBJECT	*sign_time_oid;	/* pkcs-9 id-signingTime */
ASN1_OBJECT	*rsc_oid;	/* id-ct-signedChecklist */
ASN1_OBJECT	*aspa_oid;	/* id-ct-ASPA */
ASN1_OBJECT	*tak_oid;	/* id-ct-SignedTAL */
ASN1_OBJECT	*spl_oid;	/* id-ct-signedPrefixList */
ASN1_OBJECT	*ccr_oid;	/* id-ct-rpkiCanonicalCacheRepresentation */

static const struct {
	const char	 *oid;
	ASN1_OBJECT	**ptr;
} oid_table[] = {
	{
		.oid = "1.3.6.1.5.5.7.14.2",
		.ptr = &certpol_oid,
	},
	{
		.oid = "1.3.6.1.5.5.7.48.2",
		.ptr = &caissuers_oid,
	},
	{
		.oid = "1.3.6.1.5.5.7.48.5",
		.ptr = &carepo_oid,
	},
	{
		.oid = "1.3.6.1.5.5.7.48.10",
		.ptr = &manifest_oid,
	},
	{
		.oid = "1.3.6.1.5.5.7.48.11",
		.ptr = &signedobj_oid,
	},
	{
		.oid = "1.3.6.1.5.5.7.48.13",
		.ptr = &notify_oid,
	},
	{
		.oid = "1.2.840.113549.1.9.16.1.24",
		.ptr = &roa_oid,
	},
	{
		.oid = "1.2.840.113549.1.9.16.1.26",
		.ptr = &mft_oid,
	},
	{
		.oid = "1.3.6.1.5.5.7.3.30",
		.ptr = &bgpsec_oid,
	},
	{
		.oid = "1.2.840.113549.1.9.3",
		.ptr = &cnt_type_oid,
	},
	{
		.oid = "1.2.840.113549.1.9.4",
		.ptr = &msg_dgst_oid,
	},
	{
		.oid = "1.2.840.113549.1.9.5",
		.ptr = &sign_time_oid,
	},
	{
		.oid = "1.2.840.113549.1.9.16.1.48",
		.ptr = &rsc_oid,
	},
	{
		.oid = "1.2.840.113549.1.9.16.1.49",
		.ptr = &aspa_oid,
	},
	{
		.oid = "1.2.840.113549.1.9.16.1.50",
		.ptr = &tak_oid,
	},
	{
		.oid = "1.2.840.113549.1.9.16.1.51",
		.ptr = &spl_oid,
	},
	{
		.oid = "1.2.840.113549.1.9.16.1.54",
		.ptr = &ccr_oid,
	},
};

void
x509_init_oid(void)
{
	size_t	i;

	for (i = 0; i < sizeof(oid_table) / sizeof(oid_table[0]); i++) {
		*oid_table[i].ptr = OBJ_txt2obj(oid_table[i].oid, 1);
		if (*oid_table[i].ptr == NULL)
			errx(1, "OBJ_txt2obj for %s failed", oid_table[i].oid);
	}
}

/*
 * Compute the SKI of an RSA public key in an X509_PUBKEY using SHA-1.
 * Returns allocated hex-encoded SKI on success, NULL on failure.
 */
char *
x509_pubkey_get_ski(X509_PUBKEY *pubkey, const char *fn)
{
	X509_ALGOR		*alg = NULL;
	const ASN1_OBJECT	*aobj = NULL;
	int			 ptype = 0;
	const void		*pval = NULL;
	const unsigned char	*der;
	int			 der_len;
	unsigned char		 md[EVP_MAX_MD_SIZE];
	unsigned int		 md_len = EVP_MAX_MD_SIZE;
	unsigned char		 buf[80];

	/* XXX - dedup with cert_check_spki(), add more validity checks? */

	if (!X509_PUBKEY_get0_param(NULL, &der, &der_len, &alg, pubkey)) {
		warnx("%s: X509_PUBKEY_get0_param failed", fn);
		return NULL;
	}
	X509_ALGOR_get0(&aobj, &ptype, &pval, alg);

	if (OBJ_obj2nid(aobj) == NID_rsaEncryption) {
		if (ptype != V_ASN1_NULL || pval != NULL) {
			warnx("%s: RFC 4055, 1.2, rsaEncryption "
			    "parameters not NULL", fn);
			return NULL;
		}

		goto done;
	}

	if (!experimental) {
		warnx("%s: RFC 7935, 3.1 SPKI not RSAPublicKey", fn);
		return NULL;
	}

	if (OBJ_obj2nid(aobj) == NID_X9_62_id_ecPublicKey) {
		if (ptype != V_ASN1_OBJECT) {
			warnx("%s: RFC 5480, 2.1.1, ecPublicKey "
			    "parameters not namedCurve", fn);
			return NULL;
		}
		if (OBJ_obj2nid(pval) != NID_X9_62_prime256v1) {
			warnx("%s: RFC 8608, 3.1, named curve not P-256", fn);
			return NULL;
		}

		goto done;
	}

	OBJ_obj2txt(buf, sizeof(buf), aobj, 0);
	warnx("%s: unsupported public key type %s", fn, buf);
	return NULL;

 done:
	if (!EVP_Digest(der, der_len, md, &md_len, EVP_sha1(), NULL)) {
		warnx("%s: EVP_Digest failed", fn);
		return NULL;
	}

	return hex_encode(md, md_len);
}

/*
 * Check whether all RFC 3779 extensions are set to inherit.
 * Return 1 if both AS & IP are set to inherit.
 * Return 0 on failure (such as missing extensions or no inheritance).
 */
int
x509_inherits(X509 *x)
{
	STACK_OF(IPAddressFamily)	*addrblk = NULL;
	ASIdentifiers			*asidentifiers = NULL;
	const IPAddressFamily		*af;
	int				 crit, i, rc = 0;

	addrblk = X509_get_ext_d2i(x, NID_sbgp_ipAddrBlock, &crit, NULL);
	if (addrblk == NULL) {
		if (crit != -1)
			warnx("error parsing ipAddrBlocks");
		goto out;
	}

	/*
	 * Check by hand, since X509v3_addr_inherits() success only means that
	 * at least one address family inherits, not all of them.
	 */
	for (i = 0; i < sk_IPAddressFamily_num(addrblk); i++) {
		af = sk_IPAddressFamily_value(addrblk, i);
		if (af->ipAddressChoice->type != IPAddressChoice_inherit)
			goto out;
	}

	asidentifiers = X509_get_ext_d2i(x, NID_sbgp_autonomousSysNum, NULL,
	    NULL);
	if (asidentifiers == NULL) {
		if (crit != -1)
			warnx("error parsing asIdentifiers");
		goto out;
	}

	/* We need to have AS numbers and don't want RDIs. */
	if (asidentifiers->asnum == NULL || asidentifiers->rdi != NULL)
		goto out;
	if (!X509v3_asid_inherits(asidentifiers))
		goto out;

	rc = 1;
 out:
	ASIdentifiers_free(asidentifiers);
	sk_IPAddressFamily_pop_free(addrblk, IPAddressFamily_free);
	return rc;
}

/*
 * Check whether at least one RFC 3779 extension is set to inherit.
 * Return 1 if an inherit element is encountered in AS or IP.
 * Return 0 otherwise.
 */
int
x509_any_inherits(X509 *x)
{
	STACK_OF(IPAddressFamily)	*addrblk = NULL;
	ASIdentifiers			*asidentifiers = NULL;
	int				 crit, rc = 0;

	addrblk = X509_get_ext_d2i(x, NID_sbgp_ipAddrBlock, &crit, NULL);
	if (addrblk == NULL && crit != -1)
		warnx("error parsing ipAddrBlocks");
	if (X509v3_addr_inherits(addrblk))
		rc = 1;

	asidentifiers = X509_get_ext_d2i(x, NID_sbgp_autonomousSysNum, &crit,
	    NULL);
	if (asidentifiers == NULL && crit != -1)
		warnx("error parsing asIdentifiers");
	if (X509v3_asid_inherits(asidentifiers))
		rc = 1;

	ASIdentifiers_free(asidentifiers);
	sk_IPAddressFamily_pop_free(addrblk, IPAddressFamily_free);
	return rc;
}

/*
 * Convert passed ASN1_TIME to time_t *t.
 * Returns 1 on success and 0 on failure.
 */
int
x509_get_time(const ASN1_TIME *at, time_t *t)
{
	struct tm	 tm;

	*t = 0;
	memset(&tm, 0, sizeof(tm));
	/* Fail instead of silently falling back to the current time. */
	if (at == NULL)
		return 0;
	if (!ASN1_TIME_to_tm(at, &tm))
		return 0;
	tm.tm_wday = -1;	/* sentinel for error */
	if ((*t = timegm(&tm)) == -1 && tm.tm_wday == -1)
		return 0;
	return 1;
}

int
x509_get_generalized_time(const char *fn, const char *descr,
    const ASN1_TIME *at, time_t *t)
{
	if (ASN1_STRING_length(at) != GENTIME_LENGTH) {
		warnx("%s: %s time format invalid", fn, descr);
		return 0;
	}
	if (!x509_get_time(at, t)) {
		warnx("%s: parsing %s failed", fn, descr);
		return 0;
	}
	return 1;
}

/*
 * Extract and validate an accessLocation, RFC 6487, 4.8 and RFC 8182, 3.2.
 * Returns 0 on failure and 1 on success.
 */
int
x509_location(const char *fn, const char *descr, GENERAL_NAME *location,
    char **out)
{
	const unsigned char *data;
	int length;

	assert(*out == NULL);

	if (location->type != GEN_URI) {
		warnx("%s: RFC 6487 section 4.8: %s not URI", fn, descr);
		return 0;
	}

	data = ASN1_STRING_get0_data(location->d.uniformResourceIdentifier);
	length = ASN1_STRING_length(location->d.uniformResourceIdentifier);

	if (!valid_uri(data, length, NULL)) {
		warnx("%s: RFC 6487 section 4.8: %s bad location", fn, descr);
		return 0;
	}

	if ((*out = strndup(data, length)) == NULL)
		err(1, NULL);

	return 1;
}

static int
valid_printable_octet(const uint8_t u8)
{
	/*
	 * X.680, 41.4, Table 10 lists the allowed characters in this order.
	 */

	if ('A' <= u8 && u8 <= 'Z')
		return 1;
	if ('a' <= u8 && u8 <= 'z')
		return 1;
	if ('0' <= u8 && u8 <= '9')
		return 1;

	return u8 == ' ' || u8 == '\'' || u8 == '(' || u8 == ')' || u8 == '+' ||
	    u8 == ',' || u8 == '-' || u8 == '.' || u8 == '/' || u8 == ':' ||
	    u8 == '=' || u8 == '?';
}

static int
valid_printable_string(const char *fn, const char *descr, const ASN1_STRING *as)
{
	const unsigned char *data;
	int i, length;

	/*
	 * This warning should be an error by default (not gated behind -vv).
	 * https://lists.afrinic.net/pipermail/dbwg/2023-March/000436.html
	 * https://lists.afrinic.net/pipermail/dbwg/2025-November/000546.html
	 */
	if (verbose > 1 && ASN1_STRING_type(as) != V_ASN1_PRINTABLESTRING) {
		warnx("%s: RFC 6487 section 4.5: %s commonName is"
		    " not PrintableString", fn, descr);
#if 0
		return 0;
#endif
	}

	data = ASN1_STRING_get0_data(as);
	length = ASN1_STRING_length(as);
	for (i = 0; i < length; i++) {
		if (!valid_printable_octet(data[i])) {
			warnx("%s: invalid %s: PrintableString contains 0x%02x",
			    fn, descr, data[i]);
			return 0;
		}
	}

	return 1;
}

/*
 * Check that subject or issuer only contain commonName and serialNumber.
 * Return 0 on failure.
 */
static int
x509_valid_name_internal(const char *fn, const char *descr, const X509_NAME *xn)
{
	const X509_NAME_ENTRY *ne;
	const ASN1_OBJECT *ao;
	const ASN1_STRING *as;
	int cn = 0, sn = 0;
	int i, nid;

	for (i = 0; i < X509_NAME_entry_count(xn); i++) {
		if ((ne = X509_NAME_get_entry(xn, i)) == NULL) {
			warnx("%s: X509_NAME_get_entry", fn);
			return 0;
		}
		if ((ao = X509_NAME_ENTRY_get_object(ne)) == NULL) {
			warnx("%s: X509_NAME_ENTRY_get_object", fn);
			return 0;
		}

		nid = OBJ_obj2nid(ao);
		switch (nid) {
		case NID_commonName:
			if (cn++ > 0) {
				warnx("%s: duplicate commonName in %s",
				    fn, descr);
				return 0;
			}
			if ((as = X509_NAME_ENTRY_get_data(ne)) == NULL) {
				warnx("%s: X509_NAME_ENTRY_get_data failed",
				    fn);
				return 0;
			}
			/*
			 * XXX - For some reason RFC 8209, section 3.1.1 decided
			 * to allow UTF8String for the subject of BGPsec Router
			 * Certificates, although RECOMMENDED contents fit in
			 * a PrintableString.
			 */
			if (!valid_printable_string(fn, descr, as))
				return 0;
			break;
		case NID_serialNumber:
			if (sn++ > 0) {
				warnx("%s: duplicate serialNumber in %s",
				    fn, descr);
				return 0;
			}
			break;
		case NID_undef:
			warnx("%s: OBJ_obj2nid failed", fn);
			return 0;
		default:
			warnx("%s: RFC 6487 section 4.5: unexpected attribute"
			    " %s in %s", fn, nid2str(nid), descr);
			return 0;
		}
	}

	if (cn == 0) {
		warnx("%s: RFC 6487 section 4.5: %s missing commonName",
		    fn, descr);
		return 0;
	}

	return 1;
}

int
x509_valid_subject_name(const char *fn, const X509_NAME *xn)
{
	return x509_valid_name_internal(fn, "subject", xn);
}

int
x509_valid_issuer_name(const char *fn, const X509_NAME *xn)
{
	return x509_valid_name_internal(fn, "issuer", xn);
}

/*
 * Check ASN1_INTEGER is non-negative and fits in 20 octets.
 * Returns allocated BIGNUM if true, NULL otherwise.
 */
static BIGNUM *
x509_seqnum_to_bn(const char *fn, const char *descr, const ASN1_INTEGER *i)
{
	BIGNUM *bn = NULL;

	if ((bn = ASN1_INTEGER_to_BN(i, NULL)) == NULL) {
		warnx("%s: %s: ASN1_INTEGER_to_BN error", fn, descr);
		goto out;
	}

	if (BN_is_negative(bn)) {
		warnx("%s: %s should be non-negative", fn, descr);
		goto out;
	}

	/* Reject values larger than or equal to 2^159. */
	if (BN_num_bytes(bn) > 20 || BN_is_bit_set(bn, 159)) {
		warnx("%s: %s should fit in 20 octets", fn, descr);
		goto out;
	}

	return bn;

 out:
	BN_free(bn);
	return NULL;
}

/*
 * Convert an ASN1_INTEGER into a hexstring, enforcing that it is non-negative
 * and representable by at most 20 octets (RFC 5280, section 4.1.2.2).
 * Returned string needs to be freed by the caller.
 */
char *
x509_convert_seqnum(const char *fn, const char *descr, const ASN1_INTEGER *i)
{
	BIGNUM	*bn = NULL;
	char	*s = NULL;

	if (i == NULL)
		goto out;

	if ((bn = x509_seqnum_to_bn(fn, descr, i)) == NULL)
		goto out;

	if ((s = BN_bn2hex(bn)) == NULL)
		warnx("%s: %s: BN_bn2hex error", fn, descr);

 out:
	BN_free(bn);
	return s;
}

int
x509_valid_seqnum(const char *fn, const char *descr, const ASN1_INTEGER *i)
{
	BIGNUM *bn;

	if ((bn = x509_seqnum_to_bn(fn, descr, i)) == NULL)
		return 0;

	BN_free(bn);
	return 1;
}

/*
 * Helper to check the signature algorithm in the signed part of a cert
 * or CRL matches expectations. Only accept sha256WithRSAEncryption by
 * default and in experimental mode also accept ecdsa-with-SHA256.
 * Check compliance of the parameter encoding as well.
 */
int
x509_check_tbs_sigalg(const char *fn, const X509_ALGOR *tbsalg)
{
	const ASN1_OBJECT *aobj = NULL;
	int ptype = 0;
	int nid;

	X509_ALGOR_get0(&aobj, &ptype, NULL, tbsalg);
	if ((nid = OBJ_obj2nid(aobj)) == NID_undef) {
		warnx("%s: unknown signature type", fn);
		return 0;
	}

	if (nid == NID_sha256WithRSAEncryption) {
		/*
		 * Correct encoding of parameters is explicit ASN.1 NULL
		 * (V_ASN1_NULL), but implementations MUST accept absent
		 * parameters due to an ASN.1 syntax translation mishap,
		 * see, e.g., RFC 4055, 2.1.
		 */
		if (ptype != V_ASN1_NULL && ptype != V_ASN1_UNDEF) {
			warnx("%s: RFC 4055, 5: wrong ASN.1 parameters for %s",
			    fn, LN_sha256WithRSAEncryption);
			return 0;
		}
		/*
		 * As of July 2025, there still are ~1600 ROA EE certs with this
		 * faulty encoding, all issued by ARIN before September 2020.
		 */
		if (verbose > 1 && ptype == V_ASN1_UNDEF)
			warnx("%s: RFC 4055, 5: %s without ASN.1 parameters",
			    fn, LN_sha256WithRSAEncryption);
		return 1;
	}

	if (experimental && nid == NID_ecdsa_with_SHA256) {
		if (ptype != V_ASN1_UNDEF) {
			warnx("%s: RFC 5758, 3.2: %s encoding MUST omit "
			    "the parameters", fn, SN_ecdsa_with_SHA256);
			return 0;
		}
		if (verbose)
			warnx("%s: P-256 support is experimental", fn);
		return 1;
	}

	warnx("%s: RFC 7935: wrong signature algorithm %s, want %s",
	    fn, nid2str(nid), LN_sha256WithRSAEncryption);
	return 0;
}

/*
 * Find the closest expiry moment by walking the chain of authorities.
 */
time_t
x509_find_expires(time_t notafter, struct auth *a, struct crl_tree *crls)
{
	struct crl	*crl;
	time_t		 expires;

	expires = notafter;

	for (; a != NULL; a = a->issuer) {
		if (expires > a->cert->notafter)
			expires = a->cert->notafter;
		crl = crl_get(crls, a);
		if (crl != NULL && expires > crl->nextupdate)
			expires = crl->nextupdate;
	}

	return expires;
}
