diff --git a/src/internal.c b/src/internal.c index 8a72a675d8..243660151f 100644 --- a/src/internal.c +++ b/src/internal.c @@ -13336,6 +13336,66 @@ static int MatchIPv6(const char* pattern, int patternLen, } #endif /* WOLFSSL_IP_ALT_NAME && !WOLFSSL_USER_IO */ +/* IDNA A-label prefix (Punycode-encoded internationalized labels), used to + * gate wildcard matching per RFC 6125 sec. 6.4.3 / RFC 9525 sec. 6.3. */ +static int LabelIsALabel(const char* label, word32 labelLen) +{ + if (labelLen < 4) + return 0; + return ((XTOLOWER((unsigned char)label[0]) == 'x') && + (XTOLOWER((unsigned char)label[1]) == 'n') && + (label[2] == '-') && + (label[3] == '-')); +} + +/* Returns 1 if any dot-separated label in name is an A-label. */ +static int NameHasALabel(const char* name, word32 nameLen) +{ + word32 labelStart = 0; + word32 i; + + for (i = 0; i < nameLen; i++) { + if (name[i] == '.') { + if (LabelIsALabel(name + labelStart, i - labelStart)) + return 1; + labelStart = i + 1; + } + } + if (labelStart < nameLen) { + if (LabelIsALabel(name + labelStart, nameLen - labelStart)) + return 1; + } + return 0; +} + +/* Returns 1 if any label of pattern that contains a wildcard ('*') is an + * A-label. RFC 6125 sec. 6.4.3 disallows wildcards embedded in A-labels. */ +static int PatternHasWildcardInALabel(const char* pattern, word32 patternLen) +{ + word32 labelStart = 0; + int labelHasWildcard = 0; + word32 i; + + for (i = 0; i < patternLen; i++) { + if (pattern[i] == '.') { + if (labelHasWildcard && + LabelIsALabel(pattern + labelStart, i - labelStart)) { + return 1; + } + labelStart = i + 1; + labelHasWildcard = 0; + } + else if (pattern[i] == '*') { + labelHasWildcard = 1; + } + } + if (labelHasWildcard && + LabelIsALabel(pattern + labelStart, patternLen - labelStart)) { + return 1; + } + return 0; +} + /* Match names with wildcards, each wildcard can represent a single name component or fragment but not multiple names, i.e., *.z.com matches y.z.com but not x.y.z.com @@ -13376,6 +13436,22 @@ int MatchDomainName(const char* pattern, int patternLen, const char* str, if (pattern[patternLen-1] == '.') --patternLen; + /* RFC 6125 sec. 6.4.3 / RFC 9525 sec. 6.3: do not perform wildcard + * matching when the pattern has a wildcard embedded in an A-label, nor + * when the reference identifier (hostname) contains any A-label. The + * existing single-label glob would otherwise match across the + * Punycode-encoded form (e.g., "x*.example.com" matching + * "xn--rger-koa.example.com"), which has no semantic meaning. */ + if (PatternHasWildcardInALabel(pattern, (word32)patternLen)) + return 0; + if (NameHasALabel(str, strLen)) { + int i; + for (i = 0; i < patternLen; i++) { + if (pattern[i] == '*') + return 0; + } + } + while (patternLen > 0) { /* Get the next pattern char to evaluate */ char p = (char)XTOLOWER((unsigned char)*pattern); diff --git a/tests/api/test_ossl_x509.c b/tests/api/test_ossl_x509.c index e1cf3aa0f4..30e9f82b20 100644 --- a/tests/api/test_ossl_x509.c +++ b/tests/api/test_ossl_x509.c @@ -1662,7 +1662,8 @@ int test_wolfssl_local_IsValidFQDN(void) { test_cases[i].is_FQDN); if (! EXPECT_SUCCESS()) { fprintf(stderr, "wolfssl_local_IsValidFQDN() wrong result for " - "case %d \"%s\"\n", i, test_cases[i].str); + "case %d \"%s\"\n", i, + test_cases[i].str ? test_cases[i].str : "(null)"); break; } } @@ -1706,6 +1707,94 @@ int test_wolfssl_local_IsValidFQDN(void) { return EXPECT_RESULT(); } +/* Verify that MatchDomainName() refuses to expand wildcards across IDNA + * A-labels (xn-- prefix) per RFC 6125 sec. 6.4.3 / RFC 9525 sec. 6.3. + * + * MatchDomainName() is exposed for testing via the visibility mechanism + * declared in wolfssl/internal.h. */ +int test_wolfSSL_MatchDomainName_idn(void) +{ + EXPECT_DECLS; +#if !defined(NO_ASN) && !defined(WOLFCRYPT_ONLY) && !defined(NO_CERTS) + static const struct { + const char* pattern; + const char* host; + unsigned int flags; + int expected; /* 1 = match, 0 = no match */ + const char* note; + } cases[] = { + /* Partial wildcard whose literal prefix overlaps "xn--" must NOT + * match an A-label hostname. */ + { "x*.example.com", "xn--rger-koa.example.com", 0, 0, + "partial wildcard vs A-label" }, + /* Wildcard embedded inside an A-label pattern must NOT match. */ + { "xn--*.example.com", "xn--rger-koa.example.com", 0, 0, + "wildcard inside A-label pattern" }, + /* Full left-most wildcard MUST NOT match an A-label hostname + * (RFC 9525 sec. 6.3 strengthens RFC 6125 SHOULD NOT to MUST NOT). */ + { "*.example.com", "xn--rger-koa.example.com", 0, 0, + "full wildcard vs A-label hostname" }, + /* A-label appearing in an inner label still disables wildcard + * matching against the entire reference identifier. */ + { "*.example.com", "foo.xn--bar.example.com", 0, 0, + "wildcard with A-label in inner label" }, + /* Case-insensitive A-label detection: "XN--" is also an A-label. */ + { "x*.example.com", "XN--rger-koa.example.com", 0, 0, + "uppercase A-label prefix" }, + /* Control: full wildcard SHOULD continue to match plain ASCII. */ + { "*.example.com", "foo.example.com", 0, 1, + "wildcard matches non-IDN" }, + /* Control: exact A-label match (no wildcard in pattern) must work. */ + { "xn--rger-koa.example.com", "xn--rger-koa.example.com", 0, 1, + "exact A-label match" }, + /* Control: a label that merely begins with 'x' (not 'xn--') is not + * an A-label and must still wildcard-match. */ + { "*.example.com", "xyz.example.com", 0, 1, + "non-A-label x-prefix" }, + /* Control: partial wildcard against a non-A-label still works. */ + { "x*.example.com", "xyz.example.com", 0, 1, + "partial wildcard non-IDN" }, + + /* Trailing-dot normalization: absolute-form FQDN ("example.com.") + * must match the same FQDN with or without the trailing dot, on + * either side of the comparison. RFC 1035 / RFC 6125. */ + { "example.com", "example.com.", 0, 1, + "trailing dot on host" }, + { "example.com.", "example.com", 0, 1, + "trailing dot on pattern" }, + { "example.com.", "example.com.", 0, 1, + "trailing dot on both" }, + { "*.example.com", "foo.example.com.", 0, 1, + "trailing dot on host with wildcard pattern" }, + /* Trailing dot must not cause an A-label gate to misfire. */ + { "*.example.com", "xn--rger-koa.example.com.", 0, 0, + "trailing dot on A-label host" }, + /* Same trailing-dot normalization under WOLFSSL_LEFT_MOST_WILDCARD_ONLY. */ + { "*.example.com", "foo.example.com.", + WOLFSSL_LEFT_MOST_WILDCARD_ONLY, 1, + "trailing dot, leftWildcardOnly" }, + }; + size_t i; + + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + int got = MatchDomainName( + cases[i].pattern, (int)XSTRLEN(cases[i].pattern), + cases[i].host, (word32)XSTRLEN(cases[i].host), + cases[i].flags); + ExpectIntEQ(got, cases[i].expected); + if (! EXPECT_SUCCESS()) { + fprintf(stderr, + "MatchDomainName(\"%s\", \"%s\", flags=0x%x) = %d, " + "expected %d (%s)\n", + cases[i].pattern, cases[i].host, cases[i].flags, + got, cases[i].expected, cases[i].note); + break; + } + } +#endif /* !NO_ASN && !WOLFCRYPT_ONLY && !NO_CERTS */ + return EXPECT_RESULT(); +} + int test_wolfSSL_X509_max_altnames(void) { EXPECT_DECLS; diff --git a/tests/api/test_ossl_x509.h b/tests/api/test_ossl_x509.h index f0da8a9d8e..f2844092af 100644 --- a/tests/api/test_ossl_x509.h +++ b/tests/api/test_ossl_x509.h @@ -49,6 +49,7 @@ int test_wolfSSL_X509_name_match1(void); int test_wolfSSL_X509_name_match2(void); int test_wolfSSL_X509_name_match3(void); int test_wolfssl_local_IsValidFQDN(void); +int test_wolfSSL_MatchDomainName_idn(void); int test_wolfSSL_X509_max_altnames(void); int test_wolfSSL_X509_max_name_constraints(void); int test_wolfSSL_X509_check_ca(void); @@ -81,6 +82,7 @@ int test_wolfSSL_X509_cmp(void); TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_name_match2), \ TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_name_match3), \ TEST_DECL_GROUP("ossl_x509", test_wolfssl_local_IsValidFQDN), \ + TEST_DECL_GROUP("ossl_x509", test_wolfSSL_MatchDomainName_idn), \ TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_max_altnames), \ TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_max_name_constraints), \ TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_check_ca), \ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 256d3d76c2..6ac8ad06c5 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -2228,9 +2228,12 @@ WOLFSSL_LOCAL void FreeAsyncCtx(WOLFSSL* ssl, byte freeAsync); WOLFSSL_LOCAL void FreeKeyExchange(WOLFSSL* ssl); WOLFSSL_LOCAL void FreeSuites(WOLFSSL* ssl); WOLFSSL_LOCAL int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, word32 totalSz); -WOLFSSL_LOCAL int MatchDomainName(const char* pattern, int len, - const char* str, word32 strLen, - unsigned int flags); +#ifdef WOLFSSL_API_PREFIX_MAP + #define MatchDomainName wolfSSL_MatchDomainName +#endif +WOLFSSL_TEST_VIS int MatchDomainName(const char* pattern, int len, + const char* str, word32 strLen, + unsigned int flags); #if !defined(NO_CERTS) && !defined(NO_ASN) WOLFSSL_LOCAL int CheckForAltNames(DecodedCert* dCert, const char* domain, word32 domainLen, int* checkCN,