diff --git a/docs/draft/timeout.md b/docs/draft/timeout.md
new file mode 100644
index 000000000..815720f85
--- /dev/null
+++ b/docs/draft/timeout.md
@@ -0,0 +1,88 @@
+# Timeout Functionality: Client Perspective
+
+## 1. Configuration at Init Time
+
+When creating a client, you provide a `whTimeoutConfig` specifying the timeout duration and an optional callback:
+```c
+whTimeoutConfig timeoutCfg = {
+ .timeoutUs = WH_SEC_TO_USEC(5), /* 5-second timeout */
+ .expiredCb = myTimeoutHandler, /* optional callback on expiry */
+ .cbCtx = myAppContext, /* context passed to callback */
+};
+whClientConfig clientCfg = {
+ .comm = &commConfig,
+ .respTimeoutConfig = &timeoutCfg, /* attach timeout config */
+};
+wh_Client_Init(&clientCtx, &clientCfg);
+```
+
+During `wh_Client_Init` (`src/wh_client.c:84-89`), the config is copied into an embedded `whTimeoutCtx respTimeout[1]` inside the client context via `wh_Timeout_Init()`. This stores the timeout duration and callback but doesn't start any timer yet.
+If `respTimeoutConfig` is NULL, the timeout context is left zeroed and effectively disabled (a `timeoutUs` of 0 means "never expires").
+
+## 2. What Happens During a Crypto Call
+
+Before this PR, every crypto function in `wh_client_crypto.c` had this pattern after sending a request:
+```c
+/* Old pattern -- infinite busy-wait */
+do {
+ ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len, dataPtr);
+} while (ret == WH_ERROR_NOTREADY);
+```
+
+If the server never responded, the client would spin forever.
+The PR replaces all ~30 of these with a single helper `_recvCryptoResponse()` (`src/wh_client_crypto.c:165-180`):
+```c
+static int _recvCryptoResponse(whClientContext* ctx,
+ uint16_t* group, uint16_t* action,
+ uint16_t* size, void *data)
+{
+ int ret;
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ ret = wh_Client_RecvResponseTimeout(ctx, group, action, size, data,
+ ctx->respTimeout);
+#else
+ do {
+ ret = wh_Client_RecvResponse(ctx, group, action, size, data);
+ } while (ret == WH_ERROR_NOTREADY);
+#endif
+ return ret;
+}
+```
+
+When timeout is enabled, it delegates to `wh_Client_RecvResponseTimeout`. When disabled, the old infinite-loop behavior is preserved.
+
+## 3. The Timeout Receive Loop
+`wh_Client_RecvResponseTimeout` (`src/wh_client.c:211-231`) does this:
+1. **Starts the timer** -- calls `wh_Timeout_Start()` which snapshots the current time via `WH_GETTIME_US()` into `timeout->startUs`.
+2. **Polls for a response** -- calls `wh_Client_RecvResponse()` in a loop.
+3. **On each `WH_ERROR_NOTREADY`**, checks `wh_Timeout_Expired()`:
+ - Gets the current time via `WH_GETTIME_US()`
+ - Computes `(now - startUs) >= timeoutUs`
+ - If expired: invokes the `expiredCb` (if set), then returns `WH_ERROR_TIMEOUT`
+ - If not expired: loops again
+4. **On any other return value** (success or error), returns immediately.
+```
+Client App _recvCryptoResponse wh_Timeout
+ | | |
+ |-- wh_Client_AesCbc() --------> | |
+ | |-- wh_Timeout_Start --------> capture time
+ | | |
+ | |-- RecvResponse (NOTREADY) |
+ | |-- Expired? ------------------> no
+ | |-- RecvResponse (NOTREADY) |
+ | |-- Expired? ------------------> no
+ | | ... |
+ | |-- RecvResponse (NOTREADY) |
+ | |-- Expired? ------------------> YES
+ | | |-- expiredCb()
+ |<-- WH_ERROR_TIMEOUT -----------| |
+```
+
+## 4. What the Client Sees
+From the application's perspective, the crypto APIs (`wh_Client_AesCbc`, `wh_Client_RsaFunction`, `wh_Client_EccSign`, etc.) now return `WH_ERROR_TIMEOUT` (-2010) instead of hanging indefinitely. The application can then decide how to handle it -- retry, log, fail gracefully, etc.
+The `expiredCb` fires *before* the error is returned, so you can use it for logging or cleanup without needing to check the return code first.
+
+## 5. Scope Limitations
+A few things to note about the current design:
+- **Only crypto responses are covered.** Non-crypto client calls (key management, NVM operations, comm init) still use the old infinite-wait pattern. The timeout is specifically wired into `_recvCryptoResponse`.
+- **The timeout is per-client, not per-call.** All crypto operations for a given client share the same `respTimeout` context with the same duration. You can call `wh_Timeout_Set(ctx->respTimeout, newValue)` to change it between calls, but there's no per-operation override.
diff --git a/src/wh_client.c b/src/wh_client.c
index 132841b7b..d1f4ecbd6 100644
--- a/src/wh_client.c
+++ b/src/wh_client.c
@@ -77,6 +77,15 @@ int wh_Client_Init(whClientContext* c, const whClientConfig* config)
memset(c, 0, sizeof(*c));
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ if (config->respTimeoutConfig != NULL) {
+ rc = wh_Timeout_Init(&c->respTimeout, config->respTimeoutConfig);
+ if (rc != WH_ERROR_OK) {
+ return rc;
+ }
+ }
+#endif
+
rc = wh_CommClient_Init(c->comm, config->comm);
#ifndef WOLFHSM_CFG_NO_CRYPTO
@@ -195,6 +204,37 @@ int wh_Client_RecvResponse(whClientContext *c,
return rc;
}
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+int wh_Client_RecvResponseBlockingWithTimeout(whClientContext* c,
+ uint16_t* out_group,
+ uint16_t* out_action,
+ uint16_t* out_size, void* data)
+{
+ int ret;
+ whTimeoutCtx* timeout;
+
+ if (c == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ timeout = &c->respTimeout;
+
+ ret = wh_Timeout_Start(timeout);
+ if (ret != WH_ERROR_OK) {
+ return ret;
+ }
+
+ do {
+ ret = wh_Client_RecvResponse(c, out_group, out_action, out_size, data);
+ if ((ret == WH_ERROR_NOTREADY) && wh_Timeout_Expired(timeout)) {
+ return WH_ERROR_TIMEOUT;
+ }
+ } while (ret == WH_ERROR_NOTREADY);
+
+ return ret;
+}
+#endif /* WOLFHSM_CFG_ENABLE_TIMEOUT */
+
int wh_Client_CommInitRequest(whClientContext* c)
{
whMessageCommInitRequest msg = {0};
diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c
index 3672eac48..d9865f475 100644
--- a/src/wh_client_crypto.c
+++ b/src/wh_client_crypto.c
@@ -163,6 +163,23 @@ static uint8_t* _createCryptoRequestWithSubtype(uint8_t* reqBuf, uint16_t type,
return reqBuf + sizeof(whMessageCrypto_GenericRequestHeader);
}
+static int _recvCryptoResponse(whClientContext* ctx, uint16_t* group,
+ uint16_t* action, uint16_t* size, void* data)
+{
+ int ret;
+
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ ret = wh_Client_RecvResponseBlockingWithTimeout(ctx, group, action, size,
+ data);
+#else
+ do {
+ ret = wh_Client_RecvResponse(ctx, group, action, size, data);
+ } while (ret == WH_ERROR_NOTREADY);
+#endif /* WOLFHSM_CFG_ENABLE_TIMEOUT */
+
+ return ret;
+}
+
/* Helper function to validate and extract crypto response */
/* TODO: add algoSubType checking */
static int _getCryptoResponse(uint8_t* respBuf, uint16_t type,
@@ -233,10 +250,7 @@ int wh_Client_RngGenerate(whClientContext* ctx, uint8_t* out, uint32_t size)
/* Send request and get response */
ret = wh_Client_SendRequest(ctx, group, action, req_len, dataPtr);
if (ret == 0) {
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len, dataPtr);
}
if (ret == WH_ERROR_OK) {
/* Get response */
@@ -311,10 +325,7 @@ int wh_Client_RngGenerateDma(whClientContext* ctx, uint8_t* out, uint32_t size)
if (ret == WH_ERROR_OK) {
/* Wait for and receive the response */
- do {
- ret = wh_Client_RecvResponse(ctx, NULL, NULL, &respSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, NULL, NULL, &respSz, (uint8_t*)dataPtr);
}
if (ret == WH_ERROR_OK) {
@@ -414,10 +425,7 @@ int wh_Client_AesCtr(whClientContext* ctx, Aes* aes, int enc, const uint8_t* in,
if (ret == WH_ERROR_OK) {
/* Response packet */
uint16_t res_len = 0;
- do {
- ret =
- wh_Client_RecvResponse(ctx, &group, &action, &res_len, dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len, dataPtr);
if (ret == WH_ERROR_OK) {
ret = _getCryptoResponse(dataPtr, type, (uint8_t**)&res);
if (ret == WH_ERROR_OK) {
@@ -526,10 +534,7 @@ int wh_Client_AesEcb(whClientContext* ctx, Aes* aes, int enc, const uint8_t* in,
if (ret == WH_ERROR_OK) {
/* Response packet */
uint16_t res_len = 0;
- do {
- ret =
- wh_Client_RecvResponse(ctx, &group, &action, &res_len, dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len, dataPtr);
if (ret == WH_ERROR_OK) {
ret = _getCryptoResponse(dataPtr, type, (uint8_t**)&res);
if (ret == WH_ERROR_OK) {
@@ -635,10 +640,7 @@ int wh_Client_AesCbc(whClientContext* ctx, Aes* aes, int enc, const uint8_t* in,
if (ret == WH_ERROR_OK) {
/* Response packet */
uint16_t res_len = 0;
- do {
- ret =
- wh_Client_RecvResponse(ctx, &group, &action, &res_len, dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len, dataPtr);
if (ret == WH_ERROR_OK) {
ret = _getCryptoResponse(dataPtr, type, (uint8_t**)&res);
if (ret == WH_ERROR_OK) {
@@ -756,10 +758,7 @@ int wh_Client_AesGcm(whClientContext* ctx, Aes* aes, int enc, const uint8_t* in,
ret = wh_Client_SendRequest(ctx, group, action, req_len, dataPtr);
if (ret == 0) {
uint16_t res_len = 0;
- do {
- ret =
- wh_Client_RecvResponse(ctx, &group, &action, &res_len, dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len, dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response */
@@ -956,10 +955,7 @@ int wh_Client_AesGcmDma(whClientContext* ctx, Aes* aes, int enc,
}
if (ret == 0) {
uint16_t resLen = 0;
- do {
- ret =
- wh_Client_RecvResponse(ctx, &group, &action, &resLen, dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &resLen, dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response */
@@ -1158,10 +1154,8 @@ static int _EccMakeKey(whClientContext* ctx, int size, int curveId,
if (ret == WH_ERROR_OK) {
/* Response Message */
uint16_t res_len;
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response structure pointer, validates generic header
@@ -1322,10 +1316,8 @@ int wh_Client_EccSharedSecret(whClientContext* ctx, ecc_key* priv_key,
uint16_t res_len;
/* Recv Response */
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
WH_DEBUG_CLIENT_VERBOSE("resp packet recv. ret:%d\n", ret);
if (ret == WH_ERROR_OK) {
/* Get response structure pointer, validates generic header
@@ -1458,10 +1450,8 @@ int wh_Client_EccSign(whClientContext* ctx, ecc_key* key, const uint8_t* hash,
uint16_t res_len = 0;
/* Recv Response */
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response structure pointer, validates generic header
@@ -1612,10 +1602,8 @@ int wh_Client_EccVerify(whClientContext* ctx, ecc_key* key, const uint8_t* sig,
uint16_t res_len = 0;
/* Recv Response */
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response structure pointer, validates generic header
* rc */
@@ -1690,10 +1678,8 @@ int wh_Client_EccCheckPubKey(whClientContext* ctx, ecc_key* key,
(uint8_t*)packet);
/* read response */
if (ret == 0) {
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &dataSz,
- (uint8_t*)packet);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &dataSz,
+ (uint8_t*)packet);
}
if (ret == 0) {
if (packet->rc != 0)
@@ -1835,10 +1821,8 @@ static int _Curve25519MakeKey(whClientContext* ctx, uint16_t size,
WH_DEBUG_CLIENT_VERBOSE("Curve25519 KeyGen Req sent:size:%u, ret:%d\n",
(unsigned int)req->sz, ret);
if (ret == 0) {
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &data_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &data_len,
+ (uint8_t*)dataPtr);
}
@@ -1994,10 +1978,8 @@ int wh_Client_Curve25519SharedSecret(whClientContext* ctx,
pub_evict = prv_evict = 0;
/* Recv Response */
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
WH_DEBUG_CLIENT_VERBOSE("resp packet recv. ret:%d\n",
ret);
if (ret == WH_ERROR_OK) {
@@ -2163,10 +2145,8 @@ static int _Ed25519MakeKey(whClientContext* ctx, whKeyId* inout_key_id,
return ret;
}
uint16_t res_len = 0;
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret =
+ _recvCryptoResponse(ctx, &group, &action, &res_len, (uint8_t*)dataPtr);
if (ret != WH_ERROR_OK) {
return ret;
@@ -2308,10 +2288,8 @@ int wh_Client_Ed25519Sign(whClientContext* ctx, ed25519_key* key,
evict = 0;
uint16_t res_len = 0;
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (group != WH_MESSAGE_GROUP_CRYPTO || action != WC_ALGO_TYPE_PK) {
ret = WH_ERROR_ABORTED;
@@ -2446,10 +2424,8 @@ int wh_Client_Ed25519Verify(whClientContext* ctx, ed25519_key* key,
evict = 0;
uint16_t res_len = 0;
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (group != WH_MESSAGE_GROUP_CRYPTO || action != WC_ALGO_TYPE_PK) {
ret = WH_ERROR_ABORTED;
@@ -2581,10 +2557,8 @@ int wh_Client_Ed25519SignDma(whClientContext* ctx, ed25519_key* key,
evict = 0;
uint16_t res_len = 0;
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (group != WH_MESSAGE_GROUP_CRYPTO_DMA ||
action != WC_ALGO_TYPE_PK) {
@@ -2728,10 +2702,8 @@ int wh_Client_Ed25519VerifyDma(whClientContext* ctx, ed25519_key* key,
evict = 0;
uint16_t res_len = 0;
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (group != WH_MESSAGE_GROUP_CRYPTO_DMA ||
action != WC_ALGO_TYPE_PK) {
@@ -2916,10 +2888,7 @@ static int _RsaMakeKey(whClientContext* ctx, uint32_t size, uint32_t e,
(unsigned int)req->size, (unsigned int)req->e, ret);
if (ret == 0) {
uint16_t res_len = 0;
- do {
- ret =
- wh_Client_RecvResponse(ctx, &group, &action, &res_len, dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len, dataPtr);
WH_DEBUG_CLIENT_VERBOSE("RSA KeyGen Res recv: ret:%d, res_len: %u\n", ret,
(unsigned int)res_len);
@@ -3080,10 +3049,8 @@ int wh_Client_RsaFunction(whClientContext* ctx, RsaKey* key, int rsa_type,
uint16_t res_len = 0;
/* Recv Response */
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response */
@@ -3192,10 +3159,8 @@ int wh_Client_RsaGetSize(whClientContext* ctx, const RsaKey* key, int* out_size)
uint16_t res_len = 0;
/* Recv Response */
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response */
@@ -3316,10 +3281,7 @@ static int _HkdfMakeKey(whClientContext* ctx, int hashType, whKeyId keyIdIn,
if (ret == 0) {
uint16_t res_len = 0;
- do {
- ret =
- wh_Client_RecvResponse(ctx, &group, &action, &res_len, dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len, dataPtr);
WH_DEBUG_CLIENT_VERBOSE("HKDF Res recv: ret:%d, res_len: %u\n", ret,
(unsigned int)res_len);
@@ -3483,9 +3445,7 @@ static int _CmacKdfMakeKey(whClientContext* ctx, whKeyId saltKeyId,
}
uint16_t res_len = 0;
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len, dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len, dataPtr);
if (ret == WH_ERROR_OK) {
ret = _getCryptoResponse(dataPtr, WC_ALGO_TYPE_KDF, (uint8_t**)&res);
@@ -3675,10 +3635,8 @@ int wh_Client_Cmac(whClientContext* ctx, Cmac* cmac, CmacType type,
uint16_t res_len = 0;
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response */
ret =
@@ -3706,7 +3664,6 @@ int wh_Client_Cmac(whClientContext* ctx, Cmac* cmac, CmacType type,
#endif /* !NO_AES */
-
#ifdef WOLFHSM_CFG_DMA
int wh_Client_CmacDma(whClientContext* ctx, Cmac* cmac, CmacType type,
const uint8_t* key, uint32_t keyLen, const uint8_t* in,
@@ -3808,11 +3765,7 @@ int wh_Client_CmacDma(whClientContext* ctx, Cmac* cmac, CmacType type,
}
uint16_t respSz = 0;
- do {
- ret = wh_Client_RecvResponse(ctx, NULL, NULL, &respSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
-
+ ret = _recvCryptoResponse(ctx, NULL, NULL, &respSz, (uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
ret =
_getCryptoResponse(dataPtr, WC_ALGO_TYPE_CMAC, (uint8_t**)&res);
@@ -3911,10 +3864,8 @@ static int _xferSha256BlockAndUpdateDigest(whClientContext* ctx,
WH_DEBUG_CLIENT_VERBOSE(" ret = %d\n", ret);
if (ret == 0) {
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &dataSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &dataSz,
+ (uint8_t*)dataPtr);
}
if (ret == 0) {
/* Get response */
@@ -4067,10 +4018,8 @@ int wh_Client_Sha256Dma(whClientContext* ctx, wc_Sha256* sha, const uint8_t* in,
(uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
- do {
- ret = wh_Client_RecvResponse(ctx, NULL, NULL, &respSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, NULL, NULL, &respSz,
+ (uint8_t*)dataPtr);
}
if (ret == WH_ERROR_OK) {
@@ -4096,10 +4045,8 @@ int wh_Client_Sha256Dma(whClientContext* ctx, wc_Sha256* sha, const uint8_t* in,
(uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
- do {
- ret = wh_Client_RecvResponse(ctx, NULL, NULL, &respSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, NULL, NULL, &respSz,
+ (uint8_t*)dataPtr);
}
/* Copy out the final hash value */
@@ -4197,10 +4144,8 @@ static int _xferSha224BlockAndUpdateDigest(whClientContext* ctx,
WH_DEBUG_CLIENT_VERBOSE(" ret = %d\n", ret);
if (ret == 0) {
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &dataSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &dataSz,
+ (uint8_t*)dataPtr);
}
if (ret == 0) {
/* Get response */
@@ -4352,10 +4297,8 @@ int wh_Client_Sha224Dma(whClientContext* ctx, wc_Sha224* sha, const uint8_t* in,
(uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
- do {
- ret = wh_Client_RecvResponse(ctx, NULL, NULL, &respSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, NULL, NULL, &respSz,
+ (uint8_t*)dataPtr);
}
if (ret == WH_ERROR_OK) {
@@ -4382,10 +4325,8 @@ int wh_Client_Sha224Dma(whClientContext* ctx, wc_Sha224* sha, const uint8_t* in,
(uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
- do {
- ret = wh_Client_RecvResponse(ctx, NULL, NULL, &respSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, NULL, NULL, &respSz,
+ (uint8_t*)dataPtr);
}
/* Copy out the final hash value */
@@ -4478,10 +4419,8 @@ static int _xferSha384BlockAndUpdateDigest(whClientContext* ctx,
WH_DEBUG_CLIENT_VERBOSE(" ret = %d\n", ret);
if (ret == 0) {
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &dataSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &dataSz,
+ (uint8_t*)dataPtr);
}
if (ret == 0) {
/* Get response */
@@ -4633,10 +4572,8 @@ int wh_Client_Sha384Dma(whClientContext* ctx, wc_Sha384* sha, const uint8_t* in,
(uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
- do {
- ret = wh_Client_RecvResponse(ctx, NULL, NULL, &respSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, NULL, NULL, &respSz,
+ (uint8_t*)dataPtr);
}
if (ret == WH_ERROR_OK) {
@@ -4663,10 +4600,8 @@ int wh_Client_Sha384Dma(whClientContext* ctx, wc_Sha384* sha, const uint8_t* in,
(uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
- do {
- ret = wh_Client_RecvResponse(ctx, NULL, NULL, &respSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, NULL, NULL, &respSz,
+ (uint8_t*)dataPtr);
}
/* Copy out the final hash value */
@@ -4760,10 +4695,8 @@ static int _xferSha512BlockAndUpdateDigest(whClientContext* ctx,
WH_DEBUG_CLIENT_VERBOSE(" ret = %d\n", ret);
if (ret == 0) {
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &dataSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &dataSz,
+ (uint8_t*)dataPtr);
}
if (ret == 0) {
/* Get response */
@@ -4926,10 +4859,8 @@ int wh_Client_Sha512Dma(whClientContext* ctx, wc_Sha512* sha, const uint8_t* in,
(uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
- do {
- ret = wh_Client_RecvResponse(ctx, NULL, NULL, &respSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, NULL, NULL, &respSz,
+ (uint8_t*)dataPtr);
}
if (ret == WH_ERROR_OK) {
@@ -4956,10 +4887,8 @@ int wh_Client_Sha512Dma(whClientContext* ctx, wc_Sha512* sha, const uint8_t* in,
(uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
- do {
- ret = wh_Client_RecvResponse(ctx, NULL, NULL, &respSz,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, NULL, NULL, &respSz,
+ (uint8_t*)dataPtr);
}
/* Copy out the final hash value */
@@ -5133,10 +5062,8 @@ static int _MlDsaMakeKey(whClientContext* ctx, int size, int level,
(unsigned int)req->sz, ret);
if (ret == 0) {
uint16_t res_len;
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response structure pointer, validates generic header
@@ -5294,10 +5221,8 @@ int wh_Client_MlDsaSign(whClientContext* ctx, const byte* in, word32 in_len,
uint16_t res_len = 0;
/* Recv Response */
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response structure pointer, validates generic header
@@ -5429,10 +5354,8 @@ int wh_Client_MlDsaVerify(whClientContext* ctx, const byte* sig, word32 sig_len,
uint16_t res_len = 0;
/* Recv Response */
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (ret == 0) {
/* Get response structure pointer, validates generic header
* rc */
@@ -5597,10 +5520,8 @@ static int _MlDsaMakeKeyDma(whClientContext* ctx, int level,
}
if (ret == WH_ERROR_OK) {
uint16_t res_len;
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
}
(void)wh_Client_DmaProcessClientAddress(
@@ -5756,10 +5677,8 @@ int wh_Client_MlDsaSignDma(whClientContext* ctx, const byte* in, word32 in_len,
uint16_t res_len = 0;
/* Recv Response */
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response structure pointer, validates generic header
@@ -5891,10 +5810,8 @@ int wh_Client_MlDsaVerifyDma(whClientContext* ctx, const byte* sig,
uint16_t res_len = 0;
/* Recv Response */
- do {
- ret = wh_Client_RecvResponse(ctx, &group, &action, &res_len,
- (uint8_t*)dataPtr);
- } while (ret == WH_ERROR_NOTREADY);
+ ret = _recvCryptoResponse(ctx, &group, &action, &res_len,
+ (uint8_t*)dataPtr);
if (ret == WH_ERROR_OK) {
/* Get response structure pointer, validates generic header
diff --git a/src/wh_timeout.c b/src/wh_timeout.c
new file mode 100644
index 000000000..055d06ce8
--- /dev/null
+++ b/src/wh_timeout.c
@@ -0,0 +1,101 @@
+/*
+ * 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 .
+ */
+/*
+ * src/wh_timeout.c
+ */
+
+/* Pick up compile-time configuration */
+#include "wolfhsm/wh_settings.h"
+
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+
+#include "wolfhsm/wh_timeout.h"
+#include "wolfhsm/wh_error.h"
+
+int wh_Timeout_Init(whTimeoutCtx* timeout, const whTimeoutConfig* config)
+{
+ if ((timeout == NULL) || (config == NULL)) {
+ return WH_ERROR_BADARGS;
+ }
+
+ timeout->startUs = 0;
+ timeout->timeoutUs = config->timeoutUs;
+ timeout->expiredCb = config->expiredCb;
+ timeout->cbCtx = config->cbCtx;
+
+ return WH_ERROR_OK;
+}
+
+int wh_Timeout_Set(whTimeoutCtx* timeout, uint64_t timeoutUs)
+{
+ if (timeout == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ timeout->timeoutUs = timeoutUs;
+
+ return WH_ERROR_OK;
+}
+
+int wh_Timeout_Start(whTimeoutCtx* timeout)
+{
+ if (timeout == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ timeout->startUs = WH_GETTIME_US();
+
+ return WH_ERROR_OK;
+}
+
+int wh_Timeout_Stop(whTimeoutCtx* timeout)
+{
+ if (timeout == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ timeout->startUs = 0;
+ timeout->timeoutUs = 0;
+
+ return WH_ERROR_OK;
+}
+
+int wh_Timeout_Expired(whTimeoutCtx* timeout)
+{
+ uint64_t nowUs = 0;
+ int expired = 0;
+
+ if (timeout == NULL) {
+ return 0;
+ }
+
+ if (timeout->timeoutUs == 0) {
+ return 0;
+ }
+
+ nowUs = WH_GETTIME_US();
+ expired = (nowUs - timeout->startUs) >= timeout->timeoutUs;
+ if (expired && (timeout->expiredCb != NULL)) {
+ /* Allow the callback to overwrite the expired value */
+ timeout->expiredCb(timeout, &expired);
+ }
+ return expired;
+}
+
+#endif /* WOLFHSM_CFG_ENABLE_TIMEOUT */
diff --git a/test/config/wolfhsm_cfg.h b/test/config/wolfhsm_cfg.h
index 0a4bb8789..d0519a574 100644
--- a/test/config/wolfhsm_cfg.h
+++ b/test/config/wolfhsm_cfg.h
@@ -65,4 +65,6 @@
/* Allow persistent NVM artifacts in tests */
#define WOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS
+#define WOLFHSM_CFG_ENABLE_TIMEOUT
+
#endif /* WOLFHSM_CFG_H_ */
diff --git a/test/wh_test.c b/test/wh_test.c
index 3fd9a1bb5..9dbf72448 100644
--- a/test/wh_test.c
+++ b/test/wh_test.c
@@ -42,6 +42,7 @@
#include "wh_test_log.h"
#include "wh_test_lock.h"
#include "wh_test_posix_threadsafe_stress.h"
+#include "wh_test_timeout.h"
#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER)
#include "wh_test_cert.h"
@@ -73,6 +74,9 @@ int whTest_Unit(void)
/* Component Tests */
WH_TEST_ASSERT(0 == whTest_Flash_RamSim());
WH_TEST_ASSERT(0 == whTest_NvmFlash());
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ WH_TEST_ASSERT(0 == whTest_Timeout());
+#endif
#ifdef WOLFHSM_CFG_LOGGING
WH_TEST_ASSERT(0 == whTest_Log());
#endif
diff --git a/test/wh_test_timeout.c b/test/wh_test_timeout.c
new file mode 100644
index 000000000..8a372f60e
--- /dev/null
+++ b/test/wh_test_timeout.c
@@ -0,0 +1,423 @@
+/*
+ * 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/wh_test_timeout.c
+ *
+ */
+
+#include
+#include
+
+#include "wolfhsm/wh_settings.h"
+
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+
+#include "wolfhsm/wh_timeout.h"
+#include "wolfhsm/wh_error.h"
+
+#include "wh_test_common.h"
+#include "wh_test_timeout.h"
+
+#if !defined(WOLFHSM_CFG_NO_CRYPTO)
+#include "wolfssl/wolfcrypt/settings.h"
+#ifdef HAVE_AES_CBC
+#include "wolfssl/wolfcrypt/aes.h"
+#include "wolfhsm/wh_comm.h"
+#include "wolfhsm/wh_transport_mem.h"
+#include "wolfhsm/wh_client.h"
+#include "wolfhsm/wh_client_crypto.h"
+#include "wolfhsm/wh_server.h"
+#include "wolfhsm/wh_nvm.h"
+#include "wolfhsm/wh_nvm_flash.h"
+#include "wolfhsm/wh_flash_ramsim.h"
+#endif /* HAVE_AES_CBC */
+#endif /* !WOLFHSM_CFG_NO_CRYPTO */
+
+static void whTest_TimeoutCb(whTimeoutCtx* ctx, int* isExpired)
+{
+ (void)isExpired;
+ int* counter = (int*)ctx->cbCtx;
+ if (counter != NULL) {
+ (*counter)++;
+ }
+}
+
+#if !defined(WOLFHSM_CFG_NO_CRYPTO) && defined(HAVE_AES_CBC)
+
+#define TIMEOUT_TEST_BUFFER_SIZE 4096
+#define TIMEOUT_TEST_FLASH_RAM_SIZE (1024 * 1024)
+#define TIMEOUT_TEST_FLASH_SECTOR_SIZE (128 * 1024)
+#define TIMEOUT_TEST_FLASH_PAGE_SIZE 8
+
+static whServerContext* timeoutTestServerCtx = NULL;
+
+static int _timeoutTestConnectCb(void* context, whCommConnected connected)
+{
+ (void)context;
+
+ if (timeoutTestServerCtx == NULL) {
+ WH_ERROR_PRINT(
+ "Timeout test connect callback server context is NULL\n");
+ WH_TEST_ASSERT_RETURN(0);
+ }
+
+ return wh_Server_SetConnected(timeoutTestServerCtx, connected);
+}
+
+static int whTest_TimeoutAesCbc(void)
+{
+ int rc = 0;
+
+ /* Transport memory configuration */
+ uint8_t req[TIMEOUT_TEST_BUFFER_SIZE] = {0};
+ uint8_t resp[TIMEOUT_TEST_BUFFER_SIZE] = {0};
+ whTransportMemConfig tmcf[1] = {{
+ .req = (whTransportMemCsr*)req,
+ .req_size = sizeof(req),
+ .resp = (whTransportMemCsr*)resp,
+ .resp_size = sizeof(resp),
+ }};
+
+ /* Client configuration with timeout */
+ whTimeoutConfig timeoutCfg = {
+ .timeoutUs = 1,
+ .expiredCb = NULL,
+ .cbCtx = NULL,
+ };
+
+ 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,
+ .connect_cb = _timeoutTestConnectCb,
+ }};
+ whClientConfig c_conf[1] = {{
+ .comm = cc_conf,
+ .respTimeoutConfig = &timeoutCfg,
+ }};
+ whClientContext client[1] = {0};
+
+ /* Server configuration */
+ 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 = 124,
+ }};
+
+ /* Flash/NVM configuration */
+ uint8_t flash_memory[TIMEOUT_TEST_FLASH_RAM_SIZE] = {0};
+ whFlashRamsimCtx fc[1] = {0};
+ whFlashRamsimCfg fc_conf[1] = {{
+ .size = TIMEOUT_TEST_FLASH_RAM_SIZE,
+ .sectorSize = TIMEOUT_TEST_FLASH_SECTOR_SIZE,
+ .pageSize = TIMEOUT_TEST_FLASH_PAGE_SIZE,
+ .erasedByte = ~(uint8_t)0,
+ .memory = flash_memory,
+ }};
+ const whFlashCb fcb[1] = {WH_FLASH_RAMSIM_CB};
+
+ whTestNvmBackendUnion nvm_setup;
+ whNvmConfig n_conf[1] = {0};
+ whNvmContext nvm[1] = {{0}};
+
+ WH_TEST_RETURN_ON_FAIL(whTest_NvmCfgBackend(
+ WH_NVM_TEST_BACKEND_FLASH, &nvm_setup, n_conf, fc_conf, fc, fcb));
+
+ whServerCryptoContext crypto[1] = {{
+ .devId = INVALID_DEVID,
+ }};
+
+ whServerConfig s_conf[1] = {{
+ .comm_config = cs_conf,
+ .nvm = nvm,
+ .crypto = crypto,
+ .devId = INVALID_DEVID,
+ }};
+ whServerContext server[1] = {0};
+
+ timeoutTestServerCtx = server;
+
+ WH_TEST_RETURN_ON_FAIL(wolfCrypt_Init());
+ WH_TEST_RETURN_ON_FAIL(wh_Nvm_Init(nvm, n_conf));
+ WH_TEST_RETURN_ON_FAIL(wc_InitRng_ex(crypto->rng, NULL, INVALID_DEVID));
+
+ /* Server must be initialized before client (connect callback) */
+ WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, s_conf));
+ WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client, c_conf));
+
+ /* CommInit handshake */
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CommInitRequest(client));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server));
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CommInitResponse(client, NULL, NULL));
+
+ /* Set up AES CBC encryption */
+ {
+ Aes aes[1];
+ uint8_t key[AES_BLOCK_SIZE] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
+ 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
+ 0x0D, 0x0E, 0x0F, 0x10};
+ uint8_t iv[AES_BLOCK_SIZE] = {0};
+ uint8_t plain[AES_BLOCK_SIZE] = {0xAA};
+ uint8_t cipher[AES_BLOCK_SIZE] = {0};
+
+ WH_TEST_RETURN_ON_FAIL(wc_AesInit(aes, NULL, WH_DEV_ID));
+ WH_TEST_RETURN_ON_FAIL(
+ wc_AesSetKey(aes, key, sizeof(key), iv, AES_ENCRYPTION));
+
+ /* Call AES CBC encrypt WITHOUT having server handle the request.
+ * The client should time out waiting for the response. */
+ rc = wh_Client_AesCbc(client, aes, 1, plain, sizeof(plain), cipher);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_TIMEOUT);
+
+ wc_AesFree(aes);
+ }
+
+ /* Cleanup: server still has the unhandled request in the transport buffer.
+ * Handle it before closing so the transport is in a clean state. */
+ (void)wh_Server_HandleRequestMessage(server);
+
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CommCloseRequest(client));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server));
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CommCloseResponse(client));
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_Cleanup(server));
+ WH_TEST_RETURN_ON_FAIL(wh_Client_Cleanup(client));
+
+ wc_FreeRng(crypto->rng);
+ wh_Nvm_Cleanup(nvm);
+ wolfCrypt_Cleanup();
+
+ return WH_ERROR_OK;
+}
+
+/* Callback that overrides expiration on the first invocation by resetting and
+ * restarting the timeout. On the second invocation it allows expiration. The
+ * cbCtx points to an int counter tracking how many times the callback fired. */
+static void _timeoutOverrideCb(whTimeoutCtx* ctx, int* isExpired)
+{
+ int* counter = (int*)ctx->cbCtx;
+ if (counter == NULL) {
+ return;
+ }
+
+ (*counter)++;
+
+ if (*counter <= 1) {
+ /* First expiration: override and restart the timer */
+ *isExpired = 0;
+ wh_Timeout_Start(ctx);
+ }
+ /* Subsequent expirations: let it expire normally */
+}
+
+static int whTest_TimeoutAesCbcOverride(void)
+{
+ int rc = 0;
+ int cb_count = 0;
+
+ /* Transport memory configuration */
+ uint8_t req[TIMEOUT_TEST_BUFFER_SIZE] = {0};
+ uint8_t resp[TIMEOUT_TEST_BUFFER_SIZE] = {0};
+ whTransportMemConfig tmcf[1] = {{
+ .req = (whTransportMemCsr*)req,
+ .req_size = sizeof(req),
+ .resp = (whTransportMemCsr*)resp,
+ .resp_size = sizeof(resp),
+ }};
+
+ /* Client configuration with timeout and override callback */
+ whTimeoutConfig timeoutCfg = {
+ .timeoutUs = 1,
+ .expiredCb = _timeoutOverrideCb,
+ .cbCtx = &cb_count,
+ };
+
+ 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,
+ .connect_cb = _timeoutTestConnectCb,
+ }};
+ whClientConfig c_conf[1] = {{
+ .comm = cc_conf,
+ .respTimeoutConfig = &timeoutCfg,
+ }};
+ whClientContext client[1] = {0};
+
+ /* Server configuration */
+ 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 = 124,
+ }};
+
+ /* Flash/NVM configuration */
+ uint8_t flash_memory[TIMEOUT_TEST_FLASH_RAM_SIZE] = {0};
+ whFlashRamsimCtx fc[1] = {0};
+ whFlashRamsimCfg fc_conf[1] = {{
+ .size = TIMEOUT_TEST_FLASH_RAM_SIZE,
+ .sectorSize = TIMEOUT_TEST_FLASH_SECTOR_SIZE,
+ .pageSize = TIMEOUT_TEST_FLASH_PAGE_SIZE,
+ .erasedByte = ~(uint8_t)0,
+ .memory = flash_memory,
+ }};
+ const whFlashCb fcb[1] = {WH_FLASH_RAMSIM_CB};
+
+ whTestNvmBackendUnion nvm_setup;
+ whNvmConfig n_conf[1] = {0};
+ whNvmContext nvm[1] = {{0}};
+
+ WH_TEST_RETURN_ON_FAIL(whTest_NvmCfgBackend(
+ WH_NVM_TEST_BACKEND_FLASH, &nvm_setup, n_conf, fc_conf, fc, fcb));
+
+ whServerCryptoContext crypto[1] = {{
+ .devId = INVALID_DEVID,
+ }};
+
+ whServerConfig s_conf[1] = {{
+ .comm_config = cs_conf,
+ .nvm = nvm,
+ .crypto = crypto,
+ .devId = INVALID_DEVID,
+ }};
+ whServerContext server[1] = {0};
+
+ timeoutTestServerCtx = server;
+
+ WH_TEST_RETURN_ON_FAIL(wolfCrypt_Init());
+ WH_TEST_RETURN_ON_FAIL(wh_Nvm_Init(nvm, n_conf));
+ WH_TEST_RETURN_ON_FAIL(wc_InitRng_ex(crypto->rng, NULL, INVALID_DEVID));
+
+ /* Server must be initialized before client (connect callback) */
+ WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, s_conf));
+ WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client, c_conf));
+
+ /* CommInit handshake */
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CommInitRequest(client));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server));
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CommInitResponse(client, NULL, NULL));
+
+ /* Set up AES CBC encryption */
+ {
+ Aes aes[1];
+ uint8_t key[AES_BLOCK_SIZE] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
+ 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
+ 0x0D, 0x0E, 0x0F, 0x10};
+ uint8_t iv[AES_BLOCK_SIZE] = {0};
+ uint8_t plain[AES_BLOCK_SIZE] = {0xAA};
+ uint8_t cipher[AES_BLOCK_SIZE] = {0};
+
+ WH_TEST_RETURN_ON_FAIL(wc_AesInit(aes, NULL, WH_DEV_ID));
+ WH_TEST_RETURN_ON_FAIL(
+ wc_AesSetKey(aes, key, sizeof(key), iv, AES_ENCRYPTION));
+
+ /* Call AES CBC encrypt WITHOUT having server handle the request.
+ * The override callback will suppress the first expiration, reset and
+ * restart the timer. On the second expiration it lets it through. */
+ rc = wh_Client_AesCbc(client, aes, 1, plain, sizeof(plain), cipher);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_TIMEOUT);
+
+ /* The callback should have fired twice: once overridden, once expired
+ */
+ WH_TEST_ASSERT_RETURN(cb_count == 2);
+
+ wc_AesFree(aes);
+ }
+
+ /* Cleanup */
+ (void)wh_Server_HandleRequestMessage(server);
+
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CommCloseRequest(client));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server));
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CommCloseResponse(client));
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_Cleanup(server));
+ WH_TEST_RETURN_ON_FAIL(wh_Client_Cleanup(client));
+
+ wc_FreeRng(crypto->rng);
+ wh_Nvm_Cleanup(nvm);
+ wolfCrypt_Cleanup();
+
+ return WH_ERROR_OK;
+}
+
+#endif /* !WOLFHSM_CFG_NO_CRYPTO && HAVE_AES_CBC */
+
+int whTest_Timeout(void)
+{
+ int cb_count = 0;
+ whTimeoutConfig cfg;
+ whTimeoutCtx timeout[1];
+
+ cfg.timeoutUs = 1;
+ cfg.expiredCb = whTest_TimeoutCb;
+ cfg.cbCtx = &cb_count;
+
+ wh_Timeout_Init(timeout, &cfg);
+ WH_TEST_ASSERT_RETURN(timeout->startUs == 0);
+ WH_TEST_ASSERT_RETURN(timeout->timeoutUs == cfg.timeoutUs);
+ WH_TEST_ASSERT_RETURN(timeout->expiredCb == cfg.expiredCb);
+ WH_TEST_ASSERT_RETURN(timeout->cbCtx == cfg.cbCtx);
+
+ wh_Timeout_Start(timeout);
+ WH_TEST_ASSERT_RETURN(timeout->timeoutUs > 0);
+
+ wh_Timeout_Stop(timeout);
+ WH_TEST_ASSERT_RETURN(timeout->startUs == 0);
+ WH_TEST_ASSERT_RETURN(timeout->timeoutUs == 0);
+
+ /* No expiration when disabled */
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Expired(timeout) == 0);
+
+ /* Test expired callback fires and increments counter */
+ cb_count = 0;
+ wh_Timeout_Init(timeout, &cfg);
+ wh_Timeout_Start(timeout);
+ /* timeoutUs is 1 us, so spin until expired */
+ while (wh_Timeout_Expired(timeout) == 0)
+ ;
+ WH_TEST_ASSERT_RETURN(cb_count > 0);
+
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Init(0, 0) == WH_ERROR_BADARGS);
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Set(0, 0) == WH_ERROR_BADARGS);
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Start(0) == WH_ERROR_BADARGS);
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Stop(0) == WH_ERROR_BADARGS);
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Expired(0) == 0);
+
+#if !defined(WOLFHSM_CFG_NO_CRYPTO) && defined(HAVE_AES_CBC)
+ WH_TEST_RETURN_ON_FAIL(whTest_TimeoutAesCbc());
+ WH_TEST_RETURN_ON_FAIL(whTest_TimeoutAesCbcOverride());
+#endif
+
+ return 0;
+}
+
+#endif /* WOLFHSM_CFG_ENABLE_TIMEOUT */
diff --git a/test/wh_test_timeout.h b/test/wh_test_timeout.h
new file mode 100644
index 000000000..d8ebebb36
--- /dev/null
+++ b/test/wh_test_timeout.h
@@ -0,0 +1,34 @@
+/*
+ * 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/wh_test_timeout.h
+ *
+ */
+
+#ifndef TEST_WH_TEST_TIMEOUT_H_
+#define TEST_WH_TEST_TIMEOUT_H_
+
+/**
+ * Runs timeout module tests.
+ *
+ * @return 0 on success and a non-zero error code on failure.
+ */
+int whTest_Timeout(void);
+
+#endif /* TEST_WH_TEST_TIMEOUT_H_ */
diff --git a/wolfhsm/wh_client.h b/wolfhsm/wh_client.h
index 2990ed297..6336c8b0b 100644
--- a/wolfhsm/wh_client.h
+++ b/wolfhsm/wh_client.h
@@ -48,6 +48,7 @@
/* Component includes */
#include "wolfhsm/wh_comm.h"
+#include "wolfhsm/wh_timeout.h"
#include "wolfhsm/wh_message_customcb.h"
#ifdef WOLFHSM_CFG_DMA
#include "wolfhsm/wh_dma.h"
@@ -108,6 +109,9 @@ typedef struct {
struct whClientContext_t {
uint16_t last_req_id;
uint16_t last_req_kind;
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ whTimeoutCtx respTimeout;
+#endif
#ifdef WOLFHSM_CFG_DMA
whClientDmaContext dma;
#endif /* WOLFHSM_CFG_DMA */
@@ -119,6 +123,9 @@ struct whClientConfig_t {
#ifdef WOLFHSM_CFG_DMA
whClientDmaConfig* dmaConfig;
#endif /* WOLFHSM_CFG_DMA */
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ whTimeoutConfig* respTimeoutConfig;
+#endif /* WOLFHSM_CFG_ENABLE_TIMEOUT*/
};
typedef struct whClientConfig_t whClientConfig;
@@ -177,7 +184,22 @@ int wh_Client_SendRequest(whClientContext* c, uint16_t group, uint16_t action,
int wh_Client_RecvResponse(whClientContext* c, uint16_t* out_group,
uint16_t* out_action, uint16_t* out_size,
void* data);
-
+/**
+ * Receives a response from the server with a timeout window. The timeout
+ * duration is specified by the respTimeout field in the client context.
+ *
+ * @param c The client context.
+ * @param out_group Pointer to store the received group value.
+ * @param out_action Pointer to store the received action value.
+ * @param out_size Pointer to store the received size value.
+ * @param data Pointer to store the received data.
+ * @return 0 if successful, WH_ERROR_TIMEOUT on expiration, or a negative value
+ * if an error occurred.
+ */
+int wh_Client_RecvResponseBlockingWithTimeout(whClientContext* c,
+ uint16_t* out_group,
+ uint16_t* out_action,
+ uint16_t* out_size, void* data);
/** Comm component functions */
diff --git a/wolfhsm/wh_error.h b/wolfhsm/wh_error.h
index 5ce75cdde..50d522691 100644
--- a/wolfhsm/wh_error.h
+++ b/wolfhsm/wh_error.h
@@ -45,6 +45,7 @@ enum WH_ERROR_ENUM {
compile-time configuration */
WH_ERROR_USAGE =
-2009, /* Operation not permitted based on object/key usage flags */
+ WH_ERROR_TIMEOUT = -2010, /* Timeout occurred. */
/* NVM and keystore specific status returns */
WH_ERROR_LOCKED = -2100, /* Unlock and retry if necessary */
diff --git a/wolfhsm/wh_settings.h b/wolfhsm/wh_settings.h
index 701a18e0d..e9e54be88 100644
--- a/wolfhsm/wh_settings.h
+++ b/wolfhsm/wh_settings.h
@@ -57,6 +57,9 @@
* WOLFHSM_CFG_ENABLE_SERVER - If defined, include server-specific
* functionality
*
+ * WOLFHSM_CFG_ENABLE_TIMEOUT - If defined, include client-side support for
+ * blocking request timeouts
+ *
* WOLFHSM_CFG_NVM_OBJECT_COUNT - Number of objects in ram and disk directories
* Default: 32
*
diff --git a/wolfhsm/wh_timeout.h b/wolfhsm/wh_timeout.h
new file mode 100644
index 000000000..fedeb8b77
--- /dev/null
+++ b/wolfhsm/wh_timeout.h
@@ -0,0 +1,99 @@
+/*
+ * 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 .
+ */
+/*
+ * wolfhsm/wh_timeout.h
+ *
+ * Generic timeout helpers based on WH_GETTIME_US().
+ */
+
+#ifndef WOLFHSM_WH_TIMEOUT_H_
+#define WOLFHSM_WH_TIMEOUT_H_
+
+/* Pick up compile-time configuration */
+#include "wolfhsm/wh_settings.h"
+
+#define WH_MSEC_TO_USEC(usec) ((usec) * (1000ULL))
+#define WH_SEC_TO_USEC(sec) ((sec) * (1000000ULL))
+#define WH_MIN_TO_USEC(min) ((min) * (WH_SEC_TO_USEC(60)))
+
+#include
+
+/* Forward declare so the callback typedef can reference it */
+typedef struct whTimeoutCtx whTimeoutCtx;
+typedef void (*whTimeoutExpiredCb)(whTimeoutCtx* ctx, int* isExpired);
+
+struct whTimeoutCtx {
+ uint64_t startUs;
+ uint64_t timeoutUs;
+ whTimeoutExpiredCb expiredCb;
+ void* cbCtx;
+};
+
+typedef struct {
+ uint64_t timeoutUs;
+ whTimeoutExpiredCb expiredCb;
+ void* cbCtx;
+} whTimeoutConfig;
+
+/**
+ * Initialize a timeout context from a configuration.
+ *
+ * @param timeout The timeout context to initialize.
+ * @param config The timeout configuration to apply.
+ * @return 0 on success, WH_ERROR_BADARGS on invalid input.
+ */
+int wh_Timeout_Init(whTimeoutCtx* timeout, const whTimeoutConfig* config);
+
+/**
+ * Configure a timeout value.
+ *
+ * @param timeout The timeout context to update.
+ * @param timeoutUs Timeout duration in microseconds; 0 disables the timeout.
+ * @return 0 on success, WH_ERROR_BADARGS on invalid input.
+ */
+int wh_Timeout_Set(whTimeoutCtx* timeout, uint64_t timeoutUs);
+
+/**
+ * Start or reset a timeout window using the configured timeoutUs.
+ *
+ * @param timeout The timeout context to start.
+ * @return 0 on success, WH_ERROR_BADARGS on invalid input.
+ */
+int wh_Timeout_Start(whTimeoutCtx* timeout);
+
+/**
+ * Disable a timeout and clear its bookkeeping.
+ *
+ * @param timeout The timeout context to stop.
+ * @return 0 on success, WH_ERROR_BADARGS on invalid input.
+ */
+int wh_Timeout_Stop(whTimeoutCtx* timeout);
+
+/**
+ * Check whether a timeout has expired.
+ *
+ * If the timeout is expired and an expired callback is configured, the
+ * callback is invoked before returning.
+ *
+ * @param timeout The timeout context to check.
+ * @return 1 if expired, 0 if not expired or disabled.
+ */
+int wh_Timeout_Expired(whTimeoutCtx* timeout);
+
+#endif /* !WOLFHSM_WH_TIMEOUT_H_ */