Skip to content

Commit 54de419

Browse files
committed
Fix IDNA matching
1 parent 3181e2b commit 54de419

6 files changed

Lines changed: 364 additions & 3 deletions

File tree

src/internal.c

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13320,6 +13320,66 @@ static int MatchIPv6(const char* pattern, int patternLen,
1332013320
}
1332113321
#endif /* WOLFSSL_IP_ALT_NAME && !WOLFSSL_USER_IO */
1332213322

13323+
/* IDNA A-label prefix (Punycode-encoded internationalized labels), used to
13324+
* gate wildcard matching per RFC 6125 sec. 6.4.3 / RFC 9525 sec. 6.3. */
13325+
static int LabelIsALabel(const char* label, word32 labelLen)
13326+
{
13327+
if (labelLen < 4)
13328+
return 0;
13329+
return ((XTOLOWER((unsigned char)label[0]) == 'x') &&
13330+
(XTOLOWER((unsigned char)label[1]) == 'n') &&
13331+
(label[2] == '-') &&
13332+
(label[3] == '-'));
13333+
}
13334+
13335+
/* Returns 1 if any dot-separated label in name is an A-label. */
13336+
static int NameHasALabel(const char* name, word32 nameLen)
13337+
{
13338+
word32 labelStart = 0;
13339+
word32 i;
13340+
13341+
for (i = 0; i < nameLen; i++) {
13342+
if (name[i] == '.') {
13343+
if (LabelIsALabel(name + labelStart, i - labelStart))
13344+
return 1;
13345+
labelStart = i + 1;
13346+
}
13347+
}
13348+
if (labelStart < nameLen) {
13349+
if (LabelIsALabel(name + labelStart, nameLen - labelStart))
13350+
return 1;
13351+
}
13352+
return 0;
13353+
}
13354+
13355+
/* Returns 1 if any label of pattern that contains a wildcard ('*') is an
13356+
* A-label. RFC 6125 sec. 6.4.3 disallows wildcards embedded in A-labels. */
13357+
static int PatternHasWildcardInALabel(const char* pattern, word32 patternLen)
13358+
{
13359+
word32 labelStart = 0;
13360+
int labelHasWildcard = 0;
13361+
word32 i;
13362+
13363+
for (i = 0; i < patternLen; i++) {
13364+
if (pattern[i] == '.') {
13365+
if (labelHasWildcard &&
13366+
LabelIsALabel(pattern + labelStart, i - labelStart)) {
13367+
return 1;
13368+
}
13369+
labelStart = i + 1;
13370+
labelHasWildcard = 0;
13371+
}
13372+
else if (pattern[i] == '*') {
13373+
labelHasWildcard = 1;
13374+
}
13375+
}
13376+
if (labelHasWildcard &&
13377+
LabelIsALabel(pattern + labelStart, patternLen - labelStart)) {
13378+
return 1;
13379+
}
13380+
return 0;
13381+
}
13382+
1332313383
/* Match names with wildcards, each wildcard can represent a single name
1332413384
component or fragment but not multiple names, i.e.,
1332513385
*.z.com matches y.z.com but not x.y.z.com
@@ -13344,6 +13404,38 @@ int MatchDomainName(const char* pattern, int patternLen, const char* str,
1334413404
return 1;
1334513405
#endif
1334613406

13407+
if (leftWildcardOnly && (! wolfssl_local_IsValidFQDN(str, strLen))) {
13408+
/* Not a valid FQDN -- require byte-exact match, no case folding, no
13409+
* wildcard interpretation. This is appropriate for an IPv4 match, for
13410+
* example, but also matches improvised names like "localhost", albeit
13411+
* case-sensitively.
13412+
*/
13413+
return (((word32)patternLen == strLen) &&
13414+
(XMEMCMP(pattern, str, patternLen) == 0));
13415+
}
13416+
13417+
/* strip trailing dots if necessary (FQDN designator). */
13418+
if (str[strLen-1] == '.')
13419+
--strLen;
13420+
if (pattern[patternLen-1] == '.')
13421+
--patternLen;
13422+
13423+
/* RFC 6125 sec. 6.4.3 / RFC 9525 sec. 6.3: do not perform wildcard
13424+
* matching when the pattern has a wildcard embedded in an A-label, nor
13425+
* when the reference identifier (hostname) contains any A-label. The
13426+
* existing single-label glob would otherwise match across the
13427+
* Punycode-encoded form (e.g., "x*.example.com" matching
13428+
* "xn--rger-koa.example.com"), which has no semantic meaning. */
13429+
if (PatternHasWildcardInALabel(pattern, (word32)patternLen))
13430+
return 0;
13431+
if (NameHasALabel(str, strLen)) {
13432+
int i;
13433+
for (i = 0; i < patternLen; i++) {
13434+
if (pattern[i] == '*')
13435+
return 0;
13436+
}
13437+
}
13438+
1334713439
while (patternLen > 0) {
1334813440
/* Get the next pattern char to evaluate */
1334913441
char p = (char)XTOLOWER((unsigned char)*pattern);

tests/api/test_ossl_x509.c

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,11 +1081,19 @@ int test_wolfSSL_X509_check_ip_asc(void)
10811081
ExpectIntEQ(wolfSSL_X509_check_ip_asc(cn_lit, "127.0.0.1", 0), 0);
10821082
/* CN=*.0.0.1 with no SAN must NOT wildcard-match "127.0.0.1". */
10831083
ExpectIntEQ(wolfSSL_X509_check_ip_asc(cn_wild, "127.0.0.1", 0), 0);
1084+
10841085
/* CN-based hostname matching must still work for hostname checks
10851086
* (sanity check that the fix didn't over-correct). */
10861087
ExpectIntEQ(wolfSSL_X509_check_host(cn_wild, "1.0.0.1",
10871088
XSTRLEN("1.0.0.1"), 0, NULL), 1);
10881089

1090+
/* However, when WOLFSSL_LEFT_MOST_WILDCARD_ONLY, CN-based hostname
1091+
* matching must not apply wildcards when the supplied hostname isn't a
1092+
* well-formed FQDN.
1093+
*/
1094+
ExpectIntEQ(wolfSSL_X509_check_host(cn_wild, "1.0.0.1",
1095+
XSTRLEN("1.0.0.1"), WOLFSSL_LEFT_MOST_WILDCARD_ONLY, NULL), 0);
1096+
10891097
wolfSSL_X509_free(cn_wild);
10901098
wolfSSL_X509_free(cn_lit);
10911099
}
@@ -1610,6 +1618,183 @@ int test_wolfSSL_X509_name_match3(void)
16101618
return EXPECT_RESULT();
16111619
}
16121620

1621+
int test_wolfssl_local_IsValidFQDN(void) {
1622+
EXPECT_DECLS;
1623+
#if !defined(NO_ASN) && !defined(WOLFCRYPT_ONLY) && !defined(NO_CERTS)
1624+
static const struct { const char *str; int is_FQDN; } test_cases[] = {
1625+
{"example.com", 1},
1626+
{"example.com.", 1}, /* trailing dot (absolute form) */
1627+
{"sub.example.com", 1},
1628+
{"a.b", 1}, /* minimal two-label */
1629+
{"xn--nxasmq5b.com", 1}, /* punycode / IDN (ACE form) */
1630+
{"test_underscore.example.com", 1}, /* underscore in non-TLD label */
1631+
{"_leading.example.com", 1}, /* underscore at start of label */
1632+
{"trailing_.example.com", 1},/* underscore at end of non-TLD label */
1633+
{"123.numericlabel.example.com", 1}, /* numeric labels are fine */
1634+
{"example.12a3", 1}, /* TLD with letters + digits */
1635+
{"ex--ample.com", 1}, /* double hyphen inside label (allowed) */
1636+
{"A.B.C", 1}, /* uppercase OK (case-insensitive rules) */
1637+
1638+
{"example", 0}, /* single label (not fully qualified) */
1639+
{"example.", 0}, /* becomes single label after dot strip */
1640+
{".example.com", 0}, /* leading dot -- empty first label */
1641+
{"example..com", 0}, /* empty label (consecutive dots) */
1642+
{"-example.com", 0}, /* label starts with '-' */
1643+
{"example-.com", 0}, /* label ends with '-' */
1644+
{"example.com-", 0}, /* final label ends with '-' */
1645+
{"example.com_", 0}, /* underscore in TLD (forbidden) */
1646+
{"example._com", 0}, /* underscore in TLD (forbidden) */
1647+
{"ex@mple.com", 0}, /* illegal character '@' */
1648+
{"example com.com", 0}, /* illegal character ' ' */
1649+
{"", 0}, /* empty string */
1650+
{NULL, 0}, /* NULL pointer */
1651+
{"com", 0}, /* single label */
1652+
{"123.456", 0}, /* all-numeric final label (no alpha) */
1653+
{"example.123", 0}, /* all-numeric TLD (no alpha) */
1654+
{"a", 0}, /* single label, too short */
1655+
{"example.123a", 1}, /* TLD with at least one letter -- valid */
1656+
};
1657+
1658+
int i;
1659+
for (i = 0; i < (int)(sizeof(test_cases) / sizeof(test_cases[0])); i++) {
1660+
ExpectIntEQ(wolfssl_local_IsValidFQDN(
1661+
test_cases[i].str,
1662+
test_cases[i].str ? (word32)strlen(test_cases[i].str) : 0),
1663+
test_cases[i].is_FQDN);
1664+
if (! EXPECT_SUCCESS()) {
1665+
fprintf(stderr, "wolfssl_local_IsValidFQDN() wrong result for "
1666+
"case %d \"%s\"\n", i, test_cases[i].str);
1667+
break;
1668+
}
1669+
}
1670+
1671+
/* Additional corner cases (length & label-size boundaries) */
1672+
{
1673+
char buf[300];
1674+
1675+
/* 253 chars (max allowed), with 63 byte labels (max allowed) - valid */
1676+
memset(buf, 'a', 251);
1677+
for (i=63; i < 251; i+=64)
1678+
buf[i] = '.';
1679+
buf[251] = '.';
1680+
buf[252] = 'b';
1681+
buf[253] = '\0';
1682+
ExpectIntEQ(wolfssl_local_IsValidFQDN(buf, (word32)strlen(buf)), 1);
1683+
1684+
/* 254 chars (one too long) - invalid */
1685+
memset(buf, 'a', 252);
1686+
for (i=63; i < 251; i+=64)
1687+
buf[i] = '.';
1688+
buf[252] = '.';
1689+
buf[253] = 'b';
1690+
buf[254] = '\0';
1691+
ExpectIntEQ(wolfssl_local_IsValidFQDN(buf, (word32)strlen(buf)), 0);
1692+
1693+
/* 64-char label (one too long) */
1694+
memset(buf, 'a', 64);
1695+
buf[64] = '.';
1696+
buf[65] = 'c';
1697+
buf[66] = 'o';
1698+
buf[67] = 'm';
1699+
buf[68] = '\0';
1700+
ExpectIntEQ(wolfssl_local_IsValidFQDN(buf, (word32)strlen(buf)), 0);
1701+
1702+
/* Explicit nameSz == 0 (even with non-NULL pointer) */
1703+
ExpectIntEQ(wolfssl_local_IsValidFQDN("example.com", 0), 0);
1704+
}
1705+
1706+
#endif /* !NO_ASN && !WOLFCRYPT_ONLY && !NO_CERTS */
1707+
return EXPECT_RESULT();
1708+
}
1709+
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+
16131798
int test_wolfSSL_X509_max_altnames(void)
16141799
{
16151800
EXPECT_DECLS;

tests/api/test_ossl_x509.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ int test_wolfSSL_X509_bad_altname(void);
4848
int test_wolfSSL_X509_name_match1(void);
4949
int test_wolfSSL_X509_name_match2(void);
5050
int test_wolfSSL_X509_name_match3(void);
51+
int test_wolfssl_local_IsValidFQDN(void);
52+
int test_wolfSSL_MatchDomainName_idn(void);
5153
int test_wolfSSL_X509_max_altnames(void);
5254
int test_wolfSSL_X509_max_name_constraints(void);
5355
int test_wolfSSL_X509_check_ca(void);
@@ -79,6 +81,8 @@ int test_wolfSSL_X509_cmp(void);
7981
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_name_match1), \
8082
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_name_match2), \
8183
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_name_match3), \
84+
TEST_DECL_GROUP("ossl_x509", test_wolfssl_local_IsValidFQDN), \
85+
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_MatchDomainName_idn), \
8286
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_max_altnames), \
8387
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_max_name_constraints), \
8488
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_check_ca), \

0 commit comments

Comments
 (0)