Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/wh_nvm.c
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,8 @@ int wh_Nvm_DestroyObjectsChecked(whNvmContext* context, whNvmId list_count,

for (i = 0; i < list_count; i++) {
ret = wh_Nvm_CheckPolicy(context, WH_NVM_OP_DESTROY, id_list[i], NULL);
if (ret != WH_ERROR_OK) {
/* An absent id has no policy to enforce and is not an error */
if ((ret != WH_ERROR_OK) && (ret != WH_ERROR_NOTFOUND)) {
return ret;
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/wh_nvm_flash.c
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,7 @@ int wh_NvmFlash_DestroyObjects(void* c, whNvmId list_count,
int entry = 0;
int src_part = 0;
int dest_part = 0;
int any_marked = 0;
uint32_t dest_object = 0;
uint32_t dest_data = 0;

Expand Down Expand Up @@ -1173,10 +1174,18 @@ int wh_NvmFlash_DestroyObjects(void* c, whNvmId list_count,
&entry);
if ((ret == 0) && (entry >= 0)) {
d->objects[entry].state.status = NF_STATUS_DATA_BAD;
any_marked = 1;
}
} while (entry >= 0);
}

/* Nothing matched a non-empty list: replicating would just rewrite the
* partition unchanged, so skip the flash wear. A zero list_count is a
* compaction request and must still replicate. */
if ((list_count > 0) && (any_marked == 0)) {
return WH_ERROR_OK;
}

/* Blank check the inactive partition and erase if not blank */
ret = nfPartition_BlankCheck(context, dest_part);
if (ret == WH_ERROR_NOTBLANK) {
Expand Down
14 changes: 9 additions & 5 deletions src/wh_server_keystore.c
Original file line number Diff line number Diff line change
Expand Up @@ -1275,16 +1275,20 @@ int wh_Server_KeystoreEraseKeyChecked(whServerContext* server, whNvmId keyId)
}
#endif /* WOLFHSM_CFG_HWKEYSTORE */

/* remove the key from the cache if present, enforcing policy */
/* NOTFOUND means the key was not cached, whether it is absent entirely or
* lives only in NVM; both are fine. Any other error must not be masked by
* the destroy below. */
ret = wh_Server_KeystoreEvictKeyChecked(server, keyId);
if ((ret != WH_ERROR_OK) && (ret != WH_ERROR_NOTFOUND)) {
return ret;
}

/* With no NVM, the cache eviction above is the whole erase; return its
* result so policy and not-found errors still propagate. */
/* Nothing left to destroy is a successful erase, matching
* wh_Server_KeystoreEraseKey */
if (server->nvm == NULL) {
return ret;
return WH_ERROR_OK;
}

/* destroy the object */
return wh_Nvm_DestroyObjectsChecked(server->nvm, 1, &keyId);
}

Expand Down
8 changes: 4 additions & 4 deletions test-refactor/server/wh_test_nvm_optional.c
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,10 @@ static int _RunNvmOptionalChecks(whServerContext* server)
WH_ERROR_NOTFOUND ==
wh_Server_KeystoreReadKey(server, localId, outMeta, outKey, &outSz));

/* EraseKeyChecked enforces policy and, with no NVM, returns the cache
* eviction's status. A missing key reports NOTFOUND -- unlike the
* non-checked EraseKey above, which treats "nothing to destroy" as OK. */
WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND ==
/* EraseKeyChecked enforces policy but, like the non-checked EraseKey
* above, treats "nothing to erase" as OK. This holds whether or not NVM
* is attached. */
WH_TEST_ASSERT_RETURN(WH_ERROR_OK ==
wh_Server_KeystoreEraseKeyChecked(server, missingId));

/* On a primed, policy-permissive key, EraseKeyChecked succeeds cache-only
Expand Down
230 changes: 230 additions & 0 deletions test-refactor/server/wh_test_nvm_policy.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/*
* test-refactor/server/wh_test_nvm_policy.c
*
* Server-side test that the policy-checked keystore APIs report denials
* correctly when NVM is attached. Complements wh_test_nvm_optional.c, which
* covers the same APIs with the NVM detached.
*/

#include "wolfhsm/wh_settings.h"

#include <stdint.h>
#include <string.h>

#include "wolfhsm/wh_error.h"
#include "wolfhsm/wh_common.h"
#include "wolfhsm/wh_nvm.h"
#include "wolfhsm/wh_server.h"
#include "wolfhsm/wh_server_keystore.h"

#include "wh_test_common.h"
#include "wh_test_list.h"

#if defined(WOLFHSM_CFG_ENABLE_SERVER) && !defined(WOLFHSM_CFG_NO_CRYPTO)

#define WH_TEST_NVMPOL_KEYLEN (32)

/* A destroy batch naming an absent id must still destroy the present ones */
static int _whTest_NvmPolicyDestroyBatchMissingId(whServerContext* server)
{
whNvmMetadata meta[1];
whNvmMetadata outMeta[1];
uint8_t data[] = {0xA1, 0xB2, 0xC3, 0xD4};
whNvmId present = 0x0310;
whNvmId absent = 0x0311;
whNvmId list[2];

list[0] = absent;
list[1] = present;

memset(meta, 0, sizeof(meta));
meta->id = present;
meta->len = (whNvmSize)sizeof(data);
meta->access = WH_NVM_ACCESS_ANY;
WH_TEST_ASSERT_RETURN(
WH_ERROR_OK ==
wh_Nvm_AddObjectChecked(server->nvm, meta, sizeof(data), data));

WH_TEST_ASSERT_RETURN(
WH_ERROR_OK == wh_Nvm_DestroyObjectsChecked(server->nvm, 2, list));

WH_TEST_ASSERT_RETURN(
WH_ERROR_NOTFOUND ==
wh_Nvm_GetMetadata(server->nvm, present, outMeta));

return WH_ERROR_OK;
}

/* Destroying a list whose ids are all absent must not replicate the partition.
* Reclaimable space is the observable proof, since a replication compacts it
* away. */
static int _whTest_NvmPolicyDestroyAllAbsentNoChurn(whServerContext* server)
{
whNvmMetadata meta[1];
uint8_t data[] = {0x5A, 0x6B, 0x7C, 0x8D};
whNvmId id = 0x0320;
whNvmId absent[2];
uint32_t availSize;
uint32_t reclaimBefore;
uint32_t reclaimAfter;
whNvmId availObjs;
whNvmId reclaimObjsBefore;
whNvmId reclaimObjsAfter;

absent[0] = 0x0321;
absent[1] = 0x0322;

/* Overwrite the object so the old copy becomes reclaimable */
memset(meta, 0, sizeof(meta));
meta->id = id;
meta->len = (whNvmSize)sizeof(data);
meta->access = WH_NVM_ACCESS_ANY;
WH_TEST_ASSERT_RETURN(
WH_ERROR_OK ==
wh_Nvm_AddObjectChecked(server->nvm, meta, sizeof(data), data));
WH_TEST_ASSERT_RETURN(
WH_ERROR_OK ==
wh_Nvm_AddObjectChecked(server->nvm, meta, sizeof(data), data));

WH_TEST_ASSERT_RETURN(
WH_ERROR_OK ==
wh_Nvm_GetAvailable(server->nvm, &availSize, &availObjs,
&reclaimBefore, &reclaimObjsBefore));
if (reclaimObjsBefore == 0) {
/* Backend exposes no reclaimable space, so no-churn is not observable
* here; pass without claiming it was verified. */
(void)wh_Nvm_DestroyObjectsChecked(server->nvm, 1, &id);
WH_TEST_PRINT(" no-churn NOT verified on this backend "
"(no reclaimable space to observe)\n");
return WH_ERROR_OK;
}

WH_TEST_ASSERT_RETURN(
WH_ERROR_OK == wh_Nvm_DestroyObjectsChecked(server->nvm, 2, absent));

/* Untouched reclaimable space proves the backend was never called */
WH_TEST_ASSERT_RETURN(
WH_ERROR_OK ==
wh_Nvm_GetAvailable(server->nvm, &availSize, &availObjs, &reclaimAfter,
&reclaimObjsAfter));
WH_TEST_ASSERT_RETURN(reclaimObjsAfter == reclaimObjsBefore);
WH_TEST_ASSERT_RETURN(reclaimAfter == reclaimBefore);

/* Zero-count compaction, by contrast, does reach the backend and clears
* reclaimable space, pinning the guard that lets it through */
WH_TEST_ASSERT_RETURN(
WH_ERROR_OK == wh_Nvm_DestroyObjectsChecked(server->nvm, 0, NULL));
WH_TEST_ASSERT_RETURN(
WH_ERROR_OK ==
wh_Nvm_GetAvailable(server->nvm, &availSize, &availObjs, &reclaimAfter,
&reclaimObjsAfter));
WH_TEST_ASSERT_RETURN(reclaimObjsAfter == 0);

/* Clean up the surviving object */
WH_TEST_ASSERT_RETURN(
WH_ERROR_OK == wh_Nvm_DestroyObjectsChecked(server->nvm, 1, &id));

return WH_ERROR_OK;
}

/* Erasing a key that is absent from both cache and NVM is a successful erase,
* matching the non-checked wh_Server_KeystoreEraseKey. */
static int _whTest_NvmPolicyMissingKeyEraseSucceeds(whServerContext* server)
{
whKeyId missingId;

missingId = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, WH_TEST_DEFAULT_CLIENT_ID,
0x32);

WH_TEST_ASSERT_RETURN(WH_ERROR_OK ==
wh_Server_KeystoreEraseKeyChecked(server, missingId));
WH_TEST_ASSERT_RETURN(WH_ERROR_OK ==
wh_Server_KeystoreEraseKey(server, missingId));

return WH_ERROR_OK;
}

/* A revoked key that was never committed lives only in the cache, so the NVM
* destroy step finds nothing. The erase must still report the cache eviction's
* policy denial rather than the NVM layer's "nothing to destroy" success. */
static int _whTest_NvmPolicyRevokedCacheOnlyEraseDenied(whServerContext* server)
{
whNvmMetadata meta[1];
whNvmMetadata outMeta[1];
uint8_t keyData[WH_TEST_NVMPOL_KEYLEN];
uint8_t outKey[WH_TEST_NVMPOL_KEYLEN];
uint32_t outSz;
whKeyId revokeId;
int i;

for (i = 0; i < (int)sizeof(keyData); i++) {
keyData[i] = (uint8_t)(i + 1);
}

revokeId = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, WH_TEST_DEFAULT_CLIENT_ID,
0x31);

/* Cache the key without committing it, so it never reaches NVM */
memset(meta, 0, sizeof(meta));
meta->id = revokeId;
meta->len = (whNvmSize)sizeof(keyData);
meta->flags = WH_NVM_FLAGS_USAGE_ANY;
meta->access = WH_NVM_ACCESS_ANY;
WH_TEST_ASSERT_RETURN(WH_ERROR_OK ==
wh_Server_KeystoreCacheKey(server, meta, keyData));

/* Revoking marks the cached copy NONMODIFIABLE; with the key absent from
* NVM there is nothing to commit. */
WH_TEST_ASSERT_RETURN(WH_ERROR_OK ==
wh_Server_KeystoreRevokeKey(server, revokeId));

/* The denial must survive, even though the NVM destroy would succeed */
WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS ==
wh_Server_KeystoreEraseKeyChecked(server, revokeId));

/* Nothing was erased: the key is still readable */
outSz = sizeof(outKey);
WH_TEST_ASSERT_RETURN(
WH_ERROR_OK ==
wh_Server_KeystoreReadKey(server, revokeId, outMeta, outKey, &outSz));
WH_TEST_ASSERT_RETURN(outSz == (uint32_t)sizeof(keyData));

/* Force-remove the revoked key (EraseKeyChecked could not) */
(void)wh_Server_KeystoreEvictKey(server, revokeId);

return WH_ERROR_OK;
}

int whTest_NvmPolicyChecked(whServerContext* ctx)
{
if (ctx == NULL) {
return WH_ERROR_BADARGS;
}

WH_TEST_RETURN_ON_FAIL(_whTest_NvmPolicyDestroyBatchMissingId(ctx));
WH_TEST_RETURN_ON_FAIL(_whTest_NvmPolicyDestroyAllAbsentNoChurn(ctx));
WH_TEST_RETURN_ON_FAIL(_whTest_NvmPolicyMissingKeyEraseSucceeds(ctx));
WH_TEST_RETURN_ON_FAIL(_whTest_NvmPolicyRevokedCacheOnlyEraseDenied(ctx));

return WH_ERROR_OK;
}

#endif /* WOLFHSM_CFG_ENABLE_SERVER && !WOLFHSM_CFG_NO_CRYPTO */
2 changes: 2 additions & 0 deletions test-refactor/wh_test_list.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ WH_TEST_DECL(whTest_CertReadRejectsServerOnly);
WH_TEST_DECL(whTest_HwKeystoreServer);
WH_TEST_DECL(whTest_ServerImgMgr);
WH_TEST_DECL(whTest_NvmOptional);
WH_TEST_DECL(whTest_NvmPolicyChecked);
WH_TEST_DECL(whTest_ClientCerts);
WH_TEST_DECL(whTest_Counter);
WH_TEST_DECL(whTest_Crypto_Aes);
Expand Down Expand Up @@ -116,6 +117,7 @@ const whTestCase whTestsServer[] = {
{ "whTest_CertReadRejectsServerOnly", whTest_CertReadRejectsServerOnly },
{ "whTest_ServerImgMgr", whTest_ServerImgMgr },
{ "whTest_NvmOptional", whTest_NvmOptional },
{ "whTest_NvmPolicyChecked", whTest_NvmPolicyChecked },
{ "whTest_SheMasterEcuKeyFallback", whTest_SheMasterEcuKeyFallback },
{ "whTest_SheReqSizeChecking", whTest_SheReqSizeChecking },
{ "whTest_HwKeystoreServer", whTest_HwKeystoreServer },
Expand Down
Loading
Loading