diff --git a/docs/draft/timeout.md b/docs/draft/timeout.md
new file mode 100644
index 000000000..4f9583fb0
--- /dev/null
+++ b/docs/draft/timeout.md
@@ -0,0 +1,122 @@
+# Timeout Functionality: Client Perspective
+
+## Overview
+
+The response timeout feature is primarily designed to prevent the client from blocking indefinitely when the server fails to respond to a request. This is most relevant for **blocking operations** such as wolfCrypt cryptographic calls, where the client sends a request and polls for a response in a tight loop. Without a timeout, a non-responsive server would cause the client to hang forever.
+
+Since the timeout is checked inside `wh_CommClient_RecvResponse`, it **can** also apply to the split (async) API where the caller manually polls `RecvResponse`. However, in the async case the timeout is only evaluated each time the caller invokes `RecvResponse` -- it does not proactively notify the caller or fire asynchronously. If the caller is not actively polling, the timeout has no effect.
+
+## 1. Configuration at Init Time
+
+The timeout feature uses a callback-based abstraction (similar to the lock feature) that allows platform-specific timer implementations without introducing OS dependencies in core wolfHSM code. A platform port provides a callback table implementing the timer operations, and the core timeout module delegates to these callbacks.
+
+The timeout lives in the comm layer. When creating a client, you provide a `whTimeoutConfig` in the `whCommClientConfig`:
+```c
+/* Platform-specific setup (e.g. POSIX) */
+posixTimeoutContext posixCtx = {0};
+posixTimeoutConfig posixCfg = {.timeoutUs = WH_SEC_TO_USEC(5)};
+whTimeoutCb timeoutCbTable = POSIX_TIMEOUT_CB;
+
+/* NOTE: The callback table, platform context, and expiredCtx must remain valid
+ * for the lifetime of the whCommClient/whTimeout instance. Do not use stack
+ * locals that go out of scope while the client is still in use. */
+whTimeoutConfig timeoutCfg = {
+ .cb = &timeoutCbTable, /* platform callback table */
+ .context = &posixCtx, /* platform context */
+ .config = &posixCfg, /* platform-specific config */
+ .expiredCb = myTimeoutHandler, /* optional app callback on expiry */
+ .expiredCtx = myAppContext, /* context passed to app callback */
+};
+whCommClientConfig commConfig = {
+ .transport_cb = &transportCb,
+ .transport_context = &transportCtx,
+ .transport_config = &transportCfg,
+ .client_id = 1,
+ .respTimeoutConfig = &timeoutCfg, /* attach timeout config */
+};
+whClientConfig clientCfg = {
+ .comm = &commConfig,
+};
+wh_Client_Init(&clientCtx, &clientCfg);
+```
+
+During `wh_CommClient_Init`, the timeout is initialized via `wh_Timeout_Init()`. This calls the platform `init` callback to set up timer resources but doesn't start any timer yet.
+If `respTimeoutConfig` is NULL (or `cb` is NULL), the timeout enters no-op mode and never expires.
+
+## 2. How the Timeout Works
+
+The timeout is handled transparently in the comm layer:
+
+1. **`wh_CommClient_SendRequest`**: After a successful send, starts the response timer via `wh_Timeout_Start()`.
+2. **`wh_CommClient_RecvResponse`**: When the transport returns `WH_ERROR_NOTREADY`, checks `wh_Timeout_Expired()`. If expired, returns `WH_ERROR_TIMEOUT`. On successful receive, stops the timer via `wh_Timeout_Stop()`.
+
+For blocking (synchronous) client APIs, this means the internal `do { ... } while (ret == WH_ERROR_NOTREADY)` polling loop automatically gets timeout support -- the client will return `WH_ERROR_TIMEOUT` instead of spinning forever if the server does not respond within the configured deadline.
+
+For split (async) APIs where the application calls `SendRequest` and `RecvResponse` separately, the timeout check occurs each time `RecvResponse` is called and returns `WH_ERROR_NOTREADY`. The timeout does **not** interrupt the caller or provide out-of-band notification -- it is purely poll-based.
+
+```
+Client App CommClient whTimeout
+ | | |
+ |-- wh_Client_AesCbc() -------->| |
+ | |-- SendRequest ------> cb->start()
+ | | |
+ | |-- RecvResponse (NOTREADY) |
+ | |-- Expired? -------> cb->expired() -> no
+ | |-- RecvResponse (NOTREADY) |
+ | |-- Expired? -------> cb->expired() -> no
+ | | ... |
+ | |-- RecvResponse (NOTREADY) |
+ | |-- Expired? -------> cb->expired() -> YES
+ | | |-- expiredCb()
+ |<-- WH_ERROR_TIMEOUT -----------| |
+```
+
+## 3. What the Client Sees
+
+From the application's perspective, any client API that waits for a server response can 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.
+
+## 4. Overriding Expiration via the Callback
+
+The application expired callback receives a pointer to the `isExpired` flag and can override it by setting `*isExpired = 0`. This suppresses the expiration for the current check, allowing the polling loop to continue. A common use case is to extend the timeout deadline: clear the flag, then call `wh_Timeout_Start()` to restart the timer.
+
+The callback can also return a non-zero error code to signal a failure. When it does, `wh_Timeout_Expired()` propagates that error directly to the caller instead of returning the expired flag.
+
+```c
+static int myOverrideCb(whTimeout* timeout, int* isExpired)
+{
+ int* retryCount = (int*)timeout->expiredCtx;
+ if (retryCount == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ (*retryCount)++;
+
+ if (*retryCount <= 1) {
+ /* First expiration: suppress and restart the timer */
+ *isExpired = 0;
+ wh_Timeout_Start(timeout);
+ }
+ /* Subsequent expirations: allow the timeout to fire */
+ return WH_ERROR_OK;
+}
+
+int retryCount = 0;
+posixTimeoutContext posixCtx = {0};
+posixTimeoutConfig posixCfg = {.timeoutUs = WH_SEC_TO_USEC(5)};
+whTimeoutCb timeoutCbTable = POSIX_TIMEOUT_CB;
+
+whTimeoutConfig timeoutCfg = {
+ .cb = &timeoutCbTable,
+ .context = &posixCtx,
+ .config = &posixCfg,
+ .expiredCb = myOverrideCb,
+ .expiredCtx = &retryCount,
+};
+```
+
+## 5. Design Notes
+- **Primary use case: blocking wolfCrypt operations.** The timeout is designed to prevent indefinite hangs when the server fails to respond to blocking client API calls, which currently only exist when using the wolfCrypt API for crypto. These calls internally poll `RecvResponse` in a tight loop, and the timeout provides automatic protection against a non-responsive server.
+- **Async API compatibility.** The timeout mechanism also works with the split wolfHSM `SendRequest`/`RecvResponse` API, but only checks for expiration when `RecvResponse` is called by the application. It is purely poll-driven, and there is no callback, signal, or interrupt that fires independently. If the application stops calling `RecvResponse`, the timeout will not trigger.
+- **The timeout is per-comm-client, not per-call.** All operations for a given client share the same `respTimeout` context with the same duration. You can call `wh_Timeout_Set()` to change the duration between calls, but there's no per-operation override.
+- **Timer starts on send, checks on receive.** The timer window begins when a request is successfully sent, measuring the full round-trip wait.
diff --git a/port/posix/posix_timeout.c b/port/posix/posix_timeout.c
new file mode 100644
index 000000000..b72aee7ec
--- /dev/null
+++ b/port/posix/posix_timeout.c
@@ -0,0 +1,167 @@
+/*
+ * 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 .
+ */
+/*
+ * port/posix/posix_timeout.c
+ *
+ * POSIX implementation of the wolfHSM timeout abstraction.
+ * Uses CLOCK_MONOTONIC for time measurement.
+ */
+
+#include "wolfhsm/wh_settings.h"
+
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+
+#include
+#include
+
+#include "wolfhsm/wh_error.h"
+#include "wolfhsm/wh_timeout.h"
+
+#include "port/posix/posix_timeout.h"
+
+/* Use CLOCK_MONOTONIC for timeout measurement to avoid issues with wall-clock
+ * adjustments (NTP, manual changes, etc.) that could cause spurious expirations
+ * or overly long timeouts. */
+static uint64_t _getMonotonicTimeUs(void)
+{
+ struct timespec ts;
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
+ return 0;
+ }
+ return (uint64_t)ts.tv_sec * 1000000ULL + (uint64_t)(ts.tv_nsec / 1000);
+}
+
+int posixTimeout_Init(void* context, const void* config)
+{
+ posixTimeoutContext* ctx = (posixTimeoutContext*)context;
+ const posixTimeoutConfig* cfg = (const posixTimeoutConfig*)config;
+
+ if (ctx == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ ctx->startUs = 0;
+ ctx->timeoutUs = (cfg != NULL) ? cfg->timeoutUs : 0;
+ ctx->running = 0;
+
+ ctx->initialized = 1;
+ return WH_ERROR_OK;
+}
+
+int posixTimeout_Cleanup(void* context)
+{
+ posixTimeoutContext* ctx = (posixTimeoutContext*)context;
+
+ if (ctx == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ ctx->startUs = 0;
+ ctx->timeoutUs = 0;
+ ctx->running = 0;
+ ctx->initialized = 0;
+
+ return WH_ERROR_OK;
+}
+
+int posixTimeout_Set(void* context, uint64_t timeoutUs)
+{
+ posixTimeoutContext* ctx = (posixTimeoutContext*)context;
+
+ if (ctx == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ if (!ctx->initialized) {
+ return WH_ERROR_BADARGS;
+ }
+
+ ctx->timeoutUs = timeoutUs;
+
+ return WH_ERROR_OK;
+}
+
+int posixTimeout_Start(void* context)
+{
+ posixTimeoutContext* ctx = (posixTimeoutContext*)context;
+
+ if (ctx == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ if (!ctx->initialized) {
+ return WH_ERROR_BADARGS;
+ }
+
+ ctx->startUs = _getMonotonicTimeUs();
+ if (ctx->startUs == 0) {
+ return WH_ERROR_ABORTED;
+ }
+ ctx->running = 1;
+
+ return WH_ERROR_OK;
+}
+
+int posixTimeout_Stop(void* context)
+{
+ posixTimeoutContext* ctx = (posixTimeoutContext*)context;
+
+ if (ctx == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ if (!ctx->initialized) {
+ return WH_ERROR_BADARGS;
+ }
+
+ ctx->startUs = 0;
+ ctx->running = 0;
+
+ return WH_ERROR_OK;
+}
+
+int posixTimeout_Expired(void* context, int* expired)
+{
+ posixTimeoutContext* ctx = (posixTimeoutContext*)context;
+ uint64_t nowUs;
+
+ if ((ctx == NULL) || (expired == NULL)) {
+ return WH_ERROR_BADARGS;
+ }
+
+ if (!ctx->initialized) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Not started or no timeout configured = not expired */
+ if (!ctx->running || (ctx->timeoutUs == 0)) {
+ *expired = 0;
+ return WH_ERROR_OK;
+ }
+
+ nowUs = _getMonotonicTimeUs();
+ if (nowUs == 0) {
+ return WH_ERROR_ABORTED;
+ }
+ *expired = ((nowUs - ctx->startUs) >= ctx->timeoutUs) ? 1 : 0;
+
+ return WH_ERROR_OK;
+}
+
+#endif /* WOLFHSM_CFG_ENABLE_TIMEOUT */
diff --git a/port/posix/posix_timeout.h b/port/posix/posix_timeout.h
new file mode 100644
index 000000000..8fd28b0d8
--- /dev/null
+++ b/port/posix/posix_timeout.h
@@ -0,0 +1,73 @@
+/*
+ * 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 .
+ */
+/*
+ * port/posix/posix_timeout.h
+ *
+ * POSIX implementation of the wolfHSM timeout abstraction.
+ * Uses CLOCK_MONOTONIC for time measurement.
+ */
+
+#ifndef PORT_POSIX_POSIX_TIMEOUT_H_
+#define PORT_POSIX_POSIX_TIMEOUT_H_
+
+#include "wolfhsm/wh_settings.h"
+
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+
+#include
+
+#include "wolfhsm/wh_timeout.h"
+
+/* Configuration for POSIX timeout backend */
+typedef struct posixTimeoutConfig_t {
+ uint64_t timeoutUs; /* Timeout duration in microseconds; 0 = no timeout */
+} posixTimeoutConfig;
+
+/* Context structure holding timer state */
+typedef struct posixTimeoutContext_t {
+ uint64_t startUs; /* Snapshot of start time */
+ uint64_t timeoutUs; /* Configured timeout duration */
+ int running; /* 1 if timer is running, 0 otherwise */
+ int initialized; /* 1 if initialized, 0 otherwise */
+} posixTimeoutContext;
+
+/* Callback functions matching whTimeoutCb interface */
+int posixTimeout_Init(void* context, const void* config);
+int posixTimeout_Cleanup(void* context);
+int posixTimeout_Set(void* context, uint64_t timeoutUs);
+int posixTimeout_Start(void* context);
+int posixTimeout_Stop(void* context);
+int posixTimeout_Expired(void* context, int* expired);
+
+/* Convenience macro for callback table initialization */
+/* clang-format off */
+#define POSIX_TIMEOUT_CB \
+ { \
+ .init = posixTimeout_Init, \
+ .cleanup = posixTimeout_Cleanup, \
+ .set = posixTimeout_Set, \
+ .start = posixTimeout_Start, \
+ .stop = posixTimeout_Stop, \
+ .expired = posixTimeout_Expired, \
+ }
+/* clang-format on */
+
+#endif /* WOLFHSM_CFG_ENABLE_TIMEOUT */
+
+#endif /* !PORT_POSIX_POSIX_TIMEOUT_H_ */
diff --git a/src/wh_comm.c b/src/wh_comm.c
index b8e4f6474..7d2aeba9d 100644
--- a/src/wh_comm.c
+++ b/src/wh_comm.c
@@ -89,6 +89,13 @@ int wh_CommClient_Init(whCommClient* context, const whCommClientConfig* config)
if (context->connect_cb != NULL) {
rc = context->connect_cb(context, WH_COMM_CONNECTED);
}
+
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ if (rc == 0) {
+ rc = wh_Timeout_Init(&context->respTimeout,
+ config->respTimeoutConfig);
+ }
+#endif
}
return rc;
}
@@ -127,6 +134,11 @@ int wh_CommClient_SendRequest(whCommClient* context, uint16_t magic,
context->seq++;
if (out_seq != NULL) *out_seq = context->seq;
}
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ if (rc == 0) {
+ rc = wh_Timeout_Start(&context->respTimeout);
+ }
+#endif
return rc;
}
@@ -154,6 +166,9 @@ int wh_CommClient_RecvResponse(whCommClient* context,
&size,
context->packet);
if (rc == 0) {
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ (void)wh_Timeout_Stop(&context->respTimeout);
+#endif
if (size < sizeof(*context->hdr)) {
/* Size is too small */
rc = WH_ERROR_ABORTED;
@@ -174,6 +189,17 @@ int wh_CommClient_RecvResponse(whCommClient* context,
if (out_size != NULL) *out_size = data_size;
}
}
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ else if (rc == WH_ERROR_NOTREADY) {
+ int expired = wh_Timeout_Expired(&context->respTimeout);
+ if (expired > 0) {
+ rc = WH_ERROR_TIMEOUT;
+ }
+ else if (expired < 0) {
+ rc = expired;
+ }
+ }
+#endif
return rc;
}
@@ -201,6 +227,10 @@ int wh_CommClient_Cleanup(whCommClient* context)
(void)context->connect_cb(context, WH_COMM_DISCONNECTED);
}
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ (void)wh_Timeout_Cleanup(&context->respTimeout);
+#endif
+
if ( (context->transport_cb != NULL) &&
(context->transport_cb->Cleanup != NULL)) {
rc = context->transport_cb->Cleanup(context->transport_context);
diff --git a/src/wh_timeout.c b/src/wh_timeout.c
new file mode 100644
index 000000000..a548ff13a
--- /dev/null
+++ b/src/wh_timeout.c
@@ -0,0 +1,181 @@
+/*
+ * 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
+ *
+ * Platform-agnostic timeout abstraction. Each wrapper validates arguments,
+ * checks initialization state, and delegates to platform callbacks.
+ */
+
+/* Pick up compile-time configuration */
+#include "wolfhsm/wh_settings.h"
+
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+
+#include /* For NULL */
+#include /* For memset */
+
+#include "wolfhsm/wh_timeout.h"
+#include "wolfhsm/wh_error.h"
+
+int wh_Timeout_Init(whTimeout* timeout, const whTimeoutConfig* config)
+{
+ int ret = WH_ERROR_OK;
+
+ if (timeout == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Allow NULL config for disabled mode (no-op timeout) */
+ if ((config == NULL) || (config->cb == NULL)) {
+ timeout->cb = NULL;
+ timeout->context = NULL;
+ timeout->expiredCb = NULL;
+ timeout->expiredCtx = NULL;
+ timeout->initialized = 1; /* Mark as initialized even in no-op mode */
+ return WH_ERROR_OK;
+ }
+
+ timeout->cb = config->cb;
+ timeout->context = config->context;
+ timeout->expiredCb = config->expiredCb;
+ timeout->expiredCtx = config->expiredCtx;
+
+ /* Initialize the platform timeout if callback provided */
+ if (timeout->cb->init != NULL) {
+ ret = timeout->cb->init(timeout->context, config->config);
+ if (ret != WH_ERROR_OK) {
+ timeout->cb = NULL;
+ timeout->context = NULL;
+ /* Do not set initialized on failure */
+ return ret;
+ }
+ }
+
+ timeout->initialized = 1;
+ return WH_ERROR_OK;
+}
+
+int wh_Timeout_Cleanup(whTimeout* timeout)
+{
+ if (timeout == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ if ((timeout->cb != NULL) && (timeout->cb->cleanup != NULL)) {
+ int ret = timeout->cb->cleanup(timeout->context);
+ if (ret != WH_ERROR_OK) {
+ return ret;
+ }
+ }
+
+ /* Zero the entire structure to make post-cleanup state distinguishable */
+ memset(timeout, 0, sizeof(*timeout));
+
+ return WH_ERROR_OK;
+}
+
+int wh_Timeout_Set(whTimeout* timeout, uint64_t timeoutUs)
+{
+ if (timeout == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ if (timeout->initialized == 0) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* No-op if not configured (no callbacks) */
+ if ((timeout->cb == NULL) || (timeout->cb->set == NULL)) {
+ return WH_ERROR_OK;
+ }
+
+ return timeout->cb->set(timeout->context, timeoutUs);
+}
+
+int wh_Timeout_Start(whTimeout* timeout)
+{
+ if (timeout == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ if (timeout->initialized == 0) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* No-op if not configured (no callbacks) */
+ if ((timeout->cb == NULL) || (timeout->cb->start == NULL)) {
+ return WH_ERROR_OK;
+ }
+
+ return timeout->cb->start(timeout->context);
+}
+
+int wh_Timeout_Stop(whTimeout* timeout)
+{
+ if (timeout == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ if (timeout->initialized == 0) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* No-op if not configured (no callbacks) */
+ if ((timeout->cb == NULL) || (timeout->cb->stop == NULL)) {
+ return WH_ERROR_OK;
+ }
+
+ return timeout->cb->stop(timeout->context);
+}
+
+int wh_Timeout_Expired(whTimeout* timeout)
+{
+ int expired = 0;
+ int ret = 0;
+
+ if (timeout == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Not initialized or no callbacks = never expired */
+ if ((timeout->initialized == 0) || (timeout->cb == NULL) ||
+ (timeout->cb->expired == NULL)) {
+ return 0;
+ }
+
+ ret = timeout->cb->expired(timeout->context, &expired);
+ if (ret != WH_ERROR_OK) {
+ return ret;
+ }
+
+ /* If expired and application callback is set, invoke it */
+ if (expired && (timeout->expiredCb != NULL)) {
+ /* Allow the callback to overwrite the expired value. If the callback
+ * returns an error, propagate it to the caller. */
+ ret = timeout->expiredCb(timeout, &expired);
+ if (ret != WH_ERROR_OK) {
+ return ret;
+ }
+ }
+
+ 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 c97a36897..ac0d0a316 100644
--- a/test/wh_test.c
+++ b/test/wh_test.c
@@ -43,6 +43,7 @@
#include "wh_test_lock.h"
#include "wh_test_posix_threadsafe_stress.h"
#include "wh_test_crypto_affinity.h"
+#include "wh_test_timeout.h"
#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER)
#include "wh_test_cert.h"
@@ -125,6 +126,10 @@ int whTest_Unit(void)
#endif /* !WOLFHSM_CFG_NO_CRYPTO */
+#if defined(WOLFHSM_CFG_ENABLE_TIMEOUT) && defined(WOLFHSM_CFG_TEST_POSIX)
+ WH_TEST_ASSERT(0 == whTest_TimeoutPosix());
+#endif
+
return 0;
}
#endif /* WOLFHSM_CFG_ENABLE_CLIENT && WOLFHSM_CFG_ENABLE_SERVER */
@@ -158,6 +163,10 @@ int whTest_ClientConfig(whClientConfig* clientCfg)
WH_TEST_RETURN_ON_FAIL(whTest_WolfCryptTestCfg(clientCfg));
#endif /* WOLFHSM_CFG_TEST_WOLFCRYPTTEST */
+#if defined(WOLFHSM_CFG_ENABLE_TIMEOUT)
+ WH_TEST_RETURN_ON_FAIL(whTest_TimeoutClientConfig(clientCfg));
+#endif /* WOLFHSM_CFG_ENABLE_TIMEOUT */
+
return WH_ERROR_OK;
}
diff --git a/test/wh_test_timeout.c b/test/wh_test_timeout.c
new file mode 100644
index 000000000..78076057b
--- /dev/null
+++ b/test/wh_test_timeout.c
@@ -0,0 +1,490 @@
+/*
+ * 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 "wolfhsm/wh_settings.h"
+
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+
+#include "wolfhsm/wh_timeout.h"
+#include "wolfhsm/wh_error.h"
+#include "wolfhsm/wh_comm.h"
+#include "wolfhsm/wh_transport_mem.h"
+#include "wolfhsm/wh_client.h"
+
+#include "wh_test_common.h"
+#include "wh_test_timeout.h"
+
+#ifdef WOLFHSM_CFG_TEST_POSIX
+#include "port/posix/posix_timeout.h"
+#endif /* WOLFHSM_CFG_TEST_POSIX */
+
+#if !defined(WOLFHSM_CFG_NO_CRYPTO)
+#include "wolfssl/wolfcrypt/settings.h"
+#ifdef HAVE_AES_CBC
+#include "wolfssl/wolfcrypt/aes.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 */
+
+#if defined(WOLFHSM_CFG_TEST_POSIX) && defined(WOLFHSM_CFG_ENABLE_SERVER) && \
+ !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;
+ WH_TEST_PRINT("Testing timeout AES CBC...\n");
+
+ /* 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 */
+ posixTimeoutContext posixCtx = {0};
+ posixTimeoutConfig posixCfg = {.timeoutUs = 1};
+ whTimeoutCb timeoutCbTable = POSIX_TIMEOUT_CB;
+ whTimeoutConfig timeoutCfg = {
+ .cb = &timeoutCbTable,
+ .context = &posixCtx,
+ .config = &posixCfg,
+ .expiredCb = NULL,
+ .expiredCtx = 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,
+ .respTimeoutConfig = &timeoutCfg,
+ }};
+ whClientConfig c_conf[1] = {{
+ .comm = cc_conf,
+ }};
+ 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] = {0};
+
+ 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
+ * expiredCtx points to an int counter tracking how many times the callback
+ * fired. */
+static int _timeoutOverrideCb(whTimeout* timeout, int* isExpired)
+{
+ int* counter = (int*)timeout->expiredCtx;
+ if (counter == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ (*counter)++;
+
+ if (*counter <= 1) {
+ /* First expiration: override and restart the timer */
+ *isExpired = 0;
+ return wh_Timeout_Start(timeout);
+ }
+ /* Subsequent expirations: let it expire normally */
+ return WH_ERROR_OK;
+}
+
+static int whTest_TimeoutAesCbcOverride(void)
+{
+ int rc = 0;
+ int cb_count = 0;
+ WH_TEST_PRINT("Testing timeout AES CBC with override callback...\n");
+
+ /* 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 */
+ posixTimeoutContext posixCtx = {0};
+ posixTimeoutConfig posixCfg = {.timeoutUs = 1};
+ whTimeoutCb timeoutCbTable = POSIX_TIMEOUT_CB;
+ whTimeoutConfig timeoutCfg = {
+ .cb = &timeoutCbTable,
+ .context = &posixCtx,
+ .config = &posixCfg,
+ .expiredCb = _timeoutOverrideCb,
+ .expiredCtx = &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,
+ .respTimeoutConfig = &timeoutCfg,
+ }};
+ whClientConfig c_conf[1] = {{
+ .comm = cc_conf,
+ }};
+ 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] = {0};
+
+ 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_TEST_POSIX && WOLFHSM_CFG_ENABLE_SERVER && \
+ !WOLFHSM_CFG_NO_CRYPTO && HAVE_AES_CBC */
+
+/* Generic timeout API test - no platform dependencies */
+static int whTest_TimeoutApi(void)
+{
+ whTimeout timeout[1];
+ WH_TEST_PRINT("Testing timeout API...\n");
+
+ /* Test no-op mode (NULL config = timeout disabled, never expires) */
+ wh_Timeout_Init(timeout, NULL);
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Set(timeout, 1000) == WH_ERROR_OK);
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Start(timeout) == WH_ERROR_OK);
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Expired(timeout) == 0);
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Stop(timeout) == WH_ERROR_OK);
+ wh_Timeout_Cleanup(timeout);
+
+ /* Test Set on uninitialized timeout returns error */
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Set(timeout, 1000) == WH_ERROR_BADARGS);
+
+ /* Test bad arguments */
+ 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) == WH_ERROR_BADARGS);
+ WH_TEST_ASSERT_RETURN(wh_Timeout_Cleanup(0) == WH_ERROR_BADARGS);
+
+ return WH_ERROR_OK;
+}
+
+static int whTest_TimeoutResponse(whClientContext* client)
+{
+ int rc = 0;
+ uint8_t echoData[] = "hello";
+ uint8_t respData[sizeof(echoData)] = {0};
+ uint16_t respLen = 0;
+ WH_TEST_PRINT("Testing timeout response...\n");
+
+ /* Send an echo request into the void (no server will process it) */
+ WH_TEST_RETURN_ON_FAIL(
+ wh_Client_EchoRequest(client, sizeof(echoData), echoData));
+
+ /* Poll for response - should time out */
+ do {
+ rc = wh_Client_EchoResponse(client, &respLen, respData);
+ } while (rc == WH_ERROR_NOTREADY);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_TIMEOUT);
+
+ return WH_ERROR_OK;
+}
+
+int whTest_TimeoutClientConfig(whClientConfig* config)
+{
+ whClientContext client[1] = {0};
+ WH_TEST_PRINT("Testing timeout client config...\n");
+
+ WH_TEST_RETURN_ON_FAIL(whTest_TimeoutApi());
+
+ if (config != NULL && config->comm != NULL &&
+ config->comm->respTimeoutConfig != NULL) {
+ WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client, config));
+ WH_TEST_RETURN_ON_FAIL(whTest_TimeoutResponse(client));
+ WH_TEST_RETURN_ON_FAIL(wh_Client_Cleanup(client));
+ }
+
+#if defined(WOLFHSM_CFG_TEST_POSIX) && defined(WOLFHSM_CFG_ENABLE_SERVER) && \
+ !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 WH_ERROR_OK;
+}
+
+#ifdef WOLFHSM_CFG_TEST_POSIX
+int whTest_TimeoutPosix(void)
+{
+ WH_TEST_PRINT("Testing timeout (POSIX)...\n");
+
+ uint8_t req[4096] = {0};
+ uint8_t resp[4096] = {0};
+ whTransportMemConfig tmcf[1] = {{
+ .req = (whTransportMemCsr*)req,
+ .req_size = sizeof(req),
+ .resp = (whTransportMemCsr*)resp,
+ .resp_size = sizeof(resp),
+ }};
+ posixTimeoutContext posixCtx = {0};
+ posixTimeoutConfig posixCfg = {.timeoutUs = 1};
+ whTimeoutCb timeoutCbTable = POSIX_TIMEOUT_CB;
+ whTimeoutConfig timeoutCfg = {
+ .cb = &timeoutCbTable,
+ .context = &posixCtx,
+ .config = &posixCfg,
+ };
+ whTransportClientCb tccb[1] = {WH_TRANSPORT_MEM_CLIENT_CB};
+ whTransportMemClientContext tmcc[1] = {0};
+ whCommClientConfig ccConf[1] = {{
+ .transport_cb = tccb,
+ .transport_context = (void*)tmcc,
+ .transport_config = (void*)tmcf,
+ .client_id = WH_TEST_DEFAULT_CLIENT_ID,
+ .respTimeoutConfig = &timeoutCfg,
+ }};
+ whClientConfig cConf[1] = {{
+ .comm = ccConf,
+ }};
+
+ return whTest_TimeoutClientConfig(cConf);
+}
+#endif /* WOLFHSM_CFG_TEST_POSIX */
+
+#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..22f999044
--- /dev/null
+++ b/test/wh_test_timeout.h
@@ -0,0 +1,44 @@
+/*
+ * 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_
+
+#include "wolfhsm/wh_client.h"
+
+/**
+ * Runs timeout module tests against the given client configuration.
+ *
+ * @param[in] config Client configuration with timeout support enabled.
+ * @return 0 on success and a non-zero error code on failure.
+ */
+int whTest_TimeoutClientConfig(whClientConfig* config);
+
+/**
+ * Runs timeout tests using a default POSIX configuration.
+ *
+ * @return 0 on success and a non-zero error code on failure.
+ */
+int whTest_TimeoutPosix(void);
+
+#endif /* TEST_WH_TEST_TIMEOUT_H_ */
diff --git a/wolfhsm/wh_client.h b/wolfhsm/wh_client.h
index f56bdbfbe..5fcac92fd 100644
--- a/wolfhsm/wh_client.h
+++ b/wolfhsm/wh_client.h
@@ -178,8 +178,6 @@ 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);
-
-
/** Comm component functions */
/**
diff --git a/wolfhsm/wh_comm.h b/wolfhsm/wh_comm.h
index 4b77d58f7..a0cc24206 100644
--- a/wolfhsm/wh_comm.h
+++ b/wolfhsm/wh_comm.h
@@ -45,6 +45,8 @@
#include /* For sized ints */
+#include "wolfhsm/wh_timeout.h"
+
/** Packet content types */
/* Request/response packets are composed of a single fixed-length header
* (whCommHeader) followed immediately by variable-length data between 0 and
@@ -160,6 +162,9 @@ typedef struct {
whCommSetConnectedCb connect_cb;
uint8_t client_id;
uint8_t WH_PAD[7];
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ whTimeoutConfig* respTimeoutConfig;
+#endif
} whCommClientConfig;
/* Context structure for a client. Note the client context will track the
@@ -180,6 +185,9 @@ typedef struct {
uint8_t client_id;
uint8_t server_id;
uint8_t WH_PAD[4];
+#ifdef WOLFHSM_CFG_ENABLE_TIMEOUT
+ whTimeout respTimeout;
+#endif
} whCommClient;
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..6cd676af4
--- /dev/null
+++ b/wolfhsm/wh_timeout.h
@@ -0,0 +1,206 @@
+/*
+ * 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
+ *
+ * Platform-agnostic timeout abstraction using a callback-based mechanism
+ * that allows platform-specific implementations (POSIX, RTOS, bare-metal,
+ * etc.) without introducing OS dependencies in the core wolfHSM code.
+ *
+ * When WOLFHSM_CFG_ENABLE_TIMEOUT is defined:
+ * - Timeout operations use platform callbacks for actual time measurement
+ * - NULL config results in no-op timeout (never expires)
+ *
+ * When WOLFHSM_CFG_ENABLE_TIMEOUT is not defined:
+ * - No timeout types or functions are available
+ */
+
+#ifndef WOLFHSM_WH_TIMEOUT_H_
+#define WOLFHSM_WH_TIMEOUT_H_
+
+/* Pick up compile-time configuration */
+#include "wolfhsm/wh_settings.h"
+
+#include
+
+/* Time conversion macros */
+#define WH_MSEC_TO_USEC(ms) ((ms) * (1000ULL))
+#define WH_SEC_TO_USEC(sec) ((sec) * (1000000ULL))
+#define WH_MIN_TO_USEC(min) ((min) * (WH_SEC_TO_USEC(60)))
+
+
+/**
+ * Platform callback function signatures.
+ *
+ * All callbacks receive a user-provided context pointer (from whTimeoutConfig).
+ * Return: WH_ERROR_OK on success, negative error code on failure.
+ */
+
+/** Initialize timeout resources - called once during setup */
+typedef int (*whTimeoutInitCb)(void* context, const void* config);
+
+/** Cleanup timeout resources - called once during teardown */
+typedef int (*whTimeoutCleanupCb)(void* context);
+
+/** Set the timeout duration in microseconds */
+typedef int (*whTimeoutSetCb)(void* context, uint64_t timeoutUs);
+
+/** Start or restart the timeout timer */
+typedef int (*whTimeoutStartCb)(void* context);
+
+/** Stop the timeout timer */
+typedef int (*whTimeoutStopCb)(void* context);
+
+/**
+ * Check whether the timeout has expired.
+ * Writes 1 to *expired if elapsed, 0 if not.
+ * Return: WH_ERROR_OK on success, negative error code on failure.
+ */
+typedef int (*whTimeoutCheckExpiredCb)(void* context, int* expired);
+
+/**
+ * Timeout callback table.
+ *
+ * Platforms provide implementations of these callbacks. If the entire
+ * callback table is NULL, all timeout operations become no-ops
+ * (disabled mode). Individual callbacks may also be NULL to skip
+ * specific operations.
+ */
+typedef struct whTimeoutCb_t {
+ whTimeoutInitCb init; /* Initialize timeout resources */
+ whTimeoutCleanupCb cleanup; /* Free timeout resources */
+ whTimeoutSetCb set; /* Set timeout duration */
+ whTimeoutStartCb start; /* Start/restart timer */
+ whTimeoutStopCb stop; /* Stop timer */
+ whTimeoutCheckExpiredCb expired; /* Check if timer expired */
+} whTimeoutCb;
+
+
+/* Forward declare so the application callback typedef can reference it */
+typedef struct whTimeout_t whTimeout;
+
+/**
+ * Application-level callback invoked when a timeout expires. The callback may
+ * override the expiration by setting *isExpired to 0 (e.g. to extend the
+ * timeout by calling wh_Timeout_Start() to restart the timer). Returning a
+ * non-zero error code from the callback will cause wh_Timeout_Expired() to
+ * propagate that error to its caller.
+ *
+ * @param timeout The timeout instance that expired.
+ * @param isExpired Pointer to the expired flag; set to 0 to suppress
+ * expiration.
+ * @return 0 on success, or a negative error code to signal failure.
+ */
+typedef int (*whTimeoutExpiredCb)(whTimeout* timeout, int* isExpired);
+
+/**
+ * Timeout instance structure.
+ *
+ * Holds callback table, platform-specific context, and an optional
+ * application-level expired callback. The context pointer is passed to all
+ * platform callbacks.
+ */
+struct whTimeout_t {
+ const whTimeoutCb* cb; /* Platform callbacks (may be NULL) */
+ void* context; /* Platform context */
+ whTimeoutExpiredCb expiredCb; /* Application expired callback */
+ void* expiredCtx; /* Application callback context */
+ int32_t initialized;
+ uint8_t WH_PAD[4];
+};
+
+/**
+ * Timeout configuration for initialization.
+ */
+typedef struct whTimeoutConfig_t {
+ const whTimeoutCb* cb; /* Callback table */
+ void* context; /* Platform context */
+ const void* config; /* Backend-specific config */
+ whTimeoutExpiredCb expiredCb; /* Application expired callback */
+ void* expiredCtx; /* Application callback context */
+} whTimeoutConfig;
+
+
+/**
+ * @brief Initializes a timeout instance.
+ *
+ * If config is NULL or config->cb is NULL, the timeout is disabled
+ * (no-op mode) and all operations become no-ops with Expired() always
+ * returning 0.
+ *
+ * @param[in] timeout Pointer to the timeout structure. Must not be NULL.
+ * @param[in] config Pointer to the timeout configuration (may be NULL for
+ * no-op mode).
+ * @return WH_ERROR_OK on success, WH_ERROR_BADARGS if timeout is NULL,
+ * or negative error code on callback failure.
+ */
+int wh_Timeout_Init(whTimeout* timeout, const whTimeoutConfig* config);
+
+/**
+ * @brief Cleans up a timeout instance.
+ *
+ * Calls the cleanup callback and then zeros the entire structure.
+ * Idempotent - calling cleanup on an already cleaned up or uninitialized
+ * timeout returns WH_ERROR_OK.
+ *
+ * @param[in] timeout Pointer to the timeout structure.
+ * @return WH_ERROR_OK on success, WH_ERROR_BADARGS if timeout is NULL.
+ */
+int wh_Timeout_Cleanup(whTimeout* timeout);
+
+/**
+ * @brief Set the timeout duration.
+ *
+ * @param[in] timeout The timeout instance.
+ * @param[in] timeoutUs Timeout duration in microseconds; 0 disables.
+ * @return WH_ERROR_OK on success, WH_ERROR_BADARGS on invalid input.
+ */
+int wh_Timeout_Set(whTimeout* timeout, uint64_t timeoutUs);
+
+/**
+ * @brief Start or reset a timeout window.
+ *
+ * @param[in] timeout The timeout instance.
+ * @return WH_ERROR_OK on success, WH_ERROR_BADARGS on invalid input.
+ */
+int wh_Timeout_Start(whTimeout* timeout);
+
+/**
+ * @brief Stop a timeout and clear its timer state.
+ *
+ * @param[in] timeout The timeout instance.
+ * @return WH_ERROR_OK on success, WH_ERROR_BADARGS on invalid input.
+ */
+int wh_Timeout_Stop(whTimeout* timeout);
+
+/**
+ * @brief Check whether a timeout has expired.
+ *
+ * Delegates to the platform callback to check expiration. If expired and
+ * an application expired callback is configured, the callback is invoked
+ * before returning. The callback may set *isExpired to 0 to override
+ * (suppress) the expiration.
+ *
+ * @param[in] timeout The timeout instance. Must not be NULL.
+ * @return 1 if expired, 0 if not expired or disabled, WH_ERROR_BADARGS if
+ * timeout is NULL, or negative error code on callback failure.
+ */
+int wh_Timeout_Expired(whTimeout* timeout);
+
+#endif /* !WOLFHSM_WH_TIMEOUT_H_ */