Skip to content

Commit aa1b494

Browse files
committed
Fix IDNA matching
1 parent 6a3eb6f commit aa1b494

4 files changed

Lines changed: 169 additions & 3 deletions

File tree

src/internal.c

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13336,6 +13336,66 @@ static int MatchIPv6(const char* pattern, int patternLen,
1333613336
}
1333713337
#endif /* WOLFSSL_IP_ALT_NAME && !WOLFSSL_USER_IO */
1333813338

13339+
/* IDNA A-label prefix (Punycode-encoded internationalized labels), used to
13340+
* gate wildcard matching per RFC 6125 sec. 6.4.3 / RFC 9525 sec. 6.3. */
13341+
static int LabelIsALabel(const char* label, word32 labelLen)
13342+
{
13343+
if (labelLen < 4)
13344+
return 0;
13345+
return ((XTOLOWER((unsigned char)label[0]) == 'x') &&
13346+
(XTOLOWER((unsigned char)label[1]) == 'n') &&
13347+
(label[2] == '-') &&
13348+
(label[3] == '-'));
13349+
}
13350+
13351+
/* Returns 1 if any dot-separated label in name is an A-label. */
13352+
static int NameHasALabel(const char* name, word32 nameLen)
13353+
{
13354+
word32 labelStart = 0;
13355+
word32 i;
13356+
13357+
for (i = 0; i < nameLen; i++) {
13358+
if (name[i] == '.') {
13359+
if (LabelIsALabel(name + labelStart, i - labelStart))
13360+
return 1;
13361+
labelStart = i + 1;
13362+
}
13363+
}
13364+
if (labelStart < nameLen) {
13365+
if (LabelIsALabel(name + labelStart, nameLen - labelStart))
13366+
return 1;
13367+
}
13368+
return 0;
13369+
}
13370+
13371+
/* Returns 1 if any label of pattern that contains a wildcard ('*') is an
13372+
* A-label. RFC 6125 sec. 6.4.3 disallows wildcards embedded in A-labels. */
13373+
static int PatternHasWildcardInALabel(const char* pattern, word32 patternLen)
13374+
{
13375+
word32 labelStart = 0;
13376+
int labelHasWildcard = 0;
13377+
word32 i;
13378+
13379+
for (i = 0; i < patternLen; i++) {
13380+
if (pattern[i] == '.') {
13381+
if (labelHasWildcard &&
13382+
LabelIsALabel(pattern + labelStart, i - labelStart)) {
13383+
return 1;
13384+
}
13385+
labelStart = i + 1;
13386+
labelHasWildcard = 0;
13387+
}
13388+
else if (pattern[i] == '*') {
13389+
labelHasWildcard = 1;
13390+
}
13391+
}
13392+
if (labelHasWildcard &&
13393+
LabelIsALabel(pattern + labelStart, patternLen - labelStart)) {
13394+
return 1;
13395+
}
13396+
return 0;
13397+
}
13398+
1333913399
/* Match names with wildcards, each wildcard can represent a single name
1334013400
component or fragment but not multiple names, i.e.,
1334113401
*.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,
1337613436
if (pattern[patternLen-1] == '.')
1337713437
--patternLen;
1337813438

13439+
/* RFC 6125 sec. 6.4.3 / RFC 9525 sec. 6.3: do not perform wildcard
13440+
* matching when the pattern has a wildcard embedded in an A-label, nor
13441+
* when the reference identifier (hostname) contains any A-label. The
13442+
* existing single-label glob would otherwise match across the
13443+
* Punycode-encoded form (e.g., "x*.example.com" matching
13444+
* "xn--rger-koa.example.com"), which has no semantic meaning. */
13445+
if (PatternHasWildcardInALabel(pattern, (word32)patternLen))
13446+
return 0;
13447+
if (NameHasALabel(str, strLen)) {
13448+
int i;
13449+
for (i = 0; i < patternLen; i++) {
13450+
if (pattern[i] == '*')
13451+
return 0;
13452+
}
13453+
}
13454+
1337913455
while (patternLen > 0) {
1338013456
/* Get the next pattern char to evaluate */
1338113457
char p = (char)XTOLOWER((unsigned char)*pattern);

tests/api/test_ossl_x509.c

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,6 +1707,94 @@ int test_wolfssl_local_IsValidFQDN(void) {
17071707
return EXPECT_RESULT();
17081708
}
17091709

1710+
/* Verify that MatchDomainName() refuses to expand wildcards across IDNA
1711+
* A-labels (xn-- prefix) per RFC 6125 sec. 6.4.3 / RFC 9525 sec. 6.3.
1712+
*
1713+
* MatchDomainName() is WOLFSSL_LOCAL but visible to the test binary because
1714+
* tests link against the in-tree library. */
1715+
int test_wolfSSL_MatchDomainName_idn(void)
1716+
{
1717+
EXPECT_DECLS;
1718+
#if !defined(NO_CERTS)
1719+
static const struct {
1720+
const char* pattern;
1721+
const char* host;
1722+
unsigned int flags;
1723+
int expected; /* 1 = match, 0 = no match */
1724+
const char* note;
1725+
} cases[] = {
1726+
/* Partial wildcard whose literal prefix overlaps "xn--" must NOT
1727+
* match an A-label hostname. */
1728+
{ "x*.example.com", "xn--rger-koa.example.com", 0, 0,
1729+
"partial wildcard vs A-label" },
1730+
/* Wildcard embedded inside an A-label pattern must NOT match. */
1731+
{ "xn--*.example.com", "xn--rger-koa.example.com", 0, 0,
1732+
"wildcard inside A-label pattern" },
1733+
/* Full left-most wildcard MUST NOT match an A-label hostname
1734+
* (RFC 9525 sec. 6.3 strengthens RFC 6125 SHOULD NOT to MUST NOT). */
1735+
{ "*.example.com", "xn--rger-koa.example.com", 0, 0,
1736+
"full wildcard vs A-label hostname" },
1737+
/* A-label appearing in an inner label still disables wildcard
1738+
* matching against the entire reference identifier. */
1739+
{ "*.example.com", "foo.xn--bar.example.com", 0, 0,
1740+
"wildcard with A-label in inner label" },
1741+
/* Case-insensitive A-label detection: "XN--" is also an A-label. */
1742+
{ "x*.example.com", "XN--rger-koa.example.com", 0, 0,
1743+
"uppercase A-label prefix" },
1744+
/* Control: full wildcard SHOULD continue to match plain ASCII. */
1745+
{ "*.example.com", "foo.example.com", 0, 1,
1746+
"wildcard matches non-IDN" },
1747+
/* Control: exact A-label match (no wildcard in pattern) must work. */
1748+
{ "xn--rger-koa.example.com", "xn--rger-koa.example.com", 0, 1,
1749+
"exact A-label match" },
1750+
/* Control: a label that merely begins with 'x' (not 'xn--') is not
1751+
* an A-label and must still wildcard-match. */
1752+
{ "*.example.com", "xyz.example.com", 0, 1,
1753+
"non-A-label x-prefix" },
1754+
/* Control: partial wildcard against a non-A-label still works. */
1755+
{ "x*.example.com", "xyz.example.com", 0, 1,
1756+
"partial wildcard non-IDN" },
1757+
1758+
/* Trailing-dot normalization: absolute-form FQDN ("example.com.")
1759+
* must match the same FQDN with or without the trailing dot, on
1760+
* either side of the comparison. RFC 1035 / RFC 6125. */
1761+
{ "example.com", "example.com.", 0, 1,
1762+
"trailing dot on host" },
1763+
{ "example.com.", "example.com", 0, 1,
1764+
"trailing dot on pattern" },
1765+
{ "example.com.", "example.com.", 0, 1,
1766+
"trailing dot on both" },
1767+
{ "*.example.com", "foo.example.com.", 0, 1,
1768+
"trailing dot on host with wildcard pattern" },
1769+
/* Trailing dot must not cause an A-label gate to misfire. */
1770+
{ "*.example.com", "xn--rger-koa.example.com.", 0, 0,
1771+
"trailing dot on A-label host" },
1772+
/* Same trailing-dot normalization under WOLFSSL_LEFT_MOST_WILDCARD_ONLY. */
1773+
{ "*.example.com", "foo.example.com.",
1774+
WOLFSSL_LEFT_MOST_WILDCARD_ONLY, 1,
1775+
"trailing dot, leftWildcardOnly" },
1776+
};
1777+
size_t i;
1778+
1779+
for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
1780+
int got = MatchDomainName(
1781+
cases[i].pattern, (int)XSTRLEN(cases[i].pattern),
1782+
cases[i].host, (word32)XSTRLEN(cases[i].host),
1783+
cases[i].flags);
1784+
ExpectIntEQ(got, cases[i].expected);
1785+
if (! EXPECT_SUCCESS()) {
1786+
fprintf(stderr,
1787+
"MatchDomainName(\"%s\", \"%s\", flags=0x%x) = %d, "
1788+
"expected %d (%s)\n",
1789+
cases[i].pattern, cases[i].host, cases[i].flags,
1790+
got, cases[i].expected, cases[i].note);
1791+
break;
1792+
}
1793+
}
1794+
#endif /* !NO_CERTS */
1795+
return EXPECT_RESULT();
1796+
}
1797+
17101798
int test_wolfSSL_X509_max_altnames(void)
17111799
{
17121800
EXPECT_DECLS;

tests/api/test_ossl_x509.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ int test_wolfSSL_X509_name_match1(void);
4949
int test_wolfSSL_X509_name_match2(void);
5050
int test_wolfSSL_X509_name_match3(void);
5151
int test_wolfssl_local_IsValidFQDN(void);
52+
int test_wolfSSL_MatchDomainName_idn(void);
5253
int test_wolfSSL_X509_max_altnames(void);
5354
int test_wolfSSL_X509_max_name_constraints(void);
5455
int test_wolfSSL_X509_check_ca(void);
@@ -81,6 +82,7 @@ int test_wolfSSL_X509_cmp(void);
8182
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_name_match2), \
8283
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_name_match3), \
8384
TEST_DECL_GROUP("ossl_x509", test_wolfssl_local_IsValidFQDN), \
85+
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_MatchDomainName_idn), \
8486
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_max_altnames), \
8587
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_max_name_constraints), \
8688
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_check_ca), \

wolfssl/internal.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2228,9 +2228,9 @@ WOLFSSL_LOCAL void FreeAsyncCtx(WOLFSSL* ssl, byte freeAsync);
22282228
WOLFSSL_LOCAL void FreeKeyExchange(WOLFSSL* ssl);
22292229
WOLFSSL_LOCAL void FreeSuites(WOLFSSL* ssl);
22302230
WOLFSSL_LOCAL int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, word32 totalSz);
2231-
WOLFSSL_LOCAL int MatchDomainName(const char* pattern, int len,
2232-
const char* str, word32 strLen,
2233-
unsigned int flags);
2231+
WOLFSSL_TEST_VIS int MatchDomainName(const char* pattern, int len,
2232+
const char* str, word32 strLen,
2233+
unsigned int flags);
22342234
#if !defined(NO_CERTS) && !defined(NO_ASN)
22352235
WOLFSSL_LOCAL int CheckForAltNames(DecodedCert* dCert, const char* domain,
22362236
word32 domainLen, int* checkCN,

0 commit comments

Comments
 (0)