diff --git a/src/x509.c b/src/x509.c index c6998d3514..ead625fcfe 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 6208cc2b2d..ccf2b3cc76 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 ce4eea1487..857b1f9f2a 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); + /* * Leading-dot constraint: proper subtree of hosts (apex excluded). */ @@ -1164,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 fb0967be5e..68d48903be 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 2b1589bc6d..57000f67cf 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) { + return 0; } + *host = hostStart; - if (hostSz <= 0) { + 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; } @@ -18090,6 +18182,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; } @@ -18454,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; @@ -18468,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; @@ -18550,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 70f823fc80..6a036a283e 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,