Version in base suite: 3.8.9-3+deb13u1 Base version: gnutls28_3.8.9-3+deb13u1 Target version: gnutls28_3.8.9-3+deb13u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/g/gnutls28/gnutls28_3.8.9-3+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/g/gnutls28/gnutls28_3.8.9-3+deb13u2.dsc changelog | 11 patches/49_x509-fix-incorrect-handling-in-name-constraints-merg.patch | 129 ++ patches/50_0001-x509-name_constraints-use-actual-zeroes-in-universal.patch | 99 + patches/50_0002-tests-name-constraints-ip-stop-swallowing-errors.patch | 30 patches/50_0003-x509-name_constraints-reject-some-malformed-domain-n.patch | 49 patches/50_0004-x509-name_constraints-name_constraints_node_add_-new.patch | 260 ++++ patches/50_0005-x509-name_constraints-introduce-a-rich-comparator.patch | 551 ++++++++++ patches/50_0006-x509-name_constraints-add-sorted_view-in-preparation.patch | 160 ++ patches/50_0007-x509-name_constraints-implement-name_constraints_nod.patch | 183 +++ patches/50_0008-x509-name_constraints-make-types_with_empty_intersec.patch | 162 ++ patches/50_0009-x509-name_constraints-name_constraints_node_list_int.patch | 453 ++++++++ patches/series | 10 12 files changed, 2097 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp52p5pnyz/gnutls28_3.8.9-3+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp52p5pnyz/gnutls28_3.8.9-3+deb13u2.dsc: no acceptable signature found diff -Nru gnutls28-3.8.9/debian/changelog gnutls28-3.8.9/debian/changelog --- gnutls28-3.8.9/debian/changelog 2025-11-23 13:13:38.000000000 +0000 +++ gnutls28-3.8.9/debian/changelog 2026-02-14 06:53:42.000000000 +0000 @@ -1,3 +1,14 @@ +gnutls28 (3.8.9-3+deb13u2) trixie-security; urgency=high + + * 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] + + -- Andreas Metzler Sat, 14 Feb 2026 07:53:42 +0100 + gnutls28 (3.8.9-3+deb13u1) trixie; urgency=medium * Add patch for CVE-2025-9820 / GNUTLS-SA-2025-11-18 from 3.8.11. diff -Nru gnutls28-3.8.9/debian/patches/49_x509-fix-incorrect-handling-in-name-constraints-merg.patch gnutls28-3.8.9/debian/patches/49_x509-fix-incorrect-handling-in-name-constraints-merg.patch --- gnutls28-3.8.9/debian/patches/49_x509-fix-incorrect-handling-in-name-constraints-merg.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnutls28-3.8.9/debian/patches/49_x509-fix-incorrect-handling-in-name-constraints-merg.patch 2026-02-14 06:53:42.000000000 +0000 @@ -0,0 +1,129 @@ +From 6e118a4dfe820ce62fc77130b89188bcd8fbcaad Mon Sep 17 00:00:00 2001 +From: chenjianhu +Date: Fri, 1 Aug 2025 17:18:23 +0800 +Subject: [PATCH] x509: fix incorrect handling in name constraints merging + +As mentioned in commit ca573d65 ("x509: Fix asymmetry in name +constraints intersection", 2016-07-29), the +_gnutls_name_constraints_intersect function exhibited an +asymmetry in name constraints intersection behavior, specifically +manifested as: +1. Nodes of unique types in PERMITTED (absent in PERMITTED2) were + preserved +2. Nodes of unique types in PERMITTED2 (absent in PERMITTED) were + discarded + +A 'used' flag was introduced, where if a node from PERMITTED2 was + not used for the intersection, it would be copied to PERMITTED. + +However,an unresolved edge case persisted: +- When 'removed.size > 0', the 'used' flag was unconditionally set +to 1 +- This prevented copying of PERMITTED2 nodes with unique types + +Signed-off-by: chenjianhu +Modified-by: Daiki Ueno +--- + lib/x509/name_constraints.c | 5 +++- + tests/name-constraints-merge.c | 55 ++++++++++++++++++++++++++++++++++ + 2 files changed, 59 insertions(+), 1 deletion(-) + +diff --git a/lib/x509/name_constraints.c b/lib/x509/name_constraints.c +index 3c6e306303..2be6a2aaa6 100644 +--- a/lib/x509/name_constraints.c ++++ b/lib/x509/name_constraints.c +@@ -410,15 +410,18 @@ static int name_constraints_node_list_intersect( + 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; + } +- used = 1; ++ ++ 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); +diff --git a/tests/name-constraints-merge.c b/tests/name-constraints-merge.c +index 03b3243cc7..70376aaa74 100644 +--- a/tests/name-constraints-merge.c ++++ b/tests/name-constraints-merge.c +@@ -414,12 +414,67 @@ void doit(void) + ret = gnutls_x509_name_constraints_check(nc1, GNUTLS_SAN_DNSNAME, + &name); + check_test_result(suite, ret, NAME_REJECTED, &name); + + gnutls_x509_name_constraints_deinit(nc1); + gnutls_x509_name_constraints_deinit(nc2); + ++ /* 5: variant of suite 0: after moving rfc822Name (ccc.com) ++ * from NC1 to NC2, dNSName (xxx.ccc.com) should still be ++ * rejected. ++ * ++ * NC1: permitted DNS org ++ * permitted DNS ccc.com ++ * NC2: permitted DNS org ++ * permitted email ccc.com ++ * permitted DNS aaa.bbb.ccc.com ++ */ ++ suite = 5; ++ ++ ret = gnutls_x509_name_constraints_init(&nc1); ++ check_for_error(ret); ++ ++ ret = gnutls_x509_name_constraints_init(&nc2); ++ check_for_error(ret); ++ ++ set_name("org", &name); ++ ret = gnutls_x509_name_constraints_add_permitted( ++ nc1, GNUTLS_SAN_DNSNAME, &name); ++ check_for_error(ret); ++ ++ set_name("ccc.com", &name); ++ ret = gnutls_x509_name_constraints_add_permitted( ++ nc1, GNUTLS_SAN_DNSNAME, &name); ++ check_for_error(ret); ++ ++ set_name("org", &name); ++ ret = gnutls_x509_name_constraints_add_permitted( ++ nc2, GNUTLS_SAN_DNSNAME, &name); ++ check_for_error(ret); ++ ++ set_name("ccc.com", &name); ++ ret = gnutls_x509_name_constraints_add_permitted( ++ nc2, GNUTLS_SAN_RFC822NAME, &name); ++ check_for_error(ret); ++ ++ set_name("aaa.bbb.ccc.com", &name); ++ ret = gnutls_x509_name_constraints_add_permitted( ++ nc2, GNUTLS_SAN_DNSNAME, &name); ++ check_for_error(ret); ++ ++ ret = _gnutls_x509_name_constraints_merge(nc1, nc2); ++ check_for_error(ret); ++ ++ /* check intersection of permitted */ ++ set_name("xxx.ccc.com", &name); ++ ret = gnutls_x509_name_constraints_check(nc1, GNUTLS_SAN_DNSNAME, ++ &name); ++ check_test_result(suite, ret, NAME_REJECTED, &name); ++ ++ gnutls_x509_name_constraints_deinit(nc1); ++ gnutls_x509_name_constraints_deinit(nc2); ++ + /* Test footer */ + + if (debug) + success("Test success.\n"); + } +-- +2.51.0 + diff -Nru gnutls28-3.8.9/debian/patches/50_0001-x509-name_constraints-use-actual-zeroes-in-universal.patch gnutls28-3.8.9/debian/patches/50_0001-x509-name_constraints-use-actual-zeroes-in-universal.patch --- gnutls28-3.8.9/debian/patches/50_0001-x509-name_constraints-use-actual-zeroes-in-universal.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnutls28-3.8.9/debian/patches/50_0001-x509-name_constraints-use-actual-zeroes-in-universal.patch 2026-02-14 06:53:42.000000000 +0000 @@ -0,0 +1,99 @@ +From 0b2377dfccd99be641bf3f1a0de9f0dc8dc0d4b1 Mon Sep 17 00:00:00 2001 +From: Alexander Sosedkin +Date: Mon, 26 Jan 2026 19:02:27 +0100 +Subject: [PATCH 1/9] x509/name_constraints: use actual zeroes in universal + exclude IP NC + +Signed-off-by: Alexander Sosedkin +--- + lib/x509/name_constraints.c | 9 +++++---- + 1 file changed, 5 insertions(+), 4 deletions(-) + +diff --git a/lib/x509/name_constraints.c b/lib/x509/name_constraints.c +index 2be6a2aaa6..d07482e3c9 100644 +--- a/lib/x509/name_constraints.c ++++ b/lib/x509/name_constraints.c +@@ -57,15 +57,15 @@ struct gnutls_name_constraints_st { + struct name_constraints_node_list_st nodes; /* owns elements */ + struct name_constraints_node_list_st permitted; /* borrows elements */ + struct name_constraints_node_list_st excluded; /* borrows elements */ + }; + + static struct name_constraints_node_st * + name_constraints_node_new(gnutls_x509_name_constraints_t nc, unsigned type, +- unsigned char *data, unsigned int size); ++ const unsigned char *data, unsigned int size); + + static int + name_constraints_node_list_add(struct name_constraints_node_list_st *list, + struct name_constraints_node_st *node) + { + if (!list->capacity || list->size == list->capacity) { + size_t new_capacity = list->capacity; +@@ -281,15 +281,15 @@ static void name_constraints_node_free(struct name_constraints_node_st *node) + * + * Allocate a new name constraints node and set its type, name size and name data. + * + * Returns: Pointer to newly allocated node or NULL in case of memory error. + -*/ + static struct name_constraints_node_st * + name_constraints_node_new(gnutls_x509_name_constraints_t nc, unsigned type, +- unsigned char *data, unsigned int size) ++ const unsigned char *data, unsigned int size) + { + struct name_constraints_node_st *tmp; + int ret; + + tmp = gnutls_calloc(1, sizeof(struct name_constraints_node_st)); + if (tmp == NULL) + return NULL; +@@ -335,14 +335,15 @@ static int name_constraints_node_list_intersect( + 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 }; ++ static const unsigned char universal_ip[32] = { 0 }; + + /* temporary array to see, if we need to add universal excluded constraints + * (see phase 3 for details) + * indexed directly by (gnutls_x509_subject_alt_name_t enum - 1) */ + unsigned char types_with_empty_intersection[GNUTLS_SAN_MAX]; + memset(types_with_empty_intersection, 0, + sizeof(types_with_empty_intersection)); +@@ -470,28 +471,28 @@ static int name_constraints_node_list_intersect( + _gnutls_hard_log( + "Adding universal excluded name constraint for type %d.\n", + type); + switch (type) { + case GNUTLS_SAN_IPADDRESS: + // add universal restricted range for IPv4 + tmp = name_constraints_node_new( +- nc, GNUTLS_SAN_IPADDRESS, NULL, 8); ++ nc, GNUTLS_SAN_IPADDRESS, universal_ip, 8); + if (tmp == NULL) { + gnutls_assert(); + ret = GNUTLS_E_MEMORY_ERROR; + goto cleanup; + } + ret = name_constraints_node_list_add(excluded, tmp); + if (ret < 0) { + gnutls_assert(); + goto cleanup; + } + // add universal restricted range for IPv6 + tmp = name_constraints_node_new( +- nc, GNUTLS_SAN_IPADDRESS, NULL, 32); ++ nc, GNUTLS_SAN_IPADDRESS, universal_ip, 32); + if (tmp == NULL) { + gnutls_assert(); + ret = GNUTLS_E_MEMORY_ERROR; + goto cleanup; + } + ret = name_constraints_node_list_add(excluded, tmp); + if (ret < 0) { +-- +2.51.0 + diff -Nru gnutls28-3.8.9/debian/patches/50_0002-tests-name-constraints-ip-stop-swallowing-errors.patch gnutls28-3.8.9/debian/patches/50_0002-tests-name-constraints-ip-stop-swallowing-errors.patch --- gnutls28-3.8.9/debian/patches/50_0002-tests-name-constraints-ip-stop-swallowing-errors.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnutls28-3.8.9/debian/patches/50_0002-tests-name-constraints-ip-stop-swallowing-errors.patch 2026-02-14 06:53:42.000000000 +0000 @@ -0,0 +1,30 @@ +From 85d6348a30c74d4ee3710e0f4652f634eaad6914 Mon Sep 17 00:00:00 2001 +From: Alexander Sosedkin +Date: Mon, 26 Jan 2026 19:10:58 +0100 +Subject: [PATCH 2/9] tests/name-constraints-ip: stop swallowing errors... + +... now when it started to pass + +Signed-off-by: Alexander Sosedkin +--- + tests/name-constraints-ip.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tests/name-constraints-ip.c b/tests/name-constraints-ip.c +index 7a196088dc..a0cf172b7f 100644 +--- a/tests/name-constraints-ip.c ++++ b/tests/name-constraints-ip.c +@@ -768,9 +768,9 @@ int main(int argc, char **argv) + check_empty_ipv4_intersection_ipv6_remains, setup, + teardown), + cmocka_unit_test_setup_teardown( + check_empty_ipv4v6_intersections, setup, teardown), + cmocka_unit_test_setup_teardown( + check_ipv4v6_single_constraint_each, setup, teardown) + }; +- cmocka_run_group_tests(tests, NULL, NULL); ++ return cmocka_run_group_tests(tests, NULL, NULL); + } +-- +2.51.0 + diff -Nru gnutls28-3.8.9/debian/patches/50_0003-x509-name_constraints-reject-some-malformed-domain-n.patch gnutls28-3.8.9/debian/patches/50_0003-x509-name_constraints-reject-some-malformed-domain-n.patch --- gnutls28-3.8.9/debian/patches/50_0003-x509-name_constraints-reject-some-malformed-domain-n.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnutls28-3.8.9/debian/patches/50_0003-x509-name_constraints-reject-some-malformed-domain-n.patch 2026-02-14 06:53:42.000000000 +0000 @@ -0,0 +1,49 @@ +From c28475413f82e1f34295d5c039f0c0a4ca2ee526 Mon Sep 17 00:00:00 2001 +From: Alexander Sosedkin +Date: Mon, 26 Jan 2026 20:14:33 +0100 +Subject: [PATCH 3/9] x509/name_constraints: reject some malformed domain names + +Signed-off-by: Alexander Sosedkin +--- + lib/x509/name_constraints.c | 17 +++++++++++++++++ + 1 file changed, 17 insertions(+) + +diff --git a/lib/x509/name_constraints.c b/lib/x509/name_constraints.c +index d07482e3c9..9783d92851 100644 +--- a/lib/x509/name_constraints.c ++++ b/lib/x509/name_constraints.c +@@ -155,14 +155,31 @@ static int validate_name_constraints_node(gnutls_x509_subject_alt_name_t type, + return gnutls_assert_val(GNUTLS_E_ILLEGAL_PARAMETER); + int prefix = _gnutls_mask_to_prefix(name->data + name->size / 2, + name->size / 2); + if (prefix < 0) + return gnutls_assert_val(GNUTLS_E_MALFORMED_CIDR); + } + ++ /* Validate DNS names and email addresses for malformed input */ ++ if (type == GNUTLS_SAN_DNSNAME || type == GNUTLS_SAN_RFC822NAME) { ++ unsigned int i; ++ if (name->size == 0) ++ return GNUTLS_E_SUCCESS; ++ ++ /* reject names with consecutive dots... */ ++ for (i = 0; i + 1 < name->size; i++) { ++ if (name->data[i] == '.' && name->data[i + 1] == '.') ++ return gnutls_assert_val( ++ GNUTLS_E_ILLEGAL_PARAMETER); ++ } ++ /* ... or names consisting exclusively of dots */ ++ if (name->size == 1 && name->data[0] == '.') ++ return gnutls_assert_val(GNUTLS_E_ILLEGAL_PARAMETER); ++ } ++ + return GNUTLS_E_SUCCESS; + } + + static int extract_name_constraints(gnutls_x509_name_constraints_t nc, + asn1_node c2, const char *vstr, + struct name_constraints_node_list_st *nodes) + { +-- +2.51.0 + diff -Nru gnutls28-3.8.9/debian/patches/50_0004-x509-name_constraints-name_constraints_node_add_-new.patch gnutls28-3.8.9/debian/patches/50_0004-x509-name_constraints-name_constraints_node_add_-new.patch --- gnutls28-3.8.9/debian/patches/50_0004-x509-name_constraints-name_constraints_node_add_-new.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnutls28-3.8.9/debian/patches/50_0004-x509-name_constraints-name_constraints_node_add_-new.patch 2026-02-14 06:53:42.000000000 +0000 @@ -0,0 +1,260 @@ +From 6db7da7fcfe230f445b1edbb56e2a8346120c891 Mon Sep 17 00:00:00 2001 +From: Alexander Sosedkin +Date: Thu, 5 Feb 2026 13:22:10 +0100 +Subject: [PATCH 4/9] x509/name_constraints: + name_constraints_node_add_{new,copy} + +Signed-off-by: Alexander Sosedkin +--- + lib/x509/name_constraints.c | 112 ++++++++++++++++-------------------- + 1 file changed, 51 insertions(+), 61 deletions(-) + +diff --git a/lib/x509/name_constraints.c b/lib/x509/name_constraints.c +index 9783d92851..81035eef8f 100644 +--- a/lib/x509/name_constraints.c ++++ b/lib/x509/name_constraints.c +@@ -82,14 +82,46 @@ name_constraints_node_list_add(struct name_constraints_node_list_st *list, + list->capacity = new_capacity; + list->data = new_data; + } + list->data[list->size++] = node; + return 0; + } + ++static int ++name_constraints_node_add_new(gnutls_x509_name_constraints_t nc, ++ struct name_constraints_node_list_st *list, ++ unsigned type, const unsigned char *data, ++ unsigned int size) ++{ ++ struct name_constraints_node_st *node; ++ int ret; ++ node = name_constraints_node_new(nc, type, data, size); ++ if (node == NULL) { ++ gnutls_assert(); ++ return GNUTLS_E_MEMORY_ERROR; ++ } ++ ret = name_constraints_node_list_add(list, node); ++ if (ret < 0) { ++ gnutls_assert(); ++ return ret; ++ } ++ return GNUTLS_E_SUCCESS; ++} ++ ++static int ++name_constraints_node_add_copy(gnutls_x509_name_constraints_t nc, ++ struct name_constraints_node_list_st *dest, ++ const struct name_constraints_node_st *src) ++{ ++ if (!src) ++ return gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR); ++ return name_constraints_node_add_new(nc, dest, src->type, ++ 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); + +@@ -184,15 +216,14 @@ static int extract_name_constraints(gnutls_x509_name_constraints_t nc, + struct name_constraints_node_list_st *nodes) + { + int ret; + char tmpstr[128]; + unsigned indx; + gnutls_datum_t tmp = { NULL, 0 }; + unsigned int type; +- struct name_constraints_node_st *node; + + for (indx = 1;; indx++) { + snprintf(tmpstr, sizeof(tmpstr), "%s.?%u.base", vstr, indx); + + ret = _gnutls_parse_general_name2(c2, tmpstr, -1, &tmp, &type, + 0); + +@@ -227,23 +258,17 @@ static int extract_name_constraints(gnutls_x509_name_constraints_t nc, + + ret = validate_name_constraints_node(type, &tmp); + if (ret < 0) { + gnutls_assert(); + goto cleanup; + } + +- node = name_constraints_node_new(nc, type, tmp.data, tmp.size); ++ ret = name_constraints_node_add_new(nc, nodes, type, tmp.data, ++ tmp.size); + _gnutls_free_datum(&tmp); +- if (node == NULL) { +- gnutls_assert(); +- ret = GNUTLS_E_MEMORY_ERROR; +- goto cleanup; +- } +- +- ret = name_constraints_node_list_add(nodes, node); + if (ret < 0) { + gnutls_assert(); + goto cleanup; + } + } + + assert(ret < 0); +@@ -458,22 +483,15 @@ static int name_constraints_node_list_intersect( + } + } + } + // 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) { +- tmp = name_constraints_node_new( +- nc, t2->type, t2->name.data, t2->name.size); +- if (tmp == NULL) { +- gnutls_assert(); +- ret = GNUTLS_E_MEMORY_ERROR; +- goto cleanup; +- } +- ret = name_constraints_node_list_add(permitted, tmp); ++ ret = name_constraints_node_add_copy(nc, permitted, t2); + if (ret < 0) { + gnutls_assert(); + goto cleanup; + } + } + } + +@@ -487,49 +505,34 @@ static int name_constraints_node_list_intersect( + continue; + _gnutls_hard_log( + "Adding universal excluded name constraint for type %d.\n", + type); + switch (type) { + case GNUTLS_SAN_IPADDRESS: + // add universal restricted range for IPv4 +- tmp = name_constraints_node_new( +- nc, GNUTLS_SAN_IPADDRESS, universal_ip, 8); +- if (tmp == NULL) { +- gnutls_assert(); +- ret = GNUTLS_E_MEMORY_ERROR; +- goto cleanup; +- } +- ret = name_constraints_node_list_add(excluded, tmp); ++ ret = name_constraints_node_add_new( ++ nc, excluded, GNUTLS_SAN_IPADDRESS, ++ universal_ip, 8); + if (ret < 0) { + gnutls_assert(); + goto cleanup; + } + // add universal restricted range for IPv6 +- tmp = name_constraints_node_new( +- nc, GNUTLS_SAN_IPADDRESS, universal_ip, 32); +- if (tmp == NULL) { +- gnutls_assert(); +- ret = GNUTLS_E_MEMORY_ERROR; +- goto cleanup; +- } +- ret = name_constraints_node_list_add(excluded, tmp); ++ ret = name_constraints_node_add_new( ++ nc, excluded, GNUTLS_SAN_IPADDRESS, ++ universal_ip, 32); + if (ret < 0) { + gnutls_assert(); + goto cleanup; + } + break; + case GNUTLS_SAN_DNSNAME: + case GNUTLS_SAN_RFC822NAME: +- tmp = name_constraints_node_new(nc, type, NULL, 0); +- if (tmp == NULL) { +- gnutls_assert(); +- ret = GNUTLS_E_MEMORY_ERROR; +- goto cleanup; +- } +- ret = name_constraints_node_list_add(excluded, tmp); ++ ret = name_constraints_node_add_new(nc, excluded, type, ++ NULL, 0); + if (ret < 0) { + gnutls_assert(); + goto cleanup; + } + break; + default: // do nothing, at least one node was already moved in phase 1 + break; +@@ -543,28 +546,21 @@ cleanup: + } + + static int name_constraints_node_list_concat( + gnutls_x509_name_constraints_t nc, + struct name_constraints_node_list_st *nodes, + const struct name_constraints_node_list_st *nodes2) + { +- for (size_t i = 0; i < nodes2->size; i++) { +- const struct name_constraints_node_st *node = nodes2->data[i]; +- struct name_constraints_node_st *tmp; +- int ret; ++ int ret; + +- tmp = name_constraints_node_new(nc, node->type, node->name.data, +- node->name.size); +- if (tmp == NULL) { +- return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR); +- } +- ret = name_constraints_node_list_add(nodes, tmp); ++ for (size_t i = 0; i < nodes2->size; i++) { ++ ret = name_constraints_node_add_copy(nc, nodes, ++ nodes2->data[i]); + if (ret < 0) { +- name_constraints_node_free(tmp); +- return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR); ++ return gnutls_assert_val(ret); + } + } + + return 0; + } + + /** +@@ -686,33 +682,27 @@ int gnutls_x509_name_constraints_init(gnutls_x509_name_constraints_t *nc) + return 0; + } + + static int name_constraints_add(gnutls_x509_name_constraints_t nc, + gnutls_x509_subject_alt_name_t type, + const gnutls_datum_t *name, unsigned permitted) + { +- struct name_constraints_node_st *tmp; + struct name_constraints_node_list_st *nodes; + int ret; + + ret = validate_name_constraints_node(type, name); + if (ret < 0) + return gnutls_assert_val(ret); + + nodes = permitted ? &nc->permitted : &nc->excluded; + +- tmp = name_constraints_node_new(nc, type, name->data, name->size); +- if (tmp == NULL) +- return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR); +- +- ret = name_constraints_node_list_add(nodes, tmp); +- if (ret < 0) { +- name_constraints_node_free(tmp); ++ ret = name_constraints_node_add_new(nc, nodes, type, name->data, ++ name->size); ++ if (ret < 0) + return gnutls_assert_val(ret); +- } + + return 0; + } + + /*- + * _gnutls_x509_name_constraints_merge: + * @nc: The nameconstraints +-- +2.51.0 + diff -Nru gnutls28-3.8.9/debian/patches/50_0005-x509-name_constraints-introduce-a-rich-comparator.patch gnutls28-3.8.9/debian/patches/50_0005-x509-name_constraints-introduce-a-rich-comparator.patch --- gnutls28-3.8.9/debian/patches/50_0005-x509-name_constraints-introduce-a-rich-comparator.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnutls28-3.8.9/debian/patches/50_0005-x509-name_constraints-introduce-a-rich-comparator.patch 2026-02-14 06:53:42.000000000 +0000 @@ -0,0 +1,551 @@ +From 094accd3ebec17ead6c391757eaa18763b72d83f Mon Sep 17 00:00:00 2001 +From: Alexander Sosedkin +Date: Mon, 26 Jan 2026 20:16:36 +0100 +Subject: [PATCH 5/9] x509/name_constraints: introduce a rich comparator + +These are preparatory changes before implementing N * log N intersection +over sorted lists of constraints. + +Signed-off-by: Alexander Sosedkin +--- + lib/x509/name_constraints.c | 411 ++++++++++++++++++++++++++++-------- + 1 file changed, 320 insertions(+), 91 deletions(-) + +diff --git a/lib/x509/name_constraints.c b/lib/x509/name_constraints.c +index 81035eef8f..b5d732d0c5 100644 +--- a/lib/x509/name_constraints.c ++++ b/lib/x509/name_constraints.c +@@ -35,14 +35,17 @@ + #include "x509_int.h" + #include "x509_ext_int.h" + #include + + #include "ip.h" + #include "ip-in-cidr.h" + #include "intprops.h" ++#include "minmax.h" ++ ++#include + + #define MAX_NC_CHECKS (1 << 20) + + struct name_constraints_node_st { + unsigned type; + gnutls_datum_t name; + }; +@@ -59,14 +62,290 @@ struct gnutls_name_constraints_st { + struct name_constraints_node_list_st excluded; /* borrows elements */ + }; + + static struct name_constraints_node_st * + name_constraints_node_new(gnutls_x509_name_constraints_t nc, unsigned type, + const unsigned char *data, unsigned int size); + ++/* An enum for "rich" comparisons that not only let us sort name constraints, ++ * children-before-parent, but also subsume them during intersection. */ ++enum name_constraint_relation { ++ NC_SORTS_BEFORE = -2, /* unrelated constraints */ ++ NC_INCLUDED_BY = -1, /* nc1 is included by nc2 / children sort first */ ++ NC_EQUAL = 0, /* exact match */ ++ NC_INCLUDES = 1, /* nc1 includes nc2 / parents sort last */ ++ NC_SORTS_AFTER = 2 /* unrelated constraints */ ++}; ++ ++/* A helper to compare just a pair of strings with this rich comparison */ ++static enum name_constraint_relation ++compare_strings(const void *n1, size_t n1_len, const void *n2, size_t n2_len) ++{ ++ int r = memcmp(n1, n2, MIN(n1_len, n2_len)); ++ if (r < 0) ++ return NC_SORTS_BEFORE; ++ if (r > 0) ++ return NC_SORTS_AFTER; ++ if (n1_len < n2_len) ++ return NC_SORTS_BEFORE; ++ if (n1_len > n2_len) ++ return NC_SORTS_AFTER; ++ return NC_EQUAL; ++} ++ ++/* Rich-compare DNS names. Example order/relationships: ++ * z.x.a INCLUDED_BY x.a BEFORE y.a INCLUDED_BY a BEFORE x.b BEFORE y.b */ ++static enum name_constraint_relation compare_dns_names(const gnutls_datum_t *n1, ++ const gnutls_datum_t *n2) ++{ ++ enum name_constraint_relation rel; ++ unsigned int i, j, i_end, j_end; ++ ++ /* start from the end of each name */ ++ i = i_end = n1->size; ++ j = j_end = n2->size; ++ ++ /* skip the trailing dots for the comparison */ ++ while (i && n1->data[i - 1] == '.') ++ i_end = i = i - 1; ++ while (j && n2->data[j - 1] == '.') ++ j_end = j = j - 1; ++ ++ while (1) { ++ // rewind back to beginning or an after-dot position ++ while (i && n1->data[i - 1] != '.') ++ i--; ++ while (j && n2->data[j - 1] != '.') ++ j--; ++ ++ rel = compare_strings(&n1->data[i], i_end - i, &n2->data[j], ++ j_end - j); ++ if (rel == NC_SORTS_BEFORE) /* x.a BEFORE y.a */ ++ return NC_SORTS_BEFORE; ++ if (rel == NC_SORTS_AFTER) /* y.a AFTER x.a */ ++ return NC_SORTS_AFTER; ++ if (!i && j) /* x.a INCLUDES z.x.a */ ++ return NC_INCLUDES; ++ if (i && !j) /* z.x.a INCLUDED_BY x.a */ ++ return NC_INCLUDED_BY; ++ ++ if (!i && !j) /* r == 0, we ran out of components to compare */ ++ return NC_EQUAL; ++ /* r == 0, i && j: step back past a dot and keep comparing */ ++ i_end = i = i - 1; ++ j_end = j = j - 1; ++ ++ /* support for non-standard ".gr INCLUDES example.gr" [1] */ ++ if (!i && j) /* .a INCLUDES x.a */ ++ return NC_INCLUDES; ++ if (i && !j) /* x.a INCLUDED_BY .a */ ++ return NC_INCLUDED_BY; ++ } ++} ++/* [1] https://mailarchive.ietf.org/arch/msg/saag/Bw6PtreW0G7aEG7SikfzKHES4VA */ ++ ++/* Rich-compare email name constraints. Example order/relationships: ++ * z@x.a INCLUDED_BY x.a BEFORE y.a INCLUDED_BY a BEFORE x@b BEFORE y@b */ ++static enum name_constraint_relation compare_emails(const gnutls_datum_t *n1, ++ const gnutls_datum_t *n2) ++{ ++ enum name_constraint_relation domains_rel; ++ unsigned int i, j, i_end, j_end; ++ gnutls_datum_t d1, d2; /* borrow from n1 and n2 */ ++ ++ /* start from the end of each name */ ++ i = i_end = n1->size; ++ j = j_end = n2->size; ++ ++ /* rewind to @s to look for domains */ ++ while (i && n1->data[i - 1] != '@') ++ i--; ++ d1.size = i_end - i; ++ d1.data = &n1->data[i]; ++ while (j && n2->data[j - 1] != '@') ++ j--; ++ d2.size = j_end - j; ++ d2.data = &n2->data[j]; ++ ++ domains_rel = compare_dns_names(&d1, &d2); ++ ++ /* email constraint semantics differ from DNS ++ * DNS: x.a INCLUDED_BY a ++ * Email: x.a INCLUDED_BY .a BEFORE a */ ++ if (domains_rel == NC_INCLUDED_BY || domains_rel == NC_INCLUDES) { ++ bool d1_has_dot = (d1.size > 0 && d1.data[0] == '.'); ++ bool d2_has_dot = (d2.size > 0 && d2.data[0] == '.'); ++ /* a constraint without a dot is exact, excluding subdomains */ ++ if (!d2_has_dot && domains_rel == NC_INCLUDED_BY) ++ domains_rel = NC_SORTS_BEFORE; /* x.a BEFORE a */ ++ if (!d1_has_dot && domains_rel == NC_INCLUDES) ++ domains_rel = NC_SORTS_AFTER; /* a AFTER x.a */ ++ } ++ ++ if (!i && !j) { /* both are domains-only */ ++ return domains_rel; ++ } else if (i && !j) { /* n1 is email, n2 is domain */ ++ switch (domains_rel) { ++ case NC_SORTS_AFTER: ++ return NC_SORTS_AFTER; ++ case NC_SORTS_BEFORE: ++ return NC_SORTS_BEFORE; ++ case NC_INCLUDES: /* n2 is more specific, a@x.a AFTER z.x.a */ ++ return NC_SORTS_AFTER; ++ case NC_EQUAL: /* subdomains match, z@x.a INCLUDED_BY x.a */ ++ case NC_INCLUDED_BY: /* n1 is more specific */ ++ return NC_INCLUDED_BY; ++ } ++ } else if (!i && j) { /* n1 is domain, n2 is email */ ++ switch (domains_rel) { ++ case NC_SORTS_AFTER: ++ return NC_SORTS_AFTER; ++ case NC_SORTS_BEFORE: ++ return NC_SORTS_BEFORE; ++ case NC_INCLUDES: /* n2 is more specific, a AFTER z@x.a */ ++ return NC_SORTS_AFTER; ++ case NC_EQUAL: /* subdomains match, x.a INCLUDES z@x.a */ ++ return NC_INCLUDES; ++ case NC_INCLUDED_BY: /* n1 is more specific, x.a BEFORE z@a */ ++ return NC_SORTS_BEFORE; ++ } ++ } else if (i && j) { /* both are emails */ ++ switch (domains_rel) { ++ case NC_SORTS_AFTER: ++ return NC_SORTS_AFTER; ++ case NC_SORTS_BEFORE: ++ return NC_SORTS_BEFORE; ++ case NC_INCLUDES: // n2 is more specific ++ return NC_SORTS_AFTER; ++ case NC_INCLUDED_BY: // n1 is more specific ++ return NC_SORTS_BEFORE; ++ case NC_EQUAL: // only case when we need to look before the @ ++ break; // see below for readability ++ } ++ } ++ ++ /* i && j, both are emails, domain names match, compare up to @ */ ++ return compare_strings(n1->data, i - 1, n2->data, j - 1); ++} ++ ++/* Rich-compare IP address constraints. Example order/relationships: ++ * 10.0.0.0/24 INCLUDED_BY 10.0.0.0/16 BEFORE 1::1/128 INCLUDED_BY 1::1/127 */ ++static enum name_constraint_relation compare_ip_ncs(const gnutls_datum_t *n1, ++ const gnutls_datum_t *n2) ++{ ++ unsigned int len, i; ++ int r; ++ const unsigned char *ip1, *ip2, *mask1, *mask2; ++ unsigned char masked11[16], masked22[16], masked12[16], masked21[16]; ++ ++ if (n1->size < n2->size) ++ return NC_SORTS_BEFORE; ++ if (n1->size > n2->size) ++ return NC_SORTS_AFTER; ++ len = n1->size / 2; /* 4 for IPv4, 16 for IPv6 */ ++ ++ /* data is a concatenation of prefix and mask */ ++ ip1 = n1->data; ++ ip2 = n2->data; ++ mask1 = n1->data + len; ++ mask2 = n2->data + len; ++ for (i = 0; i < len; i++) { ++ masked11[i] = ip1[i] & mask1[i]; ++ masked22[i] = ip2[i] & mask2[i]; ++ masked12[i] = ip1[i] & mask2[i]; ++ masked21[i] = ip2[i] & mask1[i]; ++ } ++ ++ r = memcmp(mask1, mask2, len); ++ if (r < 0 && !memcmp(masked11, masked21, len)) /* prefix1 < prefix2 */ ++ return NC_INCLUDES; /* ip1 & mask1 == ip2 & mask1 */ ++ if (r > 0 && !memcmp(masked12, masked22, len)) /* prefix1 > prefix2 */ ++ return NC_INCLUDED_BY; /* ip1 & mask2 == ip2 & mask2 */ ++ ++ r = memcmp(masked11, masked22, len); ++ if (r < 0) ++ return NC_SORTS_BEFORE; ++ else if (r > 0) ++ return NC_SORTS_AFTER; ++ return NC_EQUAL; ++} ++ ++static inline bool is_supported_type(unsigned type) ++{ ++ return type == GNUTLS_SAN_DNSNAME || type == GNUTLS_SAN_RFC822NAME || ++ type == GNUTLS_SAN_IPADDRESS; ++} ++ ++/* Universal comparison for name constraint nodes. ++ * Unsupported types sort before supported types to allow early handling. ++ * NULL represents end-of-list and sorts after everything else. */ ++static enum name_constraint_relation ++compare_name_constraint_nodes(const struct name_constraints_node_st *n1, ++ const struct name_constraints_node_st *n2) ++{ ++ bool n1_supported, n2_supported; ++ ++ if (!n1 && !n2) ++ return NC_EQUAL; ++ if (!n1) ++ return NC_SORTS_AFTER; ++ if (!n2) ++ return NC_SORTS_BEFORE; ++ ++ n1_supported = is_supported_type(n1->type); ++ n2_supported = is_supported_type(n2->type); ++ ++ /* unsupported types bubble up (sort first). intersect relies on this */ ++ if (!n1_supported && n2_supported) ++ return NC_SORTS_BEFORE; ++ if (n1_supported && !n2_supported) ++ return NC_SORTS_AFTER; ++ ++ /* next, sort by type */ ++ if (n1->type < n2->type) ++ return NC_SORTS_BEFORE; ++ if (n1->type > n2->type) ++ return NC_SORTS_AFTER; ++ ++ /* now look deeper */ ++ switch (n1->type) { ++ case GNUTLS_SAN_DNSNAME: ++ return compare_dns_names(&n1->name, &n2->name); ++ case GNUTLS_SAN_RFC822NAME: ++ return compare_emails(&n1->name, &n2->name); ++ case GNUTLS_SAN_IPADDRESS: ++ return compare_ip_ncs(&n1->name, &n2->name); ++ default: ++ /* unsupported types: stable lexicographic order */ ++ return compare_strings(n1->name.data, n1->name.size, ++ n2->name.data, n2->name.size); ++ } ++} ++ ++/* qsort-compatible wrapper */ ++static int compare_name_constraint_nodes_qsort(const void *a, const void *b) ++{ ++ const struct name_constraints_node_st *const *n1 = a; ++ const struct name_constraints_node_st *const *n2 = b; ++ enum name_constraint_relation rel; ++ ++ rel = compare_name_constraint_nodes(*n1, *n2); ++ switch (rel) { ++ case NC_SORTS_BEFORE: ++ case NC_INCLUDED_BY: ++ return -1; ++ case NC_SORTS_AFTER: ++ case NC_INCLUDES: ++ return 1; ++ case NC_EQUAL: ++ default: ++ return 0; ++ } ++} ++ + static int + name_constraints_node_list_add(struct name_constraints_node_list_st *list, + struct name_constraints_node_st *node) + { + if (!list->capacity || list->size == list->capacity) { + size_t new_capacity = list->capacity; + struct name_constraints_node_st **new_data; +@@ -416,17 +695,15 @@ static int name_constraints_node_list_intersect( + // we will reset this flag back to 0 then + types_with_empty_intersection[t->type - 1] = 1; + found = t2; + break; + } + } + +- if (found != NULL && (t->type == GNUTLS_SAN_DNSNAME || +- t->type == GNUTLS_SAN_RFC822NAME || +- t->type == GNUTLS_SAN_IPADDRESS)) { ++ 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 */ +@@ -823,82 +1100,32 @@ int gnutls_x509_crt_set_name_constraints(gnutls_x509_crt_t crt, + crt->use_extensions = 1; + + cleanup: + _gnutls_free_datum(&der); + return ret; + } + +-static unsigned ends_with(const gnutls_datum_t *str, +- const gnutls_datum_t *suffix) +-{ +- unsigned char *tree; +- unsigned int treelen; +- +- if (suffix->size >= str->size) +- return 0; +- +- tree = suffix->data; +- treelen = suffix->size; +- if ((treelen > 0) && (tree[0] == '.')) { +- tree++; +- treelen--; +- } +- +- if (memcmp(str->data + str->size - treelen, tree, treelen) == 0 && +- str->data[str->size - treelen - 1] == '.') +- return 1; /* match */ +- +- return 0; +-} +- +-static unsigned email_ends_with(const gnutls_datum_t *str, +- const gnutls_datum_t *suffix) +-{ +- if (suffix->size >= str->size) { +- return 0; +- } +- +- if (suffix->size > 0 && memcmp(str->data + str->size - suffix->size, +- suffix->data, suffix->size) != 0) { +- return 0; +- } +- +- if (suffix->size > 1 && suffix->data[0] == '.') { /* .domain.com */ +- return 1; /* match */ +- } else if (str->data[str->size - suffix->size - 1] == '@') { +- return 1; /* match */ +- } +- +- return 0; +-} +- + static unsigned dnsname_matches(const gnutls_datum_t *name, + const gnutls_datum_t *suffix) + { + _gnutls_hard_log("matching %.*s with DNS constraint %.*s\n", name->size, + name->data, suffix->size, suffix->data); + +- if (suffix->size == name->size && +- memcmp(suffix->data, name->data, suffix->size) == 0) +- return 1; /* match */ +- +- return ends_with(name, suffix); ++ enum name_constraint_relation rel = compare_dns_names(name, suffix); ++ return rel == NC_EQUAL || rel == NC_INCLUDED_BY; + } + + static unsigned email_matches(const gnutls_datum_t *name, + const gnutls_datum_t *suffix) + { + _gnutls_hard_log("matching %.*s with e-mail constraint %.*s\n", + name->size, name->data, suffix->size, suffix->data); + +- if (suffix->size == name->size && +- memcmp(suffix->data, name->data, suffix->size) == 0) +- return 1; /* match */ +- +- return email_ends_with(name, suffix); ++ enum name_constraint_relation rel = compare_emails(name, suffix); ++ 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, +@@ -914,50 +1141,66 @@ static int name_constraints_intersect_nodes( + 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; +- unsigned iplength = 0; +- unsigned byte; ++ enum name_constraint_relation rel; + + *_intersection = NULL; + + if (node1->type != node2->type) { + return GNUTLS_E_SUCCESS; + } + switch (node1->type) { + case GNUTLS_SAN_DNSNAME: +- if (!dnsname_matches(&node2->name, &node1->name)) ++ 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; +- to_copy = node2; ++ } + break; + case GNUTLS_SAN_RFC822NAME: +- if (!email_matches(&node2->name, &node1->name)) ++ 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; +- to_copy = node2; ++ } + break; + case GNUTLS_SAN_IPADDRESS: +- if (node1->name.size != node2->name.size) ++ 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; +- iplength = node1->name.size / 2; +- for (byte = 0; byte < iplength; byte++) { +- if (((node1->name.data[byte] ^ +- node2->name.data[byte]) // XOR of addresses +- & node1->name.data[byte + +- iplength] // AND mask from nc1 +- & node2->name.data[byte + +- iplength]) // AND mask from nc2 +- != 0) { +- // CIDRS do not intersect +- return GNUTLS_E_SUCCESS; +- } + } +- to_copy = node2; + 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 +@@ -966,28 +1209,14 @@ static int name_constraints_intersect_nodes( + 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); +- +- if (intersection->type == GNUTLS_SAN_IPADDRESS) { +- // make sure both IP addresses are correctly masked +- _gnutls_mask_ip(intersection->name.data, +- intersection->name.data + iplength, +- iplength); +- _gnutls_mask_ip(node1->name.data, +- node1->name.data + iplength, iplength); +- // update intersection, if necessary (we already know one is subset of other) +- for (byte = 0; byte < 2 * iplength; byte++) { +- intersection->name.data[byte] |= +- node1->name.data[byte]; +- } +- } + } + + return GNUTLS_E_SUCCESS; + } + + /* + * Returns: true if the certification is acceptable, and false otherwise. +-- +2.51.0 + diff -Nru gnutls28-3.8.9/debian/patches/50_0006-x509-name_constraints-add-sorted_view-in-preparation.patch gnutls28-3.8.9/debian/patches/50_0006-x509-name_constraints-add-sorted_view-in-preparation.patch --- gnutls28-3.8.9/debian/patches/50_0006-x509-name_constraints-add-sorted_view-in-preparation.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnutls28-3.8.9/debian/patches/50_0006-x509-name_constraints-add-sorted_view-in-preparation.patch 2026-02-14 06:53:42.000000000 +0000 @@ -0,0 +1,160 @@ +From bc62fbb946085527b4b1c02f337dd10c68c54690 Mon Sep 17 00:00:00 2001 +From: Alexander Sosedkin +Date: Wed, 4 Feb 2026 09:09:46 +0100 +Subject: [PATCH 6/9] x509/name_constraints: add sorted_view in preparation... + +... for actually using it later for performance gains. + +Signed-off-by: Alexander Sosedkin +--- + lib/x509/name_constraints.c | 62 ++++++++++++++++++++++++++++++------- + 1 file changed, 51 insertions(+), 11 deletions(-) + +diff --git a/lib/x509/name_constraints.c b/lib/x509/name_constraints.c +index b5d732d0c5..41f30d13b9 100644 +--- a/lib/x509/name_constraints.c ++++ b/lib/x509/name_constraints.c +@@ -50,14 +50,17 @@ struct name_constraints_node_st { + gnutls_datum_t name; + }; + + struct name_constraints_node_list_st { + struct name_constraints_node_st **data; + size_t size; + size_t capacity; ++ /* sorted-on-demand view, valid only when dirty == false */ ++ bool dirty; ++ struct name_constraints_node_st **sorted_view; + }; + + struct gnutls_name_constraints_st { + struct name_constraints_node_list_st nodes; /* owns elements */ + struct name_constraints_node_list_st permitted; /* borrows elements */ + struct name_constraints_node_list_st excluded; /* borrows elements */ + }; +@@ -338,14 +341,45 @@ static int compare_name_constraint_nodes_qsort(const void *a, const void *b) + return 1; + case NC_EQUAL: + default: + return 0; + } + } + ++/* Bring the sorted view up to date with the list data; clear the dirty flag. */ ++static int ensure_sorted(struct name_constraints_node_list_st *list) ++{ ++ struct name_constraints_node_st **new_data; ++ ++ if (!list->dirty) ++ return GNUTLS_E_SUCCESS; ++ if (!list->size) { ++ list->dirty = false; ++ return GNUTLS_E_SUCCESS; ++ } ++ ++ /* reallocate sorted view to match current size */ ++ new_data = ++ _gnutls_reallocarray(list->sorted_view, list->size, ++ sizeof(struct name_constraints_node_st *)); ++ if (!new_data) ++ return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR); ++ list->sorted_view = new_data; ++ ++ /* copy pointers and sort in-place */ ++ memcpy(list->sorted_view, list->data, ++ list->size * sizeof(struct name_constraints_node_st *)); ++ qsort(list->sorted_view, list->size, ++ sizeof(struct name_constraints_node_st *), ++ compare_name_constraint_nodes_qsort); ++ ++ list->dirty = false; ++ return GNUTLS_E_SUCCESS; ++} ++ + static int + name_constraints_node_list_add(struct name_constraints_node_list_st *list, + struct name_constraints_node_st *node) + { + if (!list->capacity || list->size == list->capacity) { + size_t new_capacity = list->capacity; + struct name_constraints_node_st **new_data; +@@ -357,18 +391,31 @@ name_constraints_node_list_add(struct name_constraints_node_list_st *list, + list->data, new_capacity, + sizeof(struct name_constraints_node_st *)); + if (!new_data) + return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR); + list->capacity = new_capacity; + list->data = new_data; + } ++ list->dirty = true; + list->data[list->size++] = node; + return 0; + } + ++static void ++name_constraints_node_list_clear(struct name_constraints_node_list_st *list) ++{ ++ gnutls_free(list->data); ++ gnutls_free(list->sorted_view); ++ list->data = NULL; ++ list->sorted_view = NULL; ++ list->capacity = 0; ++ list->size = 0; ++ list->dirty = false; ++} ++ + static int + name_constraints_node_add_new(gnutls_x509_name_constraints_t nc, + struct name_constraints_node_list_st *list, + unsigned type, const unsigned char *data, + unsigned int size) + { + struct name_constraints_node_st *node; +@@ -707,14 +754,15 @@ static int name_constraints_node_list_intersect( + 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; + } + i++; + } + + /* Phase 2 + * iterate through all combinations from PERMITTED2 and PERMITTED +@@ -904,25 +952,17 @@ cleanup: + + void _gnutls_x509_name_constraints_clear(gnutls_x509_name_constraints_t nc) + { + for (size_t i = 0; i < nc->nodes.size; i++) { + struct name_constraints_node_st *node = nc->nodes.data[i]; + name_constraints_node_free(node); + } +- gnutls_free(nc->nodes.data); +- nc->nodes.capacity = 0; +- nc->nodes.size = 0; +- +- gnutls_free(nc->permitted.data); +- nc->permitted.capacity = 0; +- nc->permitted.size = 0; +- +- gnutls_free(nc->excluded.data); +- nc->excluded.capacity = 0; +- nc->excluded.size = 0; ++ name_constraints_node_list_clear(&nc->nodes); ++ name_constraints_node_list_clear(&nc->permitted); ++ name_constraints_node_list_clear(&nc->excluded); + } + + /** + * gnutls_x509_name_constraints_deinit: + * @nc: The nameconstraints + * + * This function will deinitialize a name constraints type. +-- +2.51.0 + diff -Nru gnutls28-3.8.9/debian/patches/50_0007-x509-name_constraints-implement-name_constraints_nod.patch gnutls28-3.8.9/debian/patches/50_0007-x509-name_constraints-implement-name_constraints_nod.patch --- gnutls28-3.8.9/debian/patches/50_0007-x509-name_constraints-implement-name_constraints_nod.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnutls28-3.8.9/debian/patches/50_0007-x509-name_constraints-implement-name_constraints_nod.patch 2026-02-14 06:53:42.000000000 +0000 @@ -0,0 +1,183 @@ +From 80db5e90fa18d3e34bb91dd027bdf76d31e93dcd Mon Sep 17 00:00:00 2001 +From: Alexander Sosedkin +Date: Wed, 4 Feb 2026 13:30:08 +0100 +Subject: [PATCH 7/9] x509/name_constraints: implement + name_constraints_node_list_union + +Signed-off-by: Alexander Sosedkin +--- + lib/x509/name_constraints.c | 98 ++++++++++++++++++++++++++++++++----- + 1 file changed, 86 insertions(+), 12 deletions(-) + +diff --git a/lib/x509/name_constraints.c b/lib/x509/name_constraints.c +index 41f30d13b9..de20dd8ef4 100644 +--- a/lib/x509/name_constraints.c ++++ b/lib/x509/name_constraints.c +@@ -37,14 +37,15 @@ + #include + + #include "ip.h" + #include "ip-in-cidr.h" + #include "intprops.h" + #include "minmax.h" + ++#include + #include + + #define MAX_NC_CHECKS (1 << 20) + + struct name_constraints_node_st { + unsigned type; + gnutls_datum_t name; +@@ -866,30 +867,103 @@ static int name_constraints_node_list_intersect( + ret = GNUTLS_E_SUCCESS; + + cleanup: + gnutls_free(removed.data); + return ret; + } + +-static int name_constraints_node_list_concat( +- gnutls_x509_name_constraints_t nc, +- struct name_constraints_node_list_st *nodes, +- const struct name_constraints_node_list_st *nodes2) ++static int ++name_constraints_node_list_union(gnutls_x509_name_constraints_t nc, ++ struct name_constraints_node_list_st *nodes, ++ struct name_constraints_node_list_st *nodes2) + { + int ret; ++ size_t i = 0, j = 0; ++ struct name_constraints_node_st *nc1; ++ const struct name_constraints_node_st *nc2; ++ enum name_constraint_relation rel; ++ struct name_constraints_node_list_st result = { 0 }; ++ ++ if (nodes2->size == 0) /* nothing to do */ ++ return GNUTLS_E_SUCCESS; ++ ++ ret = ensure_sorted(nodes); ++ if (ret < 0) { ++ gnutls_assert(); ++ goto cleanup; ++ } ++ ret = ensure_sorted(nodes2); ++ if (ret < 0) { ++ gnutls_assert(); ++ goto cleanup; ++ } ++ ++ /* traverse both lists in a single pass and merge them w/o duplicates */ ++ while (i < nodes->size || j < nodes2->size) { ++ nc1 = (i < nodes->size) ? nodes->sorted_view[i] : NULL; ++ nc2 = (j < nodes2->size) ? nodes2->sorted_view[j] : NULL; + +- for (size_t i = 0; i < nodes2->size; i++) { +- ret = name_constraints_node_add_copy(nc, nodes, +- nodes2->data[i]); ++ rel = compare_name_constraint_nodes(nc1, nc2); ++ switch (rel) { ++ case NC_SORTS_BEFORE: ++ assert(nc1 != NULL); /* comparator-guaranteed */ ++ ret = name_constraints_node_list_add(&result, nc1); ++ i++; ++ break; ++ case NC_SORTS_AFTER: ++ assert(nc2 != NULL); /* comparator-guaranteed */ ++ ret = name_constraints_node_add_copy(nc, &result, nc2); ++ j++; ++ break; ++ case NC_INCLUDES: /* nc1 is broader, shallow-copy it */ ++ assert(nc1 != NULL && nc2 != NULL); /* comparator */ ++ ret = name_constraints_node_list_add(&result, nc1); ++ i++; ++ j++; ++ break; ++ case NC_INCLUDED_BY: /* nc2 is broader, deep-copy it */ ++ assert(nc1 != NULL && nc2 != NULL); /* comparator */ ++ ret = name_constraints_node_add_copy(nc, &result, nc2); ++ i++; ++ j++; ++ break; ++ case NC_EQUAL: ++ assert(nc1 != NULL && nc2 != NULL); /* loop condition */ ++ ret = name_constraints_node_list_add(&result, nc1); ++ i++; ++ j++; ++ break; ++ } + if (ret < 0) { +- return gnutls_assert_val(ret); ++ gnutls_assert(); ++ goto cleanup; + } + } + +- return 0; ++ gnutls_free(nodes->data); ++ gnutls_free(nodes->sorted_view); ++ nodes->data = result.data; ++ nodes->sorted_view = NULL; ++ nodes->size = result.size; ++ nodes->capacity = result.capacity; ++ nodes->dirty = true; ++ /* since we know it's sorted, populate sorted_view almost for free */ ++ nodes->sorted_view = gnutls_calloc( ++ nodes->size, sizeof(struct name_constraints_node_st *)); ++ if (!nodes->sorted_view) ++ return GNUTLS_E_SUCCESS; /* we tried, no harm done */ ++ memcpy(nodes->sorted_view, nodes->data, ++ nodes->size * sizeof(struct name_constraints_node_st *)); ++ nodes->dirty = false; ++ ++ result.data = NULL; ++ return GNUTLS_E_SUCCESS; ++cleanup: ++ name_constraints_node_list_clear(&result); ++ return gnutls_assert_val(ret); + } + + /** + * gnutls_x509_crt_get_name_constraints: + * @crt: should contain a #gnutls_x509_crt_t type + * @nc: The nameconstraints intermediate type + * @flags: zero or %GNUTLS_EXT_FLAG_APPEND +@@ -1022,15 +1096,15 @@ static int name_constraints_add(gnutls_x509_name_constraints_t nc, + + /*- + * _gnutls_x509_name_constraints_merge: + * @nc: The nameconstraints + * @nc2: The name constraints to be merged with + * + * This function will merge the provided name constraints structures +- * as per RFC5280 p6.1.4. That is, the excluded constraints will be appended, ++ * as per RFC5280 p6.1.4. That is, the excluded constraints will be unioned, + * and permitted will be intersected. The intersection assumes that @nc + * is the root CA constraints. + * + * The merged constraints will be placed in @nc. + * + * Returns: On success, %GNUTLS_E_SUCCESS (0) is returned, otherwise a negative error value. + * +@@ -1044,16 +1118,16 @@ int _gnutls_x509_name_constraints_merge(gnutls_x509_name_constraints_t nc, + ret = name_constraints_node_list_intersect( + nc, &nc->permitted, &nc2->permitted, &nc->excluded); + if (ret < 0) { + gnutls_assert(); + return ret; + } + +- ret = name_constraints_node_list_concat(nc, &nc->excluded, +- &nc2->excluded); ++ ret = name_constraints_node_list_union(nc, &nc->excluded, ++ &nc2->excluded); + if (ret < 0) { + gnutls_assert(); + return ret; + } + + return 0; + } +-- +2.51.0 + diff -Nru gnutls28-3.8.9/debian/patches/50_0008-x509-name_constraints-make-types_with_empty_intersec.patch gnutls28-3.8.9/debian/patches/50_0008-x509-name_constraints-make-types_with_empty_intersec.patch --- gnutls28-3.8.9/debian/patches/50_0008-x509-name_constraints-make-types_with_empty_intersec.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnutls28-3.8.9/debian/patches/50_0008-x509-name_constraints-make-types_with_empty_intersec.patch 2026-02-14 06:53:42.000000000 +0000 @@ -0,0 +1,162 @@ +From d0ac999620c8c0aeb6939e1e92d884ca8e40b759 Mon Sep 17 00:00:00 2001 +From: Alexander Sosedkin +Date: Wed, 4 Feb 2026 18:31:37 +0100 +Subject: [PATCH 8/9] x509/name_constraints: make types_with_empty_intersection + a bitmask + +Signed-off-by: Alexander Sosedkin +--- + lib/x509/name_constraints.c | 39 +++++++++++++++++++++++++++---------- + 1 file changed, 29 insertions(+), 10 deletions(-) + +diff --git a/lib/x509/name_constraints.c b/lib/x509/name_constraints.c +index de20dd8ef4..1d78d1bc50 100644 +--- a/lib/x509/name_constraints.c ++++ b/lib/x509/name_constraints.c +@@ -271,14 +271,15 @@ static enum name_constraint_relation compare_ip_ncs(const gnutls_datum_t *n1, + else if (r > 0) + return NC_SORTS_AFTER; + return NC_EQUAL; + } + + static inline bool is_supported_type(unsigned type) + { ++ /* all of these should be under GNUTLS_SAN_MAX (intersect bitmasks) */ + return type == GNUTLS_SAN_DNSNAME || type == GNUTLS_SAN_RFC822NAME || + type == GNUTLS_SAN_IPADDRESS; + } + + /* Universal comparison for name constraint nodes. + * Unsupported types sort before supported types to allow early handling. + * NULL represents end-of-list and sorts after everything else. */ +@@ -679,14 +680,29 @@ name_constraints_node_new(gnutls_x509_name_constraints_t nc, unsigned type, + name_constraints_node_free(tmp); + return NULL; + } + + return tmp; + } + ++static int ++name_constraints_node_list_union(gnutls_x509_name_constraints_t nc, ++ struct name_constraints_node_list_st *nodes, ++ struct name_constraints_node_list_st *nodes2); ++ ++#define type_bitmask_t uint8_t /* increase if GNUTLS_SAN_MAX grows */ ++#define type_bitmask_set(mask, t) ((mask) |= (1u << (t))) ++#define type_bitmask_clr(mask, t) ((mask) &= ~(1u << (t))) ++#define type_bitmask_in(mask, t) ((mask) & (1u << (t))) ++/* C99-compatible compile-time assertions; gnutls_int.h undefines verify */ ++typedef char assert_san_max[(GNUTLS_SAN_MAX < 8) ? 1 : -1]; ++typedef char assert_dnsname[(GNUTLS_SAN_DNSNAME <= GNUTLS_SAN_MAX) ? 1 : -1]; ++typedef char assert_rfc822[(GNUTLS_SAN_RFC822NAME <= GNUTLS_SAN_MAX) ? 1 : -1]; ++typedef char assert_ipaddr[(GNUTLS_SAN_IPADDRESS <= GNUTLS_SAN_MAX) ? 1 : -1]; ++ + /*- + * @brief name_constraints_node_list_intersect: + * @nc: %gnutls_x509_name_constraints_t + * @permitted: first name constraints list (permitted) + * @permitted2: name constraints list to merge with (permitted) + * @excluded: Corresponding excluded name constraints list + * +@@ -706,20 +722,17 @@ static int name_constraints_node_list_intersect( + struct name_constraints_node_st *tmp; + int ret, type, used; + struct name_constraints_node_list_st removed = { .data = NULL, + .size = 0, + .capacity = 0 }; + static const unsigned char universal_ip[32] = { 0 }; + +- /* temporary array to see, if we need to add universal excluded constraints +- * (see phase 3 for details) +- * indexed directly by (gnutls_x509_subject_alt_name_t enum - 1) */ +- unsigned char types_with_empty_intersection[GNUTLS_SAN_MAX]; +- memset(types_with_empty_intersection, 0, +- sizeof(types_with_empty_intersection)); ++ /* 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; + + /* 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. +@@ -737,15 +750,16 @@ static int name_constraints_node_list_intersect( + 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 +- types_with_empty_intersection[t->type - 1] = 1; ++ type_bitmask_set(types_with_empty_intersection, ++ t->type); + found = t2; + break; + } + } + + if (found != NULL && is_supported_type(t->type)) { + /* move node from PERMITTED to REMOVED */ +@@ -794,16 +808,16 @@ static int name_constraints_node_list_intersect( + 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 +- types_with_empty_intersection[tmp->type - 1] = +- 0; ++ 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; + } +@@ -823,15 +837,15 @@ static int name_constraints_node_list_intersect( + + /* 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). */ + for (type = 1; type <= GNUTLS_SAN_MAX; type++) { +- if (types_with_empty_intersection[type - 1] == 0) ++ if (!type_bitmask_in(types_with_empty_intersection, type)) + continue; + _gnutls_hard_log( + "Adding universal excluded name constraint for type %d.\n", + type); + switch (type) { + case GNUTLS_SAN_IPADDRESS: + // add universal restricted range for IPv4 +@@ -867,14 +881,19 @@ static int name_constraints_node_list_intersect( + ret = GNUTLS_E_SUCCESS; + + cleanup: + gnutls_free(removed.data); + return ret; + } + ++#undef type_bitmask_t ++#undef type_bitmask_set ++#undef type_bitmask_clr ++#undef type_bitmask_in ++ + static int + name_constraints_node_list_union(gnutls_x509_name_constraints_t nc, + struct name_constraints_node_list_st *nodes, + struct name_constraints_node_list_st *nodes2) + { + int ret; + size_t i = 0, j = 0; +-- +2.51.0 + diff -Nru gnutls28-3.8.9/debian/patches/50_0009-x509-name_constraints-name_constraints_node_list_int.patch gnutls28-3.8.9/debian/patches/50_0009-x509-name_constraints-name_constraints_node_list_int.patch --- gnutls28-3.8.9/debian/patches/50_0009-x509-name_constraints-name_constraints_node_list_int.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnutls28-3.8.9/debian/patches/50_0009-x509-name_constraints-name_constraints_node_list_int.patch 2026-02-14 06:53:42.000000000 +0000 @@ -0,0 +1,453 @@ +From d6054f0016db05fb5c82177ddbd0a4e8331059a1 Mon Sep 17 00:00:00 2001 +From: Alexander Sosedkin +Date: Wed, 4 Feb 2026 20:03:49 +0100 +Subject: [PATCH 9/9] x509/name_constraints: + name_constraints_node_list_intersect over sorted + +Fixes: #1773 +Fixes: GNUTLS-SA-2026-02-09-2 +Fixes: CVE-2025-14831 + +Signed-off-by: Alexander Sosedkin +--- + NEWS | 7 + + lib/x509/name_constraints.c | 350 ++++++++++++++---------------------- + 2 files changed, 142 insertions(+), 215 deletions(-) + +diff --git a/lib/x509/name_constraints.c b/lib/x509/name_constraints.c +index 1d78d1bc50..04722bdf45 100644 +--- a/lib/x509/name_constraints.c ++++ b/lib/x509/name_constraints.c +@@ -442,21 +442,14 @@ name_constraints_node_add_copy(gnutls_x509_name_constraints_t nc, + { + if (!src) + return gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR); + return name_constraints_node_add_new(nc, dest, src->type, + 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 + * @type: type (gnutls_x509_subject_alt_name_t or 0) + * + * Test whether given name constraints structure has any constraints (permitted + * or excluded) of a given type. @nc must be allocated (not NULL) before the call. +@@ -712,140 +705,151 @@ typedef char assert_ipaddr[(GNUTLS_SAN_IPADDRESS <= GNUTLS_SAN_MAX) ? 1 : -1]; + * in @excluded. + * + * Returns: On success, %GNUTLS_E_SUCCESS (0) is returned, otherwise a negative error value. + -*/ + 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", + type); + switch (type) { + case GNUTLS_SAN_IPADDRESS: + // add universal restricted range for IPv4 +@@ -870,22 +874,32 @@ static int name_constraints_node_list_intersect( + ret = name_constraints_node_add_new(nc, excluded, type, + NULL, 0); + if (ret < 0) { + gnutls_assert(); + 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; + } + + #undef type_bitmask_t + #undef type_bitmask_set + #undef type_bitmask_clr + #undef type_bitmask_in +@@ -1253,108 +1267,14 @@ static unsigned email_matches(const gnutls_datum_t *name, + _gnutls_hard_log("matching %.*s with e-mail constraint %.*s\n", + name->size, name->data, suffix->size, suffix->data); + + enum name_constraint_relation rel = compare_emails(name, suffix); + 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. + */ + static unsigned + check_unsupported_constraint(gnutls_x509_name_constraints_t nc, + gnutls_x509_subject_alt_name_t type) + { +-- +2.51.0 + diff -Nru gnutls28-3.8.9/debian/patches/series gnutls28-3.8.9/debian/patches/series --- gnutls28-3.8.9/debian/patches/series 2025-11-23 13:13:38.000000000 +0000 +++ gnutls28-3.8.9/debian/patches/series 2026-02-14 06:53:42.000000000 +0000 @@ -11,3 +11,13 @@ 47_0006-handshake-clear-HSK_PSK_SELECTED-is-when-resetting-b.patch 48_0001-pkcs11-try-to-initialize-modules-in-thread-safe-mode.patch 48_0002-pkcs11-avoid-stack-overwrite-when-initializing-a-tok.patch +49_x509-fix-incorrect-handling-in-name-constraints-merg.patch +50_0001-x509-name_constraints-use-actual-zeroes-in-universal.patch +50_0002-tests-name-constraints-ip-stop-swallowing-errors.patch +50_0003-x509-name_constraints-reject-some-malformed-domain-n.patch +50_0004-x509-name_constraints-name_constraints_node_add_-new.patch +50_0005-x509-name_constraints-introduce-a-rich-comparator.patch +50_0006-x509-name_constraints-add-sorted_view-in-preparation.patch +50_0007-x509-name_constraints-implement-name_constraints_nod.patch +50_0008-x509-name_constraints-make-types_with_empty_intersec.patch +50_0009-x509-name_constraints-name_constraints_node_list_int.patch