root/usr.sbin/rpki-client/crl.c
/*      $OpenBSD: crl.c,v 1.52 2025/12/01 14:40:56 tb Exp $ */
/*
 * Copyright (c) 2024 Theo Buehler <tb@openbsd.org>
 * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <err.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <openssl/x509.h>

#include "extern.h"

static pthread_rwlock_t  crl_lk = PTHREAD_RWLOCK_INITIALIZER;

/*
 * Check CRL Number is present, non-critical and in [0, 2^159-1].
 * Otherwise ignore it per draft-spaghetti-sidrops-rpki-crl-numbers.
 */
static int
crl_check_crl_number(const char *fn, const X509_CRL *x509_crl)
{
        ASN1_INTEGER            *aint = NULL;
        int                      crit;
        int                      ret = 0;

        aint = X509_CRL_get_ext_d2i(x509_crl, NID_crl_number, &crit, NULL);
        if (aint == NULL) {
                if (crit != -1)
                        warnx("%s: RFC 6487, section 5: "
                            "failed to parse CRL number", fn);
                else
                        warnx("%s: RFC 6487, section 5: missing CRL number",
                            fn);
                goto out;
        }
        if (crit != 0) {
                warnx("%s: RFC 6487, section 5: CRL number not non-critical",
                    fn);
                goto out;
        }

        ret = x509_valid_seqnum(fn, "CRL number", aint);

 out:
        ASN1_INTEGER_free(aint);
        return ret;
}

/*
 * Parse X509v3 authority key identifier (AKI) from the CRL.
 * Returns the AKI or NULL if it could not be parsed.
 * The AKI is formatted as a hex string.
 */
static char *
crl_get_aki(const char *fn, X509_CRL *x509_crl)
{
        AUTHORITY_KEYID         *akid = NULL;
        ASN1_OCTET_STRING       *os;
        const unsigned char     *d;
        int                      dsz, crit;
        char                    *res = NULL;

        if ((akid = X509_CRL_get_ext_d2i(x509_crl, NID_authority_key_identifier,
            &crit, NULL)) == NULL) {
                if (crit != -1)
                        warnx("%s: RFC 6487 section 4.8.3: AKI: "
                            "failed to parse CRL extension", fn);
                else
                        warnx("%s: RFC 6487 section 4.8.3: AKI: "
                            "CRL extension missing", fn);
                goto out;
        }
        if (crit != 0) {
                warnx("%s: RFC 6487 section 4.8.3: "
                    "AKI: extension not non-critical", 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;
        }

        os = akid->keyid;
        if (os == NULL) {
                warnx("%s: RFC 6487 section 4.8.3: AKI: "
                    "Key Identifier missing", fn);
                goto out;
        }

        d = ASN1_STRING_get0_data(os);
        dsz = ASN1_STRING_length(os);

        if (dsz != SHA_DIGEST_LENGTH) {
                warnx("%s: RFC 6487 section 4.8.3: AKI: "
                    "want %d bytes SHA1 hash, have %d bytes",
                    fn, SHA_DIGEST_LENGTH, dsz);
                goto out;
        }

        res = hex_encode(d, dsz);
 out:
        AUTHORITY_KEYID_free(akid);
        return res;
}

/*
 * Check that the list of revoked certificates contains only the specified
 * two fields, Serial Number and Revocation Date, and that no extensions are
 * present.
 */
static int
crl_check_revoked(const char *fn, X509_CRL *x509_crl)
{
        STACK_OF(X509_REVOKED)  *list;
        X509_REVOKED            *revoked;
        int                      count, i;

        /* If there are no revoked certificates, there's nothing to check. */
        if ((list = X509_CRL_get_REVOKED(x509_crl)) == NULL)
                return 1;

        if ((count = sk_X509_REVOKED_num(list)) <= 0) {
                /*
                 * XXX - as of May 2024, ~15% of RPKI CRLs fail this check due
                 * to a bug in rpki-rs/Krill. So silently accept this for now.
                 * https://github.com/NLnetLabs/krill/issues/1197
                 * https://github.com/NLnetLabs/rpki-rs/pull/295
                 */
                if (verbose > 1)
                        warnx("%s: RFC 5280, section 5.1.2.6: revoked "
                            "certificate list without entries disallowed", fn);
                return 1;
        }

        for (i = 0; i < count; i++) {
                revoked = sk_X509_REVOKED_value(list, i);

                /*
                 * serialNumber and revocationDate are mandatory in the ASN.1
                 * template, so no need to check their presence.
                 *
                 * XXX - due to an old bug in Krill, we can't enforce that
                 * revocationDate is in the past until at least mid-2025:
                 * https://github.com/NLnetLabs/krill/issues/788.
                 */

                if (X509_REVOKED_get0_extensions(revoked) != NULL) {
                        warnx("%s: RFC 6487, section 5: CRL entry extensions "
                            "disallowed", fn);
                        return 0;
                }
        }

        return 1;
}

static int
crl_check_sigalg(const char *fn, const struct crl *crl)
{
        const X509_CRL          *x = crl->x509_crl;
        const X509_ALGOR        *alg = NULL, *tbsalg;

        /* Retrieve AlgorithmIdentifier from CertificateList and TBSCertList. */
        X509_CRL_get0_signature(x, NULL, &alg);
        if (alg == NULL) {
                warnx("%s: missing signatureAlgorithm in certificateList", fn);
                return 0;
        }
        if ((tbsalg = X509_CRL_get0_tbs_sigalg(x)) == NULL) {
                warnx("%s: missing signature in tbsCertList", fn);
                return 0;
        }

        /* Unlike X509_verify(), X509_CRL_verify() does not check this. */
        if (X509_ALGOR_cmp(alg, tbsalg) != 0) {
                warnx("%s: RFC 5280, 5.1.1.2: signatureAlgorithm and signature "
                    "AlgorithmIdentifier mismatch", fn);
                return 0;
        }

        return x509_check_tbs_sigalg(fn, tbsalg);
}

struct crl *
crl_parse(const char *fn, const unsigned char *der, size_t len)
{
        const unsigned char     *oder;
        struct crl              *crl;
        const X509_NAME         *name;
        const ASN1_TIME         *at;
        int                      count, rc = 0;

        /* just fail for empty buffers, the warning was printed elsewhere */
        if (der == NULL)
                return NULL;

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

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

        if (X509_CRL_get_version(crl->x509_crl) != 1) {
                warnx("%s: RFC 6487 section 5: version 2 expected", fn);
                goto out;
        }

        if ((name = X509_CRL_get_issuer(crl->x509_crl)) == NULL) {
                warnx("%s: X509_CRL_get_issuer", fn);
                goto out;
        }
        if (!x509_valid_issuer_name(fn, name))
                goto out;

        if (!crl_check_sigalg(fn, crl))
                goto out;

        /*
         * RFC 6487, section 5: AKI and crlNumber MUST be present, no other
         * CRL extensions are allowed.
         */
        if ((count = X509_CRL_get_ext_count(crl->x509_crl)) != 2) {
                warnx("%s: RFC 6487 section 5: unexpected number of extensions "
                    "%d != 2", fn, count);
                goto out;
        }
        if (!crl_check_crl_number(fn, crl->x509_crl))
                goto out;
        if ((crl->aki = crl_get_aki(fn, crl->x509_crl)) == NULL)
                goto out;

        at = X509_CRL_get0_lastUpdate(crl->x509_crl);
        if (at == NULL) {
                warnx("%s: X509_CRL_get0_lastUpdate failed", fn);
                goto out;
        }
        if (!x509_get_time(at, &crl->thisupdate)) {
                warnx("%s: ASN1_TIME_to_tm failed", fn);
                goto out;
        }

        at = X509_CRL_get0_nextUpdate(crl->x509_crl);
        if (at == NULL) {
                warnx("%s: X509_CRL_get0_nextUpdate failed", fn);
                goto out;
        }
        if (!x509_get_time(at, &crl->nextupdate)) {
                warnx("%s: ASN1_TIME_to_tm failed", fn);
                goto out;
        }

        if (!crl_check_revoked(fn, crl->x509_crl))
                goto out;

        rc = 1;
 out:
        if (rc == 0) {
                crl_free(crl);
                crl = NULL;
        }
        return crl;
}

static inline int
crlcmp(struct crl *a, struct crl *b)
{
        int      cmp;

        cmp = strcmp(a->aki, b->aki);
        if (cmp > 0)
                return 1;
        if (cmp < 0)
                return -1;

        /*
         * In filemode the mftpath cannot be determined easily,
         * but it is always set in normal top-down validation.
         */
        if (a->mftpath == NULL || b->mftpath == NULL)
                return 0;

        cmp = strcmp(a->mftpath, b->mftpath);
        if (cmp > 0)
                return 1;
        if (cmp < 0)
                return -1;

        return 0;
}

RB_GENERATE_STATIC(crl_tree, crl, entry, crlcmp);

/*
 * Find a CRL based on the auth SKI value and manifest path.
 */
struct crl *
crl_get(struct crl_tree *crls, const struct auth *a)
{
        struct crl      find, *crl;
        int             error;

        /* XXX - this should be removed, but filemode relies on it. */
        if (a == NULL)
                return NULL;

        find.aki = a->cert->ski;
        find.mftpath = a->cert->mft;

        if ((error = pthread_rwlock_rdlock(&crl_lk)) != 0)
                errx(1, "pthread_rwlock_rdlock: %s", strerror(error));
        crl = RB_FIND(crl_tree, crls, &find);
        if ((error = pthread_rwlock_unlock(&crl_lk)) != 0)
                errx(1, "pthread_rwlock_unlock: %s", strerror(error));
        return crl;
}

int
crl_insert(struct crl_tree *crls, struct crl *crl)
{
        int error, rv;

        if ((error = pthread_rwlock_wrlock(&crl_lk)) != 0)
                errx(1, "pthread_rwlock_wrlock: %s", strerror(error));
        rv = RB_INSERT(crl_tree, crls, crl) == NULL;
        if ((error = pthread_rwlock_unlock(&crl_lk)) != 0)
                errx(1, "pthread_rwlock_unlock: %s", strerror(error));

        return rv;
}

void
crl_free(struct crl *crl)
{
        if (crl == NULL)
                return;
        free(crl->aki);
        free(crl->mftpath);
        X509_CRL_free(crl->x509_crl);
        free(crl);
}

void
crl_tree_free(struct crl_tree *crls)
{
        struct crl      *crl, *tcrl;
        int error;

        if ((error = pthread_rwlock_wrlock(&crl_lk)) != 0)
                errx(1, "pthread_rwlock_wrlock: %s", strerror(error));
        RB_FOREACH_SAFE(crl, crl_tree, crls, tcrl) {
                RB_REMOVE(crl_tree, crls, crl);
                crl_free(crl);
        }
        if ((error = pthread_rwlock_unlock(&crl_lk)) != 0)
                errx(1, "pthread_rwlock_unlock: %s", strerror(error));
        if ((error = pthread_rwlock_destroy(&crl_lk)) != 0)
                errx(1, "pthread_rwlock_destroy: %s", strerror(error));
}