Skip to content

Commit 5c1225e

Browse files
committed
x509: harden wolfSSL_X509_verify_cert() against alloc failure and stack pollution
Robustness fixes in the OpenSSL-compatibility certificate verifier, independent of the depth-exhaustion fix: - Fail closed on allocation failure. When the failedCerts working stack could not be allocated, the function fell through to exit with ret still set to WOLFSSL_SUCCESS and reported the chain as verified without checking anything (a fail-open regression from the leak fix that turned the early return into a goto exit). Also check the ctx->chain allocation. Both now set an error. - Remove caller-supplied intermediates from the correct stack. The intermediates appended to the working cert list during chain building were popped from ctx->store->certs by count, but they are appended to whichever stack is in use - which may be the caller's setTrustedSk (X509_STORE_CTX_set0_trusted_stack). Remove them by pointer identity from that same stack, recomputed from ctxIntermediates. Identity removal also survives the chain-building retries that reorder the stack, where a positional pop could drop a legitimate trusted entry and leave an injected intermediate behind - which a later verification reusing the store/ctx would then snapshot as a trust anchor. The removal helper walks the list once (O(n)) rather than indexing per position. - NULL-guard ctx->store->param before dereferencing its flags in the partial-chain check. Add regression tests covering: the trusted stack being restored after verification, and the retry path (tampered plus genuine same-subject intermediates, both orderings) leaving the store clean for later use.
1 parent 2d76a68 commit 5c1225e

2 files changed

Lines changed: 199 additions & 9 deletions

File tree

src/x509_str.c

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,36 @@ static int X509StoreMoveCert(WOLFSSL_STACK *certs_stack,
531531
return WOLFSSL_FAILURE;
532532
}
533533

534+
/* Remove the first node referencing `cert` (by pointer identity) from `stack`.
535+
* The certificate object itself is not freed - the stack only holds a borrowed
536+
* reference. Returns WOLFSSL_SUCCESS if a node was removed, WOLFSSL_FAILURE if
537+
* `cert` was not present, or WOLFSSL_FATAL_ERROR if `stack`/`cert` is NULL.
538+
* The only caller performs best-effort cleanup and intentionally ignores the
539+
* return value.
540+
*
541+
* Walks the linked list once (O(n)) rather than indexing with
542+
* wolfSSL_sk_X509_value() per position (which would re-walk from the head each
543+
* time, O(n^2)). */
544+
static int X509StoreRemoveCert(WOLFSSL_STACK *stack, WOLFSSL_X509 *cert) {
545+
WOLFSSL_STACK* node;
546+
int idx;
547+
int num;
548+
549+
if (stack == NULL || cert == NULL)
550+
return WOLFSSL_FATAL_ERROR;
551+
552+
num = wolfSSL_sk_X509_num(stack);
553+
for (node = stack, idx = 0; idx < num && node != NULL;
554+
node = node->next, idx++) {
555+
if (node->data.x509 == cert) {
556+
(void)wolfSSL_sk_pop_node(stack, idx);
557+
return WOLFSSL_SUCCESS;
558+
}
559+
}
560+
561+
return WOLFSSL_FAILURE;
562+
}
563+
534564

535565
/* Current certificate failed, but it is possible there is an
536566
* alternative cert with the same subject key which will work.
@@ -608,7 +638,6 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx)
608638
int done = 0;
609639
int added = 0;
610640
int i = 0;
611-
int numInterAdd = 0;
612641
int numFailedCerts = 0;
613642
int depth = 0;
614643
int origDepth = 0;
@@ -666,8 +695,9 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx)
666695
}
667696
}
668697
/* Add the intermediates provided on init to the list of untrusted
669-
* intermediates to be used */
670-
ret = addAllButSelfSigned(certs, ctx->ctxIntermediates, &numInterAdd);
698+
* intermediates to be used. They are removed again from `certs` in the
699+
* exit cleanup (by identity, recomputed from ctxIntermediates). */
700+
ret = addAllButSelfSigned(certs, ctx->ctxIntermediates, NULL);
671701
}
672702
if (ret != WOLFSSL_SUCCESS) {
673703
goto exit;
@@ -677,10 +707,19 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx)
677707
wolfSSL_sk_X509_free(ctx->chain);
678708
}
679709
ctx->chain = wolfSSL_sk_X509_new_null();
710+
if (ctx->chain == NULL) {
711+
ret = WOLFSSL_FAILURE;
712+
goto exit;
713+
}
680714

681715
failedCerts = wolfSSL_sk_X509_new_null();
682-
if (!failedCerts)
716+
if (!failedCerts) {
717+
/* Fail closed: ret is still WOLFSSL_SUCCESS from the checks above, so
718+
* an unset error here would make the function report a verified chain
719+
* after an allocation failure. */
720+
ret = WOLFSSL_FAILURE;
683721
goto exit;
722+
}
684723

685724
if (ctx->depth > 0) {
686725
depth = ctx->depth + 1;
@@ -779,7 +818,8 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx)
779818
* chains that never reach a trust anchor. Verify that
780819
* ctx->current_cert is itself in the original trust set. */
781820
if (((ctx->flags & WOLFSSL_PARTIAL_CHAIN) ||
782-
(ctx->store->param->flags & WOLFSSL_PARTIAL_CHAIN)) &&
821+
(ctx->store->param != NULL &&
822+
(ctx->store->param->flags & WOLFSSL_PARTIAL_CHAIN))) &&
783823
X509StoreCertIsTrusted(ctx->store, ctx->current_cert,
784824
origTrustedSk)) {
785825
wolfSSL_sk_X509_push(ctx->chain, ctx->current_cert);
@@ -847,10 +887,30 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx)
847887
}
848888
wolfSSL_sk_X509_pop_free(failedCerts, NULL);
849889

850-
/* Remove additional intermediates from init from the store */
851-
if (ctx != NULL && numInterAdd > 0) {
852-
for (i = 0; i < numInterAdd; i++) {
853-
wolfSSL_sk_X509_pop(ctx->store->certs);
890+
/* Remove the caller-supplied intermediates that addAllButSelfSigned
891+
* appended to `certs` during chain building, restoring it to its original
892+
* contents. Remove them by pointer identity from the same stack they were
893+
* added to (store->certs in the common case, or the caller's setTrustedSk
894+
* via X509_STORE_CTX_set0_trusted_stack), recomputed from ctxIntermediates
895+
* with the same self-signed filter as the add.
896+
*
897+
* Identity removal - not a saved count + positional pop - is required:
898+
* X509VerifyCertSetupRetry reorders `certs` during chain building, so
899+
* popping N entries off the top could drop a legitimate trusted entry and
900+
* leave an injected intermediate behind, which a later verification reusing
901+
* this store/ctx would then snapshot as a trust anchor. certsToUse is the
902+
* throwaway certs==NULL path and is freed wholesale below, so skip it. */
903+
if (ctx != NULL && certsToUse == NULL && certs != NULL &&
904+
ctx->ctxIntermediates != NULL) {
905+
int n = wolfSSL_sk_X509_num(ctx->ctxIntermediates);
906+
for (i = 0; i < n; i++) {
907+
WOLFSSL_X509* inter =
908+
wolfSSL_sk_X509_value(ctx->ctxIntermediates, i);
909+
if (inter != NULL &&
910+
wolfSSL_X509_NAME_cmp(&inter->issuer, &inter->subject)
911+
!= 0) {
912+
X509StoreRemoveCert(certs, inter);
913+
}
854914
}
855915
}
856916
/* Remove intermediates that were added to CM */

tests/api/test_ossl_x509_str.c

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,129 @@ static int test_untrusted_inter_depth_exhaustion(X509* leafDeep, X509* inter,
13011301
sk_X509_free(untrusted);
13021302
return EXPECT_RESULT();
13031303
}
1304+
1305+
/* Intermediate-stack cleanup: the caller-supplied intermediates that the
1306+
* verifier temporarily appends to its working cert list must be removed from
1307+
* the exact stack they were added to once verification finishes. When a
1308+
* trusted_stack is in use (X509_STORE_CTX_set0_trusted_stack), they are
1309+
* appended to that caller-owned stack; if they are not removed again, a later
1310+
* verification reusing the stack/ctx would snapshot them as trust anchors.
1311+
*
1312+
* leaf <- int-ca <- root, with root supplied via the trusted_stack.
1313+
*
1314+
* Verify the chain (which reaches root in the trusted stack), then assert the
1315+
* trusted stack is left exactly as the caller supplied it: only root, with the
1316+
* injected intermediate removed again. */
1317+
static int test_untrusted_inter_trusted_stack_cleanup(X509* leaf, X509* inter,
1318+
X509* root)
1319+
{
1320+
EXPECT_DECLS;
1321+
X509_STORE* store = NULL;
1322+
X509_STORE_CTX* ctx = NULL;
1323+
STACK_OF(X509)* trusted = NULL;
1324+
STACK_OF(X509)* untrusted = NULL;
1325+
1326+
ExpectNotNull(store = X509_STORE_new());
1327+
ExpectNotNull(trusted = sk_X509_new_null());
1328+
ExpectIntGT(sk_X509_push(trusted, root), 0);
1329+
ExpectNotNull(untrusted = sk_X509_new_null());
1330+
ExpectIntGT(sk_X509_push(untrusted, inter), 0);
1331+
ExpectNotNull(ctx = X509_STORE_CTX_new());
1332+
ExpectIntEQ(X509_STORE_CTX_init(ctx, store, leaf, untrusted), 1);
1333+
X509_STORE_CTX_trusted_stack(ctx, trusted);
1334+
/* Chain reaches root in the trusted stack -> verifies. */
1335+
ExpectIntEQ(X509_verify_cert(ctx), 1);
1336+
ExpectIntEQ(X509_STORE_CTX_get_error(ctx), X509_V_OK);
1337+
/* The trusted stack must be restored: the injected intermediate appended
1338+
* during verification must have been removed, leaving only root. */
1339+
ExpectIntEQ(sk_X509_num(trusted), 1);
1340+
ExpectPtrEq(sk_X509_value(trusted, 0), root);
1341+
X509_STORE_CTX_free(ctx);
1342+
X509_STORE_free(store);
1343+
sk_X509_free(untrusted);
1344+
sk_X509_free(trusted);
1345+
return EXPECT_RESULT();
1346+
}
1347+
1348+
/* One mixed-candidate verification: leaf with both the tampered intermediate
1349+
* (broken outer signature, same subject as int-ca) and the genuine int-ca in
1350+
* the untrusted stack, in the given push order. The chain must verify - the
1351+
* verifier tries the candidates and recovers via X509VerifyCertSetupRetry when
1352+
* it hits the tampered one first. Only the return value is asserted: when the
1353+
* tampered candidate is tried first wolfSSL intentionally preserves its error
1354+
* code even though the chain recovers (see the "worst-seen error must persist"
1355+
* behaviour exercised by test_X509_STORE_InvalidCa), so the error after success
1356+
* is order-dependent and not asserted here. */
1357+
static int untrusted_inter_retry_one(X509_STORE* store, X509* leaf,
1358+
X509* first, X509* second)
1359+
{
1360+
EXPECT_DECLS;
1361+
X509_STORE_CTX* ctx = NULL;
1362+
STACK_OF(X509)* mixed = NULL;
1363+
1364+
ExpectNotNull(mixed = sk_X509_new_null());
1365+
ExpectIntGT(sk_X509_push(mixed, first), 0);
1366+
ExpectIntGT(sk_X509_push(mixed, second), 0);
1367+
ExpectNotNull(ctx = X509_STORE_CTX_new());
1368+
ExpectIntEQ(X509_STORE_CTX_init(ctx, store, leaf, mixed), 1);
1369+
ExpectIntEQ(X509_verify_cert(ctx), 1);
1370+
X509_STORE_CTX_free(ctx);
1371+
sk_X509_free(mixed);
1372+
return EXPECT_RESULT();
1373+
}
1374+
1375+
/* Retry path: when several caller-supplied candidates share a subject, the
1376+
* verifier tries one, fails, and retries with another
1377+
* (X509VerifyCertSetupRetry), moving entries through an internal "failed" list.
1378+
* Exercise BOTH push orderings so that, whichever order the verifier enumerates
1379+
* same-subject candidates, at least one ordering forces it to hit the tampered
1380+
* candidate first and recover. Then prove the store is left clean: a later
1381+
* verification on the same store with only the tampered intermediate must fail
1382+
* (no genuine int-ca left behind), and one with the genuine intermediate must
1383+
* still succeed. */
1384+
static int test_untrusted_inter_retry(X509* leaf, X509* inter,
1385+
X509* tamperedInter, X509* root)
1386+
{
1387+
EXPECT_DECLS;
1388+
X509_STORE* store = NULL;
1389+
X509_STORE_CTX* ctx = NULL;
1390+
STACK_OF(X509)* goodOnly = NULL;
1391+
STACK_OF(X509)* badOnly = NULL;
1392+
1393+
ExpectNotNull(store = X509_STORE_new());
1394+
ExpectIntEQ(X509_STORE_add_cert(store, root), 1);
1395+
1396+
/* Both orderings verify and report X509_V_OK. */
1397+
ExpectIntEQ(untrusted_inter_retry_one(store, leaf, tamperedInter, inter), 1);
1398+
ExpectIntEQ(untrusted_inter_retry_one(store, leaf, inter, tamperedInter), 1);
1399+
1400+
/* Reuse the SAME store with only the tampered intermediate: must fail. If
1401+
* a retry above left the genuine int-ca behind in the store, this would
1402+
* wrongly succeed. */
1403+
ExpectNotNull(badOnly = sk_X509_new_null());
1404+
ExpectIntGT(sk_X509_push(badOnly, tamperedInter), 0);
1405+
ExpectNotNull(ctx = X509_STORE_CTX_new());
1406+
ExpectIntEQ(X509_STORE_CTX_init(ctx, store, leaf, badOnly), 1);
1407+
ExpectIntEQ(X509_verify_cert(ctx), 0);
1408+
ExpectIntNE(X509_STORE_CTX_get_error(ctx), X509_V_OK);
1409+
X509_STORE_CTX_free(ctx);
1410+
ctx = NULL;
1411+
1412+
/* Reuse the SAME store with the genuine intermediate: must still succeed,
1413+
* proving the retry left no stale state that breaks later verifications. */
1414+
ExpectNotNull(goodOnly = sk_X509_new_null());
1415+
ExpectIntGT(sk_X509_push(goodOnly, inter), 0);
1416+
ExpectNotNull(ctx = X509_STORE_CTX_new());
1417+
ExpectIntEQ(X509_STORE_CTX_init(ctx, store, leaf, goodOnly), 1);
1418+
ExpectIntEQ(X509_verify_cert(ctx), 1);
1419+
ExpectIntEQ(X509_STORE_CTX_get_error(ctx), X509_V_OK);
1420+
1421+
X509_STORE_CTX_free(ctx);
1422+
X509_STORE_free(store);
1423+
sk_X509_free(goodOnly);
1424+
sk_X509_free(badOnly);
1425+
return EXPECT_RESULT();
1426+
}
13041427
#endif /* OPENSSL_EXTRA && !NO_RSA && !NO_CERTS && !NO_FILESYSTEM */
13051428

13061429
int test_X509_verify_cert_untrusted_inter(void)
@@ -1324,6 +1447,8 @@ int test_X509_verify_cert_untrusted_inter(void)
13241447
int reusedStoreRes = 0;
13251448
int noStaleRes = 0;
13261449
int depthExhaustRes = 0;
1450+
int trustedStackCleanupRes = 0;
1451+
int retryRes = 0;
13271452

13281453
ExpectNotNull(leaf = untrusted_inter_load(UA_CERT_DIR "leaf-cert.pem"));
13291454
ExpectNotNull(leafDeep =
@@ -1355,6 +1480,9 @@ int test_X509_verify_cert_untrusted_inter(void)
13551480
root);
13561481
depthExhaustRes = test_untrusted_inter_depth_exhaustion(leafDeep,
13571482
inter, inter2, root);
1483+
trustedStackCleanupRes = test_untrusted_inter_trusted_stack_cleanup(
1484+
leaf, inter, root);
1485+
retryRes = test_untrusted_inter_retry(leaf, inter, tamperedInter, root);
13581486
ExpectIntEQ(sanityRes, 1);
13591487
ExpectIntEQ(twoLevelRes, 1);
13601488
ExpectIntEQ(emptyStoreRes, 1);
@@ -1363,6 +1491,8 @@ int test_X509_verify_cert_untrusted_inter(void)
13631491
ExpectIntEQ(reusedStoreRes, 1);
13641492
ExpectIntEQ(noStaleRes, 1);
13651493
ExpectIntEQ(depthExhaustRes, 1);
1494+
ExpectIntEQ(trustedStackCleanupRes, 1);
1495+
ExpectIntEQ(retryRes, 1);
13661496
}
13671497

13681498
X509_free(leaf);

0 commit comments

Comments
 (0)