From 89a1b8c7a7ad6b5b54e459a985b7398ce8e56f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 18 Jun 2026 15:17:01 -0700 Subject: [PATCH 1/4] X509 validation fixes --- src/internal.c | 58 +++++++++++ src/x509_str.c | 100 +++++++++++++++++++ tests/api/test_ossl_x509.c | 76 +++++++++++++++ tests/api/test_ossl_x509.h | 2 + tests/api/test_ossl_x509_str.c | 171 +++++++++++++++++++++++++++++++++ 5 files changed, 407 insertions(+) diff --git a/src/internal.c b/src/internal.c index fed9d370de..f450284ccf 100644 --- a/src/internal.c +++ b/src/internal.c @@ -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 @@ -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); diff --git a/src/x509_str.c b/src/x509_str.c index 9d1d1f1107..ad160afd47 100644 --- a/src/x509_str.c +++ b/src/x509_str.c @@ -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); diff --git a/tests/api/test_ossl_x509.c b/tests/api/test_ossl_x509.c index 30e9f82b20..cf516bb367 100644 --- a/tests/api/test_ossl_x509.c +++ b/tests/api/test_ossl_x509.c @@ -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; diff --git a/tests/api/test_ossl_x509.h b/tests/api/test_ossl_x509.h index f2844092af..5034a87351 100644 --- a/tests/api/test_ossl_x509.h +++ b/tests/api/test_ossl_x509.h @@ -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); @@ -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), \ diff --git a/tests/api/test_ossl_x509_str.c b/tests/api/test_ossl_x509_str.c index 01deabf8e1..145a264fa0 100644 --- a/tests/api/test_ossl_x509_str.c +++ b/tests/api/test_ossl_x509_str.c @@ -957,6 +957,173 @@ static int test_wolfSSL_X509_STORE_CTX_ex12(void) #endif #endif +#if defined(OPENSSL_EXTRA) && !defined(NO_CERTS) && defined(HAVE_ECC) +/* Regression test for the x509-limbo "pathlen" finding: + * wolfSSL_X509_verify_cert() must enforce the BasicConstraints + * pathLenConstraint (RFC 5280 sec. 4.2.1.9 / sec. 6.1.4). A CA asserting + * pathlen:0 may only issue end-entity certificates; it must not be permitted + * to issue a further intermediate CA. + * + * root -> ica0 (CA, pathlen:0) -> ica1 (CA, pathlen:0) -> leaf + * + * ica1 is an intermediate CA following ica0, which ica0's pathlen:0 forbids, + * so the chain must be rejected with X509_V_ERR_PATH_LENGTH_EXCEEDED. Before + * the fix this OpenSSL-compatibility path accepted the chain because each + * certificate was verified individually (as CERT_TYPE) without the issuer + * pathLen check the TLS handshake path performs. */ +static X509* pathlen_pem_to_x509(const char* pem) +{ + X509* x = NULL; + BIO* bio = BIO_new_mem_buf(pem, -1); + if (bio != NULL) { + x = PEM_read_bio_X509(bio, NULL, NULL, NULL); + BIO_free(bio); + } + return x; +} + +static int test_wolfSSL_X509_verify_cert_pathlen(void) +{ + EXPECT_DECLS; + /* x509-limbo intermediate-pathlen-0 chain (NIST P-256). */ + static const char* root_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIBjzCCATWgAwIBAgIUBr1M7Bi+lJsTuNKLX1Me8do6eaQwCgYIKoZIzj0EAwIw\n" + "GjEYMBYGA1UEAwwPeDUwOS1saW1iby1yb290MCAXDTcwMDEwMTAwMDAwMVoYDzI5\n" + "NjkwNTAzMDAwMDAxWjAaMRgwFgYDVQQDDA94NTA5LWxpbWJvLXJvb3QwWTATBgcq\n" + "hkjOPQIBBggqhkjOPQMBBwNCAAQbEiFksWbYAbT6XaE4bwPlfA9TBdDVohu/uCIL\n" + "xP51Zj39ZQijgxN9jxXyvCgo8Of/x5M0IHSUuc17eaBD+EEbo1cwVTAPBgNVHRMB\n" + "Af8EBTADAQH/MAsGA1UdDwQEAwIBBjAWBgNVHREEDzANggtleGFtcGxlLmNvbTAd\n" + "BgNVHQ4EFgQUonHZJ38dXr7snblbNqaxj6KZwOYwCgYIKoZIzj0EAwIDSAAwRQIh\n" + "ANxM3gjJ/3FIvHLgFt9MlFXDUZs38+f+90xZ+UWEtg/sAiBwDNrH5LaFE+kGvHih\n" + "aXu0ueUy9q+2v7FMk1DqDF5QMA==\n" + "-----END CERTIFICATE-----\n"; + static const char* ica0_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIB/zCCAaWgAwIBAgIUWQmq3hQYYc2aAeHbkAhBu2TswggwCgYIKoZIzj0EAwIw\n" + "GjEYMBYGA1UEAwwPeDUwOS1saW1iby1yb290MCAXDTcwMDEwMTAwMDAwMVoYDzI5\n" + "NjkwNTAzMDAwMDAxWjBmMTgwNgYDVQQLDC8zODQ3NTQ4NjM0MDcyNTc1NDQyNDQy\n" + "MTQxNDk1NTczNzU0OTc0NTAyNTYxMjE5NjEqMCgGA1UEAwwheDUwOS1saW1iby1p\n" + "bnRlcm1lZGlhdGUtcGF0aGxlbi0wMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\n" + "htjb5R5OipW7n8rfvqA7yP/yxpdq9QzwSxC41RjoE//1SX16xsjOAIOYu/1L8iEq\n" + "1Y6x7yBnkoFFpWW0R2JN0aN7MHkwEgYDVR0TAQH/BAgwBgEB/wIBADALBgNVHQ8E\n" + "BAMCAgQwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20wHwYDVR0jBBgwFoAUonHZJ38d\n" + "Xr7snblbNqaxj6KZwOYwHQYDVR0OBBYEFI5psdjr2bzbP8AYzh747r3zKXjzMAoG\n" + "CCqGSM49BAMCA0gAMEUCIQChdmrAmCCIGBKdR31PSCKcBFkhqIg1rFH5n9ISC0XT\n" + "MwIgBpm/FQrFVRPNXUq3Pjp5nCCQNuusc/UF0WQtogoxMcs=\n" + "-----END CERTIFICATE-----\n"; + static const char* ica1_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIICTDCCAfKgAwIBAgIUUzHoF6QQi7Q1abKHM2OkqXAyhXAwCgYIKoZIzj0EAwIw\n" + "ZjE4MDYGA1UECwwvMzg0NzU0ODYzNDA3MjU3NTQ0MjQ0MjE0MTQ5NTU3Mzc1NDk3\n" + "NDUwMjU2MTIxOTYxKjAoBgNVBAMMIXg1MDktbGltYm8taW50ZXJtZWRpYXRlLXBh\n" + "dGhsZW4tMDAgFw03MDAxMDEwMDAwMDFaGA8yOTY5MDUwMzAwMDAwMVowZzE5MDcG\n" + "A1UECwwwNTA4MzE1NzY5OTY4MTEzNTA3Njc4NzYzNDMxNDk4OTE0NTU3NTI4MDQ4\n" + "OTc2MzkyMSowKAYDVQQDDCF4NTA5LWxpbWJvLWludGVybWVkaWF0ZS1wYXRobGVu\n" + "LTAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQJAU5C0BQNKWv1lpCWYvrbguNZ\n" + "7Ru3850SYzxAHVdqoqpXQqz1rxMGkIIQzNk1GhUXbMUVbQD0jvaJcoGfkNO5o3sw\n" + "eTASBgNVHRMBAf8ECDAGAQH/AgEAMAsGA1UdDwQEAwICBDAWBgNVHREEDzANggtl\n" + "eGFtcGxlLmNvbTAfBgNVHSMEGDAWgBSOabHY69m82z/AGM4e+O698yl48zAdBgNV\n" + "HQ4EFgQUCRY9Zhvn1ujILJxVlXN6ngBEGUcwCgYIKoZIzj0EAwIDSAAwRQIhAPWq\n" + "eItvUILeT5ZV1sA/2T2KXLmhO+lyaIJKbayTWTluAiBAnlFFqQhLRPC9aXnmvzld\n" + "4OQO4zOBVTRVR1fyTaGRLA==\n" + "-----END CERTIFICATE-----\n"; + static const char* leaf_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIB/jCCAaOgAwIBAgIUUgfgeS9xej4Xq/a8Bg2Dzj3MedEwCgYIKoZIzj0EAwIw\n" + "ZzE5MDcGA1UECwwwNTA4MzE1NzY5OTY4MTEzNTA3Njc4NzYzNDMxNDk4OTE0NTU3\n" + "NTI4MDQ4OTc2MzkyMSowKAYDVQQDDCF4NTA5LWxpbWJvLWludGVybWVkaWF0ZS1w\n" + "YXRobGVuLTAwIBcNNzAwMTAxMDAwMDAxWhgPMjk2OTA1MDMwMDAwMDFaMBYxFDAS\n" + "BgNVBAMMC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERG/C\n" + "Q3diuQGNPeztXUpMthR9/695MnMj/tpF6NHkEBO91bBKFdjhnievo6XnpfEOer/z\n" + "nHvUEwH3UH7swoP3qKN8MHowHQYDVR0OBBYEFMorXKe6f7/o5tnf5iacR/PPM9F3\n" + "MB8GA1UdIwQYMBaAFAkWPWYb59boyCycVZVzep4ARBlHMAsGA1UdDwQEAwIHgDAT\n" + "BgNVHSUEDDAKBggrBgEFBQcDATAWBgNVHREEDzANggtleGFtcGxlLmNvbTAKBggq\n" + "hkjOPQQDAgNJADBGAiEAjQovFZD9svq8vGyuCa82Cq3/YeoHkDyyRalhv4BV7X8C\n" + "IQDqXCv7h0gMIVsWOSef8zu4DubHxn7Icm7DwJg0O2lSuw==\n" + "-----END CERTIFICATE-----\n"; + X509* root = NULL; + X509* ica0 = NULL; + X509* ica1 = NULL; + X509* leaf = NULL; + X509_STORE* store = NULL; + X509_STORE_CTX* ctx = NULL; + STACK_OF(X509)* inter = NULL; + + ExpectNotNull(root = pathlen_pem_to_x509(root_pem)); + ExpectNotNull(ica0 = pathlen_pem_to_x509(ica0_pem)); + ExpectNotNull(ica1 = pathlen_pem_to_x509(ica1_pem)); + ExpectNotNull(leaf = pathlen_pem_to_x509(leaf_pem)); + + ExpectNotNull(store = X509_STORE_new()); + ExpectIntEQ(X509_STORE_add_cert(store, root), 1); + ExpectNotNull(inter = sk_X509_new_null()); + ExpectIntGT(sk_X509_push(inter, ica0), 0); + ExpectIntGT(sk_X509_push(inter, ica1), 0); + + ExpectNotNull(ctx = X509_STORE_CTX_new()); + ExpectIntEQ(X509_STORE_CTX_init(ctx, store, leaf, inter), 1); + /* Must be rejected: ica1 violates ica0's pathlen:0 constraint. */ + ExpectIntNE(X509_verify_cert(ctx), 1); + ExpectIntEQ(X509_STORE_CTX_get_error(ctx), + X509_V_ERR_PATH_LENGTH_EXCEEDED); + + X509_STORE_CTX_free(ctx); + X509_STORE_free(store); + sk_X509_free(inter); + X509_free(root); + X509_free(ica0); + X509_free(ica1); + X509_free(leaf); + return EXPECT_RESULT(); +} + +#if !defined(NO_FILESYSTEM) && !defined(NO_RSA) +/* Positive control: a legitimate chain whose intermediates assert pathlen:1 + * must still verify, guarding the pathLen enforcement against over-rejection. + * + * ca-cert -> ca-int (pathlen:1) -> ca-int2 (pathlen:1) -> server-chain + * + * ca-int permits one following intermediate (ca-int2), so the chain is valid. */ +static int test_wolfSSL_X509_verify_cert_pathlen_ok(void) +{ + EXPECT_DECLS; + X509* ca = NULL; + X509* caInt = NULL; + X509* caInt2 = NULL; + X509* leaf = NULL; + X509_STORE* store = NULL; + X509_STORE_CTX* ctx = NULL; + + ExpectNotNull(ca = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/ca-cert.pem")); + ExpectNotNull(caInt = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/intermediate/ca-int-cert.pem")); + ExpectNotNull(caInt2 = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/intermediate/ca-int2-cert.pem")); + ExpectNotNull(leaf = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/intermediate/server-chain.pem")); + + ExpectNotNull(store = X509_STORE_new()); + ExpectIntEQ(X509_STORE_add_cert(store, ca), 1); + ExpectIntEQ(X509_STORE_add_cert(store, caInt), 1); + ExpectIntEQ(X509_STORE_add_cert(store, caInt2), 1); + ExpectNotNull(ctx = X509_STORE_CTX_new()); + ExpectIntEQ(X509_STORE_CTX_init(ctx, store, leaf, NULL), 1); + ExpectIntEQ(X509_verify_cert(ctx), 1); + ExpectIntEQ(X509_STORE_CTX_get_error(ctx), X509_V_OK); + + X509_STORE_CTX_free(ctx); + X509_STORE_free(store); + X509_free(ca); + X509_free(caInt); + X509_free(caInt2); + X509_free(leaf); + return EXPECT_RESULT(); +} +#endif /* !NO_FILESYSTEM && !NO_RSA */ +#endif /* OPENSSL_EXTRA && !NO_CERTS && HAVE_ECC */ + int test_wolfSSL_X509_STORE_CTX_ex(void) { EXPECT_DECLS; @@ -1002,6 +1169,10 @@ int test_wolfSSL_X509_STORE_CTX_ex(void) &testData), 1); #ifdef HAVE_ECC ExpectIntEQ(test_wolfSSL_X509_STORE_CTX_ex12(), 1); + ExpectIntEQ(test_wolfSSL_X509_verify_cert_pathlen(), 1); +#if !defined(NO_FILESYSTEM) && !defined(NO_RSA) + ExpectIntEQ(test_wolfSSL_X509_verify_cert_pathlen_ok(), 1); +#endif #endif if(testData.x509Ca) { From 4888ac6319bc32d19bbdcdbe6dca7fd654ad5f40 Mon Sep 17 00:00:00 2001 From: Kareem Date: Thu, 18 Jun 2026 15:18:26 -0700 Subject: [PATCH 2/4] Refactor to allow maxPathLen set to WOLFSSL_MAX_PATH_LEN. --- src/x509_str.c | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/x509_str.c b/src/x509_str.c index ad160afd47..40ea6f5e78 100644 --- a/src/x509_str.c +++ b/src/x509_str.c @@ -644,9 +644,12 @@ static int X509StoreCertIsTrusted(WOLFSSL_X509_STORE* store, * 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. + * 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. */ @@ -654,7 +657,8 @@ static int X509StoreCheckPathLen(WOLFSSL_X509_STORE_CTX* ctx) { int num; int i; - word32 maxPathLen = WOLFSSL_MAX_PATH_LEN; + word32 maxPathLen = 0; + byte haveConstraint = 0; WOLFSSL_X509* anchor; if (ctx == NULL || ctx->chain == NULL) @@ -675,8 +679,10 @@ static int X509StoreCheckPathLen(WOLFSSL_X509_STORE_CTX* ctx) * 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) + 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); @@ -689,8 +695,9 @@ static int X509StoreCheckPathLen(WOLFSSL_X509_STORE_CTX* ctx) (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) { + * unit of the issuer's remaining path length budget. Only meaningful + * once a CA above has asserted a constraint (haveConstraint). */ + if (!selfIssued && haveConstraint) { if (maxPathLen == 0) { SetupStoreCtxError_ex(ctx, WOLFSSL_X509_V_ERR_PATH_LENGTH_EXCEEDED, i); @@ -706,16 +713,16 @@ static int X509StoreCheckPathLen(WOLFSSL_X509_STORE_CTX* ctx) #endif return WOLFSSL_FAILURE; } - else if (maxPathLen != WOLFSSL_MAX_PATH_LEN) { - maxPathLen--; - } + maxPathLen--; } /* RFC 5280 sec. 6.1.4 (m): tighten the budget with this CA's own - * pathLenConstraint, if present. */ + * pathLenConstraint, if present. The first constraint encountered seeds + * the budget; subsequent ones only ever lower it. */ if (cert->isCa && cert->basicConstPlSet && - (word32)cert->pathLength < maxPathLen) { + (!haveConstraint || (word32)cert->pathLength < maxPathLen)) { maxPathLen = (word32)cert->pathLength; + haveConstraint = 1; } } From 077a2db9e5ff15d3551cbc8448b5c4bf833d356b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 19 Jun 2026 10:12:48 +0200 Subject: [PATCH 3/4] x509_str.c: fix partial-chain double-push and rework pathLen tests - Break out of the chain-build loop after the partial-chain fallback accepts a caller-trusted terminus, so it is pushed to ctx->chain once instead of twice; X509StoreCheckPathLen's anchor-skip is now defensive, not load-bearing. - Drop the now-dead cert == anchor guard and refresh the comment. - Rework the pathLen regression tests: reuse the existing certs/test-pathlen chains (chainF rejects, chainB verifies) instead of inlined report certs. --- src/x509_str.c | 20 ++- tests/api/test_ossl_x509_str.c | 286 ++++++++++++++++++++------------- tests/api/test_ossl_x509_str.h | 12 ++ 3 files changed, 199 insertions(+), 119 deletions(-) diff --git a/src/x509_str.c b/src/x509_str.c index 40ea6f5e78..b459300605 100644 --- a/src/x509_str.c +++ b/src/x509_str.c @@ -672,12 +672,11 @@ static int X509StoreCheckPathLen(WOLFSSL_X509_STORE_CTX* ctx) /* 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. */ + * 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; @@ -688,7 +687,7 @@ static int X509StoreCheckPathLen(WOLFSSL_X509_STORE_CTX* ctx) WOLFSSL_X509* cert = wolfSSL_sk_X509_value(ctx->chain, i); int selfIssued; - if (cert == NULL || cert == anchor) + if (cert == NULL) continue; selfIssued = @@ -928,6 +927,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); diff --git a/tests/api/test_ossl_x509_str.c b/tests/api/test_ossl_x509_str.c index 145a264fa0..85b4b3fb5c 100644 --- a/tests/api/test_ossl_x509_str.c +++ b/tests/api/test_ossl_x509_str.c @@ -957,113 +957,52 @@ static int test_wolfSSL_X509_STORE_CTX_ex12(void) #endif #endif -#if defined(OPENSSL_EXTRA) && !defined(NO_CERTS) && defined(HAVE_ECC) /* Regression test for the x509-limbo "pathlen" finding: * wolfSSL_X509_verify_cert() must enforce the BasicConstraints * pathLenConstraint (RFC 5280 sec. 4.2.1.9 / sec. 6.1.4). A CA asserting * pathlen:0 may only issue end-entity certificates; it must not be permitted - * to issue a further intermediate CA. + * to issue a further intermediate CA. Reuses the certs/test-pathlen chains + * (see certs/test-pathlen/assemble-chains.sh): * - * root -> ica0 (CA, pathlen:0) -> ica1 (CA, pathlen:0) -> leaf + * ca-cert -> chainF-ICA2 (CA, pathlen:0) -> chainF-ICA1 (CA) -> entity * - * ica1 is an intermediate CA following ica0, which ica0's pathlen:0 forbids, - * so the chain must be rejected with X509_V_ERR_PATH_LENGTH_EXCEEDED. Before - * the fix this OpenSSL-compatibility path accepted the chain because each - * certificate was verified individually (as CERT_TYPE) without the issuer - * pathLen check the TLS handshake path performs. */ -static X509* pathlen_pem_to_x509(const char* pem) -{ - X509* x = NULL; - BIO* bio = BIO_new_mem_buf(pem, -1); - if (bio != NULL) { - x = PEM_read_bio_X509(bio, NULL, NULL, NULL); - BIO_free(bio); - } - return x; -} - -static int test_wolfSSL_X509_verify_cert_pathlen(void) + * chainF-ICA1 is an intermediate CA following the pathlen:0 chainF-ICA2, which + * RFC 5280 sec. 6.1.4 forbids, so the chain must be rejected with + * X509_V_ERR_PATH_LENGTH_EXCEEDED. Before the fix this OpenSSL-compatibility + * path accepted the chain because each certificate was verified individually + * (as CERT_TYPE) without the issuer pathLen check the TLS handshake path + * performs. */ +int test_wolfSSL_X509_verify_cert_pathlen(void) { EXPECT_DECLS; - /* x509-limbo intermediate-pathlen-0 chain (NIST P-256). */ - static const char* root_pem = - "-----BEGIN CERTIFICATE-----\n" - "MIIBjzCCATWgAwIBAgIUBr1M7Bi+lJsTuNKLX1Me8do6eaQwCgYIKoZIzj0EAwIw\n" - "GjEYMBYGA1UEAwwPeDUwOS1saW1iby1yb290MCAXDTcwMDEwMTAwMDAwMVoYDzI5\n" - "NjkwNTAzMDAwMDAxWjAaMRgwFgYDVQQDDA94NTA5LWxpbWJvLXJvb3QwWTATBgcq\n" - "hkjOPQIBBggqhkjOPQMBBwNCAAQbEiFksWbYAbT6XaE4bwPlfA9TBdDVohu/uCIL\n" - "xP51Zj39ZQijgxN9jxXyvCgo8Of/x5M0IHSUuc17eaBD+EEbo1cwVTAPBgNVHRMB\n" - "Af8EBTADAQH/MAsGA1UdDwQEAwIBBjAWBgNVHREEDzANggtleGFtcGxlLmNvbTAd\n" - "BgNVHQ4EFgQUonHZJ38dXr7snblbNqaxj6KZwOYwCgYIKoZIzj0EAwIDSAAwRQIh\n" - "ANxM3gjJ/3FIvHLgFt9MlFXDUZs38+f+90xZ+UWEtg/sAiBwDNrH5LaFE+kGvHih\n" - "aXu0ueUy9q+2v7FMk1DqDF5QMA==\n" - "-----END CERTIFICATE-----\n"; - static const char* ica0_pem = - "-----BEGIN CERTIFICATE-----\n" - "MIIB/zCCAaWgAwIBAgIUWQmq3hQYYc2aAeHbkAhBu2TswggwCgYIKoZIzj0EAwIw\n" - "GjEYMBYGA1UEAwwPeDUwOS1saW1iby1yb290MCAXDTcwMDEwMTAwMDAwMVoYDzI5\n" - "NjkwNTAzMDAwMDAxWjBmMTgwNgYDVQQLDC8zODQ3NTQ4NjM0MDcyNTc1NDQyNDQy\n" - "MTQxNDk1NTczNzU0OTc0NTAyNTYxMjE5NjEqMCgGA1UEAwwheDUwOS1saW1iby1p\n" - "bnRlcm1lZGlhdGUtcGF0aGxlbi0wMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\n" - "htjb5R5OipW7n8rfvqA7yP/yxpdq9QzwSxC41RjoE//1SX16xsjOAIOYu/1L8iEq\n" - "1Y6x7yBnkoFFpWW0R2JN0aN7MHkwEgYDVR0TAQH/BAgwBgEB/wIBADALBgNVHQ8E\n" - "BAMCAgQwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20wHwYDVR0jBBgwFoAUonHZJ38d\n" - "Xr7snblbNqaxj6KZwOYwHQYDVR0OBBYEFI5psdjr2bzbP8AYzh747r3zKXjzMAoG\n" - "CCqGSM49BAMCA0gAMEUCIQChdmrAmCCIGBKdR31PSCKcBFkhqIg1rFH5n9ISC0XT\n" - "MwIgBpm/FQrFVRPNXUq3Pjp5nCCQNuusc/UF0WQtogoxMcs=\n" - "-----END CERTIFICATE-----\n"; - static const char* ica1_pem = - "-----BEGIN CERTIFICATE-----\n" - "MIICTDCCAfKgAwIBAgIUUzHoF6QQi7Q1abKHM2OkqXAyhXAwCgYIKoZIzj0EAwIw\n" - "ZjE4MDYGA1UECwwvMzg0NzU0ODYzNDA3MjU3NTQ0MjQ0MjE0MTQ5NTU3Mzc1NDk3\n" - "NDUwMjU2MTIxOTYxKjAoBgNVBAMMIXg1MDktbGltYm8taW50ZXJtZWRpYXRlLXBh\n" - "dGhsZW4tMDAgFw03MDAxMDEwMDAwMDFaGA8yOTY5MDUwMzAwMDAwMVowZzE5MDcG\n" - "A1UECwwwNTA4MzE1NzY5OTY4MTEzNTA3Njc4NzYzNDMxNDk4OTE0NTU3NTI4MDQ4\n" - "OTc2MzkyMSowKAYDVQQDDCF4NTA5LWxpbWJvLWludGVybWVkaWF0ZS1wYXRobGVu\n" - "LTAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQJAU5C0BQNKWv1lpCWYvrbguNZ\n" - "7Ru3850SYzxAHVdqoqpXQqz1rxMGkIIQzNk1GhUXbMUVbQD0jvaJcoGfkNO5o3sw\n" - "eTASBgNVHRMBAf8ECDAGAQH/AgEAMAsGA1UdDwQEAwICBDAWBgNVHREEDzANggtl\n" - "eGFtcGxlLmNvbTAfBgNVHSMEGDAWgBSOabHY69m82z/AGM4e+O698yl48zAdBgNV\n" - "HQ4EFgQUCRY9Zhvn1ujILJxVlXN6ngBEGUcwCgYIKoZIzj0EAwIDSAAwRQIhAPWq\n" - "eItvUILeT5ZV1sA/2T2KXLmhO+lyaIJKbayTWTluAiBAnlFFqQhLRPC9aXnmvzld\n" - "4OQO4zOBVTRVR1fyTaGRLA==\n" - "-----END CERTIFICATE-----\n"; - static const char* leaf_pem = - "-----BEGIN CERTIFICATE-----\n" - "MIIB/jCCAaOgAwIBAgIUUgfgeS9xej4Xq/a8Bg2Dzj3MedEwCgYIKoZIzj0EAwIw\n" - "ZzE5MDcGA1UECwwwNTA4MzE1NzY5OTY4MTEzNTA3Njc4NzYzNDMxNDk4OTE0NTU3\n" - "NTI4MDQ4OTc2MzkyMSowKAYDVQQDDCF4NTA5LWxpbWJvLWludGVybWVkaWF0ZS1w\n" - "YXRobGVuLTAwIBcNNzAwMTAxMDAwMDAxWhgPMjk2OTA1MDMwMDAwMDFaMBYxFDAS\n" - "BgNVBAMMC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERG/C\n" - "Q3diuQGNPeztXUpMthR9/695MnMj/tpF6NHkEBO91bBKFdjhnievo6XnpfEOer/z\n" - "nHvUEwH3UH7swoP3qKN8MHowHQYDVR0OBBYEFMorXKe6f7/o5tnf5iacR/PPM9F3\n" - "MB8GA1UdIwQYMBaAFAkWPWYb59boyCycVZVzep4ARBlHMAsGA1UdDwQEAwIHgDAT\n" - "BgNVHSUEDDAKBggrBgEFBQcDATAWBgNVHREEDzANggtleGFtcGxlLmNvbTAKBggq\n" - "hkjOPQQDAgNJADBGAiEAjQovFZD9svq8vGyuCa82Cq3/YeoHkDyyRalhv4BV7X8C\n" - "IQDqXCv7h0gMIVsWOSef8zu4DubHxn7Icm7DwJg0O2lSuw==\n" - "-----END CERTIFICATE-----\n"; +#if defined(OPENSSL_EXTRA) && !defined(NO_CERTS) && \ + !defined(NO_FILESYSTEM) && !defined(NO_RSA) X509* root = NULL; - X509* ica0 = NULL; + X509* ica2 = NULL; X509* ica1 = NULL; X509* leaf = NULL; X509_STORE* store = NULL; X509_STORE_CTX* ctx = NULL; STACK_OF(X509)* inter = NULL; - ExpectNotNull(root = pathlen_pem_to_x509(root_pem)); - ExpectNotNull(ica0 = pathlen_pem_to_x509(ica0_pem)); - ExpectNotNull(ica1 = pathlen_pem_to_x509(ica1_pem)); - ExpectNotNull(leaf = pathlen_pem_to_x509(leaf_pem)); + ExpectNotNull(root = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/ca-cert.pem")); + ExpectNotNull(ica2 = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/test-pathlen/chainF-ICA2-pathlen0.pem")); + ExpectNotNull(ica1 = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/test-pathlen/chainF-ICA1-pathlen1.pem")); + ExpectNotNull(leaf = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/test-pathlen/chainF-entity.pem")); ExpectNotNull(store = X509_STORE_new()); ExpectIntEQ(X509_STORE_add_cert(store, root), 1); ExpectNotNull(inter = sk_X509_new_null()); - ExpectIntGT(sk_X509_push(inter, ica0), 0); + ExpectIntGT(sk_X509_push(inter, ica2), 0); ExpectIntGT(sk_X509_push(inter, ica1), 0); ExpectNotNull(ctx = X509_STORE_CTX_new()); ExpectIntEQ(X509_STORE_CTX_init(ctx, store, leaf, inter), 1); - /* Must be rejected: ica1 violates ica0's pathlen:0 constraint. */ + /* Must be rejected: chainF-ICA1 violates chainF-ICA2's pathlen:0. */ ExpectIntNE(X509_verify_cert(ctx), 1); ExpectIntEQ(X509_STORE_CTX_get_error(ctx), X509_V_ERR_PATH_LENGTH_EXCEEDED); @@ -1072,57 +1011,184 @@ static int test_wolfSSL_X509_verify_cert_pathlen(void) X509_STORE_free(store); sk_X509_free(inter); X509_free(root); - X509_free(ica0); + X509_free(ica2); X509_free(ica1); X509_free(leaf); +#endif /* OPENSSL_EXTRA && !NO_CERTS && !NO_FILESYSTEM && !NO_RSA */ return EXPECT_RESULT(); } -#if !defined(NO_FILESYSTEM) && !defined(NO_RSA) /* Positive control: a legitimate chain whose intermediates assert pathlen:1 - * must still verify, guarding the pathLen enforcement against over-rejection. + * then pathlen:0 must still verify, guarding pathLen enforcement against + * over-rejection. Reuses the certs/test-pathlen chainB: * - * ca-cert -> ca-int (pathlen:1) -> ca-int2 (pathlen:1) -> server-chain + * ca-cert -> chainB-ICA2 (CA, pathlen:1) -> chainB-ICA1 (CA, pathlen:0) + * -> entity * - * ca-int permits one following intermediate (ca-int2), so the chain is valid. */ -static int test_wolfSSL_X509_verify_cert_pathlen_ok(void) + * chainB-ICA2 permits one following intermediate (chainB-ICA1), so the chain + * is valid. */ +int test_wolfSSL_X509_verify_cert_pathlen_ok(void) { EXPECT_DECLS; - X509* ca = NULL; - X509* caInt = NULL; - X509* caInt2 = NULL; +#if defined(OPENSSL_EXTRA) && !defined(NO_CERTS) && \ + !defined(NO_FILESYSTEM) && !defined(NO_RSA) + X509* root = NULL; + X509* ica2 = NULL; + X509* ica1 = NULL; X509* leaf = NULL; X509_STORE* store = NULL; X509_STORE_CTX* ctx = NULL; + STACK_OF(X509)* inter = NULL; - ExpectNotNull(ca = test_wolfSSL_X509_STORE_CTX_ex_helper( + ExpectNotNull(root = test_wolfSSL_X509_STORE_CTX_ex_helper( "./certs/ca-cert.pem")); - ExpectNotNull(caInt = test_wolfSSL_X509_STORE_CTX_ex_helper( - "./certs/intermediate/ca-int-cert.pem")); - ExpectNotNull(caInt2 = test_wolfSSL_X509_STORE_CTX_ex_helper( - "./certs/intermediate/ca-int2-cert.pem")); + ExpectNotNull(ica2 = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/test-pathlen/chainB-ICA2-pathlen1.pem")); + ExpectNotNull(ica1 = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/test-pathlen/chainB-ICA1-pathlen0.pem")); ExpectNotNull(leaf = test_wolfSSL_X509_STORE_CTX_ex_helper( - "./certs/intermediate/server-chain.pem")); + "./certs/test-pathlen/chainB-entity.pem")); ExpectNotNull(store = X509_STORE_new()); - ExpectIntEQ(X509_STORE_add_cert(store, ca), 1); - ExpectIntEQ(X509_STORE_add_cert(store, caInt), 1); - ExpectIntEQ(X509_STORE_add_cert(store, caInt2), 1); + ExpectIntEQ(X509_STORE_add_cert(store, root), 1); + ExpectNotNull(inter = sk_X509_new_null()); + ExpectIntGT(sk_X509_push(inter, ica2), 0); + ExpectIntGT(sk_X509_push(inter, ica1), 0); + ExpectNotNull(ctx = X509_STORE_CTX_new()); - ExpectIntEQ(X509_STORE_CTX_init(ctx, store, leaf, NULL), 1); + ExpectIntEQ(X509_STORE_CTX_init(ctx, store, leaf, inter), 1); ExpectIntEQ(X509_verify_cert(ctx), 1); ExpectIntEQ(X509_STORE_CTX_get_error(ctx), X509_V_OK); X509_STORE_CTX_free(ctx); X509_STORE_free(store); - X509_free(ca); - X509_free(caInt); - X509_free(caInt2); + sk_X509_free(inter); + X509_free(root); + X509_free(ica2); + X509_free(ica1); + X509_free(leaf); +#endif /* OPENSSL_EXTRA && !NO_CERTS && !NO_FILESYSTEM && !NO_RSA */ + return EXPECT_RESULT(); +} + +#if defined(OPENSSL_ALL) && !defined(NO_CERTS) && \ + !defined(NO_FILESYSTEM) && !defined(NO_RSA) +/* Records whether the pathLen violation was surfaced to the verify callback, + * then overrides it (returns 1) so verification continues - exercising the + * verify_cb override branch in X509StoreCheckPathLen(). */ +static int pathlen_override_seen = 0; +static int pathlen_override_cb(int ok, X509_STORE_CTX *ctx) +{ + (void)ok; + if (X509_STORE_CTX_get_error(ctx) == X509_V_ERR_PATH_LENGTH_EXCEEDED) + pathlen_override_seen = 1; + return 1; /* override: accept despite the error */ +} +#endif + +/* A verify callback that returns 1 must be able to override the pathLen + * violation, matching the INVALID_CA override handling in + * wolfSSL_X509_verify_cert(). Reuses the rejecting chainF: with the override + * callback installed the same chain must now verify, and the callback must have + * observed X509_V_ERR_PATH_LENGTH_EXCEEDED. */ +int test_wolfSSL_X509_verify_cert_pathlen_override(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_ALL) && !defined(NO_CERTS) && \ + !defined(NO_FILESYSTEM) && !defined(NO_RSA) + X509* root = NULL; + X509* ica2 = NULL; + X509* ica1 = NULL; + X509* leaf = NULL; + X509_STORE* store = NULL; + X509_STORE_CTX* ctx = NULL; + STACK_OF(X509)* inter = NULL; + + pathlen_override_seen = 0; + + ExpectNotNull(root = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/ca-cert.pem")); + ExpectNotNull(ica2 = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/test-pathlen/chainF-ICA2-pathlen0.pem")); + ExpectNotNull(ica1 = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/test-pathlen/chainF-ICA1-pathlen1.pem")); + ExpectNotNull(leaf = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/test-pathlen/chainF-entity.pem")); + + ExpectNotNull(store = X509_STORE_new()); + ExpectIntEQ(X509_STORE_add_cert(store, root), 1); + X509_STORE_set_verify_cb(store, pathlen_override_cb); + ExpectNotNull(inter = sk_X509_new_null()); + ExpectIntGT(sk_X509_push(inter, ica2), 0); + ExpectIntGT(sk_X509_push(inter, ica1), 0); + + ExpectNotNull(ctx = X509_STORE_CTX_new()); + ExpectIntEQ(X509_STORE_CTX_init(ctx, store, leaf, inter), 1); + /* The callback overrides the violation, so verification now succeeds... */ + ExpectIntEQ(X509_verify_cert(ctx), 1); + /* ...and the callback must actually have seen the pathLen error. */ + ExpectIntEQ(pathlen_override_seen, 1); + + X509_STORE_CTX_free(ctx); + X509_STORE_free(store); + sk_X509_free(inter); + X509_free(root); + X509_free(ica2); + X509_free(ica1); + X509_free(leaf); +#endif /* OPENSSL_ALL && !NO_CERTS && !NO_FILESYSTEM && !NO_RSA */ + return EXPECT_RESULT(); +} + +/* The trust anchor's own pathLenConstraint must bound the path (matching + * OpenSSL's -partial_chain behavior and wolfSSL's native ParseCertRelative). + * Trust chainF-ICA2 (pathlen:0) directly as a partial-chain anchor and verify + * the entity through chainF-ICA1 (a CA): chainF-ICA1 exceeds the anchor's + * pathlen:0, so it must be rejected. This exercises the anchor-seeding branch + * of X509StoreCheckPathLen() (the violation comes from the anchor's constraint, + * not an intermediate's). */ +int test_wolfSSL_X509_verify_cert_pathlen_anchor(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && !defined(NO_CERTS) && \ + !defined(NO_FILESYSTEM) && !defined(NO_RSA) + X509* ica2 = NULL; + X509* ica1 = NULL; + X509* leaf = NULL; + X509_STORE* store = NULL; + X509_STORE_CTX* ctx = NULL; + STACK_OF(X509)* inter = NULL; + + ExpectNotNull(ica2 = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/test-pathlen/chainF-ICA2-pathlen0.pem")); + ExpectNotNull(ica1 = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/test-pathlen/chainF-ICA1-pathlen1.pem")); + ExpectNotNull(leaf = test_wolfSSL_X509_STORE_CTX_ex_helper( + "./certs/test-pathlen/chainF-entity.pem")); + + /* Trust the pathlen:0 intermediate directly as a partial-chain anchor. */ + ExpectNotNull(store = X509_STORE_new()); + ExpectIntEQ(X509_STORE_add_cert(store, ica2), 1); + ExpectIntEQ(X509_STORE_set_flags(store, X509_V_FLAG_PARTIAL_CHAIN), 1); + ExpectNotNull(inter = sk_X509_new_null()); + ExpectIntGT(sk_X509_push(inter, ica1), 0); + + ExpectNotNull(ctx = X509_STORE_CTX_new()); + ExpectIntEQ(X509_STORE_CTX_init(ctx, store, leaf, inter), 1); + /* Must be rejected: chainF-ICA1 exceeds anchor chainF-ICA2's pathlen:0. */ + ExpectIntNE(X509_verify_cert(ctx), 1); + ExpectIntEQ(X509_STORE_CTX_get_error(ctx), + X509_V_ERR_PATH_LENGTH_EXCEEDED); + + X509_STORE_CTX_free(ctx); + X509_STORE_free(store); + sk_X509_free(inter); + X509_free(ica2); + X509_free(ica1); X509_free(leaf); +#endif /* OPENSSL_EXTRA && !NO_CERTS && !NO_FILESYSTEM && !NO_RSA */ return EXPECT_RESULT(); } -#endif /* !NO_FILESYSTEM && !NO_RSA */ -#endif /* OPENSSL_EXTRA && !NO_CERTS && HAVE_ECC */ int test_wolfSSL_X509_STORE_CTX_ex(void) { @@ -1169,10 +1235,6 @@ int test_wolfSSL_X509_STORE_CTX_ex(void) &testData), 1); #ifdef HAVE_ECC ExpectIntEQ(test_wolfSSL_X509_STORE_CTX_ex12(), 1); - ExpectIntEQ(test_wolfSSL_X509_verify_cert_pathlen(), 1); -#if !defined(NO_FILESYSTEM) && !defined(NO_RSA) - ExpectIntEQ(test_wolfSSL_X509_verify_cert_pathlen_ok(), 1); -#endif #endif if(testData.x509Ca) { diff --git a/tests/api/test_ossl_x509_str.h b/tests/api/test_ossl_x509_str.h index 3b13fa13b7..5f1cdc9e08 100644 --- a/tests/api/test_ossl_x509_str.h +++ b/tests/api/test_ossl_x509_str.h @@ -29,6 +29,10 @@ int test_wolfSSL_X509_STORE_check_time(void); int test_wolfSSL_X509_STORE_CTX_get0_store(void); int test_wolfSSL_X509_STORE_CTX(void); int test_wolfSSL_X509_STORE_CTX_ex(void); +int test_wolfSSL_X509_verify_cert_pathlen(void); +int test_wolfSSL_X509_verify_cert_pathlen_ok(void); +int test_wolfSSL_X509_verify_cert_pathlen_override(void); +int test_wolfSSL_X509_verify_cert_pathlen_anchor(void); int test_X509_verify_cert_untrusted_inter(void); int test_X509_verify_cert_ca_no_keycertsign(void); int test_X509_STORE_untrusted(void); @@ -53,6 +57,14 @@ int test_wolfSSL_CTX_set_cert_store(void); test_wolfSSL_X509_STORE_CTX_get0_store), \ TEST_DECL_GROUP("ossl_x509_store", test_wolfSSL_X509_STORE_CTX), \ TEST_DECL_GROUP("ossl_x509_store", test_wolfSSL_X509_STORE_CTX_ex), \ + TEST_DECL_GROUP("ossl_x509_store", \ + test_wolfSSL_X509_verify_cert_pathlen), \ + TEST_DECL_GROUP("ossl_x509_store", \ + test_wolfSSL_X509_verify_cert_pathlen_ok), \ + TEST_DECL_GROUP("ossl_x509_store", \ + test_wolfSSL_X509_verify_cert_pathlen_override), \ + TEST_DECL_GROUP("ossl_x509_store", \ + test_wolfSSL_X509_verify_cert_pathlen_anchor), \ TEST_DECL_GROUP("ossl_x509_store", test_X509_verify_cert_untrusted_inter), \ TEST_DECL_GROUP("ossl_x509_store", \ test_X509_verify_cert_ca_no_keycertsign), \ From 4e2f21ed61690d3dfb7a98a9bdd36a7cf41e1b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 19 Jun 2026 11:03:41 +0200 Subject: [PATCH 4/4] x509_str.c: gate pathLen decrement on isCa X509StoreCheckPathLen() consumed a unit of the issuer's path length budget for any non-self-issued intermediate. Gate the RFC 5280 sec. 6.1.4 (l) decrement on cert->isCa so only CA certificates count, matching ParseCertRelative() (wolfcrypt/src/asn.c) and the (m) tightening step. This prevents a false PATH_LENGTH_EXCEEDED when a non-CA intermediate is tolerated via verify_cb. --- src/x509_str.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/x509_str.c b/src/x509_str.c index b459300605..30055ba840 100644 --- a/src/x509_str.c +++ b/src/x509_str.c @@ -693,10 +693,13 @@ static int X509StoreCheckPathLen(WOLFSSL_X509_STORE_CTX* ctx) 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. Only meaningful - * once a CA above has asserted a constraint (haveConstraint). */ - if (!selfIssued && haveConstraint) { + /* 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);