Merge pull request #10738 from dgarske/zd_ecc_nonblock_certchain

Add WOLFSSL_ASYNC_CERT_YIELD: per-certificate non-blocking yield
This commit is contained in:
JacobBarthelmeh
2026-07-06 14:21:08 -06:00
committed by GitHub
8 changed files with 233 additions and 6 deletions
+78
View File
@@ -110,3 +110,81 @@ jobs:
cat "$f"
fi
done
# Per-certificate non-blocking yield (WOLFSSL_ASYNC_CERT_YIELD): the server
# presents a multi-certificate ECC chain (--cert-chain) and the client must
# return WC_PENDING_E once per certificate while verifying it.
cert_chain_yield:
if: ${{ (github.repository_owner == 'wolfssl') && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
async_mode: ['sw', 'cryptocb']
name: Per-certificate yield (${{ matrix.async_mode }})
steps:
- uses: actions/checkout@v5
name: Checkout wolfSSL
- name: Build async examples with WOLFSSL_ASYNC_CERT_YIELD
run: |
make -C examples/async clean
make -j -C examples/async ASYNC_MODE=${{ matrix.async_mode }} \
EXTRA_CFLAGS="-DWOLFSSL_ASYNC_CERT_YIELD"
- name: Run --cert-chain pair and assert per-certificate yield
run: |
set -euo pipefail
ASYNC_MODE="${{ matrix.async_mode }}"
ready="/tmp/wolfssl_cert_chain_ready"
rm -f "$ready"
WOLFSSL_ASYNC_READYFILE="$ready" \
./examples/async/async_server --ecc --cert-chain \
> /tmp/cert_chain_server.log 2>&1 &
pid=$!
rc=0
WOLFSSL_ASYNC_READYFILE="$ready" \
./examples/async/async_client --ecc --cert-chain 127.0.0.1 11111 \
> /tmp/cert_chain_client.log 2>&1 || rc=$?
kill "$pid" >/dev/null 2>&1 || true
wait "$pid" >/dev/null 2>&1 || true
cat /tmp/cert_chain_client.log
if [ "$rc" -ne 0 ]; then
echo "FAIL: handshake (exit=$rc)"
exit 1
fi
count=$(awk '/WC_PENDING_E count:/ {print $NF}' \
/tmp/cert_chain_client.log)
# The 2-cert chain (leaf + root) yields once per certificate.
# cryptocb mode has no crypto chunking, so the count is just the
# per-certificate yields (>= 2: one per intermediate plus the leaf).
# sw mode also chunks the SP ECC math, so the count is much larger.
if [ "$ASYNC_MODE" = "cryptocb" ]; then
if [ -z "$count" ] || [ "$count" -lt 2 ]; then
echo "FAIL: expected >= 2 per-certificate yields," \
"got ${count:-missing}"
exit 1
fi
else
if [ -z "$count" ] || [ "$count" -lt 100 ]; then
echo "FAIL: WC_PENDING_E count too low: ${count:-missing}"
exit 1
fi
fi
echo "PASS: $ASYNC_MODE per-certificate yield (WC_PENDING_E: $count)"
- name: Print cert-chain logs
if: ${{ failure() }}
run: |
for f in /tmp/cert_chain_server.log /tmp/cert_chain_client.log; do
if [ -f "$f" ]; then
echo "==> $f"
cat "$f"
fi
done
+1
View File
@@ -729,6 +729,7 @@ WOLFSSL_ASCON_UNROLL
WOLFSSL_ASN_EXTRA
WOLFSSL_ASN_TEMPLATE_NEED_SET_INT32
WOLFSSL_ASN_TEMPLATE_TYPE_CHECK
WOLFSSL_ASYNC_CERT_YIELD
WOLFSSL_ATECC508
WOLFSSL_ATECC508A_NOSOFTECC
WOLFSSL_ATECC508A_TLS
+8
View File
@@ -28,6 +28,14 @@ Supported hardware backends:
The wolfCrypt backend uses the same API as the hardware backends do. Once an asynchronous operation is initiated with the software backend, subsequent calls to `wolfSSL_AsyncPoll` will call into wolfCrypt to complete the operation. If non-blocking is enabled, for example, for ECC (via `WC_ECC_NONBLOCK`), each `wolfSSL_AsyncPoll` will do a chunk of work for the operation and return, to minimize blocking time.
## Per-certificate Yield During Chain Verification (`WOLFSSL_ASYNC_CERT_YIELD`)
By default the TLS handshake verifies every certificate in the peer's chain in a single `wolfSSL_connect()` / `wolfSSL_accept()` call. On a cooperative, single-threaded scheduler a long chain can therefore hold the CPU long enough to trip a watchdog. Building with `WOLFSSL_ASYNC_CRYPT` and the opt-in `WOLFSSL_ASYNC_CERT_YIELD` makes `ProcessPeerCerts()` return `WC_PENDING_E` to the caller after each chain certificate (and after the peer/leaf certificate) is verified, so the application's loop regains control between certificates and can service its watchdog or run other tasks before re-entering. This is independent of `WC_ECC_NONBLOCK`: you get one yield per certificate even when each signature verify is a single blocking call. `WC_ECC_NONBLOCK` additionally subdivides each verify into smaller chunks. The macro is registered with the example/test in `examples/async` (run the client/server with `--cert-chain`).
Important: these per-certificate yields return `WC_PENDING_E` WITHOUT enqueuing an async device event (`ssl->asyncDev` stays NULL, the event queue stays empty). They are intended for cooperative schedulers that unconditionally re-call `wolfSSL_connect()` / `wolfSSL_accept()` (optionally after a best-effort `wolfSSL_AsyncPoll()`, which simply returns 0 events). They are NOT suitable for event-loop callers that block waiting on the async device file descriptor for a hardware completion, because no such completion is delivered for these yields and the caller would stall during peer certificate processing. Leave `WOLFSSL_ASYNC_CERT_YIELD` undefined (the default) for fd/event-driven async usage.
If a handshake is abandoned after a per-certificate yield rather than driven to completion, call `wolfSSL_clear()` (or free and recreate the `WOLFSSL` object) before reusing it; `wolfSSL_clear()` clears the pending-yield state so the next handshake starts cleanly.
## API's
### ```wolfSSL_AsyncPoll```
+27 -3
View File
@@ -158,7 +158,8 @@ static int posix_net_connect(const char* host, int port)
/* ------------------------------------------------------------------ */
static void usage(const char* prog)
{
printf("usage: %s [--ecc|--x25519] [--mutual] [--tls12] [host] [port]\n",
printf("usage: %s [--ecc|--x25519] [--mutual] [--cert-chain] [--tls12] "
"[host] [port]\n",
prog);
}
@@ -175,7 +176,8 @@ static const char* group_name(word16 group)
}
static int parse_client_args(int argc, char** argv,
const char** host, int* port, word16* group, int* mutual, int* tls12)
const char** host, int* port, word16* group, int* mutual, int* tls12,
int* certChain)
{
int i;
int host_set = 0;
@@ -186,6 +188,7 @@ static int parse_client_args(int argc, char** argv,
*group = WOLFSSL_ECC_SECP256R1;
*mutual = 0;
*tls12 = 0;
*certChain = 0;
for (i = 1; i < argc; i++) {
if (XSTRCMP(argv[i], "--ecc") == 0) {
@@ -197,6 +200,10 @@ static int parse_client_args(int argc, char** argv,
else if (XSTRCMP(argv[i], "--mutual") == 0) {
*mutual = 1;
}
else if (XSTRCMP(argv[i], "--cert-chain") == 0) {
/* Verify the server's multi-certificate ECC chain (leaf + root). */
*certChain = 1;
}
else if (XSTRCMP(argv[i], "--tls12") == 0) {
*tls12 = 1;
}
@@ -216,6 +223,11 @@ static int parse_client_args(int argc, char** argv,
}
}
/* --cert-chain verifies an ECC certificate chain; it is ECC-only. */
if (*certChain && *group == WOLFSSL_ECC_X25519) {
return -1;
}
return 0;
}
@@ -252,9 +264,10 @@ int client_async_test(int argc, char** argv)
const char* mode = NULL;
int mutual = 0;
int tls12 = 0;
int certChain = 0;
if (parse_client_args(argc, argv, &host, &port, &group, &mutual,
&tls12) != 0) {
&tls12, &certChain) != 0) {
usage(argv[0]);
return 0;
}
@@ -383,6 +396,17 @@ int client_async_test(int argc, char** argv)
}
wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_PEER, NULL);
}
else if (certChain) {
/* Verify the server's multi-certificate ECC chain (leaf + root)
* against the root CA, without presenting a client certificate. */
ret = wolfSSL_CTX_load_verify_buffer(ctx, ca_ecc_cert_der_256,
sizeof_ca_ecc_cert_der_256, WOLFSSL_FILETYPE_ASN1);
if (ret != WOLFSSL_SUCCESS) {
fprintf(stderr, "ERROR: failed to load ECC CA cert.\n");
goto out;
}
wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_PEER, NULL);
}
else {
wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_NONE, NULL);
}
+54 -3
View File
@@ -117,7 +117,8 @@ static int posix_set_nonblocking(int fd)
/* ------------------------------------------------------------------ */
static void usage(const char* prog)
{
printf("usage: %s [--ecc|--x25519] [--mutual] [--tls12] [port]\n", prog);
printf("usage: %s [--ecc|--x25519] [--mutual] [--cert-chain] [--tls12] "
"[port]\n", prog);
}
static const char* group_name(word16 group)
@@ -133,7 +134,7 @@ static const char* group_name(word16 group)
}
static int parse_server_args(int argc, char** argv, int* port, word16* group,
int* mutual, int* tls12)
int* mutual, int* tls12, int* certChain)
{
int i;
int port_set = 0;
@@ -142,6 +143,7 @@ static int parse_server_args(int argc, char** argv, int* port, word16* group,
*group = WOLFSSL_ECC_SECP256R1;
*mutual = 0;
*tls12 = 0;
*certChain = 0;
for (i = 1; i < argc; i++) {
if (XSTRCMP(argv[i], "--ecc") == 0) {
@@ -153,6 +155,12 @@ static int parse_server_args(int argc, char** argv, int* port, word16* group,
else if (XSTRCMP(argv[i], "--mutual") == 0) {
*mutual = 1;
}
else if (XSTRCMP(argv[i], "--cert-chain") == 0) {
/* Present a multi-certificate ECC chain (leaf + root) so the peer
* exercises per-certificate processing (and, with
* WOLFSSL_ASYNC_CERT_YIELD, the per-cert non-blocking yield). */
*certChain = 1;
}
else if (XSTRCMP(argv[i], "--tls12") == 0) {
*tls12 = 1;
}
@@ -168,6 +176,11 @@ static int parse_server_args(int argc, char** argv, int* port, word16* group,
}
}
/* --cert-chain assembles an ECC certificate chain; it is ECC-only. */
if (*certChain && *group == WOLFSSL_ECC_X25519) {
return -1;
}
return 0;
}
@@ -187,6 +200,7 @@ int server_async_test(int argc, char** argv)
const char* mode = NULL;
int mutual = 0;
int tls12 = 0;
int certChain = 0;
#ifdef WOLFSSL_ASYNC_CRYPT
int devId = INVALID_DEVID;
#endif
@@ -216,7 +230,8 @@ int server_async_test(int argc, char** argv)
}
#endif
if (parse_server_args(argc, argv, &port, &group, &mutual, &tls12) != 0) {
if (parse_server_args(argc, argv, &port, &group, &mutual, &tls12,
&certChain) != 0) {
usage(argv[0]);
return 0;
}
@@ -378,6 +393,42 @@ int server_async_test(int argc, char** argv)
goto exit;
#endif
}
else if (certChain) {
/* Present a 2-cert ECC chain (leaf + root) assembled from the bundled
* buffers so the peer verifies a multi-certificate chain. With
* WOLFSSL_ASYNC_CERT_YIELD this exercises the per-certificate
* non-blocking yield in ProcessPeerCerts(). Kept static to avoid a
* >1KB stack buffer on the small-stack targets this example targets. */
static byte eccChain[sizeof_serv_ecc_der_256 +
sizeof_ca_ecc_cert_der_256];
XMEMCPY(eccChain, serv_ecc_der_256, sizeof_serv_ecc_der_256);
XMEMCPY(eccChain + sizeof_serv_ecc_der_256, ca_ecc_cert_der_256,
sizeof_ca_ecc_cert_der_256);
ret = wolfSSL_CTX_use_certificate_chain_buffer_format(ctx, eccChain,
(long)sizeof(eccChain), WOLFSSL_FILETYPE_ASN1);
if (ret != WOLFSSL_SUCCESS) {
fprintf(stderr, "ERROR: failed to load ECC server cert chain.\n");
goto exit;
}
ret = wolfSSL_CTX_use_PrivateKey_buffer(ctx, ecc_key_der_256,
sizeof_ecc_key_der_256, WOLFSSL_FILETYPE_ASN1);
if (ret != WOLFSSL_SUCCESS) {
fprintf(stderr, "ERROR: failed to load ECC server key buffer.\n");
goto exit;
}
if (mutual) {
/* client-ecc-cert is self-signed, so load it as its own CA */
ret = wolfSSL_CTX_load_verify_buffer(ctx, cliecc_cert_der_256,
sizeof_cliecc_cert_der_256, WOLFSSL_FILETYPE_ASN1);
if (ret != WOLFSSL_SUCCESS) {
fprintf(stderr,
"ERROR: failed to load ECC client CA cert.\n");
goto exit;
}
}
}
else {
ret = wolfSSL_CTX_use_certificate_buffer(ctx, serv_ecc_der_256,
sizeof_serv_ecc_der_256, WOLFSSL_FILETYPE_ASN1);
+50
View File
@@ -8766,6 +8766,13 @@ void FreeAsyncCtx(WOLFSSL* ssl, byte freeAsync)
ssl->async->freeArgs(ssl, ssl->async->args);
ssl->async->freeArgs = NULL;
}
#if defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_ASYNC_CERT_YIELD)
/* The per-certificate yield flag is tied to an in-progress
* ProcessPeerCerts context (which only persists across a yield, never
* across this teardown). Clear it here so a later, freshly-allocated
* ssl->async can never resume on a stale flag. */
ssl->options.certYieldPending = 0;
#endif
#if defined(WOLFSSL_ASYNC_CRYPT) && !defined(WOLFSSL_NO_TLS12)
if (ssl->options.buildArgsSet) {
FreeBuildMsgArgs(ssl, &ssl->async->buildArgs);
@@ -16052,6 +16059,17 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx,
if (ret < 0)
goto exit_ppc;
}
#ifdef WOLFSSL_ASYNC_CERT_YIELD
/* Re-entry after a deliberate per-certificate yield. No async crypto event
* was queued, so AsyncPop returns WC_NO_PENDING_E; keep the saved state and
* resume cert processing instead of resetting. The flag lives in
* ssl->options (zero-initialized) so this never fires on a fresh entry with
* a stale args scratch buffer. */
else if (ssl->options.certYieldPending) {
ssl->options.certYieldPending = 0;
ret = 0;
}
#endif
else
#endif /* WOLFSSL_ASYNC_CRYPT */
#ifdef WOLFSSL_NONBLOCK_OCSP
@@ -16775,6 +16793,23 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx,
FreeDecodedCert(args->dCert);
args->dCertInit = 0;
args->count--;
#if defined(WOLFSSL_ASYNC_CRYPT) && \
defined(WOLFSSL_ASYNC_CERT_YIELD)
/* return WC_PENDING_E after each chain certificate is
* verified so a cooperative scheduler regains control
* between certificates. The verify above has fully
* completed for this certificate; no async crypto event is
* queued, so the certYieldPending flag tells the re-entry
* path to resume the loop at the next certificate. */
if (ret == 0) {
WOLFSSL_MSG("Yielding WC_PENDING_E between chain "
"certificate verifies");
ssl->options.certYieldPending = 1;
ret = WC_PENDING_E;
goto exit_ppc;
}
#endif
} /* while (count > 1 && !args->haveTrustPeer) */
} /* if (count > 0) */
@@ -16990,6 +17025,21 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx,
/* Advance state and proceed */
ssl->options.asyncState = TLS_ASYNC_VERIFY;
#if defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_ASYNC_CERT_YIELD)
/* Opt-in (WOLFSSL_ASYNC_CERT_YIELD): yield once more after the peer
* (leaf) certificate is verified, before OCSP/CRL and finalization.
* The state has already advanced to TLS_ASYNC_VERIFY, so the
* certYieldPending re-entry path resumes there rather than
* re-processing the leaf. */
if (ret == 0 && args->count > 0) {
WOLFSSL_MSG("Yielding WC_PENDING_E after peer certificate "
"verify");
ssl->options.certYieldPending = 1;
ret = WC_PENDING_E;
goto exit_ppc;
}
#endif
} /* case TLS_ASYNC_DO */
FALL_THROUGH;
+7
View File
@@ -7901,6 +7901,13 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out,
ssl->options.acceptState = ACCEPT_BEGIN;
ssl->options.handShakeState = NULL_STATE;
ssl->options.handShakeDone = 0;
#if defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_ASYNC_CERT_YIELD)
/* A per-certificate yield (WOLFSSL_ASYNC_CERT_YIELD) sets this and it is
* normally cleared on the next ProcessPeerCerts re-entry. Clear it here
* so reusing this object after abandoning a yielded handshake cannot
* skip the ProcessPeerCerts state reset on the next fresh entry. */
ssl->options.certYieldPending = 0;
#endif
ssl->recordSzOverhead = 0;
ssl->options.processReply = 0; /* doProcessInit */
ssl->options.havePeerVerify = 0;
+8
View File
@@ -5302,6 +5302,14 @@ struct Options {
#endif
word16 returnOnGoodCh:1;
word16 disableRead:1;
#if defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_ASYNC_CERT_YIELD)
/* Opt-in (WOLFSSL_ASYNC_CERT_YIELD): set when we deliberately returned
* WC_PENDING_E between peer certificate verifies so a cooperative scheduler
* can run. Lives in (zero-initialized, persistent) ssl->options so the
* fresh-entry vs. resume decision in ProcessPeerCerts is reliable; the
* transient ProcPeerCertArgs scratch buffer is not zeroed on alloc. */
word16 certYieldPending:1;
#endif
#ifdef WOLFSSL_EARLY_DATA
word16 clientInEarlyData:1; /* Client is in wolfSSL_read_early_data */