diff --git a/.github/workflows/async.yml b/.github/workflows/async.yml index 2d11923904..69865b7c2f 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. 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.", + {"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 their built-in devices never return WC_PENDING_E; pending is exercised there by the tests that register their own pending callback (hkdf_cryptocb_async_test() in wolfCrypt, test_tls13_cryptocb_async in the api suite). 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/tests/api/test_tls13.c b/tests/api/test_tls13.c index a28870a0f3..18c640aae1 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -11241,3 +11241,374 @@ int test_tls13_is_init_finished_want_write(void) #endif return EXPECT_RESULT(); } + +#if defined(WOLFSSL_TLS13) && defined(WOLF_CRYPTO_CB) && \ + defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_ASYNC_REINVOKE) && \ + defined(HAVE_ECC) && defined(HAVE_SUPPORTED_CURVES) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + +#define TEST_TLS13_CB_PEND_DEVID_C 1 +#define TEST_TLS13_CB_PEND_DEVID_S 2 +#define TEST_TLS13_CB_PEND_JOBS 64 + +/* Which class of operation the callback pends. */ +enum TestTls13PendTarget { + TEST_TLS13_PEND_AESGCM, + TEST_TLS13_PEND_ECC_KEYGEN, + TEST_TLS13_PEND_ECDSA_SIGN, + TEST_TLS13_PEND_ECDSA_VERIFY, + TEST_TLS13_PEND_KDF, + TEST_TLS13_PEND_HMAC +}; + +typedef struct TestTls13PendCtx { + /* Job table keyed by request: first sight queues and pends, the + * identical re-invocation completes (falls through to software). */ + unsigned long jobs[TEST_TLS13_CB_PEND_JOBS]; + int jobCount; + int target; /* enum TestTls13PendTarget */ + int pended; /* WC_PENDING_E results issued; asserted non-zero */ + int seen; /* matching requests observed; asserted non-zero */ +} TestTls13PendCtx; + +static int TestTls13PendMatches(int target, wc_CryptoInfo* info) +{ + int match = 0; + + switch (target) { +#ifndef WOLF_CRYPTO_CB_ASYNC_POLL + /* With WOLF_CRYPTO_CB_ASYNC_POLL the record ciphers follow the + * poll-completion contract instead of re-invocation; that model is + * covered by tests/api/test_async.c. */ + case TEST_TLS13_PEND_AESGCM: + match = (info->algo_type == WC_ALGO_TYPE_CIPHER) && + (info->cipher.type == WC_CIPHER_AES_GCM); + break; +#endif + case TEST_TLS13_PEND_ECC_KEYGEN: + match = (info->algo_type == WC_ALGO_TYPE_PK) && + (info->pk.type == WC_PK_TYPE_EC_KEYGEN); + break; + case TEST_TLS13_PEND_ECDSA_SIGN: + match = (info->algo_type == WC_ALGO_TYPE_PK) && + (info->pk.type == WC_PK_TYPE_ECDSA_SIGN); + break; + case TEST_TLS13_PEND_ECDSA_VERIFY: + match = (info->algo_type == WC_ALGO_TYPE_PK) && + (info->pk.type == WC_PK_TYPE_ECDSA_VERIFY); + break; + case TEST_TLS13_PEND_KDF: + match = (info->algo_type == WC_ALGO_TYPE_KDF); + break; + case TEST_TLS13_PEND_HMAC: + match = (info->algo_type == WC_ALGO_TYPE_HMAC); + break; + default: + break; + } + + return match; +} + +/* Request fingerprint over the whole info struct: op class, sizes, pointers + * and inline content distinguish interleaved requests. */ +static unsigned long TestTls13PendHash(wc_CryptoInfo* info) +{ + unsigned long h = 5381; + const unsigned char* b = (const unsigned char*)info; + size_t i; + + for (i = 0; i < sizeof(*info); i++) + h = h * 33 + b[i]; + return h; +} + +static int TestTls13PendCb(int devIdArg, wc_CryptoInfo* info, void* ctx) +{ + TestTls13PendCtx* c = (TestTls13PendCtx*)ctx; + unsigned long h; + int i; + + (void)devIdArg; + + if (info == NULL || c == NULL) + return WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE); +#if defined(HAVE_HKDF) && !defined(NO_HMAC) && !defined(HAVE_SELFTEST) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION_GE(7,0)) + if (c->target == TEST_TLS13_PEND_HMAC && + info->algo_type == WC_ALGO_TYPE_KDF) { + /* Serve the key schedule synchronously in software; falling back + * with the devId set would route its internal HMACs back here and + * those cannot resume (see wolfcrypt/src/hmac.c). Only the + * TLS-layer transcript HMACs are left to pend. */ + if (info->kdf.type == WC_KDF_TYPE_HKDF_EXTRACT) { + return wc_HKDF_Extract_ex(info->kdf.hkdf_extract.hashType, + info->kdf.hkdf_extract.salt, info->kdf.hkdf_extract.saltSz, + info->kdf.hkdf_extract.inKey, info->kdf.hkdf_extract.inKeySz, + info->kdf.hkdf_extract.out, NULL, INVALID_DEVID); + } + if (info->kdf.type == WC_KDF_TYPE_HKDF_EXPAND) { + return wc_HKDF_Expand_ex(info->kdf.hkdf_expand.hashType, + info->kdf.hkdf_expand.inKey, info->kdf.hkdf_expand.inKeySz, + info->kdf.hkdf_expand.info, info->kdf.hkdf_expand.infoSz, + info->kdf.hkdf_expand.out, info->kdf.hkdf_expand.outSz, + NULL, INVALID_DEVID); + } + if (info->kdf.type == WC_KDF_TYPE_HKDF) { + return wc_HKDF_ex(info->kdf.hkdf.hashType, + info->kdf.hkdf.inKey, info->kdf.hkdf.inKeySz, + info->kdf.hkdf.salt, info->kdf.hkdf.saltSz, + info->kdf.hkdf.info, info->kdf.hkdf.infoSz, + info->kdf.hkdf.out, info->kdf.hkdf.outSz, + NULL, INVALID_DEVID); + } + } +#endif + + if (!TestTls13PendMatches(c->target, info)) + return WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE); + + c->seen++; + h = TestTls13PendHash(info); + for (i = 0; i < c->jobCount; i++) { + if (c->jobs[i] == h) { + /* Re-invocation of a pended request: complete it. */ + c->jobs[i] = c->jobs[--c->jobCount]; + return WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE); + } + } + if (c->jobCount < TEST_TLS13_CB_PEND_JOBS) { + c->jobs[c->jobCount++] = h; + c->pended++; + return WC_PENDING_E; + } + + return WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE); +} + +/* wolfSSL_write()/wolfSSL_read() can return WC_PENDING_E just as the + * handshake does; drive them with the poll-and-retry loop the application is + * expected to use. Returns the byte count, or the error. */ +static int TestTls13PendWrite(WOLFSSL* ssl, const char* buf, int sz) +{ + int ret; + int err; + int rounds = 0; + + do { + ret = wolfSSL_write(ssl, buf, sz); + if (ret > 0) + break; + err = wolfSSL_get_error(ssl, ret); + if (err == WC_NO_ERR_TRACE(WC_PENDING_E)) { + if (wolfSSL_AsyncPoll(ssl, WOLF_POLL_FLAG_CHECK_HW) < 0) + return -1; + } + else if (err != WOLFSSL_ERROR_WANT_READ && + err != WOLFSSL_ERROR_WANT_WRITE) { + return ret; + } + } while (++rounds < 100); + + return ret; +} + +static int TestTls13PendRead(WOLFSSL* ssl, char* buf, int sz) +{ + int ret; + int err; + int rounds = 0; + + do { + ret = wolfSSL_read(ssl, buf, sz); + if (ret > 0) + break; + err = wolfSSL_get_error(ssl, ret); + if (err == WC_NO_ERR_TRACE(WC_PENDING_E)) { + if (wolfSSL_AsyncPoll(ssl, WOLF_POLL_FLAG_CHECK_HW) < 0) + return -1; + } + else if (err != WOLFSSL_ERROR_WANT_READ && + err != WOLFSSL_ERROR_WANT_WRITE) { + return ret; + } + } while (++rounds < 100); + + return ret; +} + +/* One TLS 1.3 handshake with the given operation class pending on both sides, + * then application data both ways so records queued after the handshake are + * parsed by the peer as well. */ +static int test_tls13_cryptocb_pend_one(int target, int mutual) +{ + EXPECT_DECLS; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + TestTls13PendCtx cliCtx; + TestTls13PendCtx srvCtx; + struct test_memio_ctx memio; + const char msg[] = "hello over TLS 1.3"; + char buf[64]; + + XMEMSET(&cliCtx, 0, sizeof(cliCtx)); + XMEMSET(&srvCtx, 0, sizeof(srvCtx)); + XMEMSET(&memio, 0, sizeof(memio)); + cliCtx.target = target; + srvCtx.target = target; + + ExpectIntEQ(wc_CryptoCb_RegisterDevice(TEST_TLS13_CB_PEND_DEVID_C, + TestTls13PendCb, &cliCtx), 0); + ExpectIntEQ(wc_CryptoCb_RegisterDevice(TEST_TLS13_CB_PEND_DEVID_S, + TestTls13PendCb, &srvCtx), 0); + + /* devId set on the CTX before credentials and SSL objects so all of + * it inherits the devId. ECC credentials: an RSA key under a devId + * would be treated as device-held. */ + ExpectNotNull(ctx_c = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ctx_s = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(wolfSSL_CTX_SetDevId(ctx_c, TEST_TLS13_CB_PEND_DEVID_C), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_SetDevId(ctx_s, TEST_TLS13_CB_PEND_DEVID_S), + WOLFSSL_SUCCESS); +#ifdef HAVE_AESGCM + if (target == TEST_TLS13_PEND_AESGCM) { + /* Pin the AEAD so the pend assertion cannot depend on suite + * preference ordering. */ + ExpectIntEQ(wolfSSL_CTX_set_cipher_list(ctx_c, + "TLS13-AES128-GCM-SHA256"), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_set_cipher_list(ctx_s, + "TLS13-AES128-GCM-SHA256"), WOLFSSL_SUCCESS); + } +#endif + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_c, + "./certs/ca-ecc-cert.pem", 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_certificate_chain_file(ctx_s, + "./certs/server-ecc.pem"), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_s, "./certs/ecc-key.pem", + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + if (mutual) { + /* Mutual auth: pending verifies of the client's chain and CV + * cover the received-marker restores. */ + ExpectIntEQ(wolfSSL_CTX_use_certificate_chain_file(ctx_c, + "./certs/client-ecc-cert.pem"), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_c, + "./certs/ecc-client-key.pem", WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, + "./certs/client-ecc-cert.pem", 0), WOLFSSL_SUCCESS); + wolfSSL_CTX_set_verify(ctx_s, WOLFSSL_VERIFY_PEER | + WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + } + + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + + /* Pin the group so every run negotiates the same way; the default offer + * can trigger HelloRetryRequest or a PQC key share, which are separate + * scenarios from the pend-resume paths this test covers. */ + if (EXPECT_SUCCESS()) { + int groups[1]; + groups[0] = WOLFSSL_ECC_SECP256R1; + ExpectIntEQ(wolfSSL_set_groups(ssl_c, groups, 1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_groups(ssl_s, groups, 1), WOLFSSL_SUCCESS); + } + + /* The shared memio transport; its TLS path serves a byte stream, so + * the re-read patterns of pending crypto operations cannot desync it. */ + if (EXPECT_SUCCESS()) { + wolfSSL_SSLSetIORecv(ssl_c, test_memio_read_cb); + wolfSSL_SSLSetIOSend(ssl_c, test_memio_write_cb); + wolfSSL_SSLSetIORecv(ssl_s, test_memio_read_cb); + wolfSSL_SSLSetIOSend(ssl_s, test_memio_write_cb); + wolfSSL_SetIOReadCtx(ssl_c, &memio); + wolfSSL_SetIOWriteCtx(ssl_c, &memio); + wolfSSL_SetIOReadCtx(ssl_s, &memio); + wolfSSL_SetIOWriteCtx(ssl_s, &memio); + } + + /* Generous rounds: pending every KDF request costs one poll-and-retry + * round trip per operation, and a TLS 1.3 handshake with session tickets + * runs a couple of hundred of them. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 600, NULL), 0); + + /* Assert per-side pends so the test cannot pass without exercising a + * resume path; the client signs nothing unless mutual. */ + ExpectIntGT(srvCtx.seen, 0); + ExpectIntGT(srvCtx.pended, 0); + if (mutual || target != TEST_TLS13_PEND_ECDSA_SIGN) { + ExpectIntGT(cliCtx.seen, 0); + ExpectIntGT(cliCtx.pended, 0); + } + + ExpectIntEQ(TestTls13PendWrite(ssl_c, msg, (int)sizeof(msg)), + (int)sizeof(msg)); + XMEMSET(buf, 0, sizeof(buf)); + ExpectIntEQ(TestTls13PendRead(ssl_s, buf, (int)sizeof(buf)), + (int)sizeof(msg)); + ExpectIntEQ(XMEMCMP(buf, msg, sizeof(msg)), 0); + + ExpectIntEQ(TestTls13PendWrite(ssl_s, msg, (int)sizeof(msg)), + (int)sizeof(msg)); + XMEMSET(buf, 0, sizeof(buf)); + ExpectIntEQ(TestTls13PendRead(ssl_c, buf, (int)sizeof(buf)), + (int)sizeof(msg)); + ExpectIntEQ(XMEMCMP(buf, msg, sizeof(msg)), 0); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + wc_CryptoCb_UnRegisterDevice(TEST_TLS13_CB_PEND_DEVID_C); + wc_CryptoCb_UnRegisterDevice(TEST_TLS13_CB_PEND_DEVID_S); + + return EXPECT_RESULT(); +} + +#endif /* guards */ + +/* Drive TLS 1.3 handshakes and application data with the poll-and-retry + * loop while a crypto callback pends each matching request. */ +int test_tls13_cryptocb_async(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLF_CRYPTO_CB) && \ + defined(WOLFSSL_ASYNC_CRYPT) && defined(WOLFSSL_ASYNC_REINVOKE) && \ + defined(HAVE_ECC) && defined(HAVE_SUPPORTED_CURVES) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +#if defined(HAVE_AESGCM) && !defined(WOLF_CRYPTO_CB_ASYNC_POLL) + ExpectIntEQ(test_tls13_cryptocb_pend_one(TEST_TLS13_PEND_AESGCM, 0), + TEST_SUCCESS); +#endif +#ifdef HAVE_ECC + ExpectIntEQ(test_tls13_cryptocb_pend_one(TEST_TLS13_PEND_ECC_KEYGEN, 0), + TEST_SUCCESS); + ExpectIntEQ(test_tls13_cryptocb_pend_one(TEST_TLS13_PEND_ECDSA_SIGN, 0), + TEST_SUCCESS); + /* Mutual auth with pending verifies: regression for the msgsReceived + * marker restores (a pended Certificate/CertificateVerify replay used + * to leave its marker clear and fail Finished with OUT_OF_ORDER_E). */ + ExpectIntEQ(test_tls13_cryptocb_pend_one(TEST_TLS13_PEND_ECDSA_VERIFY, 1), + TEST_SUCCESS); +#endif +#if defined(HAVE_HKDF) && !defined(NO_HMAC) + ExpectIntEQ(test_tls13_cryptocb_pend_one(TEST_TLS13_PEND_KDF, 0), + TEST_SUCCESS); + ExpectIntEQ(test_tls13_cryptocb_pend_one(TEST_TLS13_PEND_KDF, 1), + TEST_SUCCESS); +#if !defined(HAVE_SELFTEST) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION_GE(7,0)) + /* Transcript HMACs (Finished verify_data) pending on both sides. */ + ExpectIntEQ(test_tls13_cryptocb_pend_one(TEST_TLS13_PEND_HMAC, 0), + TEST_SUCCESS); + ExpectIntEQ(test_tls13_cryptocb_pend_one(TEST_TLS13_PEND_HMAC, 1), + TEST_SUCCESS); +#endif +#endif +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_tls13.h b/tests/api/test_tls13.h index 88b6dbaeca..16323d61d3 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -132,6 +132,7 @@ int test_tls13_pqc_hybrid_async_server(void); int test_tls13_pha_status_request(void); int test_tls13_x25519_keyshare_masks_reserved_bit(void); int test_tls13_is_init_finished_want_write(void); +int test_tls13_cryptocb_async(void); #define TEST_TLS13_DECLS \ TEST_DECL_GROUP("tls13", test_tls13_apis), \ @@ -241,6 +242,7 @@ int test_tls13_is_init_finished_want_write(void); TEST_DECL_GROUP("tls13", test_tls13_pqc_hybrid_async_server), \ TEST_DECL_GROUP("tls13", test_tls13_pha_status_request), \ TEST_DECL_GROUP("tls13", test_tls13_x25519_keyshare_masks_reserved_bit), \ - TEST_DECL_GROUP("tls13", test_tls13_is_init_finished_want_write) + TEST_DECL_GROUP("tls13", test_tls13_is_init_finished_want_write), \ + TEST_DECL_GROUP("tls13", test_tls13_cryptocb_async) #endif /* WOLFCRYPT_TEST_TLS13_H */ diff --git a/tests/utils.c b/tests/utils.c index a8566adc86..6a4f7b4a01 100644 --- a/tests/utils.c +++ b/tests/utils.c @@ -129,7 +129,44 @@ int test_memio_read_cb(WOLFSSL *ssl, char *data, int sz, void *ctx) if (*len == 0 || *msg_pos >= *msg_count) return WOLFSSL_CBIO_ERR_WANT_READ; - /* Calculate how much we can read from current message */ + if (!is_dtls) { + /* TLS is a byte stream: serve across message boundaries so + * pending-crypto re-read patterns cannot desync the slots. */ + int rem; + + read_sz = *len; + if (read_sz > sz) + read_sz = sz; + + XMEMCPY(data, buf, (size_t)read_sz); + XMEMMOVE(buf, buf + read_sz, (size_t)(*len - read_sz)); + *len -= read_sz; + + rem = read_sz; + while (rem > 0 && *msg_pos < *msg_count) { + if (msg_sizes[*msg_pos] > rem) { + msg_sizes[*msg_pos] -= rem; + rem = 0; + } + else { + rem -= msg_sizes[*msg_pos]; + msg_sizes[*msg_pos] = 0; + (*msg_pos)++; + } + } + if (rem != 0) { + /* Slot accounting desynced from the byte count; fail loudly. */ + return WOLFSSL_CBIO_ERR_GENERAL; + } + if (*msg_pos >= *msg_count && *len == 0) { + *msg_pos = 0; + *msg_count = 0; + } + + return read_sz; + } + + /* DTLS: datagram boundaries matter, serve one message at a time. */ read_sz = msg_sizes[*msg_pos]; if (read_sz > sz) read_sz = sz;