Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/async-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions .wolfssl_known_macro_extras
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,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
Expand Down
8 changes: 8 additions & 0 deletions README-async.md
Original file line number Diff line number Diff line change
Expand Up @@ -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```
Expand Down
30 changes: 27 additions & 3 deletions examples/async/async_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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;
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand Down
57 changes: 54 additions & 3 deletions examples/async/async_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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;
}
Expand All @@ -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;
}

Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
50 changes: 50 additions & 0 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -8765,6 +8765,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);
Expand Down Expand Up @@ -16032,6 +16039,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
Expand Down Expand Up @@ -16755,6 +16773,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) */

Expand Down Expand Up @@ -16970,6 +17005,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;

Expand Down
7 changes: 7 additions & 0 deletions src/ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading