diff --git a/.github/workflows/async-examples.yml b/.github/workflows/async-examples.yml index 1d992d06f7..caca4b6c32 100644 --- a/.github/workflows/async-examples.yml +++ b/.github/workflows/async-examples.yml @@ -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 diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index 101ebf2fa8..3850dc197f 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -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 diff --git a/README-async.md b/README-async.md index 7745066880..2a37dcd842 100644 --- a/README-async.md +++ b/README-async.md @@ -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``` diff --git a/examples/async/async_client.c b/examples/async/async_client.c index c94a208d4d..510857ebdf 100644 --- a/examples/async/async_client.c +++ b/examples/async/async_client.c @@ -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); } diff --git a/examples/async/async_server.c b/examples/async/async_server.c index 0c7b936fab..9d745fbeea 100644 --- a/examples/async/async_server.c +++ b/examples/async/async_server.c @@ -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); diff --git a/src/internal.c b/src/internal.c index fed9d370de..c010b1facf 100644 --- a/src/internal.c +++ b/src/internal.c @@ -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); @@ -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 @@ -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) */ @@ -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; diff --git a/src/ssl.c b/src/ssl.c index c2a5827c9d..32270d930a 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -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; diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 3cd37c739b..987636aff1 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -5294,6 +5294,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 */