root/usr.sbin/rpki-client/cert.c
/*      $OpenBSD: cert.c,v 1.244 2026/07/13 10:53:12 tb Exp $ */
/*
 * Copyright (c) 2022,2025 Theo Buehler <tb@openbsd.org>
 * Copyright (c) 2021 Job Snijders <job@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 <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <openssl/asn1.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>

#include "extern.h"

int certid = TALSZ_MAX;

static pthread_rwlock_t cert_lk = PTHREAD_RWLOCK_INITIALIZER;

/* Helper to sort a STACK_OF(X509_EXTENSION) by OID. */
static int
cert_extension_oid_cmp(const X509_EXTENSION *const *a,
    const X509_EXTENSION *const *b)
{
        /* XXX - cast away const for OpenSSL 3 and LibreSSL */
        const ASN1_OBJECT *ao = X509_EXTENSION_get_object((X509_EXTENSION *)*a);
        const ASN1_OBJECT *bo = X509_EXTENSION_get_object((X509_EXTENSION *)*b);

        return OBJ_cmp(ao, bo);
}

/*
 * Per RFC 5280, Section 4.2, a certificate MUST NOT include more than
 * one instance of a particular extension.
 * Returns 1 if certificate conforms to this and 0 otherwise.
 */
static int
cert_extension_oids_are_unique(const char *fn, const struct cert *cert)
{
        const X509 *x509 = cert->x509;
        const STACK_OF(X509_EXTENSION) *cexts = NULL;
        STACK_OF(X509_EXTENSION) *exts = NULL;
        const X509_EXTENSION *prev, *curr;
        const ASN1_OBJECT *obj;
        int i, nid, rc = 0;

        if (X509_get_ext_count(x509) <= 1)
                goto done;

        if ((cexts = X509_get0_extensions(x509)) == NULL)
                goto out;

        if ((exts = sk_X509_EXTENSION_dup(cexts)) == NULL)
                goto out;

        (void)sk_X509_EXTENSION_set_cmp_func(exts, cert_extension_oid_cmp);
        sk_X509_EXTENSION_sort(exts);

        prev = sk_X509_EXTENSION_value(exts, 0);
        for (i = 1; i < sk_X509_EXTENSION_num(exts); i++) {
                curr = sk_X509_EXTENSION_value(exts, i);
                if (cert_extension_oid_cmp(&prev, &curr) == 0) {
                        /* XXX - cast away const for OpenSSL 3 and LibreSSL */
                        obj = X509_EXTENSION_get_object((X509_EXTENSION *)curr);
                        nid = OBJ_obj2nid(obj);
                        warnx("%s: RFC 5280 section 4.2: duplicate extension: "
                           "%s", fn, nid2str(nid));
                        goto out;
                }
                prev = curr;
        }

 done:
        rc = 1;

 out:
        sk_X509_EXTENSION_free(exts);

        return rc;
}

/*
 * Check the cert's purpose: the cA bit in basic constraints distinguishes
 * between TA/CA and EE/BGPsec router and the key usage bits must match.
 * TAs are self-signed, CAs not self-issued, EEs have no extended key usage,
 * BGPsec router have id-kp-bgpsec-router OID.
 */
static int
cert_check_purpose(const char *fn, struct cert *cert)
{
        X509                            *x = cert->x509;
        BASIC_CONSTRAINTS               *bc = NULL;
        EXTENDED_KEY_USAGE              *eku = NULL;
        const X509_EXTENSION            *ku;
        int                              crit, ext_flags, i, is_ca, ku_idx;

        cert->purpose = CERT_PURPOSE_INVALID;

        /*
         * Ensure the X.509v3 extensions can be parsed and are cached in x.
         * Avoids unexpected failure modes of API such as X509_check_ca(),
         * X509_cmp(), X509_get_extension_flags(), and X509_get*_key_usage().
         */
        if (X509_check_purpose(x, -1, 0) <= 0) {
                warnx("%s: could not cache X509v3 extensions", fn);
                goto out;
        }

        if (!cert_extension_oids_are_unique(fn, cert))
                goto out;

        ext_flags = X509_get_extension_flags(x);

        /* Key usage must be present and critical. KU bits are checked below. */
        if ((ku_idx = X509_get_ext_by_NID(x, NID_key_usage, -1)) < 0) {
                warnx("%s: RFC 6487, section 4.8.4: missing KeyUsage", fn);
                goto out;
        }
        if ((ku = X509_get_ext(x, ku_idx)) == NULL) {
                warnx("%s: RFC 6487, section 4.8.4: missing KeyUsage", fn);
                goto out;
        }
        if (!X509_EXTENSION_get_critical(ku)) {
                warnx("%s: RFC 6487, section 4.8.4: KeyUsage not critical", fn);
                goto out;
        }

        /* This weird API can return 0, 1, 2, 4, 5 but can't error... */
        if ((is_ca = X509_check_ca(x)) > 1) {
                if (is_ca == 4)
                        warnx("%s: RFC 6487: sections 4.8.1 and 4.8.4: "
                            "no basic constraints, but keyCertSign set", fn);
                else
                        warnx("%s: unexpected legacy certificate", fn);
                goto out;
        }

        if (is_ca) {
                bc = X509_get_ext_d2i(x, NID_basic_constraints, &crit, NULL);
                if (bc == NULL) {
                        if (crit != -1)
                                warnx("%s: RFC 6487 section 4.8.1: "
                                    "error parsing basic constraints", fn);
                        else
                                warnx("%s: RFC 6487 section 4.8.1: "
                                    "missing basic constraints", fn);
                        goto out;
                }
                if (crit != 1) {
                        warnx("%s: RFC 6487 section 4.8.1: Basic Constraints "
                            "must be marked critical", fn);
                        goto out;
                }
                if (bc->pathlen != NULL) {
                        warnx("%s: RFC 6487 section 4.8.1: Path Length "
                            "Constraint must be absent", fn);
                        goto out;
                }

                if (X509_get_key_usage(x) != (KU_KEY_CERT_SIGN | KU_CRL_SIGN)) {
                        warnx("%s: RFC 6487 section 4.8.4: key usage violation",
                            fn);
                        goto out;
                }

                if (X509_get_extended_key_usage(x) != UINT32_MAX) {
                        warnx("%s: RFC 6487 section 4.8.5: EKU not allowed",
                            fn);
                        goto out;
                }

                /*
                 * EXFLAG_SI means that issuer and subject are identical.
                 * EXFLAG_SS is SI plus the AKI is absent or matches the SKI.
                 * Thus, exactly the trust anchors should have EXFLAG_SS set
                 * and we should never see EXFLAG_SI without EXFLAG_SS.
                 */
                if ((ext_flags & EXFLAG_SS) != 0)
                        cert->purpose = CERT_PURPOSE_TA;
                else if ((ext_flags & EXFLAG_SI) == 0)
                        cert->purpose = CERT_PURPOSE_CA;
                else
                        warnx("%s: RFC 6487, section 4.8.3: "
                            "self-issued cert with AKI-SKI mismatch", fn);
                goto out;
        }

        if ((ext_flags & EXFLAG_BCONS) != 0) {
                warnx("%s: Basic Constraints ext in non-CA cert", fn);
                goto out;
        }

        if ((ext_flags & (EXFLAG_SI | EXFLAG_SS)) != 0) {
                warnx("%s: EE cert must not be self-issued or self-signed", fn);
                goto out;
        }

        if (X509_get_key_usage(x) != KU_DIGITAL_SIGNATURE) {
                warnx("%s: RFC 6487 section 4.8.4: KU must be digitalSignature",
                    fn);
                goto out;
        }

        /*
         * EKU is only defined for BGPsec Router certs and must be absent from
         * EE certs.
         */
        eku = X509_get_ext_d2i(x, NID_ext_key_usage, &crit, NULL);
        if (eku == NULL) {
                if (crit != -1)
                        warnx("%s: error parsing EKU", fn);
                else
                        cert->purpose = CERT_PURPOSE_EE; /* EKU absent */
                goto out;
        }
        if (crit != 0) {
                warnx("%s: EKU: extension must not be marked critical", fn);
                goto out;
        }

        /*
         * Per RFC 8209, section 3.1.3.2 the id-kp-bgpsec-router OID must be
         * present and others are allowed, which we don't need to recognize.
         * This matches RFC 5280, section 4.2.1.12.
         */
        for (i = 0; i < sk_ASN1_OBJECT_num(eku); i++) {
                if (OBJ_cmp(bgpsec_oid, sk_ASN1_OBJECT_value(eku, i)) == 0) {
                        cert->purpose = CERT_PURPOSE_BGPSEC_ROUTER;
                        goto out;
                }
        }

        warnx("%s: unknown certificate purpose", fn);
        assert(cert->purpose == CERT_PURPOSE_INVALID);

 out:
        BASIC_CONSTRAINTS_free(bc);
        EXTENDED_KEY_USAGE_free(eku);
        return cert->purpose != CERT_PURPOSE_INVALID;
}

static int
cert_check_sigalg(const char *fn, const struct cert *cert)
{
        const X509              *x = cert->x509;
        const X509_ALGOR        *alg = NULL, *tbsalg;

        /* Retrieve AlgorithmIdentifiers from Certificate and TBSCertificate. */
        X509_get0_signature(NULL, &alg, x);
        if (alg == NULL) {
                warnx("%s: missing signatureAlgorithm in certificate", fn);
                return 0;
        }
        if ((tbsalg = X509_get0_tbs_sigalg(x)) == NULL) {
                warnx("%s: missing signature in tbsCertificate", fn);
                return 0;
        }

        /* This cheap comparison is an undocumented part of X509_verify(). */
        if (X509_ALGOR_cmp(alg, tbsalg) != 0) {
                warnx("%s: RFC 5280, 4.1.1.2: signatureAlgorithm and signature "
                    "AlgorithmIdentifier mismatch", fn);
                return 0;
        }

        return x509_check_tbs_sigalg(fn, tbsalg);
}

static int
cert_check_subject_and_issuer(const char *fn, const struct cert *cert)
{
        const X509_NAME *name;

        if ((name = X509_get_subject_name(cert->x509)) == NULL) {
                warnx("%s: X509_get_subject_name", fn);
                return 0;
        }
        if (!x509_valid_subject_name(fn, name))
                return 0;

        if ((name = X509_get_issuer_name(cert->x509)) == NULL) {
                warnx("%s: X509_get_issuer_name", fn);
                return 0;
        }
        if (!x509_valid_issuer_name(fn, name))
                return 0;

        return 1;
}

static int
cert_check_validity_period(const char *fn, struct cert *cert)
{
        const ASN1_TIME *at;

        if ((at = X509_get0_notBefore(cert->x509)) == NULL) {
                warnx("%s: X509_get0_notBefore() failed", fn);
                return 0;
        }
        if (!x509_get_time(at, &cert->notbefore)) {
                warnx("%s: x509_get_time() failed", fn);
                return 0;
        }

        if ((at = X509_get0_notAfter(cert->x509)) == NULL) {
                warnx("%s: X509_get0_notAfter() failed", fn);
                return 0;
        }
        if (!x509_get_time(at, &cert->notafter)) {
                warnx("%s: x509_get_time() failed", fn);
                return 0;
        }

        if (cert->notbefore > cert->notafter) {
                warnx("%s: RFC 6487, 4.6: notAfter precedes notBefore", fn);
                return 0;
        }

        return 1;
}

static int
cert_compliant_rsa_key(const char *fn, struct cert *cert)
{
        EVP_PKEY                *pkey;
        const RSA               *rsa;
        const BIGNUM            *rsa_n, *rsa_e;

        if ((pkey = X509_get0_pubkey(cert->x509)) == NULL) {
                warnx("%s: cert without public key", fn);
                return 0;
        }
        if ((rsa = EVP_PKEY_get0_RSA(pkey)) == NULL) {
                warnx("%s: expected RSA key", fn);
                return 0;
        }
        if ((rsa_n = RSA_get0_n(rsa)) == NULL ||
            (rsa_e = RSA_get0_e(rsa)) == NULL) {
                warnx("%s: missing RSA public key component", fn);
                return 0;
        }
        if (BN_num_bits(rsa_n) != 2048) {
                warnx("%s: RFC 7935, 3: want 2048-bit RSA modulus, have %d", fn,
                    BN_num_bits(rsa_n));
                return 0;
        }
        if (!BN_is_word(rsa_e, 65537)) {
                warnx("%s: RFC 7935, 3: public RSA exponent not %d", fn, 65537);
                return 0;
        }

        return 1;
}

static int
cert_compliant_ec_key(const char *fn, struct cert *cert)
{
        EVP_PKEY                *pkey;
        const EC_KEY            *ec_key;

        if ((pkey = X509_get0_pubkey(cert->x509)) == NULL) {
                warnx("%s: cert without public key", fn);
                return 0;
        }
        if ((ec_key = EVP_PKEY_get0_EC_KEY(pkey)) == NULL) {
                warnx("%s: expected EC key", fn);
                return 0;
        }
        if (EC_KEY_get_conv_form(ec_key) != POINT_CONVERSION_UNCOMPRESSED) {
                warnx("%s: RFC 8608: 3.1 public key not uncompressed", fn);
                return 0;
        }
        if (!EC_KEY_check_key(ec_key)) {
                warnx("%s: EC_KEY_check_key failed", fn);
                return 0;
        }

        /* Prepare pubkey for the BRK tree - used in the final output dump. */
        if (cert->purpose == CERT_PURPOSE_BGPSEC_ROUTER) {
                unsigned char           *der = NULL;
                int                      der_len;

                if ((der_len = i2d_PUBKEY(pkey, &der)) <= 0) {
                        warnx("%s: i2d_PUBKEY failed", fn);
                        return 0;
                }
                if (base64_encode(der, der_len, &cert->pubkey) == -1)
                        errx(1, "base64_encode");
                free(der);
        }

        return 1;
}

static int
cert_check_spki(const char *fn, struct cert *cert)
{
        X509_PUBKEY             *pubkey;
        X509_ALGOR              *alg = NULL;
        const ASN1_OBJECT       *aobj = NULL;
        int                      ptype = 0;
        const void              *pval = NULL;
        int                      rc = 0;

        /*
         * Should be called _get0_. It returns a pointer owned by cert->x509.
         * XXX - cast away const for OpenSSL 4.
         */
        pubkey = (X509_PUBKEY *)X509_get_X509_PUBKEY(cert->x509);
        if (pubkey == NULL) {
                warnx("%s: RFC 6487, 4.7: certificate without SPKI", fn);
                goto out;
        }

        /*
         * Excessive initialization above is due to incoherent semantics of
         * the following functions, e.g., OpenSSL may or may not set pval.
         */
        if (!X509_PUBKEY_get0_param(NULL, NULL, NULL, &alg, pubkey) ||
            alg == NULL) {
                warnx("%s: RFC 6487, 4.7: no AlgorithmIdentifier in SPKI", fn);
                goto out;
        }
        X509_ALGOR_get0(&aobj, &ptype, &pval, alg);

        switch (cert->purpose) {
        case CERT_PURPOSE_TA:
        case CERT_PURPOSE_CA:
        case CERT_PURPOSE_EE:
                if (OBJ_obj2nid(aobj) == NID_rsaEncryption) {
                        if (ptype != V_ASN1_NULL || pval != NULL) {
                                warnx("%s: RFC 4055, 1.2, rsaEncryption "
                                    "parameters not NULL", fn);
                                goto out;
                        }
                        if (!cert_compliant_rsa_key(fn, cert))
                                goto out;
                        break;
                }
                if (!experimental) {
                        warnx("%s: RFC 7935, 3.1 SPKI not RSAPublicKey", fn);
                        goto out;
                }
                /* FALLTHROUGH */
        case CERT_PURPOSE_BGPSEC_ROUTER:
                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);
                                goto out;
                        }
                        if (OBJ_obj2nid(pval) != NID_X9_62_prime256v1) {
                                warnx("%s: RFC 8608, 3.1, named curve not "
                                    "P-256", fn);
                                goto out;
                        }
                        if (!cert_compliant_ec_key(fn, cert))
                                goto out;
                        break;
                }
                warnx("%s: RFC 8608, 3.1, SPKI not an ecPublicKey", fn);
                goto out;
        default:
                abort();
        }

        rc = 1;
 out:
        return rc;
}

static int
cert_ski(const char *fn, struct cert *cert, const X509_EXTENSION *ext)
{
        ASN1_OCTET_STRING       *os = NULL;
        unsigned char            md[EVP_MAX_MD_SIZE];
        unsigned int             md_len = EVP_MAX_MD_SIZE;
        int                      length, rc = 0;

        assert(cert->ski == NULL);

        if (X509_EXTENSION_get_critical(ext)) {
                warnx("%s: RFC 6487 section 4.8.2: "
                    "SKI: extension not non-critical", fn);
                goto out;
        }

        /* XXX - cast away const for OpenSSL 3 and LibreSSL */
        if ((os = X509V3_EXT_d2i((X509_EXTENSION *)ext)) == NULL) {
                warnx("%s: RFC 6487 section 4.8.2: error parsing SKI", fn);
                goto out;
        }

        if (!X509_pubkey_digest(cert->x509, EVP_sha1(), md, &md_len)) {
                warnx("%s: X509_pubkey_digest", fn);
                goto out;
        }

        length = ASN1_STRING_length(os);
        if (length < 0 || md_len != (unsigned int)length) {
                warnx("%s: RFC 6487 section 4.8.2: SKI: "
                    "want %u bytes SHA1 hash, have %d bytes",
                    fn, md_len, length);
                goto out;
        }

        if (memcmp(ASN1_STRING_get0_data(os), md, md_len) != 0) {
                warnx("%s: SKI does not match SHA1 hash of SPK", fn);
                goto out;
        }

        cert->ski = hex_encode(md, md_len);

        rc = 1;
 out:
        ASN1_OCTET_STRING_free(os);
        return rc;
}

static int
cert_aki(const char *fn, struct cert *cert, const X509_EXTENSION *ext)
{
        AUTHORITY_KEYID *akid = NULL;
        int              length, rc = 0;

        assert(cert->aki == NULL);

        if (X509_EXTENSION_get_critical(ext)) {
                warnx("%s: RFC 6487 section 4.8.3: "
                    "AKI extension not non-critical", fn);
                goto out;
        }

        /* XXX - cast away const for OpenSSL 3 and LibreSSL */
        if ((akid = X509V3_EXT_d2i((X509_EXTENSION *)ext)) == NULL) {
                warnx("%s: RFC 6487 section 4.8.3: error parsing AKI", fn);
                goto out;
        }
        if (akid->issuer != NULL || akid->serial != NULL) {
                warnx("%s: RFC 6487 section 4.8.3: AKI: authorityCertIssuer or "
                    "authorityCertSerialNumber present", fn);
                goto out;
        }

        if (akid->keyid == NULL) {
                warnx("%s: RFC 6487 section 4.8.3: AKI: Key Identifier missing",
                    fn);
                goto out;
        }
        length = ASN1_STRING_length(akid->keyid);
        if (length != SHA_DIGEST_LENGTH) {
                warnx("%s: RFC 6487 section 4.8.3: AKI: "
                    "want %d bytes SHA1 hash, have %d bytes",
                    fn, SHA_DIGEST_LENGTH, length);
                goto out;
        }

        cert->aki = hex_encode(ASN1_STRING_get0_data(akid->keyid), length);

        rc = 1;
 out:
        AUTHORITY_KEYID_free(akid);
        return rc;
}

/*
 * Parse CRL distribution point per RFC 6487, section 4.8.6.
 */
static int
cert_crldp(const char *fn, struct cert *cert, const X509_EXTENSION *ext)
{
        CRL_DIST_POINTS *crldp = NULL;
        DIST_POINT      *dp;
        GENERAL_NAMES   *names;
        GENERAL_NAME    *name;
        int              i, rc = 0;

        assert(cert->crl == NULL);

        if (cert->purpose == CERT_PURPOSE_TA) {
                warnx("%s: RFC 6487 section 4.8.6: CRL distribution point "
                    "must be omitted from TA certificates", fn);
                goto out;
        }

        if (X509_EXTENSION_get_critical(ext)) {
                warnx("%s: RFC 6487 section 4.8.6: CRL distribution point "
                    "extension not non-critical", fn);
                goto out;
        }

        /* XXX - cast away const for OpenSSL 3 and LibreSSL */
        if ((crldp = X509V3_EXT_d2i((X509_EXTENSION *)ext)) == NULL) {
                warnx("%s: RFC 6487 section 4.8.6: CRL distribution point: "
                    "failed extension parse", fn);
                goto out;
        }

        if (sk_DIST_POINT_num(crldp) != 1) {
                warnx("%s: RFC 6487 section 4.8.6: CRL distribution point: "
                    "want 1 element, have %d", fn, sk_DIST_POINT_num(crldp));
                goto out;
        }

        dp = sk_DIST_POINT_value(crldp, 0);
        if (dp->CRLissuer != NULL) {
                warnx("%s: RFC 6487 section 4.8.6: CRL CRLIssuer field"
                    " disallowed", fn);
                goto out;
        }
        if (dp->reasons != NULL) {
                warnx("%s: RFC 6487 section 4.8.6: CRL Reasons field"
                    " disallowed", fn);
                goto out;
        }
        if (dp->distpoint == NULL) {
                warnx("%s: RFC 6487 section 4.8.6: CRL: "
                    "no distribution point name", fn);
                goto out;
        }
        if (dp->distpoint->dpname != NULL) {
                warnx("%s: RFC 6487 section 4.8.6: nameRelativeToCRLIssuer"
                    " disallowed", fn);
                goto out;
        }

        /* Need to hardcode the alternative 0 due to missing macro or enum. */
        if (dp->distpoint->type != 0) {
                warnx("%s: RFC 6487 section 4.8.6: CRL DistributionPointName:"
                    " expected fullName, have %d", fn, dp->distpoint->type);
                goto out;
        }

        names = dp->distpoint->name.fullname;
        for (i = 0; i < sk_GENERAL_NAME_num(names); i++) {
                char    *crl = NULL;

                name = sk_GENERAL_NAME_value(names, i);

                if (!x509_location(fn, "CRL distribution point", name, &crl))
                        goto out;

                if (cert->crl == NULL && strncasecmp(crl, RSYNC_PROTO,
                    RSYNC_PROTO_LEN) == 0) {
                        cert->crl = crl;
                        continue;
                }
                if (verbose)
                        warnx("%s: ignoring CRL distribution point %s",
                            fn, crl);
                free(crl);
        }

        if (cert->crl == NULL) {
                warnx("%s: RFC 6487 section 4.8.6: no rsync URI in "
                    "CRL distribution point", fn);
                goto out;
        }

        rc = 1;
 out:
        CRL_DIST_POINTS_free(crldp);
        return rc;
}

/*
 * Parse "Authority Information Access" extension for non-TA certs,
 * RFC 6487, section 4.8.7.
 * Returns zero on failure, non-zero on success.
 */
static int
cert_aia(const char *fn, struct cert *cert, const X509_EXTENSION *ext)
{
        AUTHORITY_INFO_ACCESS   *aia = NULL;
        ACCESS_DESCRIPTION      *ad;
        ASN1_OBJECT             *oid;
        char                    *caissuers = NULL;
        int                      i, rc = 0;

        assert(cert->aia == NULL);

        if (cert->purpose == CERT_PURPOSE_TA) {
                warnx("%s: RFC 6487 section 4.8.7: AIA must be absent from "
                    "a self-signed certificate", fn);
                goto out;
        }

        if (X509_EXTENSION_get_critical(ext)) {
                warnx("%s: RFC 6487 section 4.8.7: AIA: "
                    "extension not non-critical", fn);
                goto out;
        }

        /* XXX - cast away const for OpenSSL 3 and LibreSSL */
        if ((aia = X509V3_EXT_d2i((X509_EXTENSION *)ext)) == NULL) {
                warnx("%s: RFC 6487 section 4.8.7: AIA: failed extension parse",
                    fn);
                goto out;
        }

        for (i = 0; i < sk_ACCESS_DESCRIPTION_num(aia); i++) {
                ad = sk_ACCESS_DESCRIPTION_value(aia, i);

                oid = ad->method;

                if (OBJ_cmp(oid, caissuers_oid) == 0) {
                        if (!x509_location(fn, "AIA: caIssuers", ad->location,
                            &caissuers))
                                goto out;
                        if (cert->aia == NULL && strncasecmp(caissuers,
                            RSYNC_PROTO, RSYNC_PROTO_LEN) == 0) {
                                cert->aia = caissuers;
                                caissuers = NULL;
                                continue;
                        }
                        /*
                         * XXX - unclear how to check "Other accessMethod URIs
                         * referencing the same object MAY be included".
                         */
                        if (verbose)
                                warnx("%s: RFC 6487 section 4.8.7: AIA: "
                                    "ignoring location %s", fn, caissuers);
                        free(caissuers);
                        caissuers = NULL;
                } else {
                        char buf[128];

                        OBJ_obj2txt(buf, sizeof(buf), oid, 0);
                        warnx("%s: RFC 6487 section 4.8.7: unexpected"
                            " accessMethod: %s", fn, buf);
                        goto out;
                }
        }

        if (cert->aia == NULL) {
                warnx("%s: RFC 6487 section 4.8.7: AIA: expected caIssuers "
                    "accessMethod with rsync protocol", fn);
                goto out;
        }

        rc = 1;
 out:
        AUTHORITY_INFO_ACCESS_free(aia);
        return rc;
}

/*
 * Parse "Subject Information Access" extension for a CA cert,
 * RFC 6487, section 4.8.8.1 and RFC 8182, section 3.2.
 * Returns zero on failure, non-zero on success.
 */
static int
cert_ca_sia(const char *fn, struct cert *cert, const X509_EXTENSION *ext)
{
        AUTHORITY_INFO_ACCESS   *sia = NULL;
        ACCESS_DESCRIPTION      *ad;
        ASN1_OBJECT             *oid;
        const char              *mftfilename;
        char                    *carepo = NULL, *rpkimft = NULL, *notify = NULL;
        int                      i, rc = 0;

        assert(cert->repo == NULL && cert->mft == NULL && cert->notify == NULL);

        if (X509_EXTENSION_get_critical(ext)) {
                warnx("%s: RFC 6487 section 4.8.8: SIA: "
                    "extension not non-critical", fn);
                goto out;
        }

        /* XXX - cast away const for OpenSSL 3 and LibreSSL */
        if ((sia = X509V3_EXT_d2i((X509_EXTENSION *)ext)) == NULL) {
                warnx("%s: RFC 6487 section 4.8.8: SIA: failed extension parse",
                    fn);
                goto out;
        }

        for (i = 0; i < sk_ACCESS_DESCRIPTION_num(sia); i++) {
                ad = sk_ACCESS_DESCRIPTION_value(sia, i);

                oid = ad->method;

                if (OBJ_cmp(oid, carepo_oid) == 0) {
                        if (!x509_location(fn, "SIA: caRepository",
                            ad->location, &carepo))
                                goto out;
                        if (cert->repo == NULL && strncasecmp(carepo,
                            RSYNC_PROTO, RSYNC_PROTO_LEN) == 0) {
                                if (carepo[strlen(carepo) - 1] != '/') {
                                        char *carepo_tmp;

                                        if (asprintf(&carepo_tmp, "%s/",
                                            carepo) == -1)
                                                errx(1, NULL);
                                        free(carepo);
                                        carepo = carepo_tmp;
                                }

                                cert->repo = carepo;
                                carepo = NULL;
                                continue;
                        }
                        if (verbose)
                                warnx("%s: RFC 6487 section 4.8.8: SIA: "
                                    "ignoring location %s", fn, carepo);
                        free(carepo);
                        carepo = NULL;
                } else if (OBJ_cmp(oid, manifest_oid) == 0) {
                        if (!x509_location(fn, "SIA: rpkiManifest",
                            ad->location, &rpkimft))
                                goto out;
                        if (cert->mft == NULL && strncasecmp(rpkimft,
                            RSYNC_PROTO, RSYNC_PROTO_LEN) == 0) {
                                cert->mft = rpkimft;
                                rpkimft = NULL;
                                continue;
                        }
                        if (verbose)
                                warnx("%s: RFC 6487 section 4.8.8: SIA: "
                                    "ignoring location %s", fn, rpkimft);
                        free(rpkimft);
                        rpkimft = NULL;
                } else if (OBJ_cmp(oid, notify_oid) == 0) {
                        if (!x509_location(fn, "SIA: rpkiNotify",
                            ad->location, &notify))
                                goto out;
                        if (strncasecmp(notify, HTTPS_PROTO,
                            HTTPS_PROTO_LEN) != 0) {
                                warnx("%s: non-https uri in rpkiNotify: %s",
                                    fn, notify);
                                free(notify);
                                goto out;
                        }
                        if (cert->notify != NULL) {
                                warnx("%s: unexpected rpkiNotify accessMethod",
                                    fn);
                                free(notify);
                                goto out;
                        }
                        cert->notify = notify;
                        notify = NULL;
                } else {
                        char buf[128];

                        OBJ_obj2txt(buf, sizeof(buf), oid, 0);
                        warnx("%s: RFC 6487 section 4.8.8.1: unexpected"
                            " accessMethod: %s", fn, buf);
                        goto out;
                }
        }

        if (cert->mft == NULL || cert->repo == NULL) {
                warnx("%s: RFC 6487 section 4.8.8: SIA: missing caRepository "
                    "or rpkiManifest", fn);
                goto out;
        }

        mftfilename = strrchr(cert->mft, '/');
        if (mftfilename == NULL) {
                warnx("%s: SIA: invalid rpkiManifest entry", fn);
                goto out;
        }
        mftfilename++;
        if (!valid_filename(mftfilename, strlen(mftfilename))) {
                warnx("%s: SIA: rpkiManifest invalid filename", fn);
                goto out;
        }

        if (strstr(cert->mft, cert->repo) != cert->mft ||
            cert->mft + strlen(cert->repo) != mftfilename) {
                warnx("%s: RFC 6487 section 4.8.8: SIA: "
                    "conflicting URIs for caRepository and rpkiManifest", fn);
                goto out;
        }

        if (rtype_from_file_extension(cert->mft) != RTYPE_MFT) {
                warnx("%s: RFC 6487 section 4.8.8: SIA: not an MFT file", fn);
                goto out;
        }

        rc = 1;
 out:
        AUTHORITY_INFO_ACCESS_free(sia);
        return rc;
}

/*
 * Parse "Subject Information Access" extension for an EE cert,
 * RFC 6487, section 4.8.8.2 and RFC 8182, section 3.2.
 * Returns zero on failure, non-zero on success.
 */
static int
cert_ee_sia(const char *fn, struct cert *cert, const X509_EXTENSION *ext)
{
        AUTHORITY_INFO_ACCESS   *sia = NULL;
        ACCESS_DESCRIPTION      *ad;
        ASN1_OBJECT             *oid;
        char                    *signedobj = NULL;
        int                      i, rc = 0;

        assert(cert->signedobj == NULL);

        if (X509_EXTENSION_get_critical(ext)) {
                warnx("%s: RFC 6487 section 4.8.8: SIA: "
                    "extension not non-critical", fn);
                goto out;
        }

        /* XXX - cast away const for OpenSSL 3 and LibreSSL */
        if ((sia = X509V3_EXT_d2i((X509_EXTENSION *)ext)) == NULL) {
                warnx("%s: RFC 6487 section 4.8.8: SIA: failed extension parse",
                    fn);
                goto out;
        }

        for (i = 0; i < sk_ACCESS_DESCRIPTION_num(sia); i++) {
                ad = sk_ACCESS_DESCRIPTION_value(sia, i);

                oid = ad->method;

                /*
                 * XXX: RFC 6487 4.8.8.2 states that the accessMethod MUST be
                 * signedObject. However, rpkiNotify accessMethods currently
                 * exist in the wild. Consider removing this special case.
                 * See also https://www.rfc-editor.org/errata/eid7239.
                 */
                if (OBJ_cmp(oid, notify_oid) == 0) {
                        if (verbose > 1)
                                warnx("%s: RFC 6487 section 4.8.8.2: SIA should"
                                    " not contain rpkiNotify accessMethod", fn);
                        continue;
                } else if (OBJ_cmp(oid, signedobj_oid) == 0) {
                        if (!x509_location(fn, "SIA: signedObject",
                            ad->location, &signedobj))
                                goto out;
                        if (cert->signedobj == NULL && strncasecmp(signedobj,
                            RSYNC_PROTO, RSYNC_PROTO_LEN) == 0) {
                                cert->signedobj = signedobj;
                                signedobj = NULL;
                                continue;
                        }
                        if (verbose)
                                warnx("%s: RFC 6487 section 4.8.8: SIA: "
                                    "ignoring location %s", fn, signedobj);
                        free(signedobj);
                        signedobj = NULL;
                } else {
                        char buf[128];

                        OBJ_obj2txt(buf, sizeof(buf), oid, 0);
                        warnx("%s: RFC 6487 section 4.8.8.1: unexpected"
                            " accessMethod: %s", fn, buf);
                        goto out;
                }
        }

        if (cert->signedobj == NULL) {
                warnx("%s: RFC 6487 section 4.8.8: SIA: no signedObject", fn);
                goto out;
        }

        if (!filemode) {
                const char *p = cert->signedobj + RSYNC_PROTO_LEN;
                size_t fnlen, plen;

                fnlen = strlen(fn);
                plen = strlen(p);

                if (fnlen < plen || strcmp(p, fn + fnlen - plen) != 0) {
                        warnx("%s: RFC 9981 section 4: mismatch between "
                            "pathname and SIA (%s)", fn, cert->signedobj);
                        goto out;
                }
        }

        rc = 1;
 out:
        AUTHORITY_INFO_ACCESS_free(sia);
        return rc;
}

static int
cert_sia(const char *fn, struct cert *cert, const X509_EXTENSION *ext)
{
        switch (cert->purpose) {
        case CERT_PURPOSE_TA:
        case CERT_PURPOSE_CA:
                return cert_ca_sia(fn, cert, ext);
        case CERT_PURPOSE_EE:
                return cert_ee_sia(fn, cert, ext);
        case CERT_PURPOSE_BGPSEC_ROUTER:
                warnx("%s: RFC 8209, 3.1.3.3, SIA MUST be omitted from "
                    "BGPsec router certs", fn);
                return 0;
        default:
                abort();
        }
}

/*
 * Parse the certificate policies extension and check that it follows RFC 7318.
 * Returns zero on failure, non-zero on success.
 */
static int
cert_policies(const char *fn, struct cert *cert, const X509_EXTENSION *ext)
{
        STACK_OF(POLICYINFO)            *policies = NULL;
        POLICYINFO                      *policy;
        STACK_OF(POLICYQUALINFO)        *qualifiers;
        POLICYQUALINFO                  *qualifier;
        int                              nid;
        int                              rc = 0;

        if (!X509_EXTENSION_get_critical(ext)) {
                warnx("%s: RFC 6487 section 4.8.9: certificatePolicies: "
                    "extension not critical", fn);
                goto out;
        }

        /* XXX - cast away const for OpenSSL 3 and LibreSSL */
        if ((policies = X509V3_EXT_d2i((X509_EXTENSION *)ext)) == NULL) {
                warnx("%s: RFC 6487 section 4.8.9: certificatePolicies: "
                    "failed extension parse", fn);
                goto out;
        }

        if (sk_POLICYINFO_num(policies) != 1) {
                warnx("%s: RFC 6487 section 4.8.9: certificatePolicies: "
                    "want 1 policy, got %d", fn, sk_POLICYINFO_num(policies));
                goto out;
        }

        policy = sk_POLICYINFO_value(policies, 0);
        assert(policy != NULL && policy->policyid != NULL);

        if (OBJ_cmp(policy->policyid, certpol_oid) != 0) {
                char pbuf[128], cbuf[128];

                OBJ_obj2txt(pbuf, sizeof(pbuf), policy->policyid, 1);
                OBJ_obj2txt(cbuf, sizeof(cbuf), certpol_oid, 1);
                warnx("%s: RFC 7318 section 2: certificatePolicies: "
                    "unexpected OID: %s, want %s", fn, pbuf, cbuf);
                goto out;
        }

        /* Policy qualifiers are optional. If they're absent, we're done. */
        if ((qualifiers = policy->qualifiers) == NULL) {
                rc = 1;
                goto out;
        }

        if (sk_POLICYQUALINFO_num(qualifiers) != 1) {
                warnx("%s: RFC 7318 section 2: certificatePolicies: "
                    "want 1 policy qualifier, got %d", fn,
                    sk_POLICYQUALINFO_num(qualifiers));
                goto out;
        }

        qualifier = sk_POLICYQUALINFO_value(qualifiers, 0);
        assert(qualifier != NULL && qualifier->pqualid != NULL);

        if ((nid = OBJ_obj2nid(qualifier->pqualid)) != NID_id_qt_cps) {
                warnx("%s: RFC 7318 section 2: certificatePolicies: "
                    "want CPS, got %s", fn, nid2str(nid));
                goto out;
        }

        rc = 1;
 out:
        sk_POLICYINFO_pop_free(policies, POLICYINFO_free);
        return rc;
}

/*
 * Parse an IP Resources X.509v3 extension, RFC 6487 4.8.10, with
 * syntax documented in RFC 3779 starting in section 2.2.
 * Returns zero on failure, non-zero on success.
 */
static int
cert_ipaddrblocks(const char *fn, struct cert *cert, const X509_EXTENSION *ext)
{
        IPAddrBlocks *addrs = NULL;
        int rc = 0;

        if (!X509_EXTENSION_get_critical(ext)) {
                warnx("%s: RFC 6487 section 4.8.10: ipAddrBlocks: "
                    "extension not critical", fn);
                goto out;
        }

        /* XXX - cast away const for OpenSSL 3 and LibreSSL */
        if ((addrs = X509V3_EXT_d2i((X509_EXTENSION *)ext)) == NULL) {
                warnx("%s: RFC 6487 section 4.8.10: ipAddrBlocks: "
                    "failed extension parse", fn);
                goto out;
        }

        if (!sbgp_parse_ipaddrblocks(fn, addrs, &cert->ips, &cert->num_ips))
                goto out;

        if (cert->num_ips == 0) {
                warnx("%s: RFC 6487 section 4.8.10: empty ipAddrBlocks", fn);
                goto out;
        }

        rc = 1;
 out:
        IPAddrBlocks_free(addrs);
        return rc;
}

static int
cert_as_inherit(const struct cert *cert)
{
        if (cert->num_ases != 1)
                return 0;

        return cert->ases[0].type == CERT_AS_INHERIT;
}

static int
cert_has_one_as(const struct cert *cert)
{
        if (cert->num_ases != 1)
                return 0;

        return cert->ases[0].type == CERT_AS_ID;
}

/*
 * Parse an AS Resources X.509v3 extension, RFC 6487 4.8.11, with
 * syntax documented in RFC 3779 starting in section 3.2.
 * Returns zero on failure, non-zero on success.
 */
static int
cert_asids(const char *fn, struct cert *cert, const X509_EXTENSION *ext)
{
        ASIdentifiers           *asidentifiers = NULL;
        int                      rc = 0;

        if (!X509_EXTENSION_get_critical(ext)) {
                warnx("%s: RFC 6487 section 4.8.11: autonomousSysIds: "
                    "extension not critical", fn);
                goto out;
        }

        /* XXX - cast away const for OpenSSL 3 and LibreSSL */
        if ((asidentifiers = X509V3_EXT_d2i((X509_EXTENSION *)ext)) == NULL) {
                warnx("%s: RFC 6487 section 4.8.11: autonomousSysIds: "
                    "failed extension parse", fn);
                goto out;
        }

        if (!sbgp_parse_asids(fn, asidentifiers, &cert->ases, &cert->num_ases))
                goto out;

        rc = 1;
 out:
        ASIdentifiers_free(asidentifiers);
        return rc;
}

/*
 * Parse extensions in a resource certificate following RFC 6487, section 4.8.
 * This must only be called after cert_check_purpose() so we know for sure
 * that issuance, basic constraints, and key usage bits are consistent and
 * what kind of cert we were passed. Annoyingly, callers of cert_parse() can't
 * distinguish BGPsec router certs (a rare kind of EE cert) from CA certs,
 * as they both are .cer files in a Manifest.
 *
 * Then walk the list of extensions, handle each one and ensure there are no
 * duplicates. Store the relevant information in cert.
 */

static int
cert_parse_extensions(const char *fn, struct cert *cert)
{
        X509 *x = cert->x509;
        const X509_EXTENSION *ext;
        const ASN1_OBJECT *obj;
        int extsz, i, nid, bc, ski, aki, ku, eku, crldp, aia, sia, cp, ip, as;

        nid = bc = ski = aki = ku = eku = crldp = aia = sia = cp = ip = as = 0;

        /* This can only work correctly after a call to cert_check_purpose(). */
        assert(cert->purpose != CERT_PURPOSE_INVALID);

        /* Look for X509v3 extensions. */
        if ((extsz = X509_get_ext_count(x)) <= 0) {
                warnx("%s: certificate without X.509v3 extensions", fn);
                goto out;
        }

        for (i = 0; i < extsz; i++) {
                ext = X509_get_ext(x, i);
                assert(ext != NULL);
                /* XXX - cast away const for OpenSSL 3 and LibreSSL */
                obj = X509_EXTENSION_get_object((X509_EXTENSION *)ext);
                assert(obj != NULL);

                /* The switch is ordered following RFC 6487, section 4.8. */
                switch (nid = OBJ_obj2nid(obj)) {
                case NID_basic_constraints:
                        if (bc++ > 0)
                                goto dup;
                        /* handled in cert_check_purpose() */
                        break;
                case NID_subject_key_identifier:
                        if (ski++ > 0)
                                goto dup;
                        if (!cert_ski(fn, cert, ext))
                                goto out;
                        break;
                case NID_authority_key_identifier:
                        if (aki++ > 0)
                                goto dup;
                        if (!cert_aki(fn, cert, ext))
                                goto out;
                        break;
                case NID_key_usage:
                        if (ku++ > 0)
                                goto dup;
                        /* handled in cert_check_purpose() */
                        break;
                case NID_ext_key_usage:
                        if (eku++ > 0)
                                goto dup;
                        /* handled in cert_check_purpose() */
                        break;
                case NID_crl_distribution_points:
                        if (crldp++ > 0)
                                goto dup;
                        if (!cert_crldp(fn, cert, ext))
                                goto out;
                        break;
                case NID_info_access:
                        if (aia++ > 0)
                                goto dup;
                        if (!cert_aia(fn, cert, ext))
                                goto out;
                        break;
                case NID_sinfo_access:
                        if (sia++ > 0)
                                goto dup;
                        if (!cert_sia(fn, cert, ext))
                                goto out;
                        break;
                case NID_certificate_policies:
                        if (cp++ > 0)
                                goto dup;
                        if (!cert_policies(fn, cert, ext))
                                goto out;
                        break;
                case NID_sbgp_ipAddrBlock:
                        if (ip++ > 0)
                                goto dup;
                        if (!cert_ipaddrblocks(fn, cert, ext))
                                goto out;
                        break;
                case NID_sbgp_autonomousSysNum:
                        if (as++ > 0)
                                goto dup;
                        if (!cert_asids(fn, cert, ext))
                                goto out;
                        break;
                default:
                        /* unexpected extensions warrant investigation */
                        {
                                char objn[64];

                                OBJ_obj2txt(objn, sizeof(objn), obj, 0);
                                if (X509_EXTENSION_get_critical(ext)) {
                                        warnx("%s: unknown critical extension "
                                            "%s (NID %d)", fn, objn, nid);
                                        goto out;
                                }
                                warnx("%s: ignoring %s (NID %d)",
                                    fn, objn, nid);
                        }
                        break;
                }
        }

        /*
         * Check specifics on presence and absence of extensions depending
         * on the certificate purpose. Some of the checks are redundant with
         * cert_check_purpose(), some of the checks are also impossible to
         * hit with the checks in the extension parsers, but it is easier to
         * check for completeness against RFC 6487 and RFC 8209 if we're not
         * trying to be smart here.
         */

        if (bc == 0) {
                if (cert->purpose == CERT_PURPOSE_TA ||
                    cert->purpose == CERT_PURPOSE_CA) {
                        warnx("%s: RFC 6487, 4.8.1: CA cert without "
                            "basic constraints", fn);
                        goto out;
                }
        } else {
                if (cert->purpose != CERT_PURPOSE_TA &&
                    cert->purpose != CERT_PURPOSE_CA) {
                        /* This also covers RFC 8209, 3.1.3.1. */
                        warnx("%s: RFC 6487, 4.8.1: non-CA cert with "
                            "basic constraints", fn);
                        goto out;
                }
        }

        if (ski == 0) {
                warnx("%s: RFC 6487, 4.8.2: cert without SKI", fn);
                goto out;
        }

        if (aki == 0) {
                if (cert->purpose != CERT_PURPOSE_TA) {
                        warnx("%s: RFC 6487, 4.8.3: non-TA cert without "
                            "AKI", fn);
                        goto out;
                }
        } else {
                if (cert->purpose == CERT_PURPOSE_TA) {
                        if (strcmp(cert->ski, cert->aki) != 0) {
                                warnx("%s: RFC 6487, 4.8.3: TA cert with "
                                    "mismatch between AKI and SKI", fn);
                                goto out;
                        }
                } else {
                        if (strcmp(cert->ski, cert->aki) == 0) {
                                warnx("%s: RFC 6487, 4.8.3: non-TA cert "
                                    "must not have matching AKI and SKI", fn);
                                goto out;
                        }
                }
        }

        if (ku == 0) {
                warnx("%s: RFC 6487, 4.8.4: cert without key usage", fn);
                goto out;
        }

        if (eku == 0) {
                if (cert->purpose == CERT_PURPOSE_BGPSEC_ROUTER) {
                        warnx("%s: RFC 8209, 3.1.3.2: BGPsec Router cert "
                            "without extended key usage", fn);
                        goto out;
                }
        } else {
                if (cert->purpose != CERT_PURPOSE_BGPSEC_ROUTER) {
                        warnx("%s: RFC 6487, 4.8.5: non-BGPsec cert with "
                            "extended key usage", fn);
                        goto out;
                }
        }

        if (crldp == 0) {
                if (cert->purpose != CERT_PURPOSE_TA) {
                        warnx("%s: RFC 6487, 4.8.6: non-TA cert without "
                            "CRL Distribution Point", fn);
                        goto out;
                }
        } else {
                if (cert->purpose == CERT_PURPOSE_TA) {
                        warnx("%s: RFC 6487, 4.8.6: TA cert with "
                            "CRL Distribution Point", fn);
                        goto out;
                }
        }

        if (aia == 0) {
                if (cert->purpose != CERT_PURPOSE_TA) {
                        warnx("%s: RFC 6487, 4.8.7: non-TA cert without "
                            "AIA", fn);
                        goto out;
                }
        } else {
                if (cert->purpose == CERT_PURPOSE_TA) {
                        warnx("%s: RFC 6487, 4.8.7: TA cert with AIA", fn);
                        goto out;
                }
        }

        if (sia == 0) {
                /*
                 * Allow missing SIA for BGPsec certs (RFC 8209, 3.1.3.3) and
                 * in filemode for the EE cert of an RSC (RFC 9323, section 2).
                 * Absence of the RSC SIA is enforced in rsc_cert_info().
                 */
                if (cert->purpose == CERT_PURPOSE_BGPSEC_ROUTER) {
                        ;
                } else if (filemode && cert->purpose == CERT_PURPOSE_EE &&
                    rtype_from_file_extension(fn) == RTYPE_RSC) {
                        ;
                } else {
                        warnx("%s: RFC 6487, 4.8.8: cert without SIA", fn);
                        goto out;
                }
        } else {
                if (cert->purpose == CERT_PURPOSE_BGPSEC_ROUTER) {
                        warnx("%s: RFC 8209, 3.1.3.3: BGPsec Router cert "
                            "with SIA", fn);
                        goto out;
                }
        }

        if (cp == 0) {
                warnx("%s: RFC 6487, 4.8.9: missing certificate policy", fn);
                goto out;
        }

        if (ip == 0 && as == 0) {
                warnx("%s: RFC 6487, 4.8.10 and 4.8.11: cert without "
                    "IP or AS resources", fn);
                goto out;
        }

        if (cert->purpose == CERT_PURPOSE_TA) {
                if (x509_any_inherits(cert->x509)) {
                        warnx("%s: RFC 8630, 2.3: Trust Anchor INRs "
                            "must not inherit", fn);
                        goto out;
                }
                if (cert->num_ips == 0 && cert->num_ases == 0) {
                        warnx("%s: RFC 8630, 2.3: Trust Anchor INR set "
                            "must not be empty", fn);
                        goto out;
                }
        }

        if (cert->purpose == CERT_PURPOSE_BGPSEC_ROUTER) {
                if (ip != 0) {
                        warnx("%s: RFC 8209, 3.1.3.4: BGPsec Router cert "
                            "with IP resources", fn);
                        goto out;
                }
                if (as == 0) {
                        warnx("%s: RFC 8209, 3.1.3.5: BGPsec Router cert "
                            "without AS resources", fn);
                        goto out;
                }
                if (cert_as_inherit(cert)) {
                        warnx("%s: RFC 8209, 3.1.3.5: BGPsec Router cert "
                            "with inherit element", fn);
                        goto out;
                }

                if (!cert_has_one_as(cert)) {
                        warnx("%s: BGPsec Router certs with more than one "
                            "AS number are not supported", fn);
                        goto out;
                }
        }

        return 1;

 dup:
        warnx("%s: RFC 5280 section 4.2: duplicate extension: %s", fn,
            nid2str(nid));
 out:
        return 0;
}

static struct cert *
cert_parse_internal(const char *fn, X509 *x)
{
        struct cert             *cert;
        const ASN1_INTEGER      *serial;
        const ASN1_BIT_STRING   *issuer_uid = NULL, *subject_uid = NULL;

        if ((cert = calloc(1, sizeof(*cert))) == NULL)
                err(1, NULL);
        cert->x509 = x;

        /*
         * Figure out the type of certificate we seem to be dealing with to
         * allow some special casing. Also cache extensions.
         */
        if (!cert_check_purpose(fn, cert))
                goto out;

        if (X509_get_version(x) != 2) {
                warnx("%s: RFC 6487 4.1: X.509 version must be v3", fn);
                goto out;
        }

        if ((serial = X509_get0_serialNumber(x)) == NULL) {
                warnx("%s: RFC 6487 4.2: missing serialNumber", fn);
                goto out;
        }
        if (!x509_valid_seqnum(fn, "RFC 6487 4.2: serialNumber", serial))
                goto out;

        if (!cert_check_sigalg(fn, cert))
                goto out;

        if (!cert_check_subject_and_issuer(fn, cert))
                goto out;

        if (!cert_check_validity_period(fn, cert))
                goto out;

        if (!cert_check_spki(fn, cert))
                goto out;

        /*
         * Disallowed in RFC 5280, 4.1.2.8. Also, uniqueness of subjects
         * per RFC 6487, 4.5 makes them meaningless.
         */
        X509_get0_uids(x, &issuer_uid, &subject_uid);
        if (issuer_uid != NULL || subject_uid != NULL) {
                warnx("%s: issuer or subject unique identifiers not allowed",
                    fn);
                goto out;
        }

        if (!cert_parse_extensions(fn, cert))
                goto out;

        return cert;

 out:
        cert_free(cert);
        return NULL;
}

/*
 * Parse an EE cert extracted from a CMS signed object. Store all cert and
 * extension data we need later in the returned struct cert.
 * Check the certificate's purpose and validate the TA constraints.
 * Returns cert on success and NULL on failure.
 */
struct cert *
cert_parse_ee_cert(const char *fn, int talid, X509 *x)
{
        struct cert             *cert = NULL;

        if (!X509_up_ref(x))
                goto out;

        if ((cert = cert_parse_internal(fn, x)) == NULL)
                goto out;
        cert->talid = talid;

        if (cert->purpose != CERT_PURPOSE_EE) {
                warnx("%s: expected EE cert, got %s", fn,
                    purpose2str(cert->purpose));
                goto out;
        }

        if (!constraints_validate(fn, cert))
                goto out;

        return cert;

 out:
        cert_free(cert);
        return NULL;
}

/*
 * This is a generic parser for resource certificates and can only do as much
 * validation as can be extracted from the bare DER. Callers should at least
 * check the cert->purpose and consider any further validation.
 */
static struct cert *
cert_deserialize_and_parse(const char *fn, const unsigned char *der, size_t len)
{
        struct cert             *cert = NULL;
        const unsigned char     *oder;
        X509                    *x = NULL;

        oder = der;
        if ((x = d2i_X509(NULL, &der, len)) == NULL) {
                warnx("%s: d2i_X509", fn);
                goto out;
        }
        if (der != oder + len) {
                warnx("%s: %td bytes trailing garbage", fn, oder + len - der);
                goto out;
        }

        if (!X509_up_ref(x)) {
                warnx("%s: X509_up_ref failed", fn);
                goto out;
        }
        if ((cert = cert_parse_internal(fn, x)) == NULL)
                goto out;

        X509_free(x);
        return cert;

 out:
        cert_free(cert);
        X509_free(x);
        return NULL;
}

/*
 * Parse a certificate file from its DER. Intended for .cer in a Manifest
 * fileList, so it must be a CA cert or a BGPsec router cert.
 * Returns cert on success or NULL on failure.
 */
struct cert *
cert_parse_ca_or_brk(const char *fn, const unsigned char *der, size_t len)
{
        struct cert *cert = NULL;

        /* Handle possible parse_load_file() failure which already warned. */
        if (der == NULL)
                return NULL;

        if ((cert = cert_deserialize_and_parse(fn, der, len)) == NULL)
                goto out;

        if (cert->purpose != CERT_PURPOSE_CA &&
            cert->purpose != CERT_PURPOSE_BGPSEC_ROUTER) {
                warnx("%s: want CA or BGPsec Router cert, got %s",
                    fn, purpose2str(cert->purpose));
                goto out;
        }

        return cert;

 out:
        cert_free(cert);
        return NULL;
}

/*
 * Parse and partially validate an RPKI X.509 certificate as defined in RFC 6487
 * from its DER encoding. This is intended to be used only from filemode.
 * Returns the parse results or NULL on failure.
 */
struct cert *
cert_parse_filemode(const char *fn, const unsigned char *der, size_t len)
{
        struct cert             *cert = NULL;

        /* Handle possible load_file() failure. Currently used by regress. */
        if (der == NULL)
                return NULL;

        if ((cert = cert_deserialize_and_parse(fn, der, len)) == NULL)
                goto out;

        if (cert->purpose == CERT_PURPOSE_EE) {
                warnx("%s: unexpected EE cert", fn);
                goto out;
        }

        return cert;

 out:
        cert_free(cert);
        return NULL;
}

/*
 * Check that the subjectPublicKeyInfo from the TAL matches the one in the cert.
 * Verify that this key signed the cert.
 * Returns 1 on success and 0 on failure.
 */
static int
ta_check_pubkey(const char *fn, struct cert *cert, const unsigned char *spki,
    size_t spkisz)
{
        EVP_PKEY        *cert_pkey, *tal_pkey;
        int              rv = 0;

        /*
         * We should really verify that the TAL's SPKI is byte-identical with
         * the cert's SPKI. There's no sane way to access the original DER, so
         * comparing internal representations is the best thing we can do.
         */
        tal_pkey = d2i_PUBKEY(NULL, &spki, spkisz);
        if (tal_pkey == NULL) {
                warnx("%s: RFC 6487 (trust anchor): bad TAL pubkey", fn);
                goto out;
        }
        if ((cert_pkey = X509_get0_pubkey(cert->x509)) == NULL) {
                warnx("%s: RFC 6487 (trust anchor): missing pubkey", fn);
                goto out;
        }
        if (EVP_PKEY_cmp(cert_pkey, tal_pkey) != 1) {
                warnx("%s: RFC 6487 (trust anchor): "
                    "pubkey does not match TAL pubkey", fn);
                goto out;
        }

        /*
         * Do not replace with a <= 0 check since OpenSSL 3 broke that:
         * https://github.com/openssl/openssl/issues/24575
         */
        if (X509_verify(cert->x509, tal_pkey) != 1) {
                warnx("%s: failed to verify signature", fn);
                goto out;
        }

        rv = 1;
 out:
        EVP_PKEY_free(tal_pkey);
        return rv;
}

static int
ta_check_validity(const char *fn, struct cert *cert)
{
        time_t           now = get_current_time();

        if (cert->notbefore > now) {
                warnx("%s: certificate not yet valid", fn);
                return 0;
        }
        if (cert->notafter < now) {
                warnx("%s: certificate has expired", fn);
                return 0;
        }

        return 1;
}

/*
 * Validate a TA against the subjectPublicKeyInfo from the TAL.
 * Check that the SPKIs match, and that the cert is self-signed
 * and currently valid.
 * Returns cert passed in on success or NULL on failure.
 */
struct cert *
ta_validate(const char *fn, struct cert *cert, const unsigned char *spki,
    size_t spkisz)
{
        if (cert == NULL)
                return NULL;

        if (cert->purpose != CERT_PURPOSE_TA) {
                warnx("%s: expected trust anchor purpose, got %s", fn,
                    purpose2str(cert->purpose));
                goto out;
        }

        if (!ta_check_pubkey(fn, cert, spki, spkisz))
                goto out;
        if (!ta_check_validity(fn, cert))
                goto out;

        return cert;

 out:
        cert_free(cert);
        return NULL;
}

/*
 * Parse a TA from its DER and validate it against SPKI from the TAL and
 * current time.
 * Returns a validated TA cert on success or NULL on failure.
 */
struct cert     *
cert_parse_ta(const char *fn, const unsigned char *der, size_t len,
    const unsigned char *spki, size_t spkisz)
{
        struct cert *cert = NULL;

        /* Handle possible load_file() failure. Will warn later if needed. */
        if (der == NULL)
                return NULL;

        if ((cert = cert_deserialize_and_parse(fn, der, len)) == NULL)
                return NULL;

        return ta_validate(fn, cert, spki, spkisz);
}

/*
 * Free parsed certificate contents.
 * Passing NULL is a noop.
 */
void
cert_free(struct cert *cert)
{
        if (cert == NULL)
                return;

        free(cert->crl);
        free(cert->repo);
        free(cert->path);
        free(cert->mft);
        free(cert->notify);
        free(cert->signedobj);
        free(cert->ips);
        free(cert->ases);
        free(cert->aia);
        free(cert->aki);
        free(cert->ski);
        free(cert->pubkey);
        X509_free(cert->x509);
        free(cert);
}

/*
 * Write certificate parsed content into buffer.
 * See cert_read() for the other side of the pipe.
 */
void
cert_buffer(struct ibuf *b, const struct cert *cert)
{
        io_simple_buffer(b, &cert->notafter, sizeof(cert->notafter));
        io_simple_buffer(b, &cert->purpose, sizeof(cert->purpose));
        io_simple_buffer(b, &cert->talid, sizeof(cert->talid));
        io_simple_buffer(b, &cert->certid, sizeof(cert->certid));
        io_simple_buffer(b, &cert->repoid, sizeof(cert->repoid));
        io_simple_buffer(b, &cert->num_ips, sizeof(cert->num_ips));
        io_simple_buffer(b, &cert->num_ases, sizeof(cert->num_ases));

        io_simple_buffer(b, cert->ips, cert->num_ips * sizeof(cert->ips[0]));
        io_simple_buffer(b, cert->ases, cert->num_ases * sizeof(cert->ases[0]));

        io_str_buffer(b, cert->path);

        if (cert->purpose == CERT_PURPOSE_TA) {
                io_str_buffer(b, cert->mft);
                io_opt_str_buffer(b, cert->notify);
                io_str_buffer(b, cert->repo);
                /* No CRL distribution point or AIA for TA certs. */
                io_opt_str_buffer(b, cert->aki);
                io_str_buffer(b, cert->ski);
        } else if (cert->purpose == CERT_PURPOSE_CA) {
                io_str_buffer(b, cert->mft);
                io_opt_str_buffer(b, cert->notify);
                io_str_buffer(b, cert->repo);
                io_str_buffer(b, cert->crl);
                io_str_buffer(b, cert->aia);
                io_str_buffer(b, cert->aki);
                io_str_buffer(b, cert->ski);
                io_simple_buffer(b, &cert->mfthash, sizeof(cert->mfthash));
        } else if (cert->purpose == CERT_PURPOSE_BGPSEC_ROUTER) {
                /* No SIA, so no mft, notify, repo. */
                io_str_buffer(b, cert->crl);
                io_str_buffer(b, cert->aia);
                io_str_buffer(b, cert->aki);
                io_str_buffer(b, cert->ski);
                io_str_buffer(b, cert->pubkey);
        } else {
                errx(1, "%s: unexpected %s", __func__,
                    purpose2str(cert->purpose));
        }
}

/*
 * Allocate and read parsed certificate content from descriptor.
 * The pointer must be freed with cert_free().
 * Always returns a valid pointer.
 */
struct cert *
cert_read(struct ibuf *b)
{
        struct cert     *cert;

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

        io_read_buf(b, &cert->notafter, sizeof(cert->notafter));
        io_read_buf(b, &cert->purpose, sizeof(cert->purpose));
        io_read_buf(b, &cert->talid, sizeof(cert->talid));
        io_read_buf(b, &cert->certid, sizeof(cert->certid));
        io_read_buf(b, &cert->repoid, sizeof(cert->repoid));
        io_read_buf(b, &cert->num_ips, sizeof(cert->num_ips));
        io_read_buf(b, &cert->num_ases, sizeof(cert->num_ases));

        if (cert->num_ips > 0) {
                cert->ips = calloc(cert->num_ips, sizeof(cert->ips[0]));
                if (cert->ips == NULL)
                        err(1, NULL);
                io_read_buf(b, cert->ips,
                    cert->num_ips * sizeof(cert->ips[0]));
        }

        if (cert->num_ases > 0) {
                cert->ases = calloc(cert->num_ases, sizeof(cert->ases[0]));
                if (cert->ases == NULL)
                        err(1, NULL);
                io_read_buf(b, cert->ases,
                    cert->num_ases * sizeof(cert->ases[0]));
        }

        io_read_str(b, &cert->path);

        if (cert->purpose == CERT_PURPOSE_TA) {
                io_read_str(b, &cert->mft);
                io_read_opt_str(b, &cert->notify);
                io_read_str(b, &cert->repo);
                /* No CRL distribution point or AIA for TA certs. */
                io_read_opt_str(b, &cert->aki);
                io_read_str(b, &cert->ski);
        } else if (cert->purpose == CERT_PURPOSE_CA) {
                io_read_str(b, &cert->mft);
                io_read_opt_str(b, &cert->notify);
                io_read_str(b, &cert->repo);
                io_read_str(b, &cert->crl);
                io_read_str(b, &cert->aia);
                io_read_str(b, &cert->aki);
                io_read_str(b, &cert->ski);
                io_read_buf(b, &cert->mfthash, sizeof(cert->mfthash));
        } else if (cert->purpose == CERT_PURPOSE_BGPSEC_ROUTER) {
                /* No SIA, so no mft, notify, repo. */
                io_read_str(b, &cert->crl);
                io_read_str(b, &cert->aia);
                io_read_str(b, &cert->aki);
                io_read_str(b, &cert->ski);
                io_read_str(b, &cert->pubkey);
        } else {
                errx(1, "%s: unexpected %s", __func__,
                    purpose2str(cert->purpose));
        }

        return cert;
}

static inline int
authcmp(struct auth *a, struct auth *b)
{
        if (a->cert->certid > b->cert->certid)
                return 1;
        if (a->cert->certid < b->cert->certid)
                return -1;
        return 0;
}

RB_GENERATE_STATIC(auth_tree, auth, entry, authcmp);

void
auth_tree_free(struct auth_tree *auths)
{
        struct auth     *auth, *tauth;
        int error;

        if ((error = pthread_rwlock_wrlock(&cert_lk)) != 0)
                errx(1, "pthread_rwlock_wrlock: %s", strerror(error));
        RB_FOREACH_SAFE(auth, auth_tree, auths, tauth) {
                RB_REMOVE(auth_tree, auths, auth);
                cert_free(auth->cert);
                free(auth);
        }
        if ((error = pthread_rwlock_unlock(&cert_lk)) != 0)
                errx(1, "pthread_rwlock_unlock: %s", strerror(error));
        if ((error = pthread_rwlock_destroy(&cert_lk)) != 0)
                errx(1, "pthread_rwlock_destroy: %s", strerror(error));
}

struct auth *
auth_find(struct auth_tree *auths, int id)
{
        struct auth a, *f;
        struct cert c;
        int error;

        c.certid = id;
        a.cert = &c;

        if ((error = pthread_rwlock_rdlock(&cert_lk)) != 0)
                errx(1, "pthread_rwlock_rdlock: %s", strerror(error));
        f = RB_FIND(auth_tree, auths, &a);
        if ((error = pthread_rwlock_unlock(&cert_lk)) != 0)
                errx(1, "pthread_rwlock_unlock: %s", strerror(error));
        return f;
}

struct auth *
auth_insert(const char *fn, struct auth_tree *auths, struct cert *cert,
    struct auth *issuer)
{
        struct auth *na;
        int error;

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

        if ((error = pthread_rwlock_wrlock(&cert_lk)) != 0)
                errx(1, "pthread_rwlock_wrlock: %s", strerror(error));
        if (issuer == NULL) {
                cert->certid = cert->talid;
        } else {
                cert->certid = ++certid;
                if (certid > CERTID_MAX) {
                        if (certid == CERTID_MAX + 1)
                                warnx("%s: too many certificates in store", fn);
                        goto fail;
                }
                na->depth = issuer->depth + 1;
        }

        if (na->depth >= MAX_CERT_DEPTH) {
                warnx("%s: maximum certificate chain depth exhausted", fn);
                goto fail;
        }

        na->issuer = issuer;
        na->cert = cert;
        na->any_inherits = x509_any_inherits(cert->x509);

        if (RB_INSERT(auth_tree, auths, na) != NULL)
                errx(1, "auth tree corrupted");

        if ((error = pthread_rwlock_unlock(&cert_lk)) != 0)
                errx(1, "pthread_rwlock_unlock: %s", strerror(error));

        return na;

 fail:
        if ((error = pthread_rwlock_unlock(&cert_lk)) != 0)
                errx(1, "pthread_rwlock_unlock: %s", strerror(error));
        free(na);
        return NULL;
}

static void
insert_brk(struct brk_tree *tree, struct cert *cert, uint32_t asid)
{
        struct brk      *b, *found;

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

        b->asid = asid;
        b->expires = cert->notafter;
        b->talid = cert->talid;
        if ((b->ski = strdup(cert->ski)) == NULL)
                err(1, NULL);
        if ((b->pubkey = strdup(cert->pubkey)) == NULL)
                err(1, NULL);

        /*
         * Check if a similar BRK already exists in the tree. If the found BRK
         * expires sooner, update it to this BRK's later expiry moment.
         */
        if ((found = RB_INSERT(brk_tree, tree, b)) != NULL) {
                if (found->expires < b->expires) {
                        found->expires = b->expires;
                        found->talid = b->talid;
                }
                free(b->ski);
                free(b->pubkey);
                free(b);
        }
}

/*
 * Add each BGPsec Router Key into the BRK tree.
 */
void
cert_insert_brks(struct brk_tree *tree, struct cert *cert)
{
        size_t           i, asid;

        for (i = 0; i < cert->num_ases; i++) {
                switch (cert->ases[i].type) {
                case CERT_AS_ID:
                        insert_brk(tree, cert, cert->ases[i].id);
                        break;
                case CERT_AS_RANGE:
                        for (asid = cert->ases[i].range.min;
                            asid <= cert->ases[i].range.max; asid++)
                                insert_brk(tree, cert, asid);
                        break;
                default:
                        warnx("invalid AS identifier type");
                        continue;
                }
        }
}

static inline int
brkcmp(struct brk *a, struct brk *b)
{
        int rv;

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

        rv = strcmp(a->ski, b->ski);
        if (rv > 0)
                return 1;
        if (rv < 0)
                return -1;

        return strcmp(a->pubkey, b->pubkey);
}

RB_GENERATE(brk_tree, brk, entry, brkcmp);