From 0ac0e1fa82007866229a2f8239389efca8056d0e Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Wed, 8 Apr 2026 16:27:07 -0500 Subject: [PATCH 1/2] Fix partial chain verification --- src/x509_str.c | 59 +++++++++++++++++++++++++++++++++- tests/api/test_ossl_x509_str.c | 37 +++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/x509_str.c b/src/x509_str.c index 90113caede..e973f9ae0c 100644 --- a/src/x509_str.c +++ b/src/x509_str.c @@ -552,6 +552,48 @@ static int X509VerifyCertSetupRetry(WOLFSSL_X509_STORE_CTX* ctx, return ret; } +/* Returns 1 if x509's DER matches an entry in either the head [0, origNum) + * slice of trustedCerts (which excludes intermediates injected for this + * verification call) or in store->trusted. Returns 0 otherwise. Used by + * the X509_V_FLAG_PARTIAL_CHAIN fallback to confirm that a chain actually + * terminates at a caller-trusted certificate. */ +static int X509StoreCertIsTrusted(WOLFSSL_X509_STORE* store, + WOLFSSL_X509* x509, WOLF_STACK_OF(WOLFSSL_X509)* trustedCerts, + int origNum) +{ + int i; + int n; + WOLFSSL_X509* cur; + + if (x509 == NULL || x509->derCert == NULL) + return 0; + + for (i = 0; i < origNum; i++) { + cur = wolfSSL_sk_X509_value(trustedCerts, i); + if (cur != NULL && cur->derCert != NULL && + cur->derCert->length == x509->derCert->length && + XMEMCMP(cur->derCert->buffer, x509->derCert->buffer, + x509->derCert->length) == 0) { + return 1; + } + } + + if (store != NULL && store->trusted != NULL) { + n = wolfSSL_sk_X509_num(store->trusted); + for (i = 0; i < n; i++) { + cur = wolfSSL_sk_X509_value(store->trusted, i); + if (cur != NULL && cur->derCert != NULL && + cur->derCert->length == x509->derCert->length && + XMEMCMP(cur->derCert->buffer, x509->derCert->buffer, + x509->derCert->length) == 0) { + return 1; + } + } + } + + return 0; +} + /* Verifies certificate chain using WOLFSSL_X509_STORE_CTX * returns 1 on success or <= 0 on failure. */ @@ -565,6 +607,7 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx) int numFailedCerts = 0; int depth = 0; int origDepth = 0; + int origCertsNum = 0; WOLFSSL_X509 *issuer = NULL; WOLFSSL_X509 *orig = NULL; WOLF_STACK_OF(WOLFSSL_X509)* certs = NULL; @@ -587,8 +630,14 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx) wolfSSL_sk_X509_num(ctx->ctxIntermediates) > 0) { certsToUse = wolfSSL_sk_X509_new_null(); ret = addAllButSelfSigned(certsToUse, ctx->ctxIntermediates, NULL); + /* certsToUse holds only injected intermediates, none are trusted. */ + origCertsNum = 0; } else { + /* Snapshot the count of pre-existing entries before injecting the + * caller-supplied untrusted intermediates. Only the entries already + * present count as trusted for the partial-chain check below. */ + origCertsNum = wolfSSL_sk_X509_num(certs); /* Add the intermediates provided on init to the list of untrusted * intermediates to be used */ ret = addAllButSelfSigned(certs, ctx->ctxIntermediates, &numInterAdd); @@ -677,9 +726,17 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx) * a trusted CA in the CM */ ret = X509StoreVerifyCert(ctx); if (ret != WOLFSSL_SUCCESS) { + /* WOLFSSL_PARTIAL_CHAIN may only terminate the chain at a + * certificate the caller actually trusts. The previous + * "added == 1" guard merely confirmed that some untrusted + * intermediate had been temporarily loaded into the + * CertManager during chain building, which would accept + * chains that never reach a trust anchor. Verify that + * ctx->current_cert is itself in the original trust set. */ if (((ctx->flags & WOLFSSL_PARTIAL_CHAIN) || (ctx->store->param->flags & WOLFSSL_PARTIAL_CHAIN)) && - (added == 1)) { + X509StoreCertIsTrusted(ctx->store, ctx->current_cert, + certs, origCertsNum)) { wolfSSL_sk_X509_push(ctx->chain, ctx->current_cert); ret = WOLFSSL_SUCCESS; } else { diff --git a/tests/api/test_ossl_x509_str.c b/tests/api/test_ossl_x509_str.c index 99b82877c3..12f4977d6d 100644 --- a/tests/api/test_ossl_x509_str.c +++ b/tests/api/test_ossl_x509_str.c @@ -785,6 +785,42 @@ static int test_wolfSSL_X509_STORE_CTX_ex11(X509_STORE_test_data *testData) return EXPECT_RESULT(); } +static int test_wolfSSL_X509_STORE_CTX_ex_partial_chain_neg( + X509_STORE_test_data *testData) +{ + EXPECT_DECLS; + X509_STORE* store = NULL; + X509_STORE_CTX* ctx = NULL; + STACK_OF(X509)* untrusted = NULL; + + /* Negative partial-chain test: with X509_V_FLAG_PARTIAL_CHAIN set, the + * intermediates are supplied ONLY as untrusted (passed through the + * X509_STORE_CTX_init "chain" argument and never added to the store). + * No certificate in the chain is in the store, so verification must + * fail. Pre-fix, wolfSSL_X509_verify_cert would incorrectly accept + * this chain because its partial-chain fallback only checked that some + * intermediate had been temporarily loaded into the CertManager, not + * that any chain certificate was actually trusted. */ + ExpectNotNull(store = X509_STORE_new()); + /* Intentionally do NOT add x509CaInt, x509CaInt2, or x509Ca. */ + ExpectIntEQ(X509_STORE_set_flags(store, X509_V_FLAG_PARTIAL_CHAIN), 1); + + ExpectNotNull(untrusted = sk_X509_new_null()); + ExpectIntGT(sk_X509_push(untrusted, testData->x509CaInt2), 0); + ExpectIntGT(sk_X509_push(untrusted, testData->x509CaInt), 0); + + ExpectNotNull(ctx = X509_STORE_CTX_new()); + ExpectIntEQ(X509_STORE_CTX_init(ctx, store, testData->x509Leaf, untrusted), + 1); + /* Must NOT verify: partial-chain does not relax the trust requirement. */ + ExpectIntNE(X509_verify_cert(ctx), 1); + + X509_STORE_CTX_free(ctx); + X509_STORE_free(store); + sk_X509_free(untrusted); + return EXPECT_RESULT(); +} + #ifdef HAVE_ECC static int test_wolfSSL_X509_STORE_CTX_ex12(void) { @@ -870,6 +906,7 @@ int test_wolfSSL_X509_STORE_CTX_ex(void) ExpectIntEQ(test_wolfSSL_X509_STORE_CTX_ex9(&testData), 1); ExpectIntEQ(test_wolfSSL_X509_STORE_CTX_ex10(&testData), 1); ExpectIntEQ(test_wolfSSL_X509_STORE_CTX_ex11(&testData), 1); + ExpectIntEQ(test_wolfSSL_X509_STORE_CTX_ex_partial_chain_neg(&testData), 1); #ifdef HAVE_ECC ExpectIntEQ(test_wolfSSL_X509_STORE_CTX_ex12(), 1); #endif From 4f904c6024d791c4b1735ed6a3b47775f2356faa Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Wed, 8 Apr 2026 16:42:34 -0500 Subject: [PATCH 2/2] Fix from review --- src/x509_str.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/x509_str.c b/src/x509_str.c index e973f9ae0c..354f21c419 100644 --- a/src/x509_str.c +++ b/src/x509_str.c @@ -637,7 +637,7 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx) /* Snapshot the count of pre-existing entries before injecting the * caller-supplied untrusted intermediates. Only the entries already * present count as trusted for the partial-chain check below. */ - origCertsNum = wolfSSL_sk_X509_num(certs); + origCertsNum = (certs != NULL) ? wolfSSL_sk_X509_num(certs) : 0; /* Add the intermediates provided on init to the list of untrusted * intermediates to be used */ ret = addAllButSelfSigned(certs, ctx->ctxIntermediates, &numInterAdd);