]> git.ipfire.org Git - thirdparty/gnutls.git/commitdiff
x509/name_constraints: name_constraints_node_list_intersect over sorted
authorAlexander Sosedkin <asosedkin@redhat.com>
Wed, 4 Feb 2026 19:03:49 +0000 (20:03 +0100)
committerAlexander Sosedkin <asosedkin@redhat.com>
Mon, 9 Feb 2026 14:08:21 +0000 (15:08 +0100)
Fixes: #1773
Fixes: GNUTLS-SA-2026-02-09-2
Fixes: CVE-2025-14831
Signed-off-by: Alexander Sosedkin <asosedkin@redhat.com>
NEWS
lib/x509/name_constraints.c

diff --git a/NEWS b/NEWS
index e506db547a76d8b6f938ae353a5a2ff80399e364..96b7484fdf591ae2e98be12817e89e90b99b6b8f 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -14,6 +14,13 @@ See the end for copying conditions.
    Reported by Jaehun Lee.
    [Fixes: GNUTLS-SA-2026-02-09-1, CVSS: high] [CVE-2026-1584]
 
+** libgnutls: Fix name constraint processing performance issue
+   Verifying certificates with pathological amounts of name constraints
+   could lead to a denial of service attack via resource exhaustion.
+   Reworked processing algorithms exhibit better performance characteristics.
+   Reported by Tim Scheckenbach.
+   [Fixes: GNUTLS-SA-2026-02-09-2, CVSS: medium] [CVE-2025-14831]
+
 ** libgnutls: Fix multiple unexploitable overflows
    Reported by Tim Rühsen (#1783, #1786).
 
index 1d78d1bc50f87fc137565cc4d8c14b158aeb9d5f..04722bdf45de8d6a85d12220ca28c6a394de0d8f 100644 (file)
@@ -446,13 +446,6 @@ name_constraints_node_add_copy(gnutls_x509_name_constraints_t nc,
                                             src->name.data, src->name.size);
 }
 
-// for documentation see the implementation
-static int name_constraints_intersect_nodes(
-       gnutls_x509_name_constraints_t nc,
-       const struct name_constraints_node_st *node1,
-       const struct name_constraints_node_st *node2,
-       struct name_constraints_node_st **intersection);
-
 /*-
  * _gnutls_x509_name_constraints_is_empty:
  * @nc: name constraints structure
@@ -716,132 +709,143 @@ typedef char assert_ipaddr[(GNUTLS_SAN_IPADDRESS <= GNUTLS_SAN_MAX) ? 1 : -1];
 static int name_constraints_node_list_intersect(
        gnutls_x509_name_constraints_t nc,
        struct name_constraints_node_list_st *permitted,
-       const struct name_constraints_node_list_st *permitted2,
+       struct name_constraints_node_list_st *permitted2,
        struct name_constraints_node_list_st *excluded)
 {
-       struct name_constraints_node_st *tmp;
-       int ret, type, used;
-       struct name_constraints_node_list_st removed = { .data = NULL,
-                                                        .size = 0,
-                                                        .capacity = 0 };
+       struct name_constraints_node_st *nc1, *nc2;
+       struct name_constraints_node_list_st result = { 0 };
+       struct name_constraints_node_list_st unsupp2 = { 0 };
+       enum name_constraint_relation rel;
+       unsigned type;
+       int ret = GNUTLS_E_SUCCESS;
+       size_t i, j, p1_unsupp = 0, p2_unsupp = 0;
+       type_bitmask_t universal_exclude_needed = 0;
+       type_bitmask_t types_in_p1 = 0, types_in_p2 = 0;
        static const unsigned char universal_ip[32] = { 0 };
 
-       /* bitmask to see if we need to add universal excluded constraints
-        * (see phase 3 for details) */
-       type_bitmask_t types_with_empty_intersection = 0;
-
        if (permitted->size == 0 || permitted2->size == 0)
-               return 0;
+               return GNUTLS_E_SUCCESS;
 
-       /* Phase 1
-        * For each name in PERMITTED, if a PERMITTED2 does not contain a name
-        * with the same type, move the original name to REMOVED.
-        * Do this also for node of unknown type (not DNS, email, IP) */
-       for (size_t i = 0; i < permitted->size;) {
-               struct name_constraints_node_st *t = permitted->data[i];
-               const struct name_constraints_node_st *found = NULL;
-
-               for (size_t j = 0; j < permitted2->size; j++) {
-                       const struct name_constraints_node_st *t2 =
-                               permitted2->data[j];
-                       if (t->type == t2->type) {
-                               // check bounds (we will use 't->type' as index)
-                               if (t->type > GNUTLS_SAN_MAX || t->type == 0) {
-                                       gnutls_assert();
-                                       ret = GNUTLS_E_INTERNAL_ERROR;
-                                       goto cleanup;
-                               }
-                               // note the possibility of empty intersection for this type
-                               // if we add something to the intersection in phase 2,
-                               // we will reset this flag back to 0 then
-                               type_bitmask_set(types_with_empty_intersection,
-                                                t->type);
-                               found = t2;
-                               break;
-                       }
-               }
+       /* make sorted views of the arrays */
+       ret = ensure_sorted(permitted);
+       if (ret < 0) {
+               gnutls_assert();
+               goto cleanup;
+       }
+       ret = ensure_sorted(permitted2);
+       if (ret < 0) {
+               gnutls_assert();
+               goto cleanup;
+       }
 
-               if (found != NULL && is_supported_type(t->type)) {
-                       /* move node from PERMITTED to REMOVED */
-                       ret = name_constraints_node_list_add(&removed, t);
-                       if (ret < 0) {
-                               gnutls_assert();
-                               goto cleanup;
-                       }
-                       /* remove node by swapping */
-                       if (i < permitted->size - 1)
-                               permitted->data[i] =
-                                       permitted->data[permitted->size - 1];
-                       permitted->size--;
-                       permitted->dirty = true;
-                       continue;
+       /* deal with the leading unsupported types first: count, then union */
+       while (p1_unsupp < permitted->size &&
+              !is_supported_type(permitted->sorted_view[p1_unsupp]->type))
+               p1_unsupp++;
+       while (p2_unsupp < permitted2->size &&
+              !is_supported_type(permitted2->sorted_view[p2_unsupp]->type))
+               p2_unsupp++;
+       if (p1_unsupp) { /* copy p1 unsupported type pointers into result */
+               result.data = gnutls_calloc(
+                       p1_unsupp, sizeof(struct name_constraints_node_st *));
+               if (!result.data) {
+                       ret = GNUTLS_E_MEMORY_ERROR;
+                       gnutls_assert();
+                       goto cleanup;
+               }
+               memcpy(result.data, permitted->sorted_view,
+                      p1_unsupp * sizeof(struct name_constraints_node_st *));
+               result.size = result.capacity = p1_unsupp;
+               result.dirty = true;
+       }
+       if (p2_unsupp) { /* union will make deep copies from p2 */
+               unsupp2.data = permitted2->sorted_view; /* so, just alias */
+               unsupp2.size = unsupp2.capacity = p2_unsupp;
+               unsupp2.dirty = false; /* we know it's sorted */
+               unsupp2.sorted_view = permitted2->sorted_view;
+               ret = name_constraints_node_list_union(nc, &result, &unsupp2);
+               if (ret < 0) {
+                       gnutls_assert();
+                       goto cleanup;
                }
-               i++;
        }
 
-       /* Phase 2
-        * iterate through all combinations from PERMITTED2 and PERMITTED
-        * and create intersections of nodes with same type */
-       for (size_t i = 0; i < permitted2->size; i++) {
-               const struct name_constraints_node_st *t2 = permitted2->data[i];
-
-               // current PERMITTED2 node has not yet been used for any intersection
-               // (and is not in REMOVED either)
-               used = 0;
-               for (size_t j = 0; j < removed.size; j++) {
-                       const struct name_constraints_node_st *t =
-                               removed.data[j];
-                       // save intersection of name constraints into tmp
-                       ret = name_constraints_intersect_nodes(nc, t, t2, &tmp);
-                       if (ret < 0) {
-                               gnutls_assert();
-                               goto cleanup;
-                       }
+       /* with that out of the way, pre-compute the supported types we have */
+       for (i = p1_unsupp; i < permitted->size; i++) {
+               type = permitted->sorted_view[i]->type;
+               if (type < 1 || type > GNUTLS_SAN_MAX) {
+                       ret = gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
+                       goto cleanup;
+               }
+               type_bitmask_set(types_in_p1, type);
+       }
+       for (j = p2_unsupp; j < permitted2->size; j++) {
+               type = permitted2->sorted_view[j]->type;
+               if (type < 1 || type > GNUTLS_SAN_MAX) {
+                       ret = gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
+                       goto cleanup;
+               }
+               type_bitmask_set(types_in_p2, type);
+       }
+       /* universal excludes might be needed for types intersecting to empty */
+       universal_exclude_needed = types_in_p1 & types_in_p2;
+
+       /* go through supported type NCs and intersect in a single pass */
+       i = p1_unsupp;
+       j = p2_unsupp;
+       while (i < permitted->size || j < permitted2->size) {
+               nc1 = (i < permitted->size) ? permitted->sorted_view[i] : NULL;
+               nc2 = (j < permitted2->size) ? permitted2->sorted_view[j] :
+                                              NULL;
+               rel = compare_name_constraint_nodes(nc1, nc2);
 
-                       if (t->type == t2->type)
-                               used = 1;
-
-                       // if intersection is not empty
-                       if (tmp !=
-                           NULL) { // intersection for this type is not empty
-                               // check bounds
-                               if (tmp->type > GNUTLS_SAN_MAX ||
-                                   tmp->type == 0) {
-                                       gnutls_free(tmp);
-                                       return gnutls_assert_val(
-                                               GNUTLS_E_INTERNAL_ERROR);
-                               }
-                               // we will not add universal excluded constraint for this type
-                               type_bitmask_clr(types_with_empty_intersection,
-                                                tmp->type);
-                               // add intersection node to PERMITTED
-                               ret = name_constraints_node_list_add(permitted,
-                                                                    tmp);
-                               if (ret < 0) {
-                                       gnutls_assert();
-                                       goto cleanup;
-                               }
-                       }
+               switch (rel) {
+               case NC_SORTS_BEFORE:
+                       assert(nc1 != NULL); /* comparator-guaranteed */
+                       /* if nothing to intersect with, shallow-copy nc1 */
+                       if (!type_bitmask_in(types_in_p2, nc1->type))
+                               ret = name_constraints_node_list_add(&result,
+                                                                    nc1);
+                       i++; /* otherwise skip nc1 */
+                       break;
+               case NC_SORTS_AFTER:
+                       assert(nc2 != NULL); /* comparator-guaranteed */
+                       /* if nothing to intersect with, deep-copy nc2 */
+                       if (!type_bitmask_in(types_in_p1, nc2->type))
+                               ret = name_constraints_node_add_copy(
+                                       nc, &result, nc2);
+                       j++; /* otherwise skip nc2 */
+                       break;
+               case NC_INCLUDED_BY: /* add nc1, shallow-copy */
+                       assert(nc1 != NULL && nc2 != NULL); /* comparator */
+                       type_bitmask_clr(universal_exclude_needed, nc1->type);
+                       ret = name_constraints_node_list_add(&result, nc1);
+                       i++;
+                       break;
+               case NC_INCLUDES: /* pick nc2, deep-copy */
+                       assert(nc1 != NULL && nc2 != NULL); /* comparator */
+                       type_bitmask_clr(universal_exclude_needed, nc2->type);
+                       ret = name_constraints_node_add_copy(nc, &result, nc2);
+                       j++;
+                       break;
+               case NC_EQUAL: /* pick whichever: nc1, shallow-copy */
+                       assert(nc1 != NULL && nc2 != NULL); /* loop condition */
+                       type_bitmask_clr(universal_exclude_needed, nc1->type);
+                       ret = name_constraints_node_list_add(&result, nc1);
+                       i++;
+                       j++;
+                       break;
                }
-               // if the node from PERMITTED2 was not used for intersection, copy it to DEST
-               // Beware: also copies nodes other than DNS, email, IP,
-               //       since their counterpart may have been moved in phase 1.
-               if (!used) {
-                       ret = name_constraints_node_add_copy(nc, permitted, t2);
-                       if (ret < 0) {
-                               gnutls_assert();
-                               goto cleanup;
-                       }
+               if (ret < 0) {
+                       gnutls_assert();
+                       goto cleanup;
                }
        }
 
-       /* Phase 3
-        * For each type: If we have empty permitted name constraints now
-        * and we didn't have at the beginning, we have to add a new
-        * excluded constraint with universal wildcard
-        * (since the intersection of permitted is now empty). */
+       /* finishing touch: add universal excluded constraints for types where
+        * both lists had constraints, but all intersections ended up empty */
        for (type = 1; type <= GNUTLS_SAN_MAX; type++) {
-               if (!type_bitmask_in(types_with_empty_intersection, type))
+               if (!type_bitmask_in(universal_exclude_needed, type))
                        continue;
                _gnutls_hard_log(
                        "Adding universal excluded name constraint for type %d.\n",
@@ -874,14 +878,24 @@ static int name_constraints_node_list_intersect(
                                goto cleanup;
                        }
                        break;
-               default: // do nothing, at least one node was already moved in phase 1
-                       break;
+               default: /* unsupported type; should be unreacheable */
+                       ret = gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
+                       goto cleanup;
                }
        }
-       ret = GNUTLS_E_SUCCESS;
 
+       gnutls_free(permitted->data);
+       gnutls_free(permitted->sorted_view);
+       permitted->data = result.data;
+       permitted->sorted_view = NULL;
+       permitted->size = result.size;
+       permitted->capacity = result.capacity;
+       permitted->dirty = true;
+
+       result.data = NULL;
+       ret = GNUTLS_E_SUCCESS;
 cleanup:
-       gnutls_free(removed.data);
+       name_constraints_node_list_clear(&result);
        return ret;
 }
 
@@ -1257,100 +1271,6 @@ static unsigned email_matches(const gnutls_datum_t *name,
        return rel == NC_EQUAL || rel == NC_INCLUDED_BY;
 }
 
-/*-
- * name_constraints_intersect_nodes:
- * @nc1: name constraints node 1
- * @nc2: name constraints node 2
- * @_intersection: newly allocated node with intersected constraints,
- *              NULL if the intersection is empty
- *
- * Inspect 2 name constraints nodes (of possibly different types) and allocate
- * a new node with intersection of given constraints.
- *
- * Returns: On success, %GNUTLS_E_SUCCESS (0) is returned, otherwise a negative error value.
- -*/
-static int name_constraints_intersect_nodes(
-       gnutls_x509_name_constraints_t nc,
-       const struct name_constraints_node_st *node1,
-       const struct name_constraints_node_st *node2,
-       struct name_constraints_node_st **_intersection)
-{
-       // presume empty intersection
-       struct name_constraints_node_st *intersection = NULL;
-       const struct name_constraints_node_st *to_copy = NULL;
-       enum name_constraint_relation rel;
-
-       *_intersection = NULL;
-
-       if (node1->type != node2->type) {
-               return GNUTLS_E_SUCCESS;
-       }
-       switch (node1->type) {
-       case GNUTLS_SAN_DNSNAME:
-               rel = compare_dns_names(&node1->name, &node2->name);
-               switch (rel) {
-               case NC_EQUAL: // equal means doesn't matter which one
-               case NC_INCLUDES: // node2 is more specific
-                       to_copy = node2;
-                       break;
-               case NC_INCLUDED_BY: // node1 is more specific
-                       to_copy = node1;
-                       break;
-               case NC_SORTS_BEFORE: // no intersection
-               case NC_SORTS_AFTER: // no intersection
-                       return GNUTLS_E_SUCCESS;
-               }
-               break;
-       case GNUTLS_SAN_RFC822NAME:
-               rel = compare_emails(&node1->name, &node2->name);
-               switch (rel) {
-               case NC_EQUAL: // equal means doesn't matter which one
-               case NC_INCLUDES: // node2 is more specific
-                       to_copy = node2;
-                       break;
-               case NC_INCLUDED_BY: // node1 is more specific
-                       to_copy = node1;
-                       break;
-               case NC_SORTS_BEFORE: // no intersection
-               case NC_SORTS_AFTER: // no intersection
-                       return GNUTLS_E_SUCCESS;
-               }
-               break;
-       case GNUTLS_SAN_IPADDRESS:
-               rel = compare_ip_ncs(&node1->name, &node2->name);
-               switch (rel) {
-               case NC_EQUAL: // equal means doesn't matter which one
-               case NC_INCLUDES: // node2 is more specific
-                       to_copy = node2;
-                       break;
-               case NC_INCLUDED_BY: // node1 is more specific
-                       to_copy = node1;
-                       break;
-               case NC_SORTS_BEFORE: // no intersection
-               case NC_SORTS_AFTER: // no intersection
-                       return GNUTLS_E_SUCCESS;
-               }
-               break;
-       default:
-               // for other types, we don't know how to do the intersection, assume empty
-               return GNUTLS_E_SUCCESS;
-       }
-
-       // copy existing node if applicable
-       if (to_copy != NULL) {
-               *_intersection = name_constraints_node_new(nc, to_copy->type,
-                                                          to_copy->name.data,
-                                                          to_copy->name.size);
-               if (*_intersection == NULL)
-                       return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR);
-               intersection = *_intersection;
-
-               assert(intersection->name.data != NULL);
-       }
-
-       return GNUTLS_E_SUCCESS;
-}
-
 /*
  * Returns: true if the certification is acceptable, and false otherwise.
  */