From 3dc729002a368c8a73f2f59cc15d0bbbfcd0f10c Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Fri, 12 Jun 2026 08:07:52 +0200 Subject: [PATCH 1/6] Extract GetUriHost from wolfssl_local_MatchUriNameConstraint Pull the URI host extraction (scheme skip, userinfo skip, IP-literal brackets, port/path delimiters) into a GetUriHost helper so it can be reused by other URI name-constraint checks. No functional change. --- wolfcrypt/src/asn.c | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index e84d252931..d78d495563 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -18121,19 +18121,18 @@ int wolfssl_local_MatchBaseName(int type, const char* name, int nameSz, return 1; } -int wolfssl_local_MatchUriNameConstraint(const char* uri, int uriSz, - const char* base, int baseSz) +static int GetUriHost(const char* uri, int uriSz, const char** host, + int* hostSz) { const char* hostStart; const char* hostEnd; const char* p; const char* uriEnd; - int hostSz; /* Need at least 3 bytes for the "://" scheme separator; rejecting short * inputs early also keeps the loop bound (uriEnd - 2) from forming a * pointer before `uri`. */ - if (uri == NULL || uriSz < 3 || base == NULL || baseSz <= 0) { + if (uri == NULL || uriSz < 3 || host == NULL || hostSz == NULL) { return 0; } @@ -18174,7 +18173,7 @@ int wolfssl_local_MatchUriNameConstraint(const char* uri, int uriSz, if (hostEnd >= uriEnd) { return 0; } - hostSz = (int)(hostEnd - hostStart); + *hostSz = (int)(hostEnd - hostStart); } else { hostEnd = hostStart; @@ -18182,10 +18181,25 @@ int wolfssl_local_MatchUriNameConstraint(const char* uri, int uriSz, *hostEnd != '?' && *hostEnd != '#') { hostEnd++; } - hostSz = (int)(hostEnd - hostStart); + *hostSz = (int)(hostEnd - hostStart); } - if (hostSz <= 0) { + if (*hostSz <= 0) { + return 0; + } + *host = hostStart; + + return 1; +} + +int wolfssl_local_MatchUriNameConstraint(const char* uri, int uriSz, + const char* base, int baseSz) +{ + const char* hostStart = NULL; + int hostSz = 0; + + if (base == NULL || baseSz <= 0 || + !GetUriHost(uri, uriSz, &hostStart, &hostSz)) { return 0; } From 4b3bb49b27184788b97ad10ac9499049f398e728 Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Fri, 12 Jun 2026 08:12:11 +0200 Subject: [PATCH 2/6] NameConstraints: Require DNS reg-name host for URI name constraints RFC 5280 4.2.1.10 defines URI name constraints in terms of a host that is a fully qualified domain name; RFC 3986 IP-literal ([...]) and IPv4address hosts are not DNS reg-names and cannot be meaningfully matched against a DNS-style constraint base. - Classify the host extracted by GetUriHost (IP-literal, IPv4address, reg-name) and validate that a reg-name has no empty labels. - wolfssl_local_MatchUriNameConstraint() no longer matches URIs whose host is an IP address. - ConfirmNameConstraints() fails closed: when URI constraints are present, a URI SAN without a DNS host is rejected. A plain non-match would otherwise let such names pass excluded-only constraints. --- tests/api.c | 16 +++++ tests/api/test_asn.c | 11 +++- wolfcrypt/src/asn.c | 134 +++++++++++++++++++++++++++++++++++++++- wolfssl/wolfcrypt/asn.h | 2 + 4 files changed, 158 insertions(+), 5 deletions(-) diff --git a/tests/api.c b/tests/api.c index d34a5afef8..0303577f64 100644 --- a/tests/api.c +++ b/tests/api.c @@ -23091,6 +23091,22 @@ static int test_NameConstraints_DnsUriWildcard(void) sanSz = build_simple_san(san, sizeof(san), URI, "https://www.host.com/"); ExpectIntGT((int)sanSz, 0); ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0); + + /* (11) RFC 5280 requires a DNS host when URI constraints are applied. + * Fail closed even for excluded-only constraints where a boolean + * non-match would otherwise pass. */ + ncSz = build_simple_nameConstraints(nc, sizeof(nc), 1, URI, + "blocked.com"); + sanSz = build_simple_san(san, sizeof(san), URI, "https://12.31.2.3/"); + ExpectIntGT((int)ncSz, 0); + ExpectIntGT((int)sanSz, 0); + ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); + + sanSz = build_simple_san(san, sizeof(san), URI, "https://[v1.addr.]/"); + ExpectIntGT((int)sanSz, 0); + ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); #endif return EXPECT_RESULT(); } diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 63cbfd365d..65537644fd 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -1153,6 +1153,9 @@ int test_wolfssl_local_MatchUriNameConstraint(void) ExpectIntEQ(uriNC("https://host.com.evil.com", "host.com"), 0); ExpectIntEQ(uriNC("https://other.com", "host.com"), 0); + /* Empty interior labels are not valid DNS host labels. */ + ExpectIntEQ(uriNC("https://a..host.com/", ".host.com"), 0); + /* * Leading-dot constraint: proper subtree of hosts (apex excluded). */ @@ -1164,10 +1167,14 @@ int test_wolfssl_local_MatchUriNameConstraint(void) ExpectIntEQ(uriNC("https://evilhost.com", ".host.com"), 0); /* - * IPv6 literal host extraction ([..]) then exact match. + * RFC 5280 URI constraints require a DNS host. IP-literals / IPvFuture + * hosts in brackets and IPv4address hosts are not DNS reg-names. */ - ExpectIntEQ(uriNC("https://[2001:db8::1]:443/x", "2001:db8::1"), 1); + ExpectIntEQ(uriNC("https://[2001:db8::1]:443/x", "2001:db8::1"), 0); ExpectIntEQ(uriNC("https://[2001:db8::1]", "2001:db8::2"), 0); + ExpectIntEQ(uriNC("https://[v1.addr.]/", "v1.addr"), 0); + ExpectIntEQ(uriNC("https://[v1.addr.]/", "v1.addr."), 0); + ExpectIntEQ(uriNC("https://12.31.2.3/", "12.31.2.3"), 0); /* * Malformed / degenerate URIs and inputs (reject). diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index d78d495563..b5e6215e12 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -18121,8 +18121,81 @@ int wolfssl_local_MatchBaseName(int type, const char* name, int nameSz, return 1; } +/* RFC 3986 host classification for URI name-constraint checks. */ +typedef enum UriHostType { + URI_HOST_REG_NAME = 0, + URI_HOST_IP_LITERAL, + URI_HOST_IPV4 +} UriHostType; + +static int UriHostIsDecOctet(const char* s, int sSz) +{ + int i; + int val = 0; + + if (s == NULL || sSz <= 0 || sSz > 3) { + return 0; + } + if (sSz > 1 && s[0] == '0') { + return 0; + } + + for (i = 0; i < sSz; i++) { + if (s[i] < '0' || s[i] > '9') { + return 0; + } + val = (val * 10) + (s[i] - '0'); + } + + return val <= 255; +} + +static int UriHostIsIpv4Address(const char* host, int hostSz) +{ + int i; + int partStart = 0; + int partCount = 0; + + if (host == NULL || hostSz <= 0) { + return 0; + } + + for (i = 0; i <= hostSz; i++) { + if (i == hostSz || host[i] == '.') { + if (!UriHostIsDecOctet(host + partStart, i - partStart)) { + return 0; + } + partCount++; + partStart = i + 1; + } + else if (host[i] < '0' || host[i] > '9') { + return 0; + } + } + + return partCount == 4; +} + +static int UriRegNameHasNonEmptyLabels(const char* host, int hostSz) +{ + int i; + + if (host == NULL || hostSz <= 0 || host[0] == '.' || + host[hostSz - 1] == '.') { + return 0; + } + + for (i = 1; i < hostSz; i++) { + if (host[i] == '.' && host[i - 1] == '.') { + return 0; + } + } + + return 1; +} + static int GetUriHost(const char* uri, int uriSz, const char** host, - int* hostSz) + int* hostSz, UriHostType* hostType) { const char* hostStart; const char* hostEnd; @@ -18132,7 +18205,8 @@ static int GetUriHost(const char* uri, int uriSz, const char** host, /* Need at least 3 bytes for the "://" scheme separator; rejecting short * inputs early also keeps the loop bound (uriEnd - 2) from forming a * pointer before `uri`. */ - if (uri == NULL || uriSz < 3 || host == NULL || hostSz == NULL) { + if (uri == NULL || uriSz < 3 || host == NULL || hostSz == NULL || + hostType == NULL) { return 0; } @@ -18174,6 +18248,7 @@ static int GetUriHost(const char* uri, int uriSz, const char** host, return 0; } *hostSz = (int)(hostEnd - hostStart); + *hostType = URI_HOST_IP_LITERAL; } else { hostEnd = hostStart; @@ -18182,6 +18257,12 @@ static int GetUriHost(const char* uri, int uriSz, const char** host, hostEnd++; } *hostSz = (int)(hostEnd - hostStart); + *hostType = UriHostIsIpv4Address(hostStart, *hostSz) ? + URI_HOST_IPV4 : URI_HOST_REG_NAME; + if (*hostType == URI_HOST_REG_NAME && + !UriRegNameHasNonEmptyLabels(hostStart, *hostSz)) { + return 0; + } } if (*hostSz <= 0) { @@ -18192,14 +18273,36 @@ static int GetUriHost(const char* uri, int uriSz, const char** host, return 1; } +int wolfssl_local_UriNameHasDnsHost(const char* uri, int uriSz) +{ + const char* host = NULL; + int hostSz = 0; + UriHostType hostType = URI_HOST_REG_NAME; + + if (!GetUriHost(uri, uriSz, &host, &hostSz, &hostType)) { + return 0; + } + + (void)host; + (void)hostSz; + return hostType == URI_HOST_REG_NAME; +} + int wolfssl_local_MatchUriNameConstraint(const char* uri, int uriSz, const char* base, int baseSz) { const char* hostStart = NULL; int hostSz = 0; + UriHostType hostType = URI_HOST_REG_NAME; if (base == NULL || baseSz <= 0 || - !GetUriHost(uri, uriSz, &hostStart, &hostSz)) { + !GetUriHost(uri, uriSz, &hostStart, &hostSz, &hostType)) { + return 0; + } + /* RFC 5280 URI constraints apply only to host names specified as fully + * qualified domain names. RFC 3986 IP-literals and IPv4address hosts are + * not DNS reg-names. */ + if (hostType != URI_HOST_REG_NAME) { return 0; } @@ -18578,12 +18681,25 @@ static int IsInExcludedList(DNS_entry* name, Base_entry* dnsList, byte nameType) } +static int NameConstraintListHasType(Base_entry* list, byte nameType) +{ + while (list != NULL) { + if (list->type == nameType) { + return 1; + } + list = list->next; + } + + return 0; +} + static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert) { const byte nameTypes[] = {ASN_RFC822_TYPE, ASN_DNS_TYPE, ASN_DIR_TYPE, ASN_IP_TYPE, ASN_URI_TYPE, ASN_OTHER_TYPE, ASN_RID_TYPE}; int i; + int uriConstraintsApply; if (signer == NULL || cert == NULL) return 0; @@ -18592,6 +18708,10 @@ static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert) !signer->extNameConstraintHasUnsupported) return 1; + uriConstraintsApply = + NameConstraintListHasType(signer->excludedNames, ASN_URI_TYPE) || + NameConstraintListHasType(signer->permittedNames, ASN_URI_TYPE); + for (i=0; i < (int)sizeof(nameTypes); i++) { byte nameType = nameTypes[i]; DNS_entry* name = NULL; @@ -18674,6 +18794,14 @@ static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert) while (name != NULL) { /* Only check entries that match the current nameType. */ if (name->type == nameType) { + if (nameType == ASN_URI_TYPE && uriConstraintsApply && + !wolfssl_local_UriNameHasDnsHost(name->name, + name->len)) { + WOLFSSL_MSG("URI name constraint applied to URI without " + "DNS host"); + return 0; + } + if (IsInExcludedList(name, signer->excludedNames, nameType) == 1) { WOLFSSL_MSG("Excluded name was found!"); diff --git a/wolfssl/wolfcrypt/asn.h b/wolfssl/wolfcrypt/asn.h index 8c82294d12..0090b8ab46 100644 --- a/wolfssl/wolfcrypt/asn.h +++ b/wolfssl/wolfcrypt/asn.h @@ -3221,6 +3221,8 @@ WOLFSSL_TEST_VIS int wolfssl_local_MatchIpSubnet(const byte* ip, int ipSz, WOLFSSL_TEST_VIS int wolfssl_local_MatchUriNameConstraint(const char* uri, int uriSz, const char* base, int baseSz); +WOLFSSL_LOCAL int wolfssl_local_UriNameHasDnsHost(const char* uri, + int uriSz); WOLFSSL_TEST_VIS int wolfssl_local_MatchDnsConstraintWildcard( const char* name, int nameSz, const char* base, int baseSz, From ef8836a34668496b2c1dab0258878742a5b399f3 Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Fri, 12 Jun 2026 08:15:38 +0200 Subject: [PATCH 3/6] Handle the absolute-FQDN trailing dot in URI name constraints One trailing dot marks an absolute FQDN and is not part of the host: "host.com." and "host.com" denote the same host. Strip it from the URI host before classification (so "12.31.2.3." is still recognized as an IPv4 address) and from the constraint base before the exact-match comparison, mirroring what wolfssl_local_MatchBaseName() already does for DNS name constraints. Only a single dot is the marker: an empty last label ("host.com..") is rejected. --- tests/api.c | 7 +++++++ tests/api/test_asn.c | 16 ++++++++++++++++ wolfcrypt/src/asn.c | 19 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/tests/api.c b/tests/api.c index 0303577f64..d0a820219d 100644 --- a/tests/api.c +++ b/tests/api.c @@ -23107,6 +23107,13 @@ static int test_NameConstraints_DnsUriWildcard(void) ExpectIntGT((int)sanSz, 0); ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); + + /* An IPv4address host with the absolute-FQDN trailing dot is still not + * a DNS host. */ + sanSz = build_simple_san(san, sizeof(san), URI, "https://12.31.2.3./"); + ExpectIntGT((int)sanSz, 0); + ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); #endif return EXPECT_RESULT(); } diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 65537644fd..ef8d2b4510 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -1153,6 +1153,18 @@ int test_wolfssl_local_MatchUriNameConstraint(void) ExpectIntEQ(uriNC("https://host.com.evil.com", "host.com"), 0); ExpectIntEQ(uriNC("https://other.com", "host.com"), 0); + /* A single trailing dot is the absolute-FQDN marker: "host.com." and + * "host.com" denote the same host and must compare equal, matching the + * DNS name-constraint path. */ + ExpectIntEQ(uriNC("https://host.com./", "host.com"), 1); + ExpectIntEQ(uriNC("https://host.com.:8443/x", "host.com"), 1); + ExpectIntEQ(uriNC("https://host.com", "host.com."), 1); + ExpectIntEQ(uriNC("https://host.com./", "host.com."), 1); + ExpectIntEQ(uriNC("https://v1.addr./", "v1.addr"), 1); + ExpectIntEQ(uriNC("https://v1.addr/", "v1.addr."), 1); + /* Only ONE trailing dot is the marker; an empty last label is not. */ + ExpectIntEQ(uriNC("https://host.com../", "host.com"), 0); + ExpectIntEQ(uriNC("https://a.host.com../", ".host.com"), 0); /* Empty interior labels are not valid DNS host labels. */ ExpectIntEQ(uriNC("https://a..host.com/", ".host.com"), 0); @@ -1175,6 +1187,10 @@ int test_wolfssl_local_MatchUriNameConstraint(void) ExpectIntEQ(uriNC("https://[v1.addr.]/", "v1.addr"), 0); ExpectIntEQ(uriNC("https://[v1.addr.]/", "v1.addr."), 0); ExpectIntEQ(uriNC("https://12.31.2.3/", "12.31.2.3"), 0); + /* An IPv4address host is still not a DNS reg-name when written with the + * absolute-FQDN trailing dot. */ + ExpectIntEQ(uriNC("https://12.31.2.3./", "12.31.2.3"), 0); + ExpectIntEQ(uriNC("https://12.31.2.3./", "12.31.2.3."), 0); /* * Malformed / degenerate URIs and inputs (reject). diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index b5e6215e12..2aa867a729 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -18257,6 +18257,16 @@ static int GetUriHost(const char* uri, int uriSz, const char** host, hostEnd++; } *hostSz = (int)(hostEnd - hostStart); + /* One trailing dot is the absolute-FQDN marker and not part of the + * host: strip it before classifying so that "12.31.2.3." is + * recognized as an IPv4 address and "host.com." denotes the same + * reg-name as "host.com". */ + if (*hostSz > 0 && hostStart[*hostSz - 1] == '.') { + (*hostSz)--; + if (*hostSz <= 0 || hostStart[*hostSz - 1] == '.') { + return 0; + } + } *hostType = UriHostIsIpv4Address(hostStart, *hostSz) ? URI_HOST_IPV4 : URI_HOST_REG_NAME; if (*hostType == URI_HOST_REG_NAME && @@ -18317,6 +18327,15 @@ int wolfssl_local_MatchUriNameConstraint(const char* uri, int uriSz, } else { int i; + /* GetUriHost already stripped the host's absolute-FQDN trailing dot; + * treat one trailing dot on the base the same way so that "host.com." + * and "host.com" compare equal. */ + if (base[baseSz - 1] == '.') { + baseSz--; + } + if (baseSz <= 0) { + return 0; + } if (hostSz != baseSz) { return 0; } From a39e8ae976e64ced8753ba887ba0e9652050525a Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Fri, 12 Jun 2026 08:16:44 +0200 Subject: [PATCH 4/6] Route DNS name-constraint checks through one helper Add wolfssl_local_MatchDnsNameConstraint() dispatching wildcard names to the subtree matcher and literal names to plain base-name matching, and use it for the ASN_DNS_TYPE branches of PermittedListOk() and IsInExcludedList(). This also drops the outer name->len >= base len byte-length guard for literal DNS names. That guard ran before MatchBaseName() could strip the absolute-FQDN trailing dot, so a constraint base like DNS:example.com. never matched the SAN example.com it denotes. --- tests/api.c | 17 +++++++++++++++++ wolfcrypt/src/asn.c | 37 +++++++++++++++++++++++++------------ wolfssl/wolfcrypt/asn.h | 3 +++ 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/tests/api.c b/tests/api.c index d0a820219d..197946eb2a 100644 --- a/tests/api.c +++ b/tests/api.c @@ -23114,6 +23114,23 @@ static int test_NameConstraints_DnsUriWildcard(void) ExpectIntGT((int)sanSz, 0); ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); + + /* (12) One trailing dot on the constraint base is the absolute-FQDN + * marker: DNS:example.com. denotes the same subtree as + * DNS:example.com, so the permitted form accepts the bare SAN and + * the excluded form rejects it. */ + ncSz = build_simple_nameConstraints(nc, sizeof(nc), 0, DNS, + "example.com."); + sanSz = build_simple_san(san, sizeof(san), DNS, "example.com"); + ExpectIntGT((int)ncSz, 0); + ExpectIntGT((int)sanSz, 0); + ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0); + + ncSz = build_simple_nameConstraints(nc, sizeof(nc), 1, DNS, + "example.com."); + ExpectIntGT((int)ncSz, 0); + ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); #endif return EXPECT_RESULT(); } diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 2aa867a729..dadb48d4a8 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -18551,6 +18551,23 @@ static int DnsNameHasWildcard(const char* name, int nameSz) return 0; } +/* Match a DNS name against a DNS name-constraint base. A wildcard name + * denotes a set of names: permitted subtrees require containment, excluded + * subtrees intersection (selected by `permitted`). Literal names use plain + * base-name matching, which normalizes the absolute-FQDN trailing dot + * before its own length check. + * Returns 1 on match, 0 otherwise. */ +int wolfssl_local_MatchDnsNameConstraint(const char* name, int nameSz, + const char* base, int baseSz, int permitted) +{ + if (DnsNameHasWildcard(name, nameSz)) { + return wolfssl_local_MatchDnsConstraintWildcard(name, nameSz, + base, baseSz, permitted); + } + return wolfssl_local_MatchBaseName(ASN_DNS_TYPE, name, nameSz, base, + baseSz); +} + /* Search through the list to find if the name is permitted. * name The DNS name to search for * dnsList The list to search through @@ -18601,12 +18618,10 @@ static int PermittedListOk(DNS_entry* name, Base_entry* dnsList, byte nameType) break; } } - else if (nameType == ASN_DNS_TYPE && - DnsNameHasWildcard(name->name, name->len)) { - /* Wildcard DNS SAN: a '*' can expand to a longer label, so the - * byte-length guard used for literal names below is invalid. - * Permit only if every expansion stays inside the subtree. */ - if (wolfssl_local_MatchDnsConstraintWildcard(name->name, + else if (nameType == ASN_DNS_TYPE) { + /* Permit only if every expansion of a wildcard stays inside + * the subtree. */ + if (wolfssl_local_MatchDnsNameConstraint(name->name, name->len, current->name, current->nameSz, 1)) { match = 1; break; @@ -18675,12 +18690,10 @@ static int IsInExcludedList(DNS_entry* name, Base_entry* dnsList, byte nameType) break; } } - else if (nameType == ASN_DNS_TYPE && - DnsNameHasWildcard(name->name, name->len)) { - /* Wildcard DNS SAN: a '*' can expand to a longer label, so the - * byte-length guard used for literal names below is invalid. - * Exclude if any expansion can fall inside the subtree. */ - if (wolfssl_local_MatchDnsConstraintWildcard(name->name, + else if (nameType == ASN_DNS_TYPE) { + /* Exclude if any expansion of a wildcard can fall inside the + * subtree. */ + if (wolfssl_local_MatchDnsNameConstraint(name->name, name->len, current->name, current->nameSz, 0)) { ret = 1; break; diff --git a/wolfssl/wolfcrypt/asn.h b/wolfssl/wolfcrypt/asn.h index 0090b8ab46..e1f13a292b 100644 --- a/wolfssl/wolfcrypt/asn.h +++ b/wolfssl/wolfcrypt/asn.h @@ -3227,6 +3227,9 @@ WOLFSSL_TEST_VIS int wolfssl_local_MatchDnsConstraintWildcard( const char* name, int nameSz, const char* base, int baseSz, int permitted); +WOLFSSL_LOCAL int wolfssl_local_MatchDnsNameConstraint(const char* name, + int nameSz, const char* base, + int baseSz, int permitted); #endif #if ((defined(HAVE_ED25519) && defined(HAVE_ED25519_KEY_IMPORT)) \ From eb9cd6c9c594948695ae5acc3e3f94c12a86eda7 Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Fri, 12 Jun 2026 08:18:55 +0200 Subject: [PATCH 5/6] Use the shared URI name-constraint matcher in the compat layer Replace ExtractHostFromUri() plus DNS-style base matching in MatchNameConstraint() with wolfssl_local_MatchUriNameConstraint(), and make wolfSSL_NAME_CONSTRAINTS_check_name() fail closed like ConfirmNameConstraints(): when URI subtrees are present, a URI name without a DNS host is rejected instead of passing excluded-only constraints as a plain non-match. This aligns the compat layer with RFC 5280 URI constraint semantics: a base without a leading dot now matches the host exactly instead of as a DNS subtree, and IP hosts no longer match at all. --- src/x509.c | 126 +++++++++------------------------ tests/api/test_ossl_x509_ext.c | 4 +- 2 files changed, 34 insertions(+), 96 deletions(-) diff --git a/src/x509.c b/src/x509.c index fd87b1da4a..a5d4b88289 100644 --- a/src/x509.c +++ b/src/x509.c @@ -5670,91 +5670,6 @@ static int MatchIpName(const char* name, int nameSz, WOLFSSL_GENERAL_NAME* gn) constraintData, constraintLen); } -/* Extract host from URI for name constraint matching. - * URI format: scheme://[userinfo@]host[:port][/path][?query][#fragment] - * IPv6 literals are enclosed in brackets: scheme://[ipv6addr]:port/path - * Returns pointer to host start and sets hostLen, or NULL on failure. */ -static const char* ExtractHostFromUri(const char* uri, int uriLen, int* hostLen) -{ - const char* hostStart; - const char* hostEnd; - const char* p; - const char* uriEnd; - - if (uri == NULL || uriLen <= 0 || hostLen == NULL) { - return NULL; - } - - uriEnd = uri + uriLen; - - /* Find "://" to skip scheme */ - hostStart = NULL; - for (p = uri; p < uriEnd - 2; p++) { - if (p[0] == ':' && p[1] == '/' && p[2] == '/') { - hostStart = p + 3; - break; - } - } - if (hostStart == NULL || hostStart >= uriEnd) { - return NULL; - } - - /* Skip userinfo if present (look for @ before any /, ?, #) - * userinfo can contain ':' (ex: user:pass@host), don't stop at ':' - * For IPv6, also don't stop at '[' in userinfo */ - for (p = hostStart; p < uriEnd; p++) { - if (*p == '@') { - hostStart = p + 1; - break; - } - if (*p == '/' || *p == '?' || *p == '#') { - /* No userinfo found */ - break; - } - /* If '[' before '@', found IPv6 literal, not userinfo */ - if (*p == '[') { - break; - } - } - if (hostStart >= uriEnd) { - return NULL; - } - - /* Check for IPv6 literal */ - if (*hostStart == '[') { - /* Find closing bracket, skip opening one */ - hostStart++; - hostEnd = hostStart; - while (hostEnd < uriEnd && *hostEnd != ']') { - hostEnd++; - } - if (hostEnd >= uriEnd) { - /* No closing bracket found, malformed */ - return NULL; - } - /* hostEnd points to closing bracket, extract content between */ - *hostLen = (int)(hostEnd - hostStart); - if (*hostLen <= 0) { - return NULL; - } - return hostStart; - } - - /* Regular hostname, find end */ - hostEnd = hostStart; - while (hostEnd < uriEnd && *hostEnd != ':' && *hostEnd != '/' && - *hostEnd != '?' && *hostEnd != '#') { - hostEnd++; - } - - *hostLen = (int)(hostEnd - hostStart); - if (*hostLen <= 0) { - return NULL; - } - - return hostStart; -} - /* Helper to check if name string matches a single GENERAL_NAME constraint. * Returns 1 if matches, 0 if not. */ static int MatchNameConstraint(int type, const char* name, int nameSz, @@ -5788,15 +5703,7 @@ static int MatchNameConstraint(int type, const char* name, int nameSz, nameSz, baseStr, baseLen); } else if (type == WOLFSSL_GEN_URI) { - const char* host; - int hostLen; - - /* For URI, extract host and match against DNS-style */ - host = ExtractHostFromUri(name, nameSz, &hostLen); - if (host == NULL) { - return 0; - } - return wolfssl_local_MatchBaseName(ASN_DNS_TYPE, host, hostLen, + return wolfssl_local_MatchUriNameConstraint(name, nameSz, baseStr, baseLen); } else { @@ -5811,6 +5718,29 @@ static int MatchNameConstraint(int type, const char* name, int nameSz, } } +static int NameConstraintsHasType(const WOLFSSL_STACK* sk, int type) +{ + int i; + int num; + + if (sk == NULL) { + return 0; + } + + num = wolfSSL_sk_GENERAL_SUBTREE_num(sk); + for (i = 0; i < num; i++) { + WOLFSSL_GENERAL_SUBTREE* subtree; + + subtree = wolfSSL_sk_GENERAL_SUBTREE_value(sk, i); + if (subtree != NULL && subtree->base != NULL && + subtree->base->type == type) { + return 1; + } + } + + return 0; +} + /* * Check if a name string satisfies given name constraints. * @@ -5841,6 +5771,14 @@ int wolfSSL_NAME_CONSTRAINTS_check_name(WOLFSSL_NAME_CONSTRAINTS* nc, return 0; } + if (type == WOLFSSL_GEN_URI && + (NameConstraintsHasType(nc->permittedSubtrees, type) || + NameConstraintsHasType(nc->excludedSubtrees, type)) && + !wolfssl_local_UriNameHasDnsHost(name, nameSz)) { + WOLFSSL_MSG("URI name constraint applied to URI without DNS host"); + return 0; + } + /* Check permitted subtrees */ if (nc->permittedSubtrees != NULL) { num = wolfSSL_sk_GENERAL_SUBTREE_num(nc->permittedSubtrees); diff --git a/tests/api/test_ossl_x509_ext.c b/tests/api/test_ossl_x509_ext.c index 05d2370a87..1b840e6656 100644 --- a/tests/api/test_ossl_x509_ext.c +++ b/tests/api/test_ossl_x509_ext.c @@ -2039,8 +2039,8 @@ int test_wolfSSL_NAME_CONSTRAINTS_uri(void) ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_URI, "https://user:pass@www.wolfssl.com/path", 38), 1); - /* IPv6 literal URIs, host extracted without brackets. - * These don't match .wolfssl.com constraint (different host type) */ + /* URI constraints require a DNS reg-name host, so IP-literals do not + * match the .wolfssl.com constraint. */ ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_URI, "https://[::1]:8080/path", 23), 0); ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_URI, From 44a22af323a00f9e9154a8fc525368101b234517 Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Fri, 12 Jun 2026 08:19:55 +0200 Subject: [PATCH 6/6] Handle wildcard DNS names in compat name-constraint checks MatchNameConstraint() compared wildcard DNS SANs literally, so *.example.com was not rejected by an excluded subtree covering foo.example.com. Route WOLFSSL_GEN_DNS through wolfssl_local_MatchDnsNameConstraint(), passing the subtree direction: permitted subtrees require every wildcard expansion to stay inside the subtree, excluded subtrees reject when any expansion can fall inside. This matches what ConfirmNameConstraints() already does. --- src/x509.c | 12 ++++--- tests/api/test_ossl_x509_ext.c | 65 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/x509.c b/src/x509.c index a5d4b88289..c3c66826b9 100644 --- a/src/x509.c +++ b/src/x509.c @@ -5671,9 +5671,11 @@ static int MatchIpName(const char* name, int nameSz, WOLFSSL_GENERAL_NAME* gn) } /* Helper to check if name string matches a single GENERAL_NAME constraint. + * permitted selects subtree semantics for wildcard DNS names: containment + * for permitted subtrees, intersection for excluded subtrees. * Returns 1 if matches, 0 if not. */ static int MatchNameConstraint(int type, const char* name, int nameSz, - WOLFSSL_GENERAL_NAME* gn) + WOLFSSL_GENERAL_NAME* gn, int permitted) { const char* baseStr; int baseLen; @@ -5708,8 +5710,8 @@ static int MatchNameConstraint(int type, const char* name, int nameSz, } else { /* WOLFSSL_GEN_DNS uses DNS-style matching */ - return wolfssl_local_MatchBaseName(ASN_DNS_TYPE, name, nameSz, - baseStr, baseLen); + return wolfssl_local_MatchDnsNameConstraint(name, nameSz, + baseStr, baseLen, permitted); } default: @@ -5795,7 +5797,7 @@ int wolfSSL_NAME_CONSTRAINTS_check_name(WOLFSSL_NAME_CONSTRAINTS* nc, } hasPermittedType = 1; - if (MatchNameConstraint(type, name, nameSz, gn)) { + if (MatchNameConstraint(type, name, nameSz, gn, 1)) { matchedPermitted = 1; break; } @@ -5822,7 +5824,7 @@ int wolfSSL_NAME_CONSTRAINTS_check_name(WOLFSSL_NAME_CONSTRAINTS* nc, continue; } - if (MatchNameConstraint(type, name, nameSz, gn)) { + if (MatchNameConstraint(type, name, nameSz, gn, 0)) { WOLFSSL_MSG("Name in excluded subtrees"); return 0; } diff --git a/tests/api/test_ossl_x509_ext.c b/tests/api/test_ossl_x509_ext.c index 1b840e6656..c1293899d5 100644 --- a/tests/api/test_ossl_x509_ext.c +++ b/tests/api/test_ossl_x509_ext.c @@ -2243,6 +2243,71 @@ int test_wolfSSL_NAME_CONSTRAINTS_check_name(void) x509 = NULL; nc = NULL; + /* Wildcard names against an excluded DNS subtree. Build the constraints + * programmatically: excluded;DNS:foo.example.com */ + ExpectNotNull(nc = wolfSSL_NAME_CONSTRAINTS_new()); + if (EXPECT_SUCCESS()) { + GENERAL_SUBTREE* subtree = NULL; + + ExpectNotNull(nc->excludedSubtrees = wolfSSL_sk_new_null()); + if (EXPECT_SUCCESS()) { + nc->excludedSubtrees->type = STACK_TYPE_GENERAL_SUBTREE; + } + ExpectNotNull(subtree = wolfSSL_GENERAL_SUBTREE_new()); + if (EXPECT_SUCCESS()) { + ExpectNotNull(subtree->base = wolfSSL_GENERAL_NAME_new()); + } + if (EXPECT_SUCCESS()) { + subtree->base->type = GEN_DNS; + ExpectIntEQ(wolfSSL_ASN1_STRING_set(subtree->base->d.ia5, + "foo.example.com", 15), WOLFSSL_SUCCESS); + } + if (EXPECT_SUCCESS()) { + ExpectIntGT(wolfSSL_sk_push(nc->excludedSubtrees, subtree), 0); + } + if (EXPECT_FAIL()) { + wolfSSL_GENERAL_SUBTREE_free(subtree); + } + } + + if (EXPECT_SUCCESS()) { + /* Literal names inside the excluded subtree are rejected. */ + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "foo.example.com", 15), 0); + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "a.foo.example.com", 17), 0); + /* Names outside the subtree pass; there are no permitted subtrees. */ + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "bar.example.com", 15), 1); + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "*.other.com", 11), 1); + /* A wildcard's '*' can expand to "foo", so "*.example.com" covers + * the excluded "foo.example.com" and must be rejected. */ + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "*.example.com", 13), 0); + } + + if (EXPECT_SUCCESS()) { + /* One trailing dot on the base is the absolute-FQDN marker: + * excluded;DNS:foo.example.com. covers the same subtree, so the + * bare name is still rejected. */ + GENERAL_SUBTREE* subtree = NULL; + + ExpectNotNull(subtree = + wolfSSL_sk_GENERAL_SUBTREE_value(nc->excludedSubtrees, 0)); + if (EXPECT_SUCCESS()) { + ExpectIntEQ(wolfSSL_ASN1_STRING_set(subtree->base->d.ia5, + "foo.example.com.", 16), WOLFSSL_SUCCESS); + } + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "foo.example.com", 15), 0); + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "bar.example.com", 15), 1); + } + + NAME_CONSTRAINTS_free(nc); + nc = NULL; + /* Test IP address constraint checking with cert-ext-ncip.pem * This cert has permitted IP 192.168.1.0/255.255.255.0 */ if ((f = XFOPEN("./certs/test/cert-ext-ncip.pem", "rb")) == XBADFILE) {