diff --git a/src/wh_client_cert.c b/src/wh_client_cert.c
index d233da5e..63fbc357 100644
--- a/src/wh_client_cert.c
+++ b/src/wh_client_cert.c
@@ -507,6 +507,65 @@ int wh_Client_CertVerifyAndCacheLeafPubKey(
inout_keyId, out_rc);
}
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+int wh_Client_CertVerifyCacheClearRequest(whClientContext* c)
+{
+ if (c == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+ return wh_Client_SendRequest(c, WH_MESSAGE_GROUP_CERT,
+ WH_MESSAGE_CERT_ACTION_VERIFY_CACHE_CLEAR, 0,
+ NULL);
+}
+
+int wh_Client_CertVerifyCacheClearResponse(whClientContext* c, int32_t* out_rc)
+{
+ int rc;
+ uint16_t group;
+ uint16_t action;
+ uint16_t size;
+ whMessageCert_SimpleResponse resp;
+
+ if (c == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ rc = wh_Client_RecvResponse(c, &group, &action, &size, &resp);
+ if (rc == WH_ERROR_OK) {
+ if ((group != WH_MESSAGE_GROUP_CERT) ||
+ (action != WH_MESSAGE_CERT_ACTION_VERIFY_CACHE_CLEAR) ||
+ (size != sizeof(resp))) {
+ rc = WH_ERROR_ABORTED;
+ }
+ else if (out_rc != NULL) {
+ *out_rc = resp.rc;
+ }
+ }
+ return rc;
+}
+
+int wh_Client_CertVerifyCacheClear(whClientContext* c, int32_t* out_rc)
+{
+ int rc = WH_ERROR_OK;
+
+ if (c == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ do {
+ rc = wh_Client_CertVerifyCacheClearRequest(c);
+ } while (rc == WH_ERROR_NOTREADY);
+
+ if (rc == WH_ERROR_OK) {
+ do {
+ rc = wh_Client_CertVerifyCacheClearResponse(c, out_rc);
+ } while (rc == WH_ERROR_NOTREADY);
+ }
+
+ return rc;
+}
+#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */
+
#ifdef WOLFHSM_CFG_DMA
int wh_Client_CertAddTrustedDmaRequest(whClientContext* c, whNvmId id,
diff --git a/src/wh_server.c b/src/wh_server.c
index 943e583c..3e188cad 100644
--- a/src/wh_server.c
+++ b/src/wh_server.c
@@ -122,6 +122,14 @@ int wh_Server_Init(whServerContext* server, whServerConfig* config)
}
#endif /* WOLFHSM_CFG_DMA */
+#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER) && !defined(WOLFHSM_CFG_NO_CRYPTO)
+ /* Register the user-supplied verify callback, if any. The cache (if
+ * compiled in) is already zero-initialized by the memset above. */
+ if (config->certConfig != NULL) {
+ server->cert.verifyCb = config->certConfig->verifyCb;
+ }
+#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO */
+
/* Log the server startup */
WH_LOG(&server->log, WH_LOG_LEVEL_INFO, "Server Initialized");
diff --git a/src/wh_server_cert.c b/src/wh_server_cert.c
index 9d343a0a..f1518be1 100644
--- a/src/wh_server_cert.c
+++ b/src/wh_server_cert.c
@@ -33,6 +33,7 @@
#include "wolfhsm/wh_error.h"
#include "wolfhsm/wh_server.h"
#include "wolfhsm/wh_server_cert.h"
+#include "wolfhsm/wh_server_cert_cache.h"
#include "wolfhsm/wh_server_nvm.h"
#include "wolfhsm/wh_server_keystore.h"
#include "wolfhsm/wh_message.h"
@@ -41,6 +42,68 @@
#include "wolfssl/wolfcrypt/types.h"
#include "wolfssl/ssl.h"
#include "wolfssl/wolfcrypt/asn.h"
+#include "wolfssl/wolfcrypt/sha256.h"
+
+
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+int wh_Server_CertVerifyCache_Lookup(whServerContext* server, whNvmId rootNvmId,
+ const uint8_t* hash)
+{
+ int i;
+ if ((server == NULL) || (hash == NULL)) {
+ return WH_ERROR_BADARGS;
+ }
+ for (i = 0; i < WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT; i++) {
+ const whCertVerifyCacheSlot* slot = &server->cert.cache.slots[i];
+ if (slot->committed && (slot->rootNvmId == rootNvmId) &&
+ (memcmp(slot->hash, hash, WH_CERT_VERIFY_CACHE_HASH_LEN) == 0)) {
+ return WH_ERROR_OK;
+ }
+ }
+ return WH_ERROR_NOTFOUND;
+}
+
+void wh_Server_CertVerifyCache_Insert(whServerContext* server,
+ whNvmId rootNvmId, const uint8_t* hash)
+{
+ whCertVerifyCacheSlot* slot;
+ uint16_t idx;
+
+ if ((server == NULL) || (hash == NULL)) {
+ return;
+ }
+ /* Skip if already present under this root */
+ if (wh_Server_CertVerifyCache_Lookup(server, rootNvmId, hash) ==
+ WH_ERROR_OK) {
+ return;
+ }
+ /* FIFO ring overwrite */
+ idx = server->cert.cache.writeIdx;
+ slot = &server->cert.cache.slots[idx];
+ slot->rootNvmId = rootNvmId;
+ memcpy(slot->hash, hash, WH_CERT_VERIFY_CACHE_HASH_LEN);
+ slot->committed = 1;
+ server->cert.cache.writeIdx =
+ (uint16_t)((idx + 1) % WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT);
+}
+
+void wh_Server_CertVerifyCache_Clear(whServerContext* server)
+{
+ if (server == NULL) {
+ return;
+ }
+ memset(&server->cert.cache, 0, sizeof(server->cert.cache));
+}
+#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */
+
+int wh_Server_CertSetVerifyCb(whServerContext* server, VerifyCallback cb)
+{
+ if (server == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+ server->cert.verifyCb = cb;
+ return WH_ERROR_OK;
+}
/* Replicates GetSequence, which is WOLFSSL_LOCAL. */
@@ -61,18 +124,23 @@ static int DerNextSequence(const uint8_t* input, uint32_t maxIdx,
}
-static int _verifyChainAgainstCmStore(whServerContext* server,
- WOLFSSL_CERT_MANAGER* cm,
- const uint8_t* chain, uint32_t chain_len,
- whCertFlags flags,
- whNvmFlags cachedKeyFlags,
- whKeyId* inout_keyId)
+static int
+_verifyChainAgainstCmStore(whServerContext* server, WOLFSSL_CERT_MANAGER* cm,
+ const uint8_t* chain, uint32_t chain_len,
+ whNvmId trustedRootNvmId, whCertFlags flags,
+ whNvmFlags cachedKeyFlags, whKeyId* inout_keyId)
{
int rc = 0;
const uint8_t* cert_ptr = chain;
uint32_t remaining_len = chain_len;
int cert_len = 0;
word32 idx = 0;
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ uint8_t certHash[WH_CERT_VERIFY_CACHE_HASH_LEN];
+ int hashed = 0;
+#else
+ (void)trustedRootNvmId;
+#endif
if (cm == NULL || chain == NULL || chain_len == 0) {
return WH_ERROR_BADARGS;
@@ -82,6 +150,9 @@ static int _verifyChainAgainstCmStore(whServerContext* server,
while (remaining_len > 0) {
/* Reset index for each certificate */
idx = 0;
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ hashed = 0;
+#endif
/* Get the length of the current certificate */
rc = DerNextSequence(cert_ptr, remaining_len, &idx, &cert_len);
@@ -94,9 +165,28 @@ static int _verifyChainAgainstCmStore(whServerContext* server,
return WH_ERROR_ABORTED;
}
- /* Verify the current certificate */
- rc = wolfSSL_CertManagerVerifyBuffer(cm, cert_ptr, cert_len + idx,
- WOLFSSL_FILETYPE_ASN1);
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ /* Hash the DER cert and check the verify cache. A hit short-circuits
+ * the public-key signature check; the cert is otherwise treated as if
+ * it had verified normally so the rest of the loop (CA decode, store
+ * load, leaf pubkey extract) continues unchanged. */
+ rc = wc_Sha256Hash_ex(cert_ptr, (word32)(cert_len + idx), certHash,
+ NULL, server->devId);
+ if (rc != 0) {
+ return rc;
+ }
+ hashed = 1;
+ if (wh_Server_CertVerifyCache_Lookup(server, trustedRootNvmId,
+ certHash) == WH_ERROR_OK) {
+ rc = WOLFSSL_SUCCESS;
+ }
+ else
+#endif
+ {
+ /* Verify the current certificate */
+ rc = wolfSSL_CertManagerVerifyBuffer(cm, cert_ptr, cert_len + idx,
+ WOLFSSL_FILETYPE_ASN1);
+ }
/* If this is not the leaf certificate and it's trusted, add it to the
@@ -170,6 +260,15 @@ static int _verifyChainAgainstCmStore(whServerContext* server,
}
}
wc_FreeDecodedCert(&dc);
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ /* Cert verified (or hit) and post-processed successfully.
+ * Record the hash bound to this trusted root so subsequent
+ * verifications under the same root short-circuit. */
+ if (hashed) {
+ wh_Server_CertVerifyCache_Insert(server, trustedRootNvmId,
+ certHash);
+ }
+#endif
}
else {
return rc;
@@ -190,7 +289,13 @@ int wh_Server_CertInit(whServerContext* server)
#ifdef DEBUG_WOLFSSL
wolfSSL_Debugging_ON();
#endif
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ if (server != NULL) {
+ wh_Server_CertVerifyCache_Clear(server);
+ }
+#else
(void)server;
+#endif
return WH_ERROR_OK;
}
@@ -304,6 +409,13 @@ int wh_Server_CertVerify(whServerContext* server, const uint8_t* cert,
return WH_ERROR_ABORTED;
}
+ /* Apply the user-supplied verify callback, if registered. wolfSSL invokes
+ * it during wolfSSL_CertManagerVerifyBuffer; cache hits short-circuit that
+ * path and so deliberately do not invoke the callback. */
+ if (server->cert.verifyCb != NULL) {
+ wolfSSL_CertManagerSetVerify(cm, server->cert.verifyCb);
+ }
+
/* Get the trusted root certificate */
rc = wh_Server_CertReadTrusted(server, trustedRootNvmId, root_cert,
&root_cert_len);
@@ -313,7 +425,8 @@ int wh_Server_CertVerify(whServerContext* server, const uint8_t* cert,
WOLFSSL_FILETYPE_ASN1);
if (rc == WOLFSSL_SUCCESS) {
/* Verify the certificate */
- rc = _verifyChainAgainstCmStore(server, cm, cert, cert_len, flags,
+ rc = _verifyChainAgainstCmStore(server, cm, cert, cert_len,
+ trustedRootNvmId, flags,
cachedKeyFlags, inout_keyId);
if (rc != WH_ERROR_OK) {
rc = WH_ERROR_CERT_VERIFY;
@@ -572,6 +685,23 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic,
*out_resp_size = sizeof(resp);
}; break;
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ case WH_MESSAGE_CERT_ACTION_VERIFY_CACHE_CLEAR: {
+ whMessageCert_SimpleResponse resp = {0};
+
+ rc = WH_SERVER_NVM_LOCK(server);
+ if (rc == WH_ERROR_OK) {
+ wh_Server_CertVerifyCache_Clear(server);
+ (void)WH_SERVER_NVM_UNLOCK(server);
+ }
+ resp.rc = rc;
+
+ wh_MessageCert_TranslateSimpleResponse(
+ magic, &resp, (whMessageCert_SimpleResponse*)resp_packet);
+ *out_resp_size = sizeof(resp);
+ }; break;
+#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */
+
#ifdef WOLFHSM_CFG_DMA
case WH_MESSAGE_CERT_ACTION_ADDTRUSTED_DMA: {
whMessageCert_AddTrustedDmaRequest req = {0};
diff --git a/test/Makefile b/test/Makefile
index 27f25921..8b8d15ba 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -165,6 +165,11 @@ ifeq ($(AUTH),1)
DEF += -DWOLFHSM_CFG_ENABLE_AUTHENTICATION
endif
+# Support trusted-cert verify-result cache
+ifeq ($(CERT_VERIFY_CACHE),1)
+ DEF += -DWOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+endif
+
## Project defines
# Option to build wolfcrypt tests
ifeq ($(TESTWOLFCRYPT),1)
diff --git a/test/wh_test_cert.c b/test/wh_test_cert.c
index ede3a0b4..4d817fea 100644
--- a/test/wh_test_cert.c
+++ b/test/wh_test_cert.c
@@ -121,7 +121,9 @@ int whTest_CertServerCfg(whServerConfig* serverCfg)
server, RAW_CERT_CHAIN_B, RAW_CERT_CHAIN_B_len, rootCertB,
WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL));
- /* attempt to verify invalid chains, should fail */
+ /* attempt to verify invalid chains, should fail. Cache entries are scoped
+ * to the trusted root NVM ID, so prior positive verifies under the true
+ * root cannot bypass these cross-root checks. */
WH_TEST_PRINT("Attempting to verify invalid certificate chains...\n");
WH_TEST_ASSERT_RETURN(WH_ERROR_CERT_VERIFY ==
wh_Server_CertVerify(server, RAW_CERT_CHAIN_A,
@@ -142,6 +144,213 @@ int whTest_CertServerCfg(whServerConfig* serverCfg)
WH_TEST_PRINT("Test completed successfully\n");
return rc;
}
+
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+/* Exercises the trusted-cert verify cache directly through the server API:
+ * - repeat-verify of the same chain under the same root stays successful
+ * - cache entries are bound to the trusted root NVM ID: chain A under root B
+ * must fail even after chain A has been cached by a verify under root A
+ * (regression test against cross-root cache bypass)
+ * - clearing the cache leaves the cross-root case still failing */
+static int whTest_CertServerVerifyCache(whServerConfig* serverCfg)
+{
+ whServerContext server[1] = {0};
+ const whNvmId rootCertA = 1;
+ const whNvmId rootCertB = 2;
+
+ WH_TEST_PRINT("=== Server cert verify-cache test ===\n");
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, serverCfg));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server));
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertAddTrusted(
+ server, rootCertA, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONMODIFIABLE, NULL,
+ 0, ROOT_A_CERT, ROOT_A_CERT_len));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertAddTrusted(
+ server, rootCertB, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONMODIFIABLE, NULL,
+ 0, ROOT_B_CERT, ROOT_B_CERT_len));
+
+ /* 1. Repeat-verify hit: verify chain A twice under root A; both succeed. */
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify(
+ server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA,
+ WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify(
+ server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA,
+ WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL));
+
+ /* 2. Cache is bound to root NVM ID: chain A under root B must fail even
+ * though every cert in chain A was cached under root A by step 1. The
+ * cache hit must not let an unsigned-by-rootB chain through. */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_CERT_VERIFY ==
+ wh_Server_CertVerify(server, RAW_CERT_CHAIN_A,
+ RAW_CERT_CHAIN_A_len, rootCertB,
+ WH_CERT_FLAGS_NONE,
+ WH_NVM_FLAGS_USAGE_ANY, NULL));
+
+ /* 3. Clear: the same cross-root verify still fails cold. */
+ wh_Server_CertVerifyCache_Clear(server);
+ WH_TEST_ASSERT_RETURN(WH_ERROR_CERT_VERIFY ==
+ wh_Server_CertVerify(server, RAW_CERT_CHAIN_A,
+ RAW_CERT_CHAIN_A_len, rootCertB,
+ WH_CERT_FLAGS_NONE,
+ WH_NVM_FLAGS_USAGE_ANY, NULL));
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertEraseTrusted(server, rootCertA));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertEraseTrusted(server, rootCertB));
+ WH_TEST_PRINT("Server cert verify-cache test PASSED\n");
+ return WH_ERROR_OK;
+}
+#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */
+
+/* State for the user-injectable verify callback test */
+static int s_verifyCb_count = 0;
+static int s_verifyCb_lastPreverify = -1;
+static int s_verifyCb_returnVal = 1;
+
+static int whTest_recordingVerifyCb(int preverify,
+ WOLFSSL_X509_STORE_CTX* store)
+{
+ (void)store;
+ s_verifyCb_count++;
+ s_verifyCb_lastPreverify = preverify;
+ return s_verifyCb_returnVal;
+}
+
+/* Exercises the user-injectable verify callback configured through
+ * whServerCertConfig. Confirms:
+ * - the callback is invoked during chain verification with preverify=1
+ * - returning zero from the callback fails the verify
+ * - cache hits bypass the callback (when the verify cache is enabled) */
+static int whTest_CertServerVerifyCallback(whServerConfig* serverCfg)
+{
+ int rc;
+ whServerContext server[1] = {0};
+ whServerCertConfig certCfg = {.verifyCb = whTest_recordingVerifyCb};
+ whServerCertConfig* savedCertConfig;
+ const whNvmId rootCertA = 1;
+
+ WH_TEST_PRINT("=== Server cert verify-callback test ===\n");
+
+ /* Inject our cert config; restore on exit. */
+ savedCertConfig = serverCfg->certConfig;
+ serverCfg->certConfig = &certCfg;
+
+ s_verifyCb_count = 0;
+ s_verifyCb_lastPreverify = -1;
+ s_verifyCb_returnVal = 1;
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, serverCfg));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server));
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertAddTrusted(
+ server, rootCertA, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONMODIFIABLE, NULL,
+ 0, ROOT_A_CERT, ROOT_A_CERT_len));
+
+ /* 1. Callback is invoked on a successful verify with preverify=1. */
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify(
+ server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA,
+ WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL));
+ WH_TEST_ASSERT_RETURN(s_verifyCb_count > 0);
+ WH_TEST_ASSERT_RETURN(s_verifyCb_lastPreverify == 1);
+
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ {
+ /* 2. Cache hit bypasses the callback: re-verifying the same chain
+ * must not increment the call count. */
+ int beforeCount = s_verifyCb_count;
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify(
+ server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA,
+ WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL));
+ WH_TEST_ASSERT_RETURN(s_verifyCb_count == beforeCount);
+
+ /* Clear cache so the next verify re-enters wolfSSL and the cb. */
+ wh_Server_CertVerifyCache_Clear(server);
+ }
+#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */
+
+ /* 3. Returning zero from the callback forces verify failure. */
+ s_verifyCb_returnVal = 0;
+ s_verifyCb_count = 0;
+ WH_TEST_ASSERT_RETURN(WH_ERROR_CERT_VERIFY ==
+ wh_Server_CertVerify(server, RAW_CERT_CHAIN_A,
+ RAW_CERT_CHAIN_A_len, rootCertA,
+ WH_CERT_FLAGS_NONE,
+ WH_NVM_FLAGS_USAGE_ANY, NULL));
+ WH_TEST_ASSERT_RETURN(s_verifyCb_count > 0);
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertEraseTrusted(server, rootCertA));
+ serverCfg->certConfig = savedCertConfig;
+ rc = WH_ERROR_OK;
+ WH_TEST_PRINT("Server cert verify-callback test PASSED\n");
+ return rc;
+}
+
+/* Exercises wh_Server_CertSetVerifyCb: register, replace, and unregister the
+ * verify callback after the server is already initialized (i.e. without
+ * supplying it via whServerCertConfig). */
+static int whTest_CertServerVerifyCallbackRuntime(whServerConfig* serverCfg)
+{
+ int rc;
+ whServerContext server[1] = {0};
+ whServerCertConfig* savedCertConfig;
+ const whNvmId rootCertA = 1;
+
+ WH_TEST_PRINT("=== Server cert verify-callback runtime test ===\n");
+
+ /* Force NULL certConfig so registration must come from the runtime API. */
+ savedCertConfig = serverCfg->certConfig;
+ serverCfg->certConfig = NULL;
+
+ s_verifyCb_count = 0;
+ s_verifyCb_lastPreverify = -1;
+ s_verifyCb_returnVal = 1;
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, serverCfg));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server));
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertAddTrusted(
+ server, rootCertA, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONMODIFIABLE, NULL,
+ 0, ROOT_A_CERT, ROOT_A_CERT_len));
+
+ /* 1. No callback registered: verify succeeds, counter stays 0. */
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify(
+ server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA,
+ WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL));
+ WH_TEST_ASSERT_RETURN(s_verifyCb_count == 0);
+
+ /* 2. Register at runtime; cb must fire on the next cold verify. */
+ WH_TEST_RETURN_ON_FAIL(
+ wh_Server_CertSetVerifyCb(server, whTest_recordingVerifyCb));
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ wh_Server_CertVerifyCache_Clear(server);
+#endif
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify(
+ server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA,
+ WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL));
+ WH_TEST_ASSERT_RETURN(s_verifyCb_count > 0);
+ WH_TEST_ASSERT_RETURN(s_verifyCb_lastPreverify == 1);
+
+ /* 3. Unregister at runtime; verify still succeeds, counter stays 0. */
+ s_verifyCb_count = 0;
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertSetVerifyCb(server, NULL));
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ wh_Server_CertVerifyCache_Clear(server);
+#endif
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify(
+ server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA,
+ WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL));
+ WH_TEST_ASSERT_RETURN(s_verifyCb_count == 0);
+
+ /* 4. NULL server is rejected. */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_Server_CertSetVerifyCb(NULL, NULL));
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_CertEraseTrusted(server, rootCertA));
+ serverCfg->certConfig = savedCertConfig;
+ rc = WH_ERROR_OK;
+ WH_TEST_PRINT("Server cert verify-callback runtime test PASSED\n");
+ return rc;
+}
#endif /* WOLFHSM_CFG_ENABLE_SERVER */
#ifdef WOLFHSM_CFG_ENABLE_CLIENT
@@ -212,7 +421,9 @@ int whTest_CertClient(whClientContext* client)
client, RAW_CERT_CHAIN_B, RAW_CERT_CHAIN_B_len, rootCertB_id, &out_rc));
WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK);
- /* attempt to verify invalid chains, should fail */
+ /* attempt to verify invalid chains, should fail. Cache entries are scoped
+ * to the trusted root NVM ID, so prior positive verifies under the true
+ * root cannot bypass these cross-root checks. */
WH_TEST_PRINT("Attempting to verify invalid certificate chains...\n");
WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerify(
client, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertB_id, &out_rc));
@@ -259,6 +470,64 @@ int whTest_CertClient(whClientContext* client)
/* Test non-exportable flag enforcement */
WH_TEST_RETURN_ON_FAIL(whTest_CertNonExportable(client));
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ /* Verify-cache scenarios over the full client/server RPC: cross-root
+ * recognition, the clear RPC, and re-verify behavior after clear. */
+ {
+ whNvmId rootCertA_id_c = 1;
+ whNvmId rootCertB_id_c = 2;
+
+ WH_TEST_PRINT("=== Client cert verify-cache test ===\n");
+
+ WH_TEST_RETURN_ON_FAIL(
+ wh_Client_CertAddTrusted(client, rootCertA_id_c, WH_NVM_ACCESS_ANY,
+ WH_NVM_FLAGS_NONMODIFIABLE, NULL, 0,
+ ROOT_A_CERT, ROOT_A_CERT_len, &out_rc));
+ WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK);
+ WH_TEST_RETURN_ON_FAIL(
+ wh_Client_CertAddTrusted(client, rootCertB_id_c, WH_NVM_ACCESS_ANY,
+ WH_NVM_FLAGS_NONMODIFIABLE, NULL, 0,
+ ROOT_B_CERT, ROOT_B_CERT_len, &out_rc));
+ WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK);
+
+ /* Start from a known-empty cache. */
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerifyCacheClear(client, &out_rc));
+ WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK);
+
+ /* Warm the cache by verifying chain A under its true root. */
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerify(client, RAW_CERT_CHAIN_A,
+ RAW_CERT_CHAIN_A_len,
+ rootCertA_id_c, &out_rc));
+ WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK);
+
+ /* Cache entries are bound to the trusted root NVM ID: chain A under
+ * root B fails even though every cert in chain A is cached under
+ * root A. The cache hit must not bypass the cross-root check. */
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerify(client, RAW_CERT_CHAIN_A,
+ RAW_CERT_CHAIN_A_len,
+ rootCertB_id_c, &out_rc));
+ WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_CERT_VERIFY);
+
+ /* After clear, the cross-root verify still fails cold. Exercises the
+ * clear RPC path. */
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerifyCacheClear(client, &out_rc));
+ WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK);
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerify(client, RAW_CERT_CHAIN_A,
+ RAW_CERT_CHAIN_A_len,
+ rootCertB_id_c, &out_rc));
+ WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_CERT_VERIFY);
+
+ /* Cleanup */
+ WH_TEST_RETURN_ON_FAIL(
+ wh_Client_CertEraseTrusted(client, rootCertA_id_c, &out_rc));
+ WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK);
+ WH_TEST_RETURN_ON_FAIL(
+ wh_Client_CertEraseTrusted(client, rootCertB_id_c, &out_rc));
+ WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK);
+ WH_TEST_PRINT("Client cert verify-cache test PASSED\n");
+ }
+#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */
+
WH_TEST_PRINT("Certificate client test completed successfully\n");
return rc;
@@ -397,7 +666,9 @@ int whTest_CertClientDma_ClientServerTestInternal(whClientContext* client)
client, RAW_CERT_CHAIN_B, RAW_CERT_CHAIN_B_len, rootCertB_id, &out_rc));
WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK);
- /* attempt to verify invalid chains, should fail */
+ /* attempt to verify invalid chains, should fail. Cache entries are scoped
+ * to the trusted root NVM ID, so prior positive verifies under the true
+ * root cannot bypass these cross-root checks. */
WH_TEST_PRINT("Attempting to verify invalid certificate chains...\n");
WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerifyDma(
client, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertB_id, &out_rc));
@@ -651,6 +922,30 @@ int whTest_CertRamSim(whTestNvmBackendType nvmType)
WH_ERROR_PRINT("Certificate server config tests failed: %d\n", rc);
}
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ if (rc == WH_ERROR_OK) {
+ rc = whTest_CertServerVerifyCache(s_conf);
+ if (rc != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Cert verify-cache tests failed: %d\n", rc);
+ }
+ }
+#endif
+
+ if (rc == WH_ERROR_OK) {
+ rc = whTest_CertServerVerifyCallback(s_conf);
+ if (rc != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Cert verify-callback tests failed: %d\n", rc);
+ }
+ }
+
+ if (rc == WH_ERROR_OK) {
+ rc = whTest_CertServerVerifyCallbackRuntime(s_conf);
+ if (rc != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Cert verify-callback runtime tests failed: %d\n",
+ rc);
+ }
+ }
+
/* Cleanup NVM */
wh_Nvm_Cleanup(nvm);
#ifndef WOLFHSM_CFG_NO_CRYPTO
diff --git a/wolfhsm/wh_client.h b/wolfhsm/wh_client.h
index a71a788c..1cecfab3 100644
--- a/wolfhsm/wh_client.h
+++ b/wolfhsm/wh_client.h
@@ -2571,6 +2571,39 @@ int wh_Client_CertVerifyAndCacheLeafPubKey(
int32_t* out_rc);
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+/**
+ * @brief Send a request to clear the server's trusted certificate verify cache.
+ *
+ * Subsequent verification of any certificate will re-run the public-key
+ * signature check until that cert is verified again and re-cached.
+ *
+ * @param[in] c Pointer to the client context.
+ * @return int Returns 0 on success, or a negative error code on failure.
+ */
+int wh_Client_CertVerifyCacheClearRequest(whClientContext* c);
+
+/**
+ * @brief Receive the response to a verify-cache clear request.
+ *
+ * @param[in] c Pointer to the client context.
+ * @param[out] out_rc Pointer to store the response code from the server.
+ * @return int Returns 0 on success, or a negative error code on failure.
+ */
+int wh_Client_CertVerifyCacheClearResponse(whClientContext* c, int32_t* out_rc);
+
+/**
+ * @brief Synchronous helper to clear the server's trusted certificate verify
+ * cache.
+ *
+ * @param[in] c Pointer to the client context.
+ * @param[out] out_rc Pointer to store the response code from the server.
+ * @return int Returns 0 on success, or a negative error code on failure.
+ */
+int wh_Client_CertVerifyCacheClear(whClientContext* c, int32_t* out_rc);
+#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */
+
+
#ifdef WOLFHSM_CFG_DMA
/**
diff --git a/wolfhsm/wh_message_cert.h b/wolfhsm/wh_message_cert.h
index c2370bd3..5c376a07 100644
--- a/wolfhsm/wh_message_cert.h
+++ b/wolfhsm/wh_message_cert.h
@@ -34,16 +34,17 @@
#include "wolfhsm/wh_nvm.h"
enum WH_MESSAGE_CERT_ACTION_ENUM {
- WH_MESSAGE_CERT_ACTION_INIT = 0x1,
- WH_MESSAGE_CERT_ACTION_ADDTRUSTED = 0x2,
- WH_MESSAGE_CERT_ACTION_ERASETRUSTED = 0x3,
- WH_MESSAGE_CERT_ACTION_READTRUSTED = 0x4,
- WH_MESSAGE_CERT_ACTION_VERIFY = 0x5,
- WH_MESSAGE_CERT_ACTION_ADDTRUSTED_DMA = 0x22,
- WH_MESSAGE_CERT_ACTION_READTRUSTED_DMA = 0x24,
- WH_MESSAGE_CERT_ACTION_VERIFY_DMA = 0x25,
- WH_MESSAGE_CERT_ACTION_VERIFY_ACERT = 0x26,
- WH_MESSAGE_CERT_ACTION_VERIFY_ACERT_DMA = 0x27,
+ WH_MESSAGE_CERT_ACTION_INIT = 0x1,
+ WH_MESSAGE_CERT_ACTION_ADDTRUSTED = 0x2,
+ WH_MESSAGE_CERT_ACTION_ERASETRUSTED = 0x3,
+ WH_MESSAGE_CERT_ACTION_READTRUSTED = 0x4,
+ WH_MESSAGE_CERT_ACTION_VERIFY = 0x5,
+ WH_MESSAGE_CERT_ACTION_VERIFY_CACHE_CLEAR = 0x6,
+ WH_MESSAGE_CERT_ACTION_ADDTRUSTED_DMA = 0x22,
+ WH_MESSAGE_CERT_ACTION_READTRUSTED_DMA = 0x24,
+ WH_MESSAGE_CERT_ACTION_VERIFY_DMA = 0x25,
+ WH_MESSAGE_CERT_ACTION_VERIFY_ACERT = 0x26,
+ WH_MESSAGE_CERT_ACTION_VERIFY_ACERT_DMA = 0x27,
};
/* Simple reusable response message */
diff --git a/wolfhsm/wh_server.h b/wolfhsm/wh_server.h
index f57cf2d7..e332434a 100644
--- a/wolfhsm/wh_server.h
+++ b/wolfhsm/wh_server.h
@@ -39,6 +39,7 @@ typedef struct whServerContext_t whServerContext;
#include "wolfhsm/wh_common.h"
#include "wolfhsm/wh_comm.h"
#include "wolfhsm/wh_keycache.h"
+#include "wolfhsm/wh_server_cert_cache.h"
#include "wolfhsm/wh_nvm.h"
#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION
#include "wolfhsm/wh_auth.h"
@@ -160,6 +161,9 @@ typedef struct whServerConfig_t {
#ifdef WOLFHSM_CFG_LOGGING
whLogConfig* logConfig;
#endif /* WOLFHSM_CFG_LOGGING */
+#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER) && !defined(WOLFHSM_CFG_NO_CRYPTO)
+ whServerCertConfig* certConfig; /* optional; NULL = no verify callback */
+#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO */
} whServerConfig;
@@ -186,6 +190,9 @@ struct whServerContext_t {
#ifdef WOLFHSM_CFG_LOGGING
whLogContext log;
#endif /* WOLFHSM_CFG_LOGGING */
+#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER) && !defined(WOLFHSM_CFG_NO_CRYPTO)
+ whServerCertContext cert; /* verify callback + verify cache */
+#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO */
};
diff --git a/wolfhsm/wh_server_cert.h b/wolfhsm/wh_server_cert.h
index 0d0be42b..6914d2e0 100644
--- a/wolfhsm/wh_server_cert.h
+++ b/wolfhsm/wh_server_cert.h
@@ -95,6 +95,26 @@ int wh_Server_CertVerify(whServerContext* server, const uint8_t* cert,
whCertFlags flags, whNvmFlags cachedKeyFlags,
whKeyId* inout_keyId);
+#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER) && !defined(WOLFHSM_CFG_NO_CRYPTO)
+/**
+ * @brief Register a verify callback at runtime.
+ *
+ * Replaces the callback previously set via whServerCertConfig.verifyCb (or by
+ * a prior call to this function). Pass NULL to unregister.
+ *
+ * The callback is applied to the per-request WOLFSSL_CERT_MANAGER created by
+ * wh_Server_CertVerify, so it participates in chain verification the same way
+ * a callback registered with wolfSSL_CertManagerSetVerify would. Verify-cache
+ * hits (when WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE is enabled) bypass the
+ * callback because they bypass wolfSSL's verify path entirely.
+ *
+ * @param server The server context.
+ * @param cb The callback to register, or NULL to unregister.
+ * @return WH_ERROR_OK on success, WH_ERROR_BADARGS if server is NULL.
+ */
+int wh_Server_CertSetVerifyCb(whServerContext* server, VerifyCallback cb);
+#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO */
+
#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT)
/**
* @brief Verifies an attribute certificate against a trusted root certificate
diff --git a/wolfhsm/wh_server_cert_cache.h b/wolfhsm/wh_server_cert_cache.h
new file mode 100644
index 00000000..af18a017
--- /dev/null
+++ b/wolfhsm/wh_server_cert_cache.h
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2025 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_server_cert_cache.h
+ *
+ * Server-side cert subsystem types embedded in whServerContext:
+ * - whServerCertContext / whServerCertConfig: hold the user-injectable
+ * verify callback and (optionally) the trusted-cert verify cache.
+ * - whCertVerifyCacheContext: trusted-cert verify-result cache. Records
+ * SHA-256 hashes of DER-encoded certificates that have already been
+ * successfully verified under a specific trusted-root NVM ID, so
+ * subsequent verifications encountering the same cert under the same
+ * root can short-circuit the public-key signature check. Hits apply
+ * across clients but are scoped to the trusted root that anchored
+ * the prior verify; a cert cached under root A will not match a
+ * lookup under root B.
+ *
+ * Lives in its own header to avoid circular dependencies between wh_server.h
+ * and wh_server_cert.h.
+ */
+
+#ifndef WOLFHSM_WH_SERVER_CERT_CACHE_H_
+#define WOLFHSM_WH_SERVER_CERT_CACHE_H_
+
+/* Pick up compile-time configuration */
+#include "wolfhsm/wh_settings.h"
+
+#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER) && !defined(WOLFHSM_CFG_NO_CRYPTO)
+
+#include
+
+#include "wolfhsm/wh_common.h" /* for whNvmId */
+
+#include "wolfssl/ssl.h" /* for VerifyCallback */
+
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+
+#ifndef WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT
+#define WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT 16
+#endif
+
+#define WH_CERT_VERIFY_CACHE_HASH_LEN 32 /* SHA-256 digest size */
+
+typedef struct whCertVerifyCacheSlot {
+ uint8_t committed; /* 0 = empty, 1 = valid */
+ uint8_t WH_PAD[1];
+ whNvmId rootNvmId; /* trusted root NVM ID this entry is bound to */
+ uint8_t hash[WH_CERT_VERIFY_CACHE_HASH_LEN];
+} whCertVerifyCacheSlot;
+
+typedef struct whCertVerifyCacheContext {
+ whCertVerifyCacheSlot slots[WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT];
+ uint16_t writeIdx; /* FIFO ring write position */
+ uint8_t WH_PAD[6];
+} whCertVerifyCacheContext;
+
+#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */
+
+/* Per-server cert subsystem config, supplied via whServerConfig.certConfig.
+ * The verify callback signature matches wolfSSL's VerifyCallback so the same
+ * callback registered with wolfSSL_CertManagerSetVerify can be used here. */
+typedef struct {
+ VerifyCallback verifyCb; /* user-supplied; NULL = no callback */
+} whServerCertConfig;
+
+/* Per-server cert subsystem context, embedded by value in whServerContext.
+ * Holds the registered verify callback and (optionally) the verify cache. */
+typedef struct {
+ VerifyCallback verifyCb;
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+ whCertVerifyCacheContext cache;
+#endif
+} whServerCertContext;
+
+/* Forward declaration to avoid pulling in wh_server.h */
+struct whServerContext_t;
+
+#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE
+/**
+ * @brief Look up a cert hash in the verify cache, scoped to a trusted root.
+ * @param server The server context.
+ * @param rootNvmId NVM ID of the trusted root that must have anchored a prior
+ * successful verify of this cert.
+ * @param hash Pointer to a SHA-256 (32-byte) digest of the DER cert.
+ * @return WH_ERROR_OK if the (rootNvmId, hash) pair is present,
+ * WH_ERROR_NOTFOUND if absent, WH_ERROR_BADARGS on invalid arguments.
+ */
+int wh_Server_CertVerifyCache_Lookup(struct whServerContext_t* server,
+ whNvmId rootNvmId, const uint8_t* hash);
+
+/**
+ * @brief Insert a cert hash into the verify cache, bound to a trusted root.
+ * No-op if the (rootNvmId, hash) pair is already present.
+ * Uses FIFO ring overwrite when full.
+ * @param server The server context.
+ * @param rootNvmId NVM ID of the trusted root that anchored the verify.
+ * @param hash Pointer to a SHA-256 (32-byte) digest of the DER cert.
+ */
+void wh_Server_CertVerifyCache_Insert(struct whServerContext_t* server,
+ whNvmId rootNvmId, const uint8_t* hash);
+
+/**
+ * @brief Clear all entries from the verify cache.
+ * @param server The server context.
+ */
+void wh_Server_CertVerifyCache_Clear(struct whServerContext_t* server);
+#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */
+
+#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO */
+
+#endif /* !WOLFHSM_WH_SERVER_CERT_CACHE_H_ */