From: Theo Buehler Subject: rpki-client: reject certs with duplicate extension OIDs To: tech@openbsd.org Date: Fri, 12 Jun 2026 17:40:27 +0200 We check this for extensions we know about in cert_parse_extensions(). I think we should keep doing it there since we need to keep track of the extensions we encountered anyway. While cert_parse_extensions() rejects certs with critical extensions we don't know about, we allow duplicates non-critical ones mainly because that's annoying to keep track of. LibreSSL's libcrypto checks for this and rejects the cert when caching the extensions, OpenSSL 4 adds an EXFLAG_DUPLICATE flag and accepts the cert, OpenSSL 3 ignores duplicates. In short: we get to do it ourselves. This check is basically lifted from libcrypto's x509_purp.c with a few extra contortions due to const sprinkling and making things opaque. The warning is the same as the one already present in cert_parse_extensions(). I do not NULL check X509_EXTENSION_get_object() because the extension parsed, so an OID is present. Index: cert.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/cert.c,v diff -u -p -r1.237 cert.c --- cert.c 16 May 2026 07:27:03 -0000 1.237 +++ cert.c 12 Jun 2026 15:36:24 -0000 @@ -34,6 +34,68 @@ 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. @@ -60,6 +122,9 @@ cert_check_purpose(const char *fn, struc 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);