Skip to content

Commit f1b7001

Browse files
Merge pull request #10738 from dgarske/zd_ecc_nonblock_certchain
Add WOLFSSL_ASYNC_CERT_YIELD: per-certificate non-blocking yield
2 parents e79c581 + c431ad6 commit f1b7001

8 files changed

Lines changed: 233 additions & 6 deletions

File tree

.github/workflows/async-examples.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,81 @@ jobs:
110110
cat "$f"
111111
fi
112112
done
113+
114+
# Per-certificate non-blocking yield (WOLFSSL_ASYNC_CERT_YIELD): the server
115+
# presents a multi-certificate ECC chain (--cert-chain) and the client must
116+
# return WC_PENDING_E once per certificate while verifying it.
117+
cert_chain_yield:
118+
if: ${{ (github.repository_owner == 'wolfssl') && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
119+
runs-on: ubuntu-24.04
120+
timeout-minutes: 10
121+
strategy:
122+
fail-fast: false
123+
matrix:
124+
async_mode: ['sw', 'cryptocb']
125+
name: Per-certificate yield (${{ matrix.async_mode }})
126+
steps:
127+
- uses: actions/checkout@v5
128+
name: Checkout wolfSSL
129+
130+
- name: Build async examples with WOLFSSL_ASYNC_CERT_YIELD
131+
run: |
132+
make -C examples/async clean
133+
make -j -C examples/async ASYNC_MODE=${{ matrix.async_mode }} \
134+
EXTRA_CFLAGS="-DWOLFSSL_ASYNC_CERT_YIELD"
135+
136+
- name: Run --cert-chain pair and assert per-certificate yield
137+
run: |
138+
set -euo pipefail
139+
ASYNC_MODE="${{ matrix.async_mode }}"
140+
ready="/tmp/wolfssl_cert_chain_ready"
141+
rm -f "$ready"
142+
143+
WOLFSSL_ASYNC_READYFILE="$ready" \
144+
./examples/async/async_server --ecc --cert-chain \
145+
> /tmp/cert_chain_server.log 2>&1 &
146+
pid=$!
147+
148+
rc=0
149+
WOLFSSL_ASYNC_READYFILE="$ready" \
150+
./examples/async/async_client --ecc --cert-chain 127.0.0.1 11111 \
151+
> /tmp/cert_chain_client.log 2>&1 || rc=$?
152+
153+
kill "$pid" >/dev/null 2>&1 || true
154+
wait "$pid" >/dev/null 2>&1 || true
155+
156+
cat /tmp/cert_chain_client.log
157+
if [ "$rc" -ne 0 ]; then
158+
echo "FAIL: handshake (exit=$rc)"
159+
exit 1
160+
fi
161+
162+
count=$(awk '/WC_PENDING_E count:/ {print $NF}' \
163+
/tmp/cert_chain_client.log)
164+
# The 2-cert chain (leaf + root) yields once per certificate.
165+
# cryptocb mode has no crypto chunking, so the count is just the
166+
# per-certificate yields (>= 2: one per intermediate plus the leaf).
167+
# sw mode also chunks the SP ECC math, so the count is much larger.
168+
if [ "$ASYNC_MODE" = "cryptocb" ]; then
169+
if [ -z "$count" ] || [ "$count" -lt 2 ]; then
170+
echo "FAIL: expected >= 2 per-certificate yields," \
171+
"got ${count:-missing}"
172+
exit 1
173+
fi
174+
else
175+
if [ -z "$count" ] || [ "$count" -lt 100 ]; then
176+
echo "FAIL: WC_PENDING_E count too low: ${count:-missing}"
177+
exit 1
178+
fi
179+
fi
180+
echo "PASS: $ASYNC_MODE per-certificate yield (WC_PENDING_E: $count)"
181+
182+
- name: Print cert-chain logs
183+
if: ${{ failure() }}
184+
run: |
185+
for f in /tmp/cert_chain_server.log /tmp/cert_chain_client.log; do
186+
if [ -f "$f" ]; then
187+
echo "==> $f"
188+
cat "$f"
189+
fi
190+
done

.wolfssl_known_macro_extras

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,7 @@ WOLFSSL_ASCON_UNROLL
729729
WOLFSSL_ASN_EXTRA
730730
WOLFSSL_ASN_TEMPLATE_NEED_SET_INT32
731731
WOLFSSL_ASN_TEMPLATE_TYPE_CHECK
732+
WOLFSSL_ASYNC_CERT_YIELD
732733
WOLFSSL_ATECC508
733734
WOLFSSL_ATECC508A_NOSOFTECC
734735
WOLFSSL_ATECC508A_TLS

README-async.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ Supported hardware backends:
2828

2929
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.
3030

31+
## Per-certificate Yield During Chain Verification (`WOLFSSL_ASYNC_CERT_YIELD`)
32+
33+
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`).
34+
35+
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.
36+
37+
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.
38+
3139
## API's
3240

3341
### ```wolfSSL_AsyncPoll```

examples/async/async_client.c

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ static int posix_net_connect(const char* host, int port)
158158
/* ------------------------------------------------------------------ */
159159
static void usage(const char* prog)
160160
{
161-
printf("usage: %s [--ecc|--x25519] [--mutual] [--tls12] [host] [port]\n",
161+
printf("usage: %s [--ecc|--x25519] [--mutual] [--cert-chain] [--tls12] "
162+
"[host] [port]\n",
162163
prog);
163164
}
164165

@@ -175,7 +176,8 @@ static const char* group_name(word16 group)
175176
}
176177

177178
static int parse_client_args(int argc, char** argv,
178-
const char** host, int* port, word16* group, int* mutual, int* tls12)
179+
const char** host, int* port, word16* group, int* mutual, int* tls12,
180+
int* certChain)
179181
{
180182
int i;
181183
int host_set = 0;
@@ -186,6 +188,7 @@ static int parse_client_args(int argc, char** argv,
186188
*group = WOLFSSL_ECC_SECP256R1;
187189
*mutual = 0;
188190
*tls12 = 0;
191+
*certChain = 0;
189192

190193
for (i = 1; i < argc; i++) {
191194
if (XSTRCMP(argv[i], "--ecc") == 0) {
@@ -197,6 +200,10 @@ static int parse_client_args(int argc, char** argv,
197200
else if (XSTRCMP(argv[i], "--mutual") == 0) {
198201
*mutual = 1;
199202
}
203+
else if (XSTRCMP(argv[i], "--cert-chain") == 0) {
204+
/* Verify the server's multi-certificate ECC chain (leaf + root). */
205+
*certChain = 1;
206+
}
200207
else if (XSTRCMP(argv[i], "--tls12") == 0) {
201208
*tls12 = 1;
202209
}
@@ -216,6 +223,11 @@ static int parse_client_args(int argc, char** argv,
216223
}
217224
}
218225

226+
/* --cert-chain verifies an ECC certificate chain; it is ECC-only. */
227+
if (*certChain && *group == WOLFSSL_ECC_X25519) {
228+
return -1;
229+
}
230+
219231
return 0;
220232
}
221233

@@ -252,9 +264,10 @@ int client_async_test(int argc, char** argv)
252264
const char* mode = NULL;
253265
int mutual = 0;
254266
int tls12 = 0;
267+
int certChain = 0;
255268

256269
if (parse_client_args(argc, argv, &host, &port, &group, &mutual,
257-
&tls12) != 0) {
270+
&tls12, &certChain) != 0) {
258271
usage(argv[0]);
259272
return 0;
260273
}
@@ -383,6 +396,17 @@ int client_async_test(int argc, char** argv)
383396
}
384397
wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_PEER, NULL);
385398
}
399+
else if (certChain) {
400+
/* Verify the server's multi-certificate ECC chain (leaf + root)
401+
* against the root CA, without presenting a client certificate. */
402+
ret = wolfSSL_CTX_load_verify_buffer(ctx, ca_ecc_cert_der_256,
403+
sizeof_ca_ecc_cert_der_256, WOLFSSL_FILETYPE_ASN1);
404+
if (ret != WOLFSSL_SUCCESS) {
405+
fprintf(stderr, "ERROR: failed to load ECC CA cert.\n");
406+
goto out;
407+
}
408+
wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_PEER, NULL);
409+
}
386410
else {
387411
wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_NONE, NULL);
388412
}

examples/async/async_server.c

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ static int posix_set_nonblocking(int fd)
117117
/* ------------------------------------------------------------------ */
118118
static void usage(const char* prog)
119119
{
120-
printf("usage: %s [--ecc|--x25519] [--mutual] [--tls12] [port]\n", prog);
120+
printf("usage: %s [--ecc|--x25519] [--mutual] [--cert-chain] [--tls12] "
121+
"[port]\n", prog);
121122
}
122123

123124
static const char* group_name(word16 group)
@@ -133,7 +134,7 @@ static const char* group_name(word16 group)
133134
}
134135

135136
static int parse_server_args(int argc, char** argv, int* port, word16* group,
136-
int* mutual, int* tls12)
137+
int* mutual, int* tls12, int* certChain)
137138
{
138139
int i;
139140
int port_set = 0;
@@ -142,6 +143,7 @@ static int parse_server_args(int argc, char** argv, int* port, word16* group,
142143
*group = WOLFSSL_ECC_SECP256R1;
143144
*mutual = 0;
144145
*tls12 = 0;
146+
*certChain = 0;
145147

146148
for (i = 1; i < argc; i++) {
147149
if (XSTRCMP(argv[i], "--ecc") == 0) {
@@ -153,6 +155,12 @@ static int parse_server_args(int argc, char** argv, int* port, word16* group,
153155
else if (XSTRCMP(argv[i], "--mutual") == 0) {
154156
*mutual = 1;
155157
}
158+
else if (XSTRCMP(argv[i], "--cert-chain") == 0) {
159+
/* Present a multi-certificate ECC chain (leaf + root) so the peer
160+
* exercises per-certificate processing (and, with
161+
* WOLFSSL_ASYNC_CERT_YIELD, the per-cert non-blocking yield). */
162+
*certChain = 1;
163+
}
156164
else if (XSTRCMP(argv[i], "--tls12") == 0) {
157165
*tls12 = 1;
158166
}
@@ -168,6 +176,11 @@ static int parse_server_args(int argc, char** argv, int* port, word16* group,
168176
}
169177
}
170178

179+
/* --cert-chain assembles an ECC certificate chain; it is ECC-only. */
180+
if (*certChain && *group == WOLFSSL_ECC_X25519) {
181+
return -1;
182+
}
183+
171184
return 0;
172185
}
173186

@@ -187,6 +200,7 @@ int server_async_test(int argc, char** argv)
187200
const char* mode = NULL;
188201
int mutual = 0;
189202
int tls12 = 0;
203+
int certChain = 0;
190204
#ifdef WOLFSSL_ASYNC_CRYPT
191205
int devId = INVALID_DEVID;
192206
#endif
@@ -216,7 +230,8 @@ int server_async_test(int argc, char** argv)
216230
}
217231
#endif
218232

219-
if (parse_server_args(argc, argv, &port, &group, &mutual, &tls12) != 0) {
233+
if (parse_server_args(argc, argv, &port, &group, &mutual, &tls12,
234+
&certChain) != 0) {
220235
usage(argv[0]);
221236
return 0;
222237
}
@@ -378,6 +393,42 @@ int server_async_test(int argc, char** argv)
378393
goto exit;
379394
#endif
380395
}
396+
else if (certChain) {
397+
/* Present a 2-cert ECC chain (leaf + root) assembled from the bundled
398+
* buffers so the peer verifies a multi-certificate chain. With
399+
* WOLFSSL_ASYNC_CERT_YIELD this exercises the per-certificate
400+
* non-blocking yield in ProcessPeerCerts(). Kept static to avoid a
401+
* >1KB stack buffer on the small-stack targets this example targets. */
402+
static byte eccChain[sizeof_serv_ecc_der_256 +
403+
sizeof_ca_ecc_cert_der_256];
404+
XMEMCPY(eccChain, serv_ecc_der_256, sizeof_serv_ecc_der_256);
405+
XMEMCPY(eccChain + sizeof_serv_ecc_der_256, ca_ecc_cert_der_256,
406+
sizeof_ca_ecc_cert_der_256);
407+
ret = wolfSSL_CTX_use_certificate_chain_buffer_format(ctx, eccChain,
408+
(long)sizeof(eccChain), WOLFSSL_FILETYPE_ASN1);
409+
if (ret != WOLFSSL_SUCCESS) {
410+
fprintf(stderr, "ERROR: failed to load ECC server cert chain.\n");
411+
goto exit;
412+
}
413+
414+
ret = wolfSSL_CTX_use_PrivateKey_buffer(ctx, ecc_key_der_256,
415+
sizeof_ecc_key_der_256, WOLFSSL_FILETYPE_ASN1);
416+
if (ret != WOLFSSL_SUCCESS) {
417+
fprintf(stderr, "ERROR: failed to load ECC server key buffer.\n");
418+
goto exit;
419+
}
420+
421+
if (mutual) {
422+
/* client-ecc-cert is self-signed, so load it as its own CA */
423+
ret = wolfSSL_CTX_load_verify_buffer(ctx, cliecc_cert_der_256,
424+
sizeof_cliecc_cert_der_256, WOLFSSL_FILETYPE_ASN1);
425+
if (ret != WOLFSSL_SUCCESS) {
426+
fprintf(stderr,
427+
"ERROR: failed to load ECC client CA cert.\n");
428+
goto exit;
429+
}
430+
}
431+
}
381432
else {
382433
ret = wolfSSL_CTX_use_certificate_buffer(ctx, serv_ecc_der_256,
383434
sizeof_serv_ecc_der_256, WOLFSSL_FILETYPE_ASN1);

src/internal.c

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8766,6 +8766,13 @@ void FreeAsyncCtx(WOLFSSL* ssl, byte freeAsync)
87668766
ssl->async->freeArgs(ssl, ssl->async->args);
87678767
ssl->async->freeArgs = NULL;
87688768
}
8769+
#if defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_ASYNC_CERT_YIELD)
8770+
/* The per-certificate yield flag is tied to an in-progress
8771+
* ProcessPeerCerts context (which only persists across a yield, never
8772+
* across this teardown). Clear it here so a later, freshly-allocated
8773+
* ssl->async can never resume on a stale flag. */
8774+
ssl->options.certYieldPending = 0;
8775+
#endif
87698776
#if defined(WOLFSSL_ASYNC_CRYPT) && !defined(WOLFSSL_NO_TLS12)
87708777
if (ssl->options.buildArgsSet) {
87718778
FreeBuildMsgArgs(ssl, &ssl->async->buildArgs);
@@ -16052,6 +16059,17 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx,
1605216059
if (ret < 0)
1605316060
goto exit_ppc;
1605416061
}
16062+
#ifdef WOLFSSL_ASYNC_CERT_YIELD
16063+
/* Re-entry after a deliberate per-certificate yield. No async crypto event
16064+
* was queued, so AsyncPop returns WC_NO_PENDING_E; keep the saved state and
16065+
* resume cert processing instead of resetting. The flag lives in
16066+
* ssl->options (zero-initialized) so this never fires on a fresh entry with
16067+
* a stale args scratch buffer. */
16068+
else if (ssl->options.certYieldPending) {
16069+
ssl->options.certYieldPending = 0;
16070+
ret = 0;
16071+
}
16072+
#endif
1605516073
else
1605616074
#endif /* WOLFSSL_ASYNC_CRYPT */
1605716075
#ifdef WOLFSSL_NONBLOCK_OCSP
@@ -16775,6 +16793,23 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx,
1677516793
FreeDecodedCert(args->dCert);
1677616794
args->dCertInit = 0;
1677716795
args->count--;
16796+
16797+
#if defined(WOLFSSL_ASYNC_CRYPT) && \
16798+
defined(WOLFSSL_ASYNC_CERT_YIELD)
16799+
/* return WC_PENDING_E after each chain certificate is
16800+
* verified so a cooperative scheduler regains control
16801+
* between certificates. The verify above has fully
16802+
* completed for this certificate; no async crypto event is
16803+
* queued, so the certYieldPending flag tells the re-entry
16804+
* path to resume the loop at the next certificate. */
16805+
if (ret == 0) {
16806+
WOLFSSL_MSG("Yielding WC_PENDING_E between chain "
16807+
"certificate verifies");
16808+
ssl->options.certYieldPending = 1;
16809+
ret = WC_PENDING_E;
16810+
goto exit_ppc;
16811+
}
16812+
#endif
1677816813
} /* while (count > 1 && !args->haveTrustPeer) */
1677916814
} /* if (count > 0) */
1678016815

@@ -16990,6 +17025,21 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx,
1699017025

1699117026
/* Advance state and proceed */
1699217027
ssl->options.asyncState = TLS_ASYNC_VERIFY;
17028+
17029+
#if defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_ASYNC_CERT_YIELD)
17030+
/* Opt-in (WOLFSSL_ASYNC_CERT_YIELD): yield once more after the peer
17031+
* (leaf) certificate is verified, before OCSP/CRL and finalization.
17032+
* The state has already advanced to TLS_ASYNC_VERIFY, so the
17033+
* certYieldPending re-entry path resumes there rather than
17034+
* re-processing the leaf. */
17035+
if (ret == 0 && args->count > 0) {
17036+
WOLFSSL_MSG("Yielding WC_PENDING_E after peer certificate "
17037+
"verify");
17038+
ssl->options.certYieldPending = 1;
17039+
ret = WC_PENDING_E;
17040+
goto exit_ppc;
17041+
}
17042+
#endif
1699317043
} /* case TLS_ASYNC_DO */
1699417044
FALL_THROUGH;
1699517045

src/ssl.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7901,6 +7901,13 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out,
79017901
ssl->options.acceptState = ACCEPT_BEGIN;
79027902
ssl->options.handShakeState = NULL_STATE;
79037903
ssl->options.handShakeDone = 0;
7904+
#if defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_ASYNC_CERT_YIELD)
7905+
/* A per-certificate yield (WOLFSSL_ASYNC_CERT_YIELD) sets this and it is
7906+
* normally cleared on the next ProcessPeerCerts re-entry. Clear it here
7907+
* so reusing this object after abandoning a yielded handshake cannot
7908+
* skip the ProcessPeerCerts state reset on the next fresh entry. */
7909+
ssl->options.certYieldPending = 0;
7910+
#endif
79047911
ssl->recordSzOverhead = 0;
79057912
ssl->options.processReply = 0; /* doProcessInit */
79067913
ssl->options.havePeerVerify = 0;

0 commit comments

Comments
 (0)