Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -13515,6 +13515,57 @@ static int PatternHasWildcardInALabel(const char* pattern, word32 patternLen)
return 0;
}

/* Validate the placement of a wildcard ('*') in a presented identifier per
* RFC 6125 sec. 6.4.3 / RFC 9525 sec. 6.3 and CA/Browser Forum Baseline
* Requirements sec. 3.2.2.6:
* - a wildcard may only appear in the left-most label of the pattern, and
* - a left-most label consisting solely of the wildcard ("*") may match only
* when at least two further labels (i.e. at least two dots) follow it.
*
* This rejects a bare "*" (matches any single-label name), "*.com" (wildcard
* immediately to the left of a registry/public suffix), and
* "foo.*.example.com" (wildcard not in the left-most label), while still
* accepting the legitimate "*.example.com" form. Partial left-most wildcards
* such as "a*" or "a*b*" retain their existing matching behavior - they are
* not bare wildcard labels and are not subject to the two-label requirement.
* pattern/patternLen must already have any single trailing FQDN dot stripped.
*
* Returns 1 if the pattern has no wildcard or its wildcard placement is
* acceptable, 0 otherwise. */
static int WildcardPlacementOK(const char* pattern, word32 patternLen)
{
word32 i;
int sawWildcard = 0;
int sawDot = 0;
int dots = 0;

for (i = 0; i < patternLen; i++) {
if (pattern[i] == '*') {
/* A wildcard is only permitted in the left-most label: reject any
* '*' that appears after a label separator. */
if (sawDot)
return 0;
sawWildcard = 1;
}
else if (pattern[i] == '.') {
sawDot = 1;
dots++;
}
}

if (!sawWildcard)
return 1;

/* A left-most label that is exactly "*" (a bare wildcard label) requires at
* least two further labels. This rejects a bare "*" (0 dots) and "*.tld"
* (1 dot) but still allows "*.example.com" (2 dots). */
if (pattern[0] == '*' && (patternLen == 1 || pattern[1] == '.') &&
dots < 2)
return 0;

return 1;
}

/* 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
Expand Down Expand Up @@ -13571,6 +13622,13 @@ int MatchDomainName(const char* pattern, int patternLen, const char* str,
}
}

/* RFC 6125 sec. 6.4.3 / RFC 9525 sec. 6.3 + CA/Browser Forum BR
* sec. 3.2.2.6: reject a pattern whose wildcard is not confined to the
* left-most label, or that has fewer than two labels to the right of the
* wildcard (e.g. "*", "*.com", "foo.*.example.com"). */
if (!WildcardPlacementOK(pattern, (word32)patternLen))
return 0;

while (patternLen > 0) {
/* Get the next pattern char to evaluate */
char p = (char)XTOLOWER((unsigned char)*pattern);
Expand Down
116 changes: 116 additions & 0 deletions src/x509_str.c
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,108 @@ static int X509StoreCertIsTrusted(WOLFSSL_X509_STORE* store,
return 0;
}

/* Enforce the BasicConstraints pathLenConstraint (RFC 5280 sec. 4.2.1.9 and
* the path validation rules in sec. 6.1.4 (l)/(m)) over the certification path
* assembled in ctx->chain.
*
* wolfSSL_X509_verify_cert() authenticates each certificate individually via
* the CertManager, which parses every certificate as CERT_TYPE. The issuer
* pathLen check in ParseCertRelative() is gated on a non-CERT_TYPE certificate
* type (it is reached on the TLS handshake path via CHAIN_CERT_TYPE), so the
* OpenSSL-compatibility path never enforced it. Re-create that check here over
* the completed path so that a CA asserting pathlen:N cannot issue more than N
* subordinate intermediate CAs.
*
* ctx->chain is ordered leaf first (index 0) up to the trust anchor (highest
* index). Walk from the trust anchor down toward the leaf, tracking the
* remaining number of non-self-issued intermediate certificates permitted.
* The budget is only enforced once some CA in the path actually asserts a
* pathLenConstraint; an explicit "haveConstraint" flag tracks that, so every
* value 0..WOLFSSL_MAX_PATH_LEN (the parser's hard cap on pathLenConstraint)
* is a usable budget rather than overloading the cap as a "no constraint"
* sentinel. The leaf (index 0) issues nothing and is therefore not subject to
* the constraint.
*
* Returns WOLFSSL_SUCCESS if the path satisfies every pathLenConstraint, or
* WOLFSSL_FAILURE (with ctx->error set) on the first violation. */
static int X509StoreCheckPathLen(WOLFSSL_X509_STORE_CTX* ctx)
{
int num;
int i;
word32 maxPathLen = 0;
byte haveConstraint = 0;
WOLFSSL_X509* anchor;

if (ctx == NULL || ctx->chain == NULL)
return WOLFSSL_SUCCESS;

num = wolfSSL_sk_X509_num(ctx->chain);
/* A pathLen violation requires at least one intermediate between the leaf
* (index 0) and the trust anchor, i.e. a chain of three or more. */
if (num < 3)
return WOLFSSL_SUCCESS;

/* The trust anchor (top of chain) is not part of the prospective
* certification path (RFC 5280 sec. 6.1): it does not consume path-length
* budget, and the loop below runs from num-2 down to 1 so the anchor is
* never processed as an intermediate. A self-signed anchor that asserts its
* own pathLenConstraint does still bound the path, matching
* ParseCertRelative()'s trust-anchor handling, so seed the budget from
* it. */
anchor = wolfSSL_sk_X509_value(ctx->chain, num - 1);
if (anchor != NULL && anchor->isCa && anchor->basicConstPlSet) {
maxPathLen = (word32)anchor->pathLength;
haveConstraint = 1;
}

for (i = num - 2; i >= 1; i--) {
WOLFSSL_X509* cert = wolfSSL_sk_X509_value(ctx->chain, i);
int selfIssued;

if (cert == NULL)
continue;

selfIssued =
(wolfSSL_X509_NAME_cmp(&cert->issuer, &cert->subject) == 0);

/* RFC 5280 sec. 6.1.4 (l): a non-self-issued *CA* certificate consumes
* one unit of the issuer's remaining path length budget. Gate on isCa
* to match ParseCertRelative() (wolfcrypt/src/asn.c) and the (m) step
* below, so a non-CA intermediate tolerated via verify_cb does not
* trigger a false PATH_LENGTH_EXCEEDED. Only meaningful once a CA above
* has asserted a constraint (haveConstraint). */
if (!selfIssued && cert->isCa && haveConstraint) {
if (maxPathLen == 0) {
SetupStoreCtxError_ex(ctx,
WOLFSSL_X509_V_ERR_PATH_LENGTH_EXCEEDED, i);
#if defined(OPENSSL_ALL) || defined(WOLFSSL_QT)
/* Allow an application verify callback to override, matching
* the INVALID_CA handling in wolfSSL_X509_verify_cert(). */
if (ctx->store != NULL && ctx->store->verify_cb != NULL &&
ctx->store->verify_cb(0, ctx) == 1) {
/* Overridden: keep walking without decrementing (budget is
* already exhausted). */
continue;
}
#endif
return WOLFSSL_FAILURE;
}
maxPathLen--;
}

/* RFC 5280 sec. 6.1.4 (m): tighten the budget with this CA's own
* pathLenConstraint, if present. The first constraint encountered seeds
* the budget; subsequent ones only ever lower it. */
if (cert->isCa && cert->basicConstPlSet &&
(!haveConstraint || (word32)cert->pathLength < maxPathLen)) {
maxPathLen = (word32)cert->pathLength;
haveConstraint = 1;
}
}

return WOLFSSL_SUCCESS;
}

/* Verifies certificate chain using WOLFSSL_X509_STORE_CTX
* returns 1 on success or <= 0 on failure.
*/
Expand Down Expand Up @@ -828,6 +930,13 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx)
* chain at a caller-trusted certificate. */
ctx->error = 0;
ret = WOLFSSL_SUCCESS;
/* The caller-trusted certificate terminates the path:
* it is the anchor, so stop here rather than falling
* through to the "finish building the chain" push below,
* which would add ctx->current_cert to ctx->chain a
* second time. Mirrors the self-issued terminus break
* above; the depth>0/done==0 success path accepts it. */
break;
} else {
X509VerifyCertSetupRetry(ctx, certs, failedCerts,
&depth, origDepth);
Expand Down Expand Up @@ -878,6 +987,13 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx)
ret = WOLFSSL_FAILURE;
}

/* RFC 5280 sec. 6.1.4: the per-certificate CertManager verification above
* does not enforce the issuer's BasicConstraints pathLenConstraint on this
* API path, so check it over the assembled path before reporting success. */
if (ret == WOLFSSL_SUCCESS) {
ret = X509StoreCheckPathLen(ctx);
}

exit:
/* Copy back failed certs. */
numFailedCerts = wolfSSL_sk_X509_num(failedCerts);
Expand Down
76 changes: 76 additions & 0 deletions tests/api/test_ossl_x509.c
Original file line number Diff line number Diff line change
Expand Up @@ -1795,6 +1795,82 @@ int test_wolfSSL_MatchDomainName_idn(void)
return EXPECT_RESULT();
}

/* Verify that MatchDomainName() enforces RFC 6125 sec. 6.4.3 / RFC 9525
* sec. 6.3 and CA/Browser Forum BR sec. 3.2.2.6 wildcard placement rules:
* a wildcard is confined to the left-most label, and a bare wildcard label
* ("*") requires at least two further labels. Regression test for the
* x509-limbo findings that "*", "*.com" and "foo.*.example.com" were matched.
*
* MatchDomainName() is exposed for testing via the visibility mechanism
* declared in wolfssl/internal.h. */
int test_wolfSSL_MatchDomainName_wildcard(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[] = {
/* --- The reported forbidden patterns must NOT match. --- */
/* Bare wildcard: matches any single-label name. */
{ "*", "com", 0, 0, "bare wildcard" },
{ "*", "anything", 0, 0,
"bare wildcard 2" },
/* Wildcard not in the left-most label. */
{ "foo.*.example.com", "foo.bar.example.com", 0, 0,
"wildcard in middle label" },
{ "foo.*.example.com", "foo.x.example.com", 0, 0,
"wildcard in middle label 2" },
/* Bare wildcard immediately left of a public/registry suffix. */
{ "*.com", "example.com", 0, 0,
"public-suffix wildcard" },
{ "*.com", "evil.com", 0, 0,
"public-suffix wildcard 2" },
/* Two label-spanning wildcards: the second is not left-most. */
{ "*.*.example.com", "a.b.example.com", 0, 0,
"second wildcard not left-most" },

/* --- Legitimate wildcards must still match. --- */
{ "*.example.com", "foo.example.com", 0, 1,
"single left-most wildcard" },
{ "*.example.com", "bar.example.com", 0, 1,
"single left-most wildcard 2" },
/* Two labels after the wildcard is sufficient; no public-suffix list
* is consulted (matching OpenSSL). */
{ "*.co.uk", "foo.co.uk", 0, 1,
"two labels after wildcard" },
/* Partial left-most wildcards retain their existing behavior and are
* not subject to the bare-wildcard two-label requirement. */
{ "a*.example.com", "abc.example.com", 0, 1,
"partial left-most wildcard" },
/* A wildcard never spans a label separator. */
{ "*.example.com", "foo.bar.example.com", 0, 0,
"wildcard does not cross a dot" },
};
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;
Expand Down
2 changes: 2 additions & 0 deletions tests/api/test_ossl_x509.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ 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_MatchDomainName_wildcard(void);
int test_wolfSSL_X509_max_altnames(void);
int test_wolfSSL_X509_max_name_constraints(void);
int test_wolfSSL_X509_check_ca(void);
Expand Down Expand Up @@ -83,6 +84,7 @@ int test_wolfSSL_X509_cmp(void);
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_MatchDomainName_wildcard), \
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), \
Expand Down
Loading
Loading