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_ */