Skip to content

Commit 80c9d3f

Browse files
authored
Merge pull request #10183 from douzzer/20260409-IsValidFQDN
20260409-IsValidFQDN
2 parents c0bc5ef + 6733e49 commit 80c9d3f

5 files changed

Lines changed: 195 additions & 0 deletions

File tree

src/internal.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13344,6 +13344,22 @@ int MatchDomainName(const char* pattern, int patternLen, const char* str,
1334413344
return 1;
1334513345
#endif
1334613346

13347+
if (leftWildcardOnly && (! wolfssl_local_IsValidFQDN(str, strLen))) {
13348+
/* Not a valid FQDN -- require byte-exact match, no case folding, no
13349+
* wildcard interpretation. This is appropriate for an IPv4 match, for
13350+
* example, but also matches improvised names like "localhost", albeit
13351+
* case-sensitively.
13352+
*/
13353+
return (((word32)patternLen == strLen) &&
13354+
(XMEMCMP(pattern, str, patternLen) == 0));
13355+
}
13356+
13357+
/* strip trailing dots if necessary (FQDN designator). */
13358+
if (str[strLen-1] == '.')
13359+
--strLen;
13360+
if (pattern[patternLen-1] == '.')
13361+
--patternLen;
13362+
1334713363
while (patternLen > 0) {
1334813364
/* Get the next pattern char to evaluate */
1334913365
char p = (char)XTOLOWER((unsigned char)*pattern);

tests/api/test_ossl_x509.c

Lines changed: 97 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,95 @@ 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+
16131710
int test_wolfSSL_X509_max_altnames(void)
16141711
{
16151712
EXPECT_DECLS;

tests/api/test_ossl_x509.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ 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);
5152
int test_wolfSSL_X509_max_altnames(void);
5253
int test_wolfSSL_X509_max_name_constraints(void);
5354
int test_wolfSSL_X509_check_ca(void);
@@ -79,6 +80,7 @@ int test_wolfSSL_X509_cmp(void);
7980
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_name_match1), \
8081
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_name_match2), \
8182
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_name_match3), \
83+
TEST_DECL_GROUP("ossl_x509", test_wolfssl_local_IsValidFQDN), \
8284
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_max_altnames), \
8385
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_max_name_constraints), \
8486
TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_check_ca), \

wolfcrypt/src/asn.c

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18027,6 +18027,81 @@ static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert)
1802718027

1802818028
#endif /* IGNORE_NAME_CONSTRAINTS */
1802918029

18030+
#if !defined(WOLFCRYPT_ONLY) && !defined(NO_CERTS)
18031+
/* Returns 1 if name is a syntactically valid DNS FQDN per RFC 952/1123.
18032+
*
18033+
* Rules enforced:
18034+
* - Total effective length (excluding optional trailing dot) in [1, 253]
18035+
* - Each label is 1-63 octets of [a-zA-Z0-9-], with _ allowed in all but
18036+
* the last label.
18037+
* - No label starts or ends with '-'
18038+
* - At least two labels (single-label names are not "fully qualified")
18039+
* - Final label (TLD) contains at least one letter (rejects all-numeric
18040+
* strings that could be confused with IPv4 literals, and matches the
18041+
* ICANN constraint that TLDs are alphabetic)
18042+
* - Optional trailing dot is accepted (absolute FQDN form)
18043+
* - Internationalized names are valid in their ACE/punycode (xn--) form
18044+
*/
18045+
int wolfssl_local_IsValidFQDN(const char* name, word32 nameSz)
18046+
{
18047+
word32 i;
18048+
int labelLen = 0;
18049+
int labelCount = 0;
18050+
int curLabelHasAlpha = 0;
18051+
int curLabelHasUnderscore = 0;
18052+
18053+
if (name == NULL || nameSz == 0)
18054+
return 0;
18055+
18056+
/* Strip a single optional trailing dot before measuring. "example.com."
18057+
* is the absolute form of the same FQDN.
18058+
*/
18059+
if (name[nameSz - 1] == '.')
18060+
--nameSz;
18061+
18062+
if (nameSz < 1 || nameSz > 253)
18063+
return 0;
18064+
18065+
for (i = 0; i < nameSz; i++) {
18066+
byte c = (byte)name[i];
18067+
18068+
if (c == '.') {
18069+
if (labelLen == 0 || name[i - 1] == '-')
18070+
return 0;
18071+
++labelCount;
18072+
labelLen = 0;
18073+
curLabelHasAlpha = 0;
18074+
curLabelHasUnderscore = 0;
18075+
continue;
18076+
}
18077+
18078+
if (++labelLen > 63)
18079+
return 0;
18080+
18081+
if (c == '-') {
18082+
if (labelLen == 1)
18083+
return 0;
18084+
}
18085+
else if (((c | 0x20) >= 'a') && ((c | 0x20) <= 'z')) {
18086+
curLabelHasAlpha = 1;
18087+
}
18088+
else if (c == '_') {
18089+
curLabelHasUnderscore = 1;
18090+
}
18091+
else if ((c < '0') || (c > '9')) {
18092+
return 0;
18093+
}
18094+
}
18095+
18096+
/* Final label (no trailing dot in the effective range to close it) */
18097+
if ((labelLen == 0) || (name[nameSz - 1] == '-') || curLabelHasUnderscore)
18098+
return 0;
18099+
++labelCount;
18100+
18101+
return ((labelCount > 1) && curLabelHasAlpha);
18102+
}
18103+
#endif /* !WOLFCRYPT_ONLY && !NO_CERTS */
18104+
1803018105
#ifdef WOLFSSL_ASN_TEMPLATE
1803118106
#if defined(WOLFSSL_SEP) || defined(WOLFSSL_FPKI)
1803218107
/* ASN.1 template for OtherName of an X.509 certificate.

wolfssl/wolfcrypt/asn.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3136,6 +3136,11 @@ WOLFSSL_TEST_VIS int wolfssl_local_MatchIpSubnet(const byte* ip, int ipSz,
31363136
int constraintSz);
31373137
#endif
31383138

3139+
#if !defined(WOLFCRYPT_ONLY) && !defined(NO_CERTS)
3140+
WOLFSSL_TEST_VIS int wolfssl_local_IsValidFQDN(const char* name,
3141+
word32 nameSz);
3142+
#endif
3143+
31393144
#if ((defined(HAVE_ED25519) && defined(HAVE_ED25519_KEY_IMPORT)) \
31403145
|| (defined(HAVE_CURVE25519) && defined(HAVE_CURVE25519_KEY_IMPORT)) \
31413146
|| (defined(HAVE_ED448) && defined(HAVE_ED448_KEY_IMPORT)) \

0 commit comments

Comments
 (0)