diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index fa8d55807..c2abba9f8 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -36,6 +36,10 @@ jobs:
- name: Build and test
run: cd test && make clean && make -j WOLFSSL_DIR=../wolfssl && make run
+ # Build and test ARMv8-M TrustZone NSC bridge transport (port/armv8m-tz)
+ - name: Build and test ARMV8M_TZ_NSC ASAN
+ run: cd test && make clean && make -j ARMV8M_TZ_NSC=1 ASAN=1 WOLFSSL_DIR=../wolfssl && make run
+
# Build and test standard build, with DMA and ASAN enabled
- name: Build and test DMA ASAN
run: cd test && make clean && make -j DMA=1 ASAN=1 WOLFSSL_DIR=../wolfssl && make run
diff --git a/docs/src/chapter08.md b/docs/src/chapter08.md
index 095e7b7be..5b017325a 100644
--- a/docs/src/chapter08.md
+++ b/docs/src/chapter08.md
@@ -46,6 +46,16 @@ The distribution of this port is restricted by the vendor. Please contact suppo
- 1x 100MHz e200z0 PowerPC HSM core with NVM
- Crypto offload: TRNG, AES128
+### ARMv8-M TrustZone (NSC bridge)
+
+The `port/armv8m-tz` port provides a synchronous TrustZone non-secure-callable bridge transport for any ARMv8-M Cortex-M target (Cortex-M23 / M33 / M35P / M55 / M85). It is designed for deployments in which a secure-side image hosts a wolfHSM server and exposes it to the non-secure application through a single `cmse_nonsecure_entry` veneer (`wcs_wolfhsm_transmit`). The first integration is wolfBoot on STM32H5; see `wolfBoot/docs/wolfHSM.md` for the build, flash, and test recipe.
+
+The port provides:
+- Single-call NSC transport (no polling, no shared-memory ring): client `Send` invokes the host-supplied veneer inline and caches the response; client `Recv` consumes the cached response on the first call (subsequent calls return `WH_ERROR_NOTREADY` until the next `Send`).
+- Server-side callbacks that consume the request the host's veneer parked in a static context and write the response back to the non-secure caller's buffer.
+
+The transport is target-agnostic. Bringing it up on a new ARMv8-M part is a porting exercise on the host side only: provide the `cmse_nonsecure_entry` veneer that fronts `wcs_wolfhsm_transmit`, plus whatever flash/NVM adapter and server init the deployment needs.
+
### POSIX
The POSIX port provides multiple and fully functional implementations of different wolfHSM abstractions that can be used to better understand the exact functionality expected for different hardware abstractions.
diff --git a/port/armv8m-tz/wh_transport_nsc.c b/port/armv8m-tz/wh_transport_nsc.c
new file mode 100644
index 000000000..233c0d0fc
--- /dev/null
+++ b/port/armv8m-tz/wh_transport_nsc.c
@@ -0,0 +1,237 @@
+/*
+ * port/armv8m-tz/wh_transport_nsc.c
+ *
+ * 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 .
+ */
+
+#include "wolfhsm/wh_settings.h"
+
+#ifdef WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC
+
+#include
+#include
+
+#include "wolfhsm/wh_comm.h"
+#include "wolfhsm/wh_error.h"
+#include "wh_transport_nsc.h"
+
+/*
+ * Resolved on the non-secure side via the wolfBoot --cmse-implib import
+ * library; on the secure side the same symbol is provided by the host's
+ * NSC veneer (wolfBoot's src/wolfhsm_callable.c). The server callbacks
+ * below never call this; --gc-sections strips client-side code from the
+ * secure image.
+ */
+extern int wcs_wolfhsm_transmit(const uint8_t* cmd, uint32_t cmdSz,
+ uint8_t* rsp, uint32_t* rspSz);
+
+
+/* ============================================================
+ * Non-secure (client) callbacks
+ * ============================================================ */
+
+static int _NscClientInit(void* context, const void* config,
+ whCommSetConnectedCb connectcb, void* connectcb_arg)
+{
+ whTransportNscClientContext* ctx = (whTransportNscClientContext*)context;
+
+ (void)config;
+
+ if (ctx == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->initialized = 1;
+
+ /* Synchronous bridge: the secure side is always reachable once linked. */
+ if (connectcb != NULL) {
+ connectcb(connectcb_arg, WH_COMM_CONNECTED);
+ }
+ return WH_ERROR_OK;
+}
+
+static int _NscClientSend(void* context, uint16_t size, const void* data)
+{
+ whTransportNscClientContext* ctx = (whTransportNscClientContext*)context;
+ uint32_t rspSz;
+ int rc;
+
+ if (ctx == NULL || data == NULL || ctx->initialized == 0U) {
+ return WH_ERROR_BADARGS;
+ }
+ if (size == 0U || size > WH_TRANSPORT_NSC_BUFFER_SIZE) {
+ return WH_ERROR_BADARGS;
+ }
+ /* prior response must be consumed before next Send */
+ if (ctx->last_rsp_size != 0U) {
+ return WH_ERROR_NOTREADY;
+ }
+
+ rspSz = (uint32_t)WH_TRANSPORT_NSC_BUFFER_SIZE;
+ rc = wcs_wolfhsm_transmit((const uint8_t*)data, (uint32_t)size,
+ ctx->rsp_buf, &rspSz);
+ if (rc != 0) {
+ ctx->last_rsp_size = 0;
+ /* propagate known wolfHSM error codes, collapse unknowns */
+ if (rc == WH_ERROR_BADARGS || rc == WH_ERROR_NOTREADY ||
+ rc == WH_ERROR_ABORTED) {
+ return rc;
+ }
+ return WH_ERROR_ABORTED;
+ }
+ if (rspSz == 0U || rspSz > (uint32_t)WH_TRANSPORT_NSC_BUFFER_SIZE) {
+ ctx->last_rsp_size = 0;
+ return WH_ERROR_ABORTED;
+ }
+
+ ctx->last_rsp_size = (uint16_t)rspSz;
+ return WH_ERROR_OK;
+}
+
+static int _NscClientRecv(void* context, uint16_t* out_size, void* data)
+{
+ whTransportNscClientContext* ctx = (whTransportNscClientContext*)context;
+
+ if (ctx == NULL || out_size == NULL || data == NULL ||
+ ctx->initialized == 0U) {
+ return WH_ERROR_BADARGS;
+ }
+ if (ctx->last_rsp_size == 0U) {
+ return WH_ERROR_NOTREADY;
+ }
+ /* out_size is in/out capacity; reject truncation, keep cached response */
+ if (*out_size < ctx->last_rsp_size) {
+ return WH_ERROR_BADARGS;
+ }
+
+ memcpy(data, ctx->rsp_buf, ctx->last_rsp_size);
+ *out_size = ctx->last_rsp_size;
+ ctx->last_rsp_size = 0;
+ return WH_ERROR_OK;
+}
+
+static int _NscClientCleanup(void* context)
+{
+ whTransportNscClientContext* ctx = (whTransportNscClientContext*)context;
+ if (ctx == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+ ctx->initialized = 0;
+ return WH_ERROR_OK;
+}
+
+const whTransportClientCb whTransportNscClient_Cb = {
+ .Init = _NscClientInit,
+ .Send = _NscClientSend,
+ .Recv = _NscClientRecv,
+ .Cleanup = _NscClientCleanup,
+};
+
+
+/* ============================================================
+ * Secure-side (server) callbacks
+ *
+ * The host's NSC veneer populates req_buf/req_size/rsp_buf/rsp_capacity
+ * and sets request_pending = 1 before calling wh_Server_HandleRequestMessage.
+ * Recv hands the request to the dispatcher; Send writes the response back
+ * into rsp_buf and stores its size for the veneer to read.
+ * ============================================================ */
+
+static int _NscServerInit(void* context, const void* config,
+ whCommSetConnectedCb connectcb, void* connectcb_arg)
+{
+ whTransportNscServerContext* ctx = (whTransportNscServerContext*)context;
+
+ (void)config;
+
+ if (ctx == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ memset(ctx, 0, sizeof(*ctx));
+
+ if (connectcb != NULL) {
+ connectcb(connectcb_arg, WH_COMM_CONNECTED);
+ }
+ return WH_ERROR_OK;
+}
+
+static int _NscServerRecv(void* context, uint16_t* inout_size, void* data)
+{
+ whTransportNscServerContext* ctx = (whTransportNscServerContext*)context;
+
+ if (ctx == NULL || inout_size == NULL || data == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+ if (!ctx->request_pending || ctx->req_buf == NULL || ctx->req_size == 0U) {
+ return WH_ERROR_NOTREADY;
+ }
+ /* clear stale rsp_size up-front so every exit path leaves a clean state */
+ ctx->rsp_size = 0;
+
+ if (ctx->req_size > *inout_size) {
+ ctx->request_pending = 0;
+ return WH_ERROR_ABORTED;
+ }
+
+ memcpy(data, ctx->req_buf, ctx->req_size);
+ *inout_size = ctx->req_size;
+ ctx->request_pending = 0;
+ return WH_ERROR_OK;
+}
+
+static int _NscServerSend(void* context, uint16_t size, const void* data)
+{
+ /* veneer is responsible for Recv/Send pairing; Send does not enforce it */
+ whTransportNscServerContext* ctx = (whTransportNscServerContext*)context;
+
+ if (ctx == NULL || data == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+ if (size == 0U || size > ctx->rsp_capacity) {
+ return WH_ERROR_BADARGS;
+ }
+ if (ctx->rsp_buf == NULL) {
+ return WH_ERROR_ABORTED;
+ }
+
+ memcpy(ctx->rsp_buf, data, size);
+ ctx->rsp_size = size;
+ return WH_ERROR_OK;
+}
+
+static int _NscServerCleanup(void* context)
+{
+ whTransportNscServerContext* ctx = (whTransportNscServerContext*)context;
+ if (ctx == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+ /* clear stale NS pointers so they cannot survive reinit */
+ memset(ctx, 0, sizeof(*ctx));
+ return WH_ERROR_OK;
+}
+
+const whTransportServerCb whTransportNscServer_Cb = {
+ .Init = _NscServerInit,
+ .Recv = _NscServerRecv,
+ .Send = _NscServerSend,
+ .Cleanup = _NscServerCleanup,
+};
+
+#endif /* WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC */
diff --git a/port/armv8m-tz/wh_transport_nsc.h b/port/armv8m-tz/wh_transport_nsc.h
new file mode 100644
index 000000000..c91199bc1
--- /dev/null
+++ b/port/armv8m-tz/wh_transport_nsc.h
@@ -0,0 +1,91 @@
+/*
+ * port/armv8m-tz/wh_transport_nsc.h
+ *
+ * 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 .
+ */
+
+/*
+ * Synchronous TrustZone NSC bridge transport for wolfHSM.
+ *
+ * The non-secure (client) side calls a single ARMv8-M Cortex-M
+ * cmse_nonsecure_entry veneer (`wcs_wolfhsm_transmit`) provided by the
+ * secure-side host. The veneer hands the request to the secure-side
+ * server context, runs `wh_Server_HandleRequestMessage` once inline,
+ * and returns the response in the same call. There is no polling,
+ * notify counter, or async producer/consumer — Send delivers the
+ * response, Recv just hands it back.
+ *
+ * The transport is target-agnostic across ARMv8-M TrustZone parts;
+ * the target-specific NSC veneer is provided by the host.
+ */
+
+#ifndef WH_TRANSPORT_NSC_H_
+#define WH_TRANSPORT_NSC_H_
+
+#include "wolfhsm/wh_settings.h"
+
+#ifdef WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC
+
+#include
+#include "wolfhsm/wh_comm.h"
+
+#define WH_TRANSPORT_NSC_BUFFER_SIZE WH_COMM_MTU
+
+/*
+ * Non-secure (client) context. Owns the response buffer in NS .bss.
+ * Not internally thread-safe.
+ */
+typedef struct {
+ uint8_t rsp_buf[WH_TRANSPORT_NSC_BUFFER_SIZE];
+ uint16_t last_rsp_size;
+ uint8_t initialized;
+ uint8_t WH_PAD[5]; /* trailing slack */
+} whTransportNscClientContext;
+
+/* Empty config; Init accepts NULL since there is nothing to read. */
+typedef struct {
+ uint8_t WH_PAD[1];
+} whTransportNscClientConfig;
+
+/*
+ * Secure-side server context. Populated by the NSC veneer per call:
+ * before invoking `wh_Server_HandleRequestMessage` the host sets
+ * req_buf/req_size/rsp_buf/rsp_capacity; after the dispatcher returns,
+ * the host reads rsp_size to pass back to the non-secure caller.
+ */
+typedef struct {
+ const uint8_t* req_buf;
+ uint8_t* rsp_buf;
+ uint16_t req_size;
+ uint16_t rsp_capacity;
+ uint16_t rsp_size; /* set by Send, read by veneer */
+ uint8_t request_pending; /* set by veneer, cleared by Recv */
+ uint8_t WH_PAD[1];
+} whTransportNscServerContext;
+
+typedef struct {
+ uint8_t WH_PAD[1];
+} whTransportNscServerConfig;
+
+/* Pre-populated tables; callbacks are file-local in wh_transport_nsc.c */
+extern const whTransportClientCb whTransportNscClient_Cb;
+extern const whTransportServerCb whTransportNscServer_Cb;
+
+#endif /* WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC */
+
+#endif /* WH_TRANSPORT_NSC_H_ */
diff --git a/test/Makefile b/test/Makefile
index de09e7d04..4a0bf0dfd 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -165,6 +165,14 @@ ifeq ($(AUTH),1)
DEF += -DWOLFHSM_CFG_ENABLE_AUTHENTICATION
endif
+# Build the ARMv8-M TrustZone NSC bridge transport plus its host unit test
+ifeq ($(ARMV8M_TZ_NSC),1)
+ DEF += -DWOLFHSM_CFG_PORT_ARMV8M_TZ_NSC
+ WOLFHSM_ARMV8M_TZ_DIR := $(WOLFHSM_DIR)/port/armv8m-tz
+ INC += -I$(WOLFHSM_ARMV8M_TZ_DIR)
+ SRC_C += $(wildcard $(WOLFHSM_ARMV8M_TZ_DIR)/*.c)
+endif
+
## Project defines
# Option to build wolfcrypt tests
ifeq ($(TESTWOLFCRYPT),1)
diff --git a/test/wh_test.c b/test/wh_test.c
index 6bfc6c4ce..f6d956066 100644
--- a/test/wh_test.c
+++ b/test/wh_test.c
@@ -46,6 +46,9 @@
#include "wh_test_timeout.h"
#include "wh_test_dma.h"
#include "wh_test_keystore_reqsize.h"
+#ifdef WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC
+#include "wh_test_transport_nsc.h"
+#endif
#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION
#include "wh_test_auth.h"
#endif /* WOLFHSM_CFG_ENABLE_AUTHENTICATION */
@@ -105,6 +108,10 @@ int whTest_Unit(void)
WH_TEST_ASSERT(0 == whTest_Comm());
WH_TEST_ASSERT(0 == whTest_ClientServer());
+#ifdef WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC
+ WH_TEST_ASSERT(0 == whTest_TransportNsc());
+#endif
+
#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION
/* Auth tests */
WH_TEST_ASSERT(0 == whTest_AuthMEM());
diff --git a/test/wh_test_check_struct_padding.c b/test/wh_test_check_struct_padding.c
index bb4b550d0..e2dbfdce7 100644
--- a/test/wh_test_check_struct_padding.c
+++ b/test/wh_test_check_struct_padding.c
@@ -203,4 +203,10 @@ whMessageCert_VerifyAcertRequest whMessageCert_VerifyAcertRequest_test;
#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT */
#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER */
+#ifdef WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC
+#include "wh_transport_nsc.h"
+whTransportNscClientContext whTransportNscClientContext_test;
+whTransportNscServerContext whTransportNscServerContext_test;
+#endif /* WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC */
+
#endif /* WH_TEST_CHECK_STRUCT_PADDING_C_ */
diff --git a/test/wh_test_transport_nsc.c b/test/wh_test_transport_nsc.c
new file mode 100644
index 000000000..a55317861
--- /dev/null
+++ b/test/wh_test_transport_nsc.c
@@ -0,0 +1,236 @@
+/*
+ * test/wh_test_transport_nsc.c
+ *
+ * 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 .
+ */
+
+#include "wolfhsm/wh_settings.h"
+
+#ifdef WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC
+
+#include
+#include
+
+#include "wolfhsm/wh_comm.h"
+#include "wolfhsm/wh_error.h"
+
+#include "wh_transport_nsc.h"
+#include "wh_test_common.h"
+#include "wh_test_transport_nsc.h"
+
+/* Stub of the secure-side veneer the client transport normally resolves at
+ * link time. Echoes the request back with a 1-byte tag. */
+static int g_stub_force_rc;
+static uint32_t g_stub_force_rspSz;
+
+int wcs_wolfhsm_transmit(const uint8_t* cmd, uint32_t cmdSz, uint8_t* rsp,
+ uint32_t* rspSz)
+{
+ if (g_stub_force_rc != 0) {
+ return g_stub_force_rc;
+ }
+ if (cmd == NULL || rsp == NULL || rspSz == NULL) {
+ return -1;
+ }
+ if (cmdSz == 0U || cmdSz > WH_TRANSPORT_NSC_BUFFER_SIZE) {
+ return -1;
+ }
+ if (*rspSz < cmdSz + 1U) {
+ return -1;
+ }
+ rsp[0] = 0xAB;
+ memcpy(rsp + 1, cmd, cmdSz);
+ *rspSz = (g_stub_force_rspSz != 0U) ? g_stub_force_rspSz : (cmdSz + 1U);
+ return 0;
+}
+
+static int test_client_callbacks(void)
+{
+ whTransportNscClientContext ctx;
+ static const uint8_t data_in[] = {1, 2, 3, 4};
+ uint8_t data_out[WH_TRANSPORT_NSC_BUFFER_SIZE];
+ uint16_t sz;
+ int rc;
+
+ g_stub_force_rc = 0;
+ g_stub_force_rspSz = 0;
+
+ rc = whTransportNscClient_Cb.Init(&ctx, NULL, NULL, NULL);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_OK);
+ WH_TEST_ASSERT_RETURN(ctx.initialized == 1U);
+
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscClient_Cb.Send(NULL, sizeof(data_in), data_in));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscClient_Cb.Send(&ctx, sizeof(data_in), NULL));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscClient_Cb.Send(&ctx, 0, data_in));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscClient_Cb.Send(&ctx,
+ (uint16_t)WH_TRANSPORT_NSC_BUFFER_SIZE + 1U, data_in));
+
+ sz = sizeof(data_out);
+ WH_TEST_ASSERT_RETURN(WH_ERROR_NOTREADY ==
+ whTransportNscClient_Cb.Recv(&ctx, &sz, data_out));
+
+ rc = whTransportNscClient_Cb.Send(&ctx, sizeof(data_in), data_in);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_OK);
+ sz = sizeof(data_out);
+ rc = whTransportNscClient_Cb.Recv(&ctx, &sz, data_out);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_OK);
+ WH_TEST_ASSERT_RETURN(sz == sizeof(data_in) + 1U);
+ WH_TEST_ASSERT_RETURN(data_out[0] == 0xABU);
+ WH_TEST_ASSERT_RETURN(memcmp(data_out + 1, data_in, sizeof(data_in)) == 0);
+
+ WH_TEST_ASSERT_RETURN(WH_ERROR_NOTREADY ==
+ whTransportNscClient_Cb.Recv(&ctx, &sz, data_out));
+
+ /* too-small buffer returns BADARGS, cached response preserved */
+ rc = whTransportNscClient_Cb.Send(&ctx, sizeof(data_in), data_in);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_OK);
+ sz = (uint16_t)(sizeof(data_in));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscClient_Cb.Recv(&ctx, &sz, data_out));
+ /* second Send before Recv consumes prior reply is NOTREADY */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_NOTREADY ==
+ whTransportNscClient_Cb.Send(&ctx, sizeof(data_in), data_in));
+ sz = sizeof(data_out);
+ rc = whTransportNscClient_Cb.Recv(&ctx, &sz, data_out);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_OK);
+ WH_TEST_ASSERT_RETURN(sz == sizeof(data_in) + 1U);
+
+ g_stub_force_rc = -42;
+ rc = whTransportNscClient_Cb.Send(&ctx, sizeof(data_in), data_in);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_ABORTED);
+ g_stub_force_rc = 0;
+
+ /* WH_ERROR_* return values from the veneer are propagated unchanged. */
+ g_stub_force_rc = WH_ERROR_BADARGS;
+ rc = whTransportNscClient_Cb.Send(&ctx, sizeof(data_in), data_in);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS);
+ g_stub_force_rc = WH_ERROR_NOTREADY;
+ rc = whTransportNscClient_Cb.Send(&ctx, sizeof(data_in), data_in);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_NOTREADY);
+ g_stub_force_rc = 0;
+
+ g_stub_force_rspSz = (uint32_t)WH_TRANSPORT_NSC_BUFFER_SIZE + 1U;
+ rc = whTransportNscClient_Cb.Send(&ctx, sizeof(data_in),
+ data_in);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_ABORTED);
+ g_stub_force_rspSz = 0;
+
+ rc = whTransportNscClient_Cb.Cleanup(&ctx);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_OK);
+ WH_TEST_ASSERT_RETURN(ctx.initialized == 0U);
+
+ /* initialized==0 check */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscClient_Cb.Send(&ctx, sizeof(data_in), data_in));
+ sz = sizeof(data_out);
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscClient_Cb.Recv(&ctx, &sz, data_out));
+
+ return WH_TEST_SUCCESS;
+}
+
+static int test_server_callbacks(void)
+{
+ whTransportNscServerContext ctx;
+ static const uint8_t req[] = {0xAA, 0xBB, 0xCC};
+ uint8_t rsp_buf[WH_TRANSPORT_NSC_BUFFER_SIZE];
+ uint8_t data_buf[WH_TRANSPORT_NSC_BUFFER_SIZE];
+ uint8_t* saved_rsp_buf;
+ uint16_t sz;
+ int rc;
+
+ rc = whTransportNscServer_Cb.Init(&ctx, NULL, NULL, NULL);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_OK);
+
+ sz = sizeof(data_buf);
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscServer_Cb.Recv(NULL, &sz, data_buf));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscServer_Cb.Recv(&ctx, NULL, data_buf));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscServer_Cb.Recv(&ctx, &sz, NULL));
+
+ WH_TEST_ASSERT_RETURN(WH_ERROR_NOTREADY ==
+ whTransportNscServer_Cb.Recv(&ctx, &sz, data_buf));
+
+ /* Stage as the host's NSC veneer would. */
+ ctx.req_buf = req;
+ ctx.req_size = (uint16_t)sizeof(req);
+ ctx.rsp_buf = rsp_buf;
+ ctx.rsp_capacity = (uint16_t)sizeof(rsp_buf);
+ ctx.rsp_size = 0xBEEF;
+ ctx.request_pending = 1;
+
+ /* oversize must clear request_pending and rsp_size */
+ sz = (uint16_t)(sizeof(req) - 1U);
+ rc = whTransportNscServer_Cb.Recv(&ctx, &sz, data_buf);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_ABORTED);
+ WH_TEST_ASSERT_RETURN(ctx.request_pending == 0U);
+ WH_TEST_ASSERT_RETURN(ctx.rsp_size == 0U);
+
+ ctx.req_buf = req;
+ ctx.req_size = (uint16_t)sizeof(req);
+ ctx.rsp_buf = rsp_buf;
+ ctx.rsp_capacity = (uint16_t)sizeof(rsp_buf);
+ ctx.rsp_size = 0xBEEF;
+ ctx.request_pending = 1;
+
+ sz = sizeof(data_buf);
+ rc = whTransportNscServer_Cb.Recv(&ctx, &sz, data_buf);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_OK);
+ WH_TEST_ASSERT_RETURN(sz == sizeof(req));
+ WH_TEST_ASSERT_RETURN(memcmp(data_buf, req, sizeof(req)) == 0);
+ WH_TEST_ASSERT_RETURN(ctx.request_pending == 0U);
+ WH_TEST_ASSERT_RETURN(ctx.rsp_size == 0U); /* reset by Recv */
+
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscServer_Cb.Send(&ctx, 0, req));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ whTransportNscServer_Cb.Send(&ctx, ctx.rsp_capacity + 1U, req));
+
+ saved_rsp_buf = ctx.rsp_buf;
+ ctx.rsp_buf = NULL;
+ WH_TEST_ASSERT_RETURN(WH_ERROR_ABORTED ==
+ whTransportNscServer_Cb.Send(&ctx, sizeof(req), req));
+ ctx.rsp_buf = saved_rsp_buf;
+
+ rc = whTransportNscServer_Cb.Send(&ctx, sizeof(req), req);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_OK);
+ WH_TEST_ASSERT_RETURN(ctx.rsp_size == sizeof(req));
+ WH_TEST_ASSERT_RETURN(memcmp(rsp_buf, req, sizeof(req)) == 0);
+
+ rc = whTransportNscServer_Cb.Cleanup(&ctx);
+ WH_TEST_ASSERT_RETURN(rc == WH_ERROR_OK);
+
+ return WH_TEST_SUCCESS;
+}
+
+int whTest_TransportNsc(void)
+{
+ WH_TEST_PRINT("Enter NSC transport tests\n");
+ WH_TEST_RETURN_ON_FAIL(test_client_callbacks());
+ WH_TEST_RETURN_ON_FAIL(test_server_callbacks());
+ WH_TEST_PRINT("NSC transport tests passed\n");
+ return WH_TEST_SUCCESS;
+}
+
+#endif /* WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC */
diff --git a/test/wh_test_transport_nsc.h b/test/wh_test_transport_nsc.h
new file mode 100644
index 000000000..709338620
--- /dev/null
+++ b/test/wh_test_transport_nsc.h
@@ -0,0 +1,26 @@
+/*
+ * test/wh_test_transport_nsc.h
+ *
+ * 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 .
+ */
+#ifndef WH_TEST_TRANSPORT_NSC_H
+#define WH_TEST_TRANSPORT_NSC_H
+
+int whTest_TransportNsc(void);
+
+#endif /* WH_TEST_TRANSPORT_NSC_H */
diff --git a/wolfhsm/wh_settings.h b/wolfhsm/wh_settings.h
index 18d23bd57..c4f6d3579 100644
--- a/wolfhsm/wh_settings.h
+++ b/wolfhsm/wh_settings.h
@@ -129,6 +129,14 @@
* WOLFHSM_CFG_PORT_GETTIME - Function-like macro returning the current system
* time in microseconds as a uint64_t. Must be defined in wolfhsm_cfg.h for
* the active port UNLESS WOLFHSM_CFG_NO_SYS_TIME is defined
+ *
+ * WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC - If defined, build the ARMv8-M TrustZone
+ * non-secure-callable (NSC) bridge transport in
+ * port/armv8m-tz/wh_transport_nsc.{c,h}. Target-agnostic across
+ * ARMv8-M Cortex-M parts; the non-secure client calls into the
+ * secure-side server through a single cmse_nonsecure_entry veneer
+ * provided by the host (e.g. wolfBoot's WOLFCRYPT_TZ_WOLFHSM engine).
+ * Default: Not defined
* Overridable porting functions:
*