From 2746f1c29c2bff13344acd9703fff54258ed03b4 Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Thu, 4 Jun 2026 20:08:24 +0200 Subject: [PATCH 1/2] NameConstraints: normalize trailing dot in URI exact-host match wolfssl_local_MatchUriNameConstraint() compared the URI host against a no-leading-dot constraint with a raw length/byte check, so the absolute form "host.com." failed to match the constraint "host.com". Strip one trailing dot from both the extracted host and the base before the exact comparison, matching the existing DNS handling in wolfssl_local_MatchBaseName. Add regression cases to test_wolfssl_local_MatchUriNameConstraint. --- tests/api/test_asn.c | 10 ++++++++++ wolfcrypt/src/asn.c | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index ce4eea1487f..c24d589bd45 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -1153,6 +1153,16 @@ 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); + /* Only ONE trailing dot is the marker; an empty last label is not. */ + ExpectIntEQ(uriNC("https://host.com../", "host.com"), 0); + /* * Leading-dot constraint: proper subtree of hosts (apex excluded). */ diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 2b1589bc6d6..de5a477ba1e 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -18090,6 +18090,18 @@ int wolfssl_local_MatchUriNameConstraint(const char* uri, int uriSz, } else { int i; + /* Treat one trailing dot as the absolute-FQDN marker, matching the + * DNS constraint path in wolfssl_local_MatchBaseName so that + * "host.com." and "host.com" compare equal. */ + if (hostStart[hostSz - 1] == '.') { + hostSz--; + } + if (base[baseSz - 1] == '.') { + baseSz--; + } + if (hostSz <= 0 || baseSz <= 0) { + return 0; + } if (hostSz != baseSz) { return 0; } From 5bdb96cf7d18803fef2dc7fd960825a3dc85b389 Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Mon, 8 Jun 2026 19:50:13 +0200 Subject: [PATCH 2/2] wip --- src/x509.c | 126 ++++++++----------------------- tests/api.c | 16 ++++ tests/api/test_asn.c | 10 ++- tests/api/test_ossl_x509_ext.c | 6 +- wolfcrypt/src/asn.c | 132 +++++++++++++++++++++++++++++++-- wolfssl/wolfcrypt/asn.h | 2 + 6 files changed, 186 insertions(+), 106 deletions(-) diff --git a/src/x509.c b/src/x509.c index c6998d35140..ead625fcfe1 100644 --- a/src/x509.c +++ b/src/x509.c @@ -5666,91 +5666,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, @@ -5784,15 +5699,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 { @@ -5807,6 +5714,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. * @@ -5837,6 +5767,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.c b/tests/api.c index 6208cc2b2dd..ccf2b3cc760 100644 --- a/tests/api.c +++ b/tests/api.c @@ -23046,6 +23046,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 c24d589bd45..857b1f9f2a6 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -1160,6 +1160,8 @@ int test_wolfssl_local_MatchUriNameConstraint(void) 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); @@ -1174,10 +1176,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/tests/api/test_ossl_x509_ext.c b/tests/api/test_ossl_x509_ext.c index fb0967be5e7..68d48903be3 100644 --- a/tests/api/test_ossl_x509_ext.c +++ b/tests/api/test_ossl_x509_ext.c @@ -98,6 +98,7 @@ int test_wolfSSL_X509_get_extension_flags(void) return EXPECT_RESULT(); } + int test_wolfSSL_X509_get_ext(void) { EXPECT_DECLS; @@ -2037,8 +2038,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, @@ -2431,4 +2432,3 @@ int test_wolfSSL_NAME_CONSTRAINTS_excluded(void) * !IGNORE_NAME_CONSTRAINTS */ return EXPECT_RESULT(); } - diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index de5a477ba1e..57000f67cf9 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -18011,19 +18011,71 @@ 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) +#define URI_HOST_REG_NAME 0 +#define URI_HOST_IP_LITERAL 1 +#define URI_HOST_IPV4 2 + +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 GetUriHost(const char* uri, int uriSz, const char** host, + int* hostSz, int* hostType) { 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 || + hostType == NULL) { return 0; } @@ -18064,7 +18116,8 @@ int wolfssl_local_MatchUriNameConstraint(const char* uri, int uriSz, if (hostEnd >= uriEnd) { return 0; } - hostSz = (int)(hostEnd - hostStart); + *hostSz = (int)(hostEnd - hostStart); + *hostType = URI_HOST_IP_LITERAL; } else { hostEnd = hostStart; @@ -18072,10 +18125,49 @@ int wolfssl_local_MatchUriNameConstraint(const char* uri, int uriSz, *hostEnd != '?' && *hostEnd != '#') { hostEnd++; } - hostSz = (int)(hostEnd - hostStart); + *hostSz = (int)(hostEnd - hostStart); + *hostType = UriHostIsIpv4Address(hostStart, *hostSz) ? + URI_HOST_IPV4 : URI_HOST_REG_NAME; } - if (hostSz <= 0) { + if (*hostSz <= 0) { + return 0; + } + *host = hostStart; + + return 1; +} + +int wolfssl_local_UriNameHasDnsHost(const char* uri, int uriSz) +{ + const char* host = NULL; + int hostSz = 0; + int 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; + int hostType = URI_HOST_REG_NAME; + + if (base == NULL || baseSz <= 0 || + !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; } @@ -18466,12 +18558,26 @@ 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; @@ -18480,6 +18586,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; @@ -18562,6 +18672,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 70f823fc809..6a036a283e7 100644 --- a/wolfssl/wolfcrypt/asn.h +++ b/wolfssl/wolfcrypt/asn.h @@ -3216,6 +3216,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,