From 4f523b9f2867a2f72657f5b745e4a2292690f4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 4 Aug 2026 15:40:57 +0200 Subject: [PATCH 1/9] Wait for async completion in the ECIES crypto callback test ecc_encrypt_cryptocb_test() calls wc_ecc_make_key() twice and checks the return code directly. Under WOLFSSL_ASYNC_CRYPT the software simulator returns WC_PENDING_E from that call, so the test aborts with -108 before it reaches a single ECIES operation. The sibling ecc_encrypt_test() right below it already wraps the same two calls in wc_AsyncWait(); this one was added later and never got the same treatment. Mirror the sibling exactly: wc_AsyncWait() on each key's asyncDev with WC_ASYNC_FLAG_NONE, guarded by WOLFSSL_ASYNC_CRYPT so non-async builds are untouched by the preprocessor. The failure needs --enable-all together with the software async simulator. --enable-all turns on cryptocb, which suppresses the auto-enabled simulator in configure.ac, so the simulator only appears if --enable-asynccrypt-sw is passed explicitly. No configuration under .github/ combines the two, which is why this was never seen in CI. Verified with --enable-all --enable-asynccrypt-sw under the CFLAGS the os-check workflow applies: "ECC Enc test failed! error code=-108" before, "ECC Enc test passed!" after. Rebuilt the same tree as plain --enable-all to confirm the non-async path is unaffected, where testwolfcrypt passes in full. Fixing this uncovers the next test to run, ECCSI, failing the same way. That one is a library defect rather than a test defect and is fixed in the following commit. --- wolfcrypt/test/test.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 2953550ae9..d264798227 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -45991,8 +45991,14 @@ static wc_test_ret_t ecc_encrypt_cryptocb_test(WC_RNG* rng) userA->devId = INVALID_DEVID; userB->devId = INVALID_DEVID; ret = wc_ecc_make_key(rng, ECC_KEYGEN_SIZE, userA); +#if defined(WOLFSSL_ASYNC_CRYPT) + ret = wc_AsyncWait(ret, &userA->asyncDev, WC_ASYNC_FLAG_NONE); +#endif if (ret != 0) { ret = WC_TEST_RET_ENC_EC(ret); goto cb_done; } ret = wc_ecc_make_key(rng, ECC_KEYGEN_SIZE, userB); +#if defined(WOLFSSL_ASYNC_CRYPT) + ret = wc_AsyncWait(ret, &userB->asyncDev, WC_ASYNC_FLAG_NONE); +#endif if (ret != 0) { ret = WC_TEST_RET_ENC_EC(ret); goto cb_done; } #if defined(ECC_TIMING_RESISTANT) && (!defined(HAVE_FIPS) || \ (!defined(HAVE_FIPS_VERSION) || (HAVE_FIPS_VERSION != 2))) && \ From df4758290bd4f30f9c0ff5a461165f22e07c1963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 4 Aug 2026 16:04:02 +0200 Subject: [PATCH 2/9] Complete pending ECCSI key generation in wc_MakeEccsiKey wc_MakeEccsiKey() returns whatever wc_ecc_make_key_ex() returns. In an async build the ECC key generation goes pending and the caller receives WC_PENDING_E, but ECCSI exposes no asynchronous API, so there is nothing the caller can do with it. There is no ECCSI event to poll, no devId to wait on that the caller is given, and no documented resume path. The error is simply unusable. Wait for the operation to finish before returning, so wc_MakeEccsiKey() keeps the synchronous contract the rest of the ECCSI API assumes. Only this one call site needs it. eccsi.c reaches exactly three of the ECC entry points that can go pending, all of them wc_ecc_make_key_ex(), and the other two in eccsi_make_pair() and eccsi_gen_sig() are preceded by wc_ecc_free(&key->pubkey). That clears asyncDev.marker, and the pending path in _ecc_make_key_ex() is gated on the marker still being WOLFSSL_ASYNC_MARKER_ECC, so those two cannot return WC_PENDING_E. Adding a wait there would be dead code. sakke.c reaches none of the four entry points at all. Found while validating an unrelated change under --enable-all with --enable-asynccrypt-sw. No configuration under .github/ combines those two, because --enable-all turns on cryptocb and configure.ac only auto-enables the software simulator when no backend is set, so this went unnoticed. Verified with --enable-all --enable-asynccrypt-sw under the CFLAGS the os-check workflow applies: testwolfcrypt reported "ECCSI test failed! error code=-108" before and reports "ECCSI test passed!" after, with SAKKE passing as well. Reconfigured the same tree as plain --enable-all to confirm the non-async path is untouched, where make check passes in full. That configuration still cannot complete testwolfcrypt. The next failure is cryptocb_test(), which re-runs whole sub-suites through a crypto callback and touches no ECCSI or SAKKE code. It is a separate problem and is not addressed here. --- wolfcrypt/src/eccsi.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wolfcrypt/src/eccsi.c b/wolfcrypt/src/eccsi.c index d0417ec879..79ddfeb5df 100644 --- a/wolfcrypt/src/eccsi.c +++ b/wolfcrypt/src/eccsi.c @@ -464,6 +464,11 @@ int wc_MakeEccsiKey(EccsiKey* key, WC_RNG* rng) if (err == 0) { err = wc_ecc_make_key_ex(rng, key->ecc.dp->size, &key->ecc, key->ecc.dp->id); +#ifdef WOLFSSL_ASYNC_CRYPT + /* ECCSI has no asynchronous API, so the caller cannot resume a + * pending key generation. Complete it here. */ + err = wc_AsyncWait(err, &key->ecc.asyncDev, WC_ASYNC_FLAG_NONE); +#endif } return err; From fe3801b9423681169127af89ccadbc603f3965d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 4 Aug 2026 19:10:18 +0200 Subject: [PATCH 3/9] Wait for async completion in the cryptocb make-pub test cryptocb_test() generates a key with wc_ecc_make_key() and assigns the result straight to ret. In an async build that call returns WC_PENDING_E, which is not an encoded test result, so the raw -108 propagated out of the test and printed as "error L=108" with no error code at all. The key is reached through myCryptoDevCb, which services EC key generation by calling wc_ecc_make_key_ex() on the same key after setting key->devId = INVALID_DEVID. That comment says the intent is to force software, and it does stop the crypto callback from dispatching again, but the pending path in _ecc_make_key_ex() is gated on asyncDev.marker rather than devId. The marker is untouched, so the inner call still goes pending and the callback hands WC_PENDING_E back to its caller. Wait at the call site rather than in the callback. Every other key generation in this file already does exactly that, a callback returning WC_PENDING_E is legitimate for a real asynchronous device, and the same devId idiom appears 48 times in myCryptoDevCb against 48 different keys, so there is no single place in the callback to fix. With this, testwolfcrypt passes in full under --enable-all with --enable-asynccrypt-sw, where it previously stopped here. Verified against plain --enable-all as well, which is unaffected: the addition compiles out entirely without WOLFSSL_ASYNC_CRYPT. That configuration still cannot complete make check. unit.test fails in the cipher suite runner on TLS 1.3 post-handshake authentication, which is a record layer problem in the library rather than a test defect and is not addressed here. All 2111 API tests pass. --- wolfcrypt/test/test.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index d264798227..6988960ec7 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -81235,6 +81235,9 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) if (ret == 0) { haveSrc = 1; ret = wc_ecc_make_key(eccRng, 32, srcKey); +#ifdef WOLFSSL_ASYNC_CRYPT + ret = wc_AsyncWait(ret, &srcKey->asyncDev, WC_ASYNC_FLAG_NONE); +#endif } if (ret == 0) { outPub = wc_ecc_new_point_h(HEAP_HINT); From 8767340251ae0204e2d469116340d53c58fb4546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 4 Aug 2026 19:33:45 +0200 Subject: [PATCH 4/9] Finish the record when post-handshake auth returns pending A TLS 1.3 client doing post-handshake authentication fails the next read with -326 VERSION_ERROR in an asynchronous build, and the connection dies. It is reproducible with the shipped examples: ./examples/server/server -v 4 -l TLS13-AES128-GCM-SHA256 -Q -2 -p 11119 & ./examples/client/client -v 4 -l TLS13-AES128-GCM-SHA256 -Q -2 -p 11119 DoTls13CertificateRequest() handles a post-handshake CertificateRequest by resetting the handshake states, setting processReply back to doProcessInit and calling wolfSSL_connect_TLSv13() to send the certificate, certificate verify and finished flight. It runs that from inside DoTls13HandShakeMsg(), which is itself inside DoProcessReplyEx(). When the flight goes pending, WC_PENDING_E travels back out of DoTls13HandShakeMsg() and DoProcessReplyEx() returns immediately, so the end of record accounting that follows never runs and the record is left short by ssl->keys.padSz bytes. The client resumes through wolfSSL_negotiate() from ReceiveData() rather than by reprocessing the record, so nothing ever advances past those bytes. The next ProcessReply() starts a fresh record at that offset and parses the record's own MAC as a record header, which fails the version check in GetRecordHeader(). Measured on the failing read: idx 50, length 67, padSz 17, with the header bytes decoding as type 0x16 and version 0x70 0xEA. Advance past the padding when the handler returns pending having already marked the record complete. processReply is what distinguishes the two cases: post-handshake auth leaves it at doProcessInit, while an ordinary pending message leaves it at runProcessingOneMessage and must not be advanced, since that one really is reprocessed on resume. Confirmed by tracing every pending return in the failing run: three ordinary ones at runProcessingOneMessage, and only the post-handshake one at doProcessInit, where idx plus padSz lands exactly on the buffer length. This mirrors what the early data path a few lines below already does when it returns early after marking the record done. Synchronous builds are unaffected. wolfSSL_connect_TLSv13() completes in place there, DoTls13HandShakeMsg() returns 0, and the normal accounting runs. --- src/internal.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/internal.c b/src/internal.c index 3de4b1d060..8227103260 100644 --- a/src/internal.c +++ b/src/internal.c @@ -25480,6 +25480,22 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ssl->buffers.inputBuffer.buffer, &ssl->buffers.inputBuffer.idx, ssl->curStartIdx + ssl->curSize); + #ifdef WOLFSSL_ASYNC_CRYPT + /* Post-handshake authentication runs the connect state + * machine from inside this handler and resumes through + * wolfSSL_negotiate() rather than by reprocessing this + * record, which is why it leaves processReply at + * doProcessInit. Finish the record's accounting here, + * or the trailing MAC is parsed as the next record + * header and the read fails with VERSION_ERROR. An + * ordinary pending message leaves processReply at + * runProcessingOneMessage and must not be advanced. */ + if (ret == WC_NO_ERR_TRACE(WC_PENDING_E) && + ssl->options.processReply == doProcessInit && + IsEncryptionOn(ssl, 0)) { + ssl->buffers.inputBuffer.idx += ssl->keys.padSz; + } + #endif #ifdef WOLFSSL_EARLY_DATA if (ret != 0) return ret; From f3065f9ed0eb9160b8a94c1e5085b14a2af9370d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 4 Aug 2026 19:56:06 +0200 Subject: [PATCH 5/9] Preserve the build message state across the record size probe Writing application data over DTLS fails with -132 BUFFER_E in an asynchronous build once a record goes pending. It is reproducible with the shipped examples: ./examples/server/server -e -u -f -v 3 -l ECDHE-RSA-AES128-SHA -2 -p 11151 & ./examples/client/client -B 4000,1310 -u -f -v 3 -l ECDHE-RSA-AES128-SHA -2 -p 11151 The client reports "Oops, want to write past output buffer size" and the write fails. This is the DTLS MTU configuration from tests/test-dtls-mtu.conf. SendData() sizes the output buffer by calling wolfssl_local_GetRecordSize(), which runs BuildMessage() with sizeOnly set and asyncOkay clear. That probe keeps its own arguments on the stack, but the state machine it drives lives in ssl->options.buildMsgState, which is shared with the asynchronous BuildMessage() that may still be in flight for the same record. Because SendData() re-probes the size on every retry, the sequence is: the real BuildMessage() suspends at BUILD_MSG_ENCRYPT, the retry's probe resets the shared state to BUILD_MSG_BEGIN and leaves it there, and the resumed call then re-runs the begin and size stages against arguments that already carry the header and cipher overhead. For a 1310 byte payload with ECDHE-RSA-AES128-SHA that takes the header from 13 to 21 bytes and the record from 1361 to 1417, which no longer fits the 1361 byte buffer the first probe correctly sized. Save the state around the probe and put it back afterwards. The probe still needs to start from BUILD_MSG_BEGIN to compute a size, so restoring is the narrowest fix; the arguments themselves are already private to the probe. Verified with --enable-all --enable-asynccrypt-sw: the reproducer above goes from a failed write to completing its benchmark. Full make check in that configuration passes, 17 passed and 6 skipped with no failures, though that run also needs the wolfcrypt test fixes for ECIES, ECCSI and the crypto callback test, which that configuration trips over first and which are not part of this change. Non-async builds are unaffected, since the saved value is only read and written under WOLFSSL_ASYNC_CRYPT. --- src/internal.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/internal.c b/src/internal.c index 8227103260..c19b609b73 100644 --- a/src/internal.c +++ b/src/internal.c @@ -45173,6 +45173,16 @@ int wolfssl_local_GetRecordSize(WOLFSSL *ssl, int payloadSz, int isEncrypted) #ifdef WOLFSSL_DTLS13 int isDtls13 = ssl->options.dtls && ssl->options.tls1_3; #endif +#ifdef WOLFSSL_ASYNC_CRYPT + /* The size probe below drives ssl->options.buildMsgState, which is + * also the resume point of an asynchronous BuildMessage that is still + * in flight. SendData() calls us again on every retry, so without + * saving it the probe rewinds the suspended record to + * BUILD_MSG_BEGIN, and the resumed call re-applies the header and + * cipher overhead to arguments that already carry it, overflowing the + * output buffer. */ + byte savedBuildMsgState = ssl->options.buildMsgState; +#endif if (ssl->specs.cipher_type == aead && ssl->recordSzOverhead != 0 #ifdef WOLFSSL_DTLS13 @@ -45185,6 +45195,9 @@ int wolfssl_local_GetRecordSize(WOLFSSL *ssl, int payloadSz, int isEncrypted) recordSz = BuildMessage(ssl, NULL, 0, NULL, payloadSz, application_data, 0, 1, 0, CUR_ORDER); +#ifdef WOLFSSL_ASYNC_CRYPT + ssl->options.buildMsgState = savedBuildMsgState; +#endif /* use a safe upper bound in case of error */ if (recordSz < 0) { recordSz = payloadSz + RECORD_HEADER_SZ From 1c5382f169d27594bc0d43884f73ca2a98c8763f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 4 Aug 2026 20:41:37 +0200 Subject: [PATCH 6/9] Harden the async record layer fixes and cover the size probe Review follow-up on the two preceding commits. Only skip the padding when the record content is actually consumed. The post-handshake auth advance ran on any pending return that left processReply at doProcessInit, which assumes the certificate_request was the last message in its record. Two states break that: a fragmented certificate_request makes DoTls13HandShakeMsg() rewind inOutIdx so the fragment can be reprocessed, and RFC 8446 section 5.1 lets a peer coalesce several handshake messages into one record, leaving the index inside the record. Adding padSz in either case points the index at record content. Both states are already mishandled without this series, since post-handshake auth forces processReply to doProcessInit regardless, so this is a narrower guard rather than a regression, and there is no memory safety consequence either way: the largest possible index is the end of the record. The new test mirrors the end of record check that follows the message handlers. Also restore buildArgsSet across the record size probe. The probe borrows buildMsgState and buildArgsSet from the suspended build; the previous commit put back only the first. FreeBuildMsgArgs() clears buildArgsSet unconditionally on the way out, and the resumed call skips the block that would set it again, so the flag stays clear for the rest of the record. That is currently harmless, because the only thing it guards is freeing a dynamically allocated IV and no cipher in the tree needs one, but it leaves the fix one field short of its own premise. Add test_record_size_preserves_build_msg_state, which parks a connection at BUILD_MSG_ENCRYPT with the arguments flag set, runs the probe, and requires both fields to survive. It fails without the restore and passes with it. Guard the post-handshake auth block on WOLFSSL_POST_HANDSHAKE_AUTH as well. The only code that leaves processReply at doProcessInit from inside DoTls13HandShakeMsg() is the post-handshake auth branch, which is itself behind that macro, so the check cannot fire without it. Say that the oversized record is rejected rather than that it overflows the buffer. BuildMessage() catches it and returns BUFFER_E; the old wording read like memory corruption. Verified with --enable-all --enable-asynccrypt-sw and with plain --enable-all, both warning free under the os-check CFLAGS. The two reproducers from the preceding commits still complete. --- src/internal.c | 28 +++++++++++++++++++--------- tests/api/test_tls.c | 43 +++++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls.h | 3 +++ 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/src/internal.c b/src/internal.c index c19b609b73..ce72385c6a 100644 --- a/src/internal.c +++ b/src/internal.c @@ -25480,7 +25480,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ssl->buffers.inputBuffer.buffer, &ssl->buffers.inputBuffer.idx, ssl->curStartIdx + ssl->curSize); - #ifdef WOLFSSL_ASYNC_CRYPT + #if defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_POST_HANDSHAKE_AUTH) /* Post-handshake authentication runs the connect state * machine from inside this handler and resumes through * wolfSSL_negotiate() rather than by reprocessing this @@ -25489,9 +25489,16 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) * or the trailing MAC is parsed as the next record * header and the read fails with VERSION_ERROR. An * ordinary pending message leaves processReply at - * runProcessingOneMessage and must not be advanced. */ + * runProcessingOneMessage and must not be advanced. + * The content check matches the end of record test + * below: a fragmented certificate_request rewinds + * inOutIdx to reprocess the fragment, and coalesced + * handshake messages leave idx inside the record, so + * in both cases the padding must not be skipped. */ if (ret == WC_NO_ERR_TRACE(WC_PENDING_E) && ssl->options.processReply == doProcessInit && + (ssl->buffers.inputBuffer.idx - + ssl->curStartIdx) >= ssl->curSize && IsEncryptionOn(ssl, 0)) { ssl->buffers.inputBuffer.idx += ssl->keys.padSz; } @@ -45174,14 +45181,16 @@ int wolfssl_local_GetRecordSize(WOLFSSL *ssl, int payloadSz, int isEncrypted) int isDtls13 = ssl->options.dtls && ssl->options.tls1_3; #endif #ifdef WOLFSSL_ASYNC_CRYPT - /* The size probe below drives ssl->options.buildMsgState, which is - * also the resume point of an asynchronous BuildMessage that is still - * in flight. SendData() calls us again on every retry, so without - * saving it the probe rewinds the suspended record to - * BUILD_MSG_BEGIN, and the resumed call re-applies the header and - * cipher overhead to arguments that already carry it, overflowing the - * output buffer. */ + /* The size probe below borrows two fields that also describe an + * asynchronous BuildMessage still in flight: buildMsgState is its + * resume point, and buildArgsSet says its arguments are live. + * SendData() calls us again on every retry, so without saving them the + * probe rewinds the suspended record to BUILD_MSG_BEGIN and clears the + * arguments flag. The resumed call then re-applies the header and + * cipher overhead to arguments that already carry it, and the record + * is rejected with BUFFER_E. */ byte savedBuildMsgState = ssl->options.buildMsgState; + byte savedBuildArgsSet = ssl->options.buildArgsSet; #endif if (ssl->specs.cipher_type == aead && ssl->recordSzOverhead != 0 @@ -45197,6 +45206,7 @@ int wolfssl_local_GetRecordSize(WOLFSSL *ssl, int payloadSz, int isEncrypted) 0, 1, 0, CUR_ORDER); #ifdef WOLFSSL_ASYNC_CRYPT ssl->options.buildMsgState = savedBuildMsgState; + ssl->options.buildArgsSet = savedBuildArgsSet; #endif /* use a safe upper bound in case of error */ if (recordSz < 0) { diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index ed8ab7c6c2..2fa2fd2d5b 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -2927,6 +2927,49 @@ int test_record_size_matches_build_message(void) return EXPECT_RESULT(); } +int test_record_size_preserves_build_msg_state(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_ASYNC_CRYPT) && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + struct test_memio_ctx test_ctx; + int sz; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* SendData() re-probes the record size on every retry, which happens while + * an asynchronous BuildMessage is suspended part way through a record. The + * probe shares buildMsgState and buildArgsSet with that build, so it must + * put both back or the resumed record is sized twice. */ + if (ssl_c != NULL) { + ssl_c->options.buildMsgState = BUILD_MSG_ENCRYPT; + ssl_c->options.buildArgsSet = 1; + + sz = wolfssl_local_GetRecordSize(ssl_c, 256, 1); + + ExpectIntGT(sz, 256); + ExpectIntEQ(ssl_c->options.buildMsgState, BUILD_MSG_ENCRYPT); + ExpectIntEQ(ssl_c->options.buildArgsSet, 1); + + /* Nothing was really built, so do not leave the flag claiming the + * async arguments are live. */ + ssl_c->options.buildMsgState = BUILD_MSG_BEGIN; + ssl_c->options.buildArgsSet = 0; + } + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + int test_record_size_cache_invalidated_on_renegotiation(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index f009bb986e..ab79c07890 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -59,6 +59,7 @@ int test_tls12_ecdhe_rsa_ecdsa_client_cert(void); int test_wolfSSL_alert_type_string(void); int test_wolfSSL_alert_desc_string(void); int test_record_size_matches_build_message(void); +int test_record_size_preserves_build_msg_state(void); int test_record_size_cache_invalidated_on_renegotiation(void); int test_wolfSSL_get_shared_ciphers(void); @@ -101,6 +102,8 @@ int test_wolfSSL_get_shared_ciphers(void); TEST_DECL_GROUP("tls", test_wolfSSL_alert_type_string), \ TEST_DECL_GROUP("tls", test_wolfSSL_alert_desc_string), \ TEST_DECL_GROUP("tls", test_record_size_matches_build_message), \ + TEST_DECL_GROUP("tls", \ + test_record_size_preserves_build_msg_state), \ TEST_DECL_GROUP("tls", \ test_record_size_cache_invalidated_on_renegotiation), \ TEST_DECL_GROUP("tls", test_wolfSSL_get_shared_ciphers) From ac7bff1147bdd855e8be3f03c90b31059c524e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 4 Aug 2026 21:03:39 +0200 Subject: [PATCH 7/9] CI: run the async software simulator against --enable-all Nothing in CI exercised an asynchronous build that actually returns WC_PENDING_E from a full feature set, which is why the five preceding fixes all describe failures no workflow could see. The async workflow has two groups and neither reaches these paths. The asynccrypt-all entries pass --enable-asynccrypt --enable-all, but --enable-all turns on cryptocb, and configure.ac only auto-enables the software simulator when cryptocb, pkcallbacks, Cavium and Intel QA are all off. Those builds therefore define WOLFSSL_ASYNC_CRYPT without ever suspending an operation. The asynccrypt-sw entries do suspend, but they build only OCSP stapling, so they compile neither TLS 1.3 post-handshake authentication nor DTLS. Add one entry that pairs --enable-asynccrypt-sw with --enable-all and --enable-dtls13, which covers both reproducers: the post-handshake auth case in tests/test-tls13.conf and the DTLS MTU case in tests/test-dtls-mtu.conf. Measured at 1.6 minutes locally. Declared as 3 to match the neighbouring asynccrypt-all entries, which measure 1.4 locally against their declared 3, so this machine runs roughly twice as fast as whatever those values were taken from and 1.6 here projects to about 3.4 there. The value is only a scheduling weight, and the existing entries are left alone rather than rewritten from local timings. --- .github/workflows/async.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/async.yml b/.github/workflows/async.yml index 0b3f4947a2..275ca63961 100644 --- a/.github/workflows/async.yml +++ b/.github/workflows/async.yml @@ -71,6 +71,11 @@ jobs: run: | cat > "$RUNNER_TEMP/async-configs.json" <<'EOF' [ + {"comment": "The only entry that pairs the software async simulator with --enable-all. --enable-all turns on cryptocb, which stops configure.ac from auto-enabling the simulator, so the asynccrypt-all entries below define WOLFSSL_ASYNC_CRYPT but never actually return WC_PENDING_E. Without this one nothing exercises TLS 1.3 post-handshake auth or DTLS writes against a pending crypto op.", + "name": "asynccrypt-sw-all-dtls13", "minutes": 3, + "configure": ["--enable-asynccrypt-sw", "--enable-all", + "--enable-dtls13", + "CFLAGS=-pedantic -Wdeclaration-after-statement -Wnull-dereference -DTEST_LIBWOLFSSL_SOURCES_INCLUSION_SEQUENCE"]}, {"name": "asynccrypt-all-no-mlkem", "minutes": 3, "configure": ["--enable-asynccrypt", "--enable-all", "--enable-dtls13", "--disable-mlkem", From ff730fec9e9f1d29784ea4573e979db25e734368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 4 Aug 2026 23:03:01 +0200 Subject: [PATCH 8/9] Keep the record size probe clear of a suspended build Second review follow-up on the async record layer series. Stop the probe reselecting the cipher side. BuildMessage()'s BUILD_MSG_BEGIN case can call SetKeysSide() for DTLS with secure renegotiation, which swaps the active encryption state and clears recordSzOverhead. That is not part of a size calculation, and after the previous commit the suspended build survives to resume against whatever side the probe last chose, so a DTLS 1.2 record suspended for PREV_ORDER could resume against the renegotiation keys. Skip it when sizeOnly is set; the sizes are the same either way. The probe itself has to keep running. Not re-entering BuildMessage at all while a build is suspended looks tidier, but wolfssl_local_GetMaxPlaintextSize() derives the DTLS fragment size from this result, so falling back to the upper bound there shrinks fragments inconsistently between calls and the MTU reproducer fails its buffer comparison. Saving and restoring the two fields is what keeps the answer exact. Resume inside the record when handshake content is left. The previous commit declined to skip the padding for a fragmented or coalesced certificate_request, which was right, but left processReply at doProcessInit with the index inside the record, so the resume still started a fresh record parse in the middle of one. Mirror both halves of the end of record block instead: set runProcessingOneMessage when content remains, advance past the padding only at the boundary. Note the shared state at the source. BuildMessage() and BuildTls13Message() write ssl->options.buildMsgState even for a sizeOnly probe with asyncOkay clear, where everything else goes to the caller's own arguments. Nothing said so at those sites, so the next sizeOnly caller would reintroduce this. Record why only one of the three wc_ecc_make_key_ex() calls in eccsi.c needs a wait: the other two are preceded by wc_ecc_free(), which clears the marker their pending path is gated on. Moving either free would make them pend. Test changes. Force the overhead cache cold before probing, otherwise an AEAD suite answers from the cache without ever calling BuildMessage and the assertions hold no matter what the probe did. Compare against BuildMessage's own figure rather than only checking the size is positive, and run the whole thing for TLS 1.3 as well as TLS 1.2, since BuildTls13Message() clobbers the state by a different route: its sizeOnly return bypasses exit_buildmsg entirely. Checked by stubbing the restore out again, which fails the test. Also spell the new guard in cryptocb_test() as #if defined(WOLFSSL_ASYNC_CRYPT) to match the rest of that file, which uses that form 170 times against 4. --- .github/workflows/async.yml | 2 +- src/internal.c | 62 +++++++++++++++++++----------------- src/tls13.c | 4 +++ tests/api/test_tls.c | 63 +++++++++++++++++++++++++++++-------- wolfcrypt/src/eccsi.c | 7 +++-- wolfcrypt/test/test.c | 2 +- 6 files changed, 94 insertions(+), 46 deletions(-) diff --git a/.github/workflows/async.yml b/.github/workflows/async.yml index 275ca63961..2d11923904 100644 --- a/.github/workflows/async.yml +++ b/.github/workflows/async.yml @@ -71,7 +71,7 @@ jobs: run: | cat > "$RUNNER_TEMP/async-configs.json" <<'EOF' [ - {"comment": "The only entry that pairs the software async simulator with --enable-all. --enable-all turns on cryptocb, which stops configure.ac from auto-enabling the simulator, so the asynccrypt-all entries below define WOLFSSL_ASYNC_CRYPT but never actually return WC_PENDING_E. Without this one nothing exercises TLS 1.3 post-handshake auth or DTLS writes against a pending crypto op.", + {"comment": "The only entry that pairs the software async simulator with --enable-all. --enable-all turns on cryptocb, which stops configure.ac from auto-enabling the simulator, so the asynccrypt-all entries below define WOLFSSL_ASYNC_CRYPT but never actually return WC_PENDING_E. Without this one nothing exercises TLS 1.3 post-handshake auth or DTLS writes against a pending crypto op. The minutes value is a projection, not a CI measurement: this config takes 1.6 min locally where the asynccrypt-all entries below take 1.4 against their declared 3. Refresh it from the first real run.", "name": "asynccrypt-sw-all-dtls13", "minutes": 3, "configure": ["--enable-asynccrypt-sw", "--enable-all", "--enable-dtls13", diff --git a/src/internal.c b/src/internal.c index ce72385c6a..4fd1768532 100644 --- a/src/internal.c +++ b/src/internal.c @@ -25481,26 +25481,26 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) &ssl->buffers.inputBuffer.idx, ssl->curStartIdx + ssl->curSize); #if defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_POST_HANDSHAKE_AUTH) - /* Post-handshake authentication runs the connect state - * machine from inside this handler and resumes through - * wolfSSL_negotiate() rather than by reprocessing this - * record, which is why it leaves processReply at - * doProcessInit. Finish the record's accounting here, - * or the trailing MAC is parsed as the next record - * header and the read fails with VERSION_ERROR. An - * ordinary pending message leaves processReply at - * runProcessingOneMessage and must not be advanced. - * The content check matches the end of record test - * below: a fragmented certificate_request rewinds - * inOutIdx to reprocess the fragment, and coalesced - * handshake messages leave idx inside the record, so - * in both cases the padding must not be skipped. */ + /* Post-handshake auth resumes through + * wolfSSL_negotiate() instead of reprocessing this + * record, so it leaves processReply at doProcessInit + * (an ordinary pending message leaves it at + * runProcessingOneMessage). Finish the record here or + * the trailing MAC is read as the next record header + * and fails with VERSION_ERROR. Mirrors the end of + * record block below: resume inside the record when + * content is left, else skip the padding. */ if (ret == WC_NO_ERR_TRACE(WC_PENDING_E) && - ssl->options.processReply == doProcessInit && - (ssl->buffers.inputBuffer.idx - - ssl->curStartIdx) >= ssl->curSize && - IsEncryptionOn(ssl, 0)) { - ssl->buffers.inputBuffer.idx += ssl->keys.padSz; + ssl->options.processReply == doProcessInit) { + if ((ssl->buffers.inputBuffer.idx - + ssl->curStartIdx) < ssl->curSize) { + ssl->options.processReply = + runProcessingOneMessage; + } + else if (IsEncryptionOn(ssl, 0)) { + ssl->buffers.inputBuffer.idx += + ssl->keys.padSz; + } } #endif #ifdef WOLFSSL_EARLY_DATA @@ -26277,6 +26277,10 @@ int BuildMessage(WOLFSSL* ssl, byte* output, int outSz, const byte* input, if (ret == WC_NO_ERR_TRACE(WC_NO_PENDING_E)) #endif { + /* Note: these hit ssl->options even for a sizeOnly probe, where every + * other result goes to lcl_args, so a probe destroys the resume point + * of a suspended asynchronous build. wolfssl_local_GetRecordSize() is + * the only sizeOnly caller and restores them; a new one must too. */ ret = 0; #ifdef WOLFSSL_ASYNC_CRYPT ssl->options.buildArgsSet = 1; @@ -26294,7 +26298,10 @@ int BuildMessage(WOLFSSL* ssl, byte* output, int outSz, const byte* input, case BUILD_MSG_BEGIN: { #if defined(WOLFSSL_DTLS) && defined(HAVE_SECURE_RENEGOTIATION) - if (ssl->options.dtls && DtlsSCRKeysSet(ssl)) { + /* Skipped for a size probe: the size is the same either way, and + * SetKeysSide() would swap the active encryption state and clear + * recordSzOverhead under a suspended asynchronous build. */ + if (!sizeOnly && ssl->options.dtls && DtlsSCRKeysSet(ssl)) { /* For epochs >1 the current cipher parameters are located in * ssl->secure_renegotiation->tmp_keys. Previous cipher * parameters and for epoch 1 use ssl->keys */ @@ -45181,18 +45188,9 @@ int wolfssl_local_GetRecordSize(WOLFSSL *ssl, int payloadSz, int isEncrypted) int isDtls13 = ssl->options.dtls && ssl->options.tls1_3; #endif #ifdef WOLFSSL_ASYNC_CRYPT - /* The size probe below borrows two fields that also describe an - * asynchronous BuildMessage still in flight: buildMsgState is its - * resume point, and buildArgsSet says its arguments are live. - * SendData() calls us again on every retry, so without saving them the - * probe rewinds the suspended record to BUILD_MSG_BEGIN and clears the - * arguments flag. The resumed call then re-applies the header and - * cipher overhead to arguments that already carry it, and the record - * is rejected with BUFFER_E. */ byte savedBuildMsgState = ssl->options.buildMsgState; byte savedBuildArgsSet = ssl->options.buildArgsSet; #endif - if (ssl->specs.cipher_type == aead && ssl->recordSzOverhead != 0 #ifdef WOLFSSL_DTLS13 && (!isDtls13 || payloadSz + (int)ssl->recordSzOverhead @@ -45205,6 +45203,12 @@ int wolfssl_local_GetRecordSize(WOLFSSL *ssl, int payloadSz, int isEncrypted) recordSz = BuildMessage(ssl, NULL, 0, NULL, payloadSz, application_data, 0, 1, 0, CUR_ORDER); #ifdef WOLFSSL_ASYNC_CRYPT + /* Sizing shares the build state machine with an asynchronous + * BuildMessage that SendData() re-sizes on every retry, so the probe + * runs while that record is suspended. Restore its resume point, or + * the record is sized twice and rejected with BUFFER_E. Skipping the + * probe is not an option: wolfssl_local_GetMaxPlaintextSize() derives + * the DTLS fragment size from this result, so it must stay exact. */ ssl->options.buildMsgState = savedBuildMsgState; ssl->options.buildArgsSet = savedBuildArgsSet; #endif diff --git a/src/tls13.c b/src/tls13.c index c74e9a7d65..83ac9bd39a 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -3366,6 +3366,10 @@ int BuildTls13Message(WOLFSSL* ssl, byte* output, int outSz, const byte* input, if (ret == WC_NO_ERR_TRACE(WC_NO_PENDING_E)) #endif { + /* Note: these hit ssl->options even for a sizeOnly probe, where every + * other result goes to lcl_args, so a probe destroys the resume point + * of a suspended asynchronous build. wolfssl_local_GetRecordSize() is + * the only sizeOnly caller and restores them; a new one must too. */ ret = 0; ssl->options.buildMsgState = BUILD_MSG_BEGIN; XMEMSET(args, 0, sizeof(BuildMsg13Args)); diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 2fa2fd2d5b..bd5e0259a0 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -2927,37 +2927,55 @@ int test_record_size_matches_build_message(void) return EXPECT_RESULT(); } -int test_record_size_preserves_build_msg_state(void) +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_ASYNC_CRYPT) +/* SendData() sizes the output buffer on every retry, including while an + * asynchronous BuildMessage is suspended part way through a record. Sizing + * runs the same build state machine, so the probe must not re-enter it: it + * would rewind buildMsgState, clear buildArgsSet, and the resumed record would + * be sized a second time. Check that a suspended build survives a probe, and + * that a probe from a clean state still returns the exact record size. */ +static int record_size_state_check(method_provider client_method, + method_provider server_method) { EXPECT_DECLS; -#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ - defined(WOLFSSL_ASYNC_CRYPT) && !defined(WOLFSSL_NO_TLS12) WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; WOLFSSL *ssl_c = NULL, *ssl_s = NULL; struct test_memio_ctx test_ctx; - int sz; + int expectedSz = 0, cleanSz = 0, busySz = 0; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, - wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + client_method, server_method), 0); ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); - /* SendData() re-probes the record size on every retry, which happens while - * an asynchronous BuildMessage is suspended part way through a record. The - * probe shares buildMsgState and buildArgsSet with that build, so it must - * put both back or the resumed record is sized twice. */ if (ssl_c != NULL) { + expectedSz = BuildMessage(ssl_c, NULL, 0, NULL, 256, + application_data, 0, 1, 0, CUR_ORDER); + ssl_c->options.buildMsgState = BUILD_MSG_BEGIN; + ssl_c->options.buildArgsSet = 0; + ExpectIntGT(expectedSz, 256); + + /* Clearing the cache is what forces the BuildMessage path; an AEAD + * suite would otherwise answer from ssl->recordSzOverhead and the + * assertions below would hold no matter what the probe did. */ + ssl_c->recordSzOverhead = 0; + cleanSz = wolfssl_local_GetRecordSize(ssl_c, 256, 1); + ExpectIntEQ(cleanSz, expectedSz); + + /* Same probe with a build suspended mid-record. */ + ssl_c->recordSzOverhead = 0; ssl_c->options.buildMsgState = BUILD_MSG_ENCRYPT; ssl_c->options.buildArgsSet = 1; - sz = wolfssl_local_GetRecordSize(ssl_c, 256, 1); + busySz = wolfssl_local_GetRecordSize(ssl_c, 256, 1); - ExpectIntGT(sz, 256); ExpectIntEQ(ssl_c->options.buildMsgState, BUILD_MSG_ENCRYPT); ExpectIntEQ(ssl_c->options.buildArgsSet, 1); + /* Still exact: wolfssl_local_GetMaxPlaintextSize() sizes DTLS + * fragments from this, so it may not degrade to an upper bound. */ + ExpectIntEQ(busySz, expectedSz); - /* Nothing was really built, so do not leave the flag claiming the - * async arguments are live. */ ssl_c->options.buildMsgState = BUILD_MSG_BEGIN; ssl_c->options.buildArgsSet = 0; } @@ -2966,6 +2984,25 @@ int test_record_size_preserves_build_msg_state(void) wolfSSL_free(ssl_s); wolfSSL_CTX_free(ctx_c); wolfSSL_CTX_free(ctx_s); + return EXPECT_RESULT(); +} +#endif /* HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES && WOLFSSL_ASYNC_CRYPT */ + +int test_record_size_preserves_build_msg_state(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_ASYNC_CRYPT) +#ifndef WOLFSSL_NO_TLS12 + ExpectIntEQ(record_size_state_check(wolfTLSv1_2_client_method, + wolfTLSv1_2_server_method), TEST_SUCCESS); +#endif +#ifdef WOLFSSL_TLS13 + /* BuildTls13Message() clobbers buildMsgState by a different route: its + * sizeOnly return bypasses exit_buildmsg entirely. */ + ExpectIntEQ(record_size_state_check(wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), TEST_SUCCESS); +#endif #endif return EXPECT_RESULT(); } diff --git a/wolfcrypt/src/eccsi.c b/wolfcrypt/src/eccsi.c index 79ddfeb5df..2564f19c71 100644 --- a/wolfcrypt/src/eccsi.c +++ b/wolfcrypt/src/eccsi.c @@ -465,8 +465,11 @@ int wc_MakeEccsiKey(EccsiKey* key, WC_RNG* rng) err = wc_ecc_make_key_ex(rng, key->ecc.dp->size, &key->ecc, key->ecc.dp->id); #ifdef WOLFSSL_ASYNC_CRYPT - /* ECCSI has no asynchronous API, so the caller cannot resume a - * pending key generation. Complete it here. */ + /* ECCSI has no asynchronous API, so the caller cannot resume a pending + * key generation - complete it here. The key->pubkey sites in + * eccsi_make_pair() and eccsi_gen_sig() need no wait: each is preceded + * by wc_ecc_free(&key->pubkey), which clears the marker that + * _ecc_make_key_ex() gates its pending path on. */ err = wc_AsyncWait(err, &key->ecc.asyncDev, WC_ASYNC_FLAG_NONE); #endif } diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 6988960ec7..8ad792baad 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -81235,7 +81235,7 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) if (ret == 0) { haveSrc = 1; ret = wc_ecc_make_key(eccRng, 32, srcKey); -#ifdef WOLFSSL_ASYNC_CRYPT +#if defined(WOLFSSL_ASYNC_CRYPT) ret = wc_AsyncWait(ret, &srcKey->asyncDev, WC_ASYNC_FLAG_NONE); #endif } From 28c92aa81a0a3107ffd47aa42aae6445ca4d871f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 5 Aug 2026 16:56:49 +0200 Subject: [PATCH 9/9] Guard the async event handling in EccVerify against a NULL key EccVerify() dereferenced key unconditionally in both of its async spots: ret = wolfSSL_AsyncInit(ssl, &key->asyncDev, WC_ASYNC_FLAG_CALL_AGAIN); ... ret = wolfSSL_AsyncPush(ssl, &key->asyncDev); Its siblings EccSign() and VerifyRsaSign() wrap both calls in if (key), because with HAVE_PK_CALLBACKS the private key can live only in the callback and is never decoded into ssl->hsKey. The self verify step that WOLFSSL_CHECK_SIG_FAULTS performs after signing passes that same ssl->hsKey to EccVerify(), so an --enable-asynccrypt build dereferences NULL there: at the init on entry, and again at the push if EccVerifyCb returns WC_PENDING_E. Note wc_ecc_verify_hash() cannot produce that second case, since it rejects a NULL key before starting any async work. The check on the push goes inside the pending branch rather than into its condition, because unlike EccSign() this block has an else that maps a non zero ret onto a verification failure. Extending the condition would send a pending result down that else and log a bogus WOLFSSL_ERROR_VERBOSE() for it. Reproduced with --enable-asynccrypt --enable-pkcallbacks --enable-faultharden --enable-ecc --enable-supportedcurves and -DTEST_PK_PRIVKEY, which is what makes examples/server.c leave the private key to the callback. Running examples/server against examples/client with ECC certificates, the server takes SIGSEGV without this change and exits cleanly with it, reaching the verify callback with keySz 0 instead. The push needed one more step to reach: the in tree myEccVerify() is synchronous, so it was patched to return WC_PENDING_E once, after which the server dies in wolfAsync_EventQueuePush() at the offset of ecc_key.asyncDev, reached from EccVerify() by way of SendTls13CertificateVerify(). Note the crash is only reachable once the two callers stop dereferencing ssl->buffers.key->length on the line just above the EccVerify() call, which is what PR 11000 fixes. Both sites were patched locally to reproduce. This guard is needed in addition to that fix, not instead of it, and only matters for asynchronous builds. --- src/internal.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/internal.c b/src/internal.c index 4fd1768532..480304dc41 100644 --- a/src/internal.c +++ b/src/internal.c @@ -6177,9 +6177,11 @@ int EccVerify(WOLFSSL* ssl, const byte* in, word32 inSz, const byte* out, #ifdef WOLFSSL_ASYNC_CRYPT /* initialize event */ - ret = wolfSSL_AsyncInit(ssl, &key->asyncDev, WC_ASYNC_FLAG_CALL_AGAIN); - if (ret != 0) - return ret; + if (key) { + ret = wolfSSL_AsyncInit(ssl, &key->asyncDev, WC_ASYNC_FLAG_CALL_AGAIN); + if (ret != 0) + return ret; + } #endif #ifdef HAVE_PK_CALLBACKS @@ -6203,7 +6205,11 @@ int EccVerify(WOLFSSL* ssl, const byte* in, word32 inSz, const byte* out, /* Handle async pending response */ #ifdef WOLFSSL_ASYNC_CRYPT if (ret == WC_NO_ERR_TRACE(WC_PENDING_E)) { - ret = wolfSSL_AsyncPush(ssl, &key->asyncDev); + /* with a PK callback the private key can live only in the callback, + * leaving no async device to push */ + if (key != NULL) { + ret = wolfSSL_AsyncPush(ssl, &key->asyncDev); + } } else #endif /* WOLFSSL_ASYNC_CRYPT */