X509 validation fixes

This commit is contained in:
Tobias Frauenschläger
2026-06-18 15:17:01 -07:00
parent 675d5df2cd
commit cc78d130c4
5 changed files with 407 additions and 0 deletions
+58
View File
@@ -13542,6 +13542,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
@@ -13598,6 +13649,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);
+100
View File
@@ -629,6 +629,99 @@ 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.
* WOLFSSL_MAX_PATH_LEN is used as the "no constraint" sentinel, mirroring
* InitDecodedCert_ex()/ParseCertRelative(). 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 = WOLFSSL_MAX_PATH_LEN;
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. 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. The partial-chain branch of
* wolfSSL_X509_verify_cert() pushes the terminal certificate twice, so the
* anchor pointer can also appear at num-2; it is skipped by pointer below
* to avoid double-counting. */
anchor = wolfSSL_sk_X509_value(ctx->chain, num - 1);
if (anchor != NULL && anchor->isCa && anchor->basicConstPlSet)
maxPathLen = (word32)anchor->pathLength;
for (i = num - 2; i >= 1; i--) {
WOLFSSL_X509* cert = wolfSSL_sk_X509_value(ctx->chain, i);
int selfIssued;
if (cert == NULL || cert == anchor)
continue;
selfIssued =
(wolfSSL_X509_NAME_cmp(&cert->issuer, &cert->subject) == 0);
/* RFC 5280 sec. 6.1.4 (l): a non-self-issued certificate consumes one
* unit of the issuer's remaining path length budget. */
if (!selfIssued) {
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;
}
else if (maxPathLen != WOLFSSL_MAX_PATH_LEN) {
maxPathLen--;
}
}
/* RFC 5280 sec. 6.1.4 (m): tighten the budget with this CA's own
* pathLenConstraint, if present. */
if (cert->isCa && cert->basicConstPlSet &&
(word32)cert->pathLength < maxPathLen) {
maxPathLen = (word32)cert->pathLength;
}
}
return WOLFSSL_SUCCESS;
}
/* Verifies certificate chain using WOLFSSL_X509_STORE_CTX
* returns 1 on success or <= 0 on failure.
*/
@@ -878,6 +971,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);