diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index d0189b33a..640733f8c 100644 --- a/src/wh_client_crypto.c +++ b/src/wh_client_crypto.c @@ -3008,6 +3008,14 @@ static int _Curve25519MakeKey(whClientContext* ctx, uint16_t size, (uint8_t**)&res); /* wolfCrypt allows positive error codes on success in some scenarios */ if (ret >= 0) { + const size_t hdr_sz = + sizeof(whMessageCrypto_GenericResponseHeader) + sizeof(*res); + + /* Defensive bound: res->len must fit within the received frame */ + if (data_len < hdr_sz || res->len > (data_len - hdr_sz)) { + return WH_ERROR_ABORTED; + } + WH_DEBUG_CLIENT_VERBOSE("Curve25519 KeyGen Res recv:keyid:%u, len:%u, " "ret:%d\n", (unsigned int)res->keyId, (unsigned int)res->len, ret); @@ -3470,6 +3478,14 @@ static int _Ed25519MakeKey(whClientContext* ctx, whKeyId* inout_key_id, ret = _getCryptoResponse(dataPtr, WC_PK_TYPE_ED25519_KEYGEN, (uint8_t**)&res); if (ret >= 0) { + const size_t hdr_sz = + sizeof(whMessageCrypto_GenericResponseHeader) + sizeof(*res); + + /* Defensive bound: res->outSz must fit within the received frame */ + if (res_len < hdr_sz || res->outSz > (res_len - hdr_sz)) { + return WH_ERROR_ABORTED; + } + key_id = (whKeyId)res->keyId; if (inout_key_id != NULL) { *inout_key_id = key_id; @@ -3479,11 +3495,6 @@ static int _Ed25519MakeKey(whClientContext* ctx, whKeyId* inout_key_id, if (flags & WH_NVM_FLAGS_EPHEMERAL) { uint8_t* out = (uint8_t*)(res + 1); uint16_t outSz = (uint16_t)res->outSz; - if (outSz + sizeof(whMessageCrypto_GenericResponseHeader) + - sizeof(*res) > - WOLFHSM_CFG_COMM_DATA_LEN) { - return WH_ERROR_ABORTED; - } ret = wh_Crypto_Ed25519DeserializeKeyDer(out, outSz, key); } } diff --git a/test-refactor/posix/wh_test_malformed_response.c b/test-refactor/posix/wh_test_malformed_response.c new file mode 100644 index 000000000..e2e532b66 --- /dev/null +++ b/test-refactor/posix/wh_test_malformed_response.c @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfHSM. + * + * wolfHSM is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfHSM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfHSM. If not, see . + */ +/* + * test-refactor/posix/wh_test_malformed_response.c + * + * Negative-path coverage for the client-side response frame bounds + * checks, driven by a fake server that declares a payload length its + * frame does not carry. POSIX/pthread only, and port-owned rather than + * in the common list, which runs against a real server process. + */ + +#include +#include +#include + +#include "wolfhsm/wh_settings.h" + +#include "wh_test_common.h" +#include "wh_test_list.h" + +#if defined(WOLFHSM_CFG_TEST_POSIX) && defined(WOLFHSM_CFG_ENABLE_CLIENT) && \ + !defined(WOLFHSM_CFG_NO_CRYPTO) + +#include "wolfssl/wolfcrypt/settings.h" +#include "wolfssl/wolfcrypt/curve25519.h" +#include "wolfssl/wolfcrypt/ecc.h" +#include "wolfssl/wolfcrypt/ed25519.h" +#include "wolfssl/wolfcrypt/rsa.h" + +#include "wolfhsm/wh_client.h" +#include "wolfhsm/wh_client_crypto.h" +#include "wolfhsm/wh_comm.h" +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_message.h" +#include "wolfhsm/wh_message_crypto.h" +#include "wolfhsm/wh_transport_mem.h" + +#define WH_TEST_MALFORMED_BUFFER_SIZE 4096 + +/* Inside WOLFHSM_CFG_COMM_DATA_LEN, so only a frame-length check rejects it */ +#define WH_TEST_MALFORMED_CLAIMED_SZ 64 + +typedef enum { + WH_TEST_MALFORMED_TRUNCATED = 0, + WH_TEST_MALFORMED_OVERSIZED, + WH_TEST_MALFORMED_SHAPE_COUNT +} whTestMalformedShape; + +typedef int (*whTestMalformedFn)(whClientContext* client); + +typedef struct { + const char* name; + whTestMalformedFn call; + uint16_t resSize; /* size of the algorithm's response struct */ +} whTestMalformedCase; + +/* Every keygen response opens with this pair */ +typedef struct { + uint32_t keyId; + uint32_t len; +} whTestMalformedKeyGenBody; + + +static int _callEd25519MakeKey(whClientContext* client) +{ + ed25519_key key[1]; + int ret; + + ret = wc_ed25519_init_ex(key, NULL, INVALID_DEVID); + if (ret != 0) { + return ret; + } + ret = wh_Client_Ed25519MakeExportKey(client, key); + wc_ed25519_free(key); + return ret; +} + +static int _callEccMakeKey(whClientContext* client) +{ + ecc_key key[1]; + int ret; + + ret = wc_ecc_init_ex(key, NULL, INVALID_DEVID); + if (ret != 0) { + return ret; + } + ret = wh_Client_EccMakeExportKey(client, 32, ECC_SECP256R1, key); + wc_ecc_free(key); + return ret; +} + +static int _callRsaMakeKey(whClientContext* client) +{ + RsaKey key[1]; + int ret; + + ret = wc_InitRsaKey_ex(key, NULL, INVALID_DEVID); + if (ret != 0) { + return ret; + } + ret = wh_Client_RsaMakeExportKey(client, 2048, 65537, key); + wc_FreeRsaKey(key); + return ret; +} + +static int _callCurve25519MakeKey(whClientContext* client) +{ + curve25519_key key[1]; + int ret; + + ret = wc_curve25519_init_ex(key, NULL, INVALID_DEVID); + if (ret != 0) { + return ret; + } + ret = wh_Client_Curve25519MakeExportKey(client, CURVE25519_KEYSIZE, key); + wc_curve25519_free(key); + return ret; +} + +static const whTestMalformedCase _malformedCases[] = { + {"Ed25519 keygen", _callEd25519MakeKey, + (uint16_t)sizeof(whMessageCrypto_Ed25519KeyGenResponse)}, + {"ECC keygen", _callEccMakeKey, + (uint16_t)sizeof(whMessageCrypto_EccKeyGenResponse)}, + {"RSA keygen", _callRsaMakeKey, + (uint16_t)sizeof(whMessageCrypto_RsaKeyGenResponse)}, + {"Curve25519 keygen", _callCurve25519MakeKey, + (uint16_t)sizeof(whMessageCrypto_Curve25519KeyGenResponse)}, +}; + +#define WH_TEST_MALFORMED_CASE_COUNT \ + (sizeof(_malformedCases) / sizeof(_malformedCases[0])) + +static const struct timespec WH_TEST_MALFORMED_ONE_MS = {0, 1000000}; + +/* Bounds the 1 ms retry loops so a stalled peer fails the suite, not hangs it */ +#define WH_TEST_MALFORMED_MAX_TRIES 5000 + +static int _malformedClientRc = WH_TEST_SUCCESS; +static int _malformedServerRc = WH_TEST_SUCCESS; + + +/* Echoes the request kind and seq so the client accepts the reply */ +static void* _malformedServerThread(void* cf) +{ + whCommServerConfig* config = (whCommServerConfig*)cf; + whCommServer server[1]; + uint8_t rx_req[WH_TEST_MALFORMED_BUFFER_SIZE]; + uint8_t tx_resp[WH_TEST_MALFORMED_BUFFER_SIZE]; + uint16_t rx_req_len = 0; + uint16_t rx_req_flags = 0; + uint16_t rx_req_type = 0; + uint16_t rx_req_seq = 0; + uint16_t tx_resp_len = 0; + size_t step; + size_t shape; + size_t idx; + int tries; + int ret; + + whMessageCrypto_GenericResponseHeader* hdr = + (whMessageCrypto_GenericResponseHeader*)tx_resp; + whTestMalformedKeyGenBody* body = + (whTestMalformedKeyGenBody*)(tx_resp + sizeof(*hdr)); + + ret = wh_CommServer_Init(server, config, NULL, NULL); + WH_TEST_ASSERT_MSG(0 == ret, "Fake server Init: ret=%d", ret); + + /* Flattened so a timeout exits the exchange in one break */ + for (step = 0; step < (size_t)WH_TEST_MALFORMED_SHAPE_COUNT * + WH_TEST_MALFORMED_CASE_COUNT; + step++) { + shape = step / WH_TEST_MALFORMED_CASE_COUNT; + idx = step % WH_TEST_MALFORMED_CASE_COUNT; + + tries = 0; + do { + ret = wh_CommServer_RecvRequest(server, &rx_req_flags, + &rx_req_type, &rx_req_seq, + &rx_req_len, sizeof(rx_req), + rx_req); + if (ret != WH_ERROR_NOTREADY) { + break; + } + /* Ignored: ending the loop on an interrupted sleep would + * strand the client waiting for a response. */ + (void)nanosleep(&WH_TEST_MALFORMED_ONE_MS, NULL); + tries++; + } while (tries < WH_TEST_MALFORMED_MAX_TRIES); + + if (ret != WH_ERROR_OK) { + WH_ERROR_PRINT("Fake server RecvRequest for %s: ret=%d\n", + _malformedCases[idx].name, ret); + _malformedServerRc = WH_TEST_FAIL; + break; + } + + memset(tx_resp, 0, sizeof(tx_resp)); + /* From the request header, not the comm kind, or the client + * rejects the reply before reaching the bounds check. */ + hdr->rc = WH_ERROR_OK; + hdr->algoType = + ((whMessageCrypto_GenericRequestHeader*)rx_req)->algoType; + + if (shape == (size_t)WH_TEST_MALFORMED_TRUNCATED) { + /* Stop short of the response struct the client reads */ + tx_resp_len = (uint16_t)sizeof(*hdr); + } + else { + body->keyId = 0; + body->len = WH_TEST_MALFORMED_CLAIMED_SZ; + tx_resp_len = + (uint16_t)(sizeof(*hdr) + _malformedCases[idx].resSize); + } + + tries = 0; + do { + ret = wh_CommServer_SendResponse(server, rx_req_flags, rx_req_type, + rx_req_seq, tx_resp_len, tx_resp); + if (ret != WH_ERROR_NOTREADY) { + break; + } + (void)nanosleep(&WH_TEST_MALFORMED_ONE_MS, NULL); + tries++; + } while (tries < WH_TEST_MALFORMED_MAX_TRIES); + + if (ret != WH_ERROR_OK) { + WH_ERROR_PRINT("Fake server SendResponse for %s: ret=%d\n", + _malformedCases[idx].name, ret); + _malformedServerRc = WH_TEST_FAIL; + break; + } + } + + ret = wh_CommServer_Cleanup(server); + WH_TEST_ASSERT_MSG(0 == ret, "Fake server Cleanup: ret=%d", ret); + return NULL; +} + + +static void* _malformedClientThread(void* cf) +{ + whClientConfig* config = (whClientConfig*)cf; + whClientContext client[1]; + size_t shape; + size_t idx; + int ret; + + ret = wh_Client_Init(client, config); + WH_TEST_ASSERT_MSG(0 == ret, "Fake client Init: ret=%d", ret); + if (ret != 0) { + _malformedClientRc = ret; + return NULL; + } + + for (shape = 0; shape < (size_t)WH_TEST_MALFORMED_SHAPE_COUNT; shape++) { + for (idx = 0; idx < WH_TEST_MALFORMED_CASE_COUNT; idx++) { + ret = _malformedCases[idx].call(client); + if (ret != WH_ERROR_ABORTED) { + WH_ERROR_PRINT("%s shape %u: expected WH_ERROR_ABORTED, " + "got %d\n", + _malformedCases[idx].name, (unsigned)shape, ret); + _malformedClientRc = WH_TEST_FAIL; + } + } + } + + (void)wh_Client_Cleanup(client); + return NULL; +} + + +int whTest_MalformedCryptoResponse(void* ctx) +{ + uint8_t req[WH_TEST_MALFORMED_BUFFER_SIZE] = {0}; + uint8_t resp[WH_TEST_MALFORMED_BUFFER_SIZE] = {0}; + + whTransportMemConfig tmcf[1] = {{ + .req = (whTransportMemCsr*)req, + .req_size = sizeof(req), + .resp = (whTransportMemCsr*)resp, + .resp_size = sizeof(resp), + }}; + + whTransportClientCb tccb[1] = {WH_TRANSPORT_MEM_CLIENT_CB}; + whTransportMemClientContext tmcc[1] = {0}; + whCommClientConfig cc_conf[1] = {{ + .transport_cb = tccb, + .transport_context = (void*)tmcc, + .transport_config = (void*)tmcf, + .client_id = WH_TEST_DEFAULT_CLIENT_ID, + }}; + whClientConfig c_conf[1] = {{ + .comm = cc_conf, + }}; + + whTransportServerCb tscb[1] = {WH_TRANSPORT_MEM_SERVER_CB}; + whTransportMemServerContext tmsc[1] = {0}; + whCommServerConfig cs_conf[1] = {{ + .transport_cb = tscb, + .transport_context = (void*)tmsc, + .transport_config = (void*)tmcf, + .server_id = 1, + }}; + + pthread_t sthread; + pthread_t cthread; + int rc; + + (void)ctx; + + _malformedClientRc = WH_TEST_SUCCESS; + _malformedServerRc = WH_TEST_SUCCESS; + + rc = pthread_create(&sthread, NULL, _malformedServerThread, cs_conf); + if (rc != 0) { + WH_ERROR_PRINT("Fake server thread create: rc=%d\n", rc); + return WH_TEST_FAIL; + } + + rc = pthread_create(&cthread, NULL, _malformedClientThread, c_conf); + if (rc != 0) { + WH_ERROR_PRINT("Fake client thread create: rc=%d\n", rc); + (void)pthread_cancel(sthread); + (void)pthread_join(sthread, NULL); + return WH_TEST_FAIL; + } + + (void)pthread_join(cthread, NULL); + (void)pthread_join(sthread, NULL); + + if (_malformedClientRc != WH_TEST_SUCCESS) { + return _malformedClientRc; + } + return _malformedServerRc; +} + +#else /* !WOLFHSM_CFG_TEST_POSIX || !WOLFHSM_CFG_ENABLE_CLIENT || + * WOLFHSM_CFG_NO_CRYPTO */ + +int whTest_MalformedCryptoResponse(void* ctx) +{ + (void)ctx; + return WH_TEST_SKIPPED; +} + +#endif diff --git a/test-refactor/posix/wh_test_posix_main.c b/test-refactor/posix/wh_test_posix_main.c index 756ed6041..34a9029c5 100644 --- a/test-refactor/posix/wh_test_posix_main.c +++ b/test-refactor/posix/wh_test_posix_main.c @@ -76,6 +76,10 @@ int whTest_NvmAddOverwriteDestroy(void* ctx); int whTest_NvmFlashLog(void* ctx); int whTest_NvmRecovery(void* ctx); +/* Malformed-response test. Owns its own client and fake server over a + * private transport pair, so it can't use the runner's live contexts. */ +int whTest_MalformedCryptoResponse(void* ctx); + /* * Port-owned contexts. The thread functions fill these in and * hand them to the group functions, paralleling the firmware @@ -284,6 +288,13 @@ int main(void) if (rc != 0 && rc != WH_TEST_SKIPPED && miscRc == 0) { miscRc = rc; } + /* Runs here rather than in the client slot so its private + * client and fake server never overlap the real server. */ + rc = whTestGroup_RunOne("whTest_MalformedCryptoResponse", + whTest_MalformedCryptoResponse, NULL); + if (rc != 0 && rc != WH_TEST_SKIPPED && miscRc == 0) { + miscRc = rc; + } } rc = pthread_create(&sthread, NULL, _serverThread, NULL);