-
Notifications
You must be signed in to change notification settings - Fork 39
Enforce NONMODIFIABLE and TRUSTED NVM policy on keystore key commit #492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -245,8 +245,28 @@ static int _KeystoreCheckPolicy(whServerContext* server, whKsOp op, | |
| break; | ||
|
|
||
| case WH_KS_OP_COMMIT: | ||
|
Frauschi marked this conversation as resolved.
Frauschi marked this conversation as resolved.
Frauschi marked this conversation as resolved.
Frauschi marked this conversation as resolved.
|
||
| /* Stored flags decide, not cached, so an unchecked cache path | ||
| * cannot launder them; same pair as wh_Nvm_AddObjectChecked. | ||
| * Fetched here too, so cache residency cannot change the verdict. */ | ||
| if (!foundInNvm && (server->nvm != NULL)) { | ||
| ret = wh_Nvm_GetMetadata(server->nvm, keyId, &nvmMeta); | ||
| if (ret == WH_ERROR_OK) { | ||
| foundInNvm = 1; | ||
| } | ||
| else if (ret != WH_ERROR_NOTFOUND) { | ||
| /* Unreadable flags cannot be enforced: deny. */ | ||
| return ret; | ||
| } | ||
| } | ||
| if (foundInNvm && | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [Medium] No test covers the TRUSTED half of the new commit mask - the actual KEK-overwrite scenario The new branch denies commit when the stored object carries The It cannot be tested through Suggestion: Add to test-refactor/server/ (sketch): /* Stored object is TRUSTED; cache slot claims it is not. */
meta.id = kekId; meta.flags = WH_NVM_FLAGS_TRUSTED; meta.len = sizeof(kek);
WH_TEST_RETURN_ON_FAIL(wh_Nvm_AddObject(nvm, &meta, sizeof(kek), kek));
meta.flags = WH_NVM_FLAGS_NONE;
WH_TEST_RETURN_ON_FAIL(wh_Server_KeystoreCacheKey(server, &meta, attacker));
WH_TEST_ASSERT_RETURN(
wh_Server_KeystoreCommitKeyChecked(server, kekId) == WH_ERROR_ACCESS);
/* stored bytes must still be the KEK */ |
||
| (nvmMeta.flags & | ||
| (WH_NVM_FLAGS_NONMODIFIABLE | WH_NVM_FLAGS_TRUSTED))) { | ||
| return WH_ERROR_ACCESS; | ||
| } | ||
| break; | ||
|
|
||
| case WH_KS_OP_REVOKE: | ||
| /* Always allowed */ | ||
| /* Revocation only tightens policy */ | ||
| break; | ||
| default: | ||
| /* unknown operation */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -839,13 +839,149 @@ static int _whTest_NonExportableKeystore(whClientContext* ctx) | |
| return 0; | ||
| } | ||
|
|
||
| #if defined(WOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS) | ||
|
Frauschi marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [Medium] Deny-path test could live in test-refactor/server/ and avoid the persistence gate and the new CI job entirely Driving the test only through A test in The client-driven test is still valuable as an end-to-end check of the client-visible error code; the point is that it should not be the only coverage. Suggestion: Move the core deny assertions into a |
||
| /* Committing a NONMODIFIABLE key leaves an object that | ||
| * wh_Nvm_DestroyObjectsChecked refuses to erase, so it occupies one NVM | ||
| * slot for the rest of the run. Gated like the keypolicy revocation test. */ | ||
| static int _whTest_NonModifiableCommit(whClientContext* ctx) | ||
| { | ||
| int ret = 0; | ||
| whKeyId keyId = WH_KEYID_ERASED; | ||
| uint8_t key[WH_TEST_KEYSTORE_TEST_SZ] = { | ||
| 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, | ||
| 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, | ||
| 0x32, 0x10, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10}; | ||
| uint8_t exportedKey[WH_TEST_KEYSTORE_TEST_SZ] = {0}; | ||
| uint8_t label[WH_NVM_LABEL_LEN] = "NonModifiableCommitKey"; | ||
| uint8_t exportedLabel[WH_NVM_LABEL_LEN] = {0}; | ||
| uint16_t exportedKeySize; | ||
|
|
||
| WH_TEST_PRINT("Testing non-modifiable commit enforcement...\n"); | ||
|
|
||
| /* Test 1: first commit of a NONMODIFIABLE key stores it, and the commit | ||
| * leaves the slot cached, so a repeat commit is an overwrite attempt. */ | ||
| ret = wh_Client_KeyCache(ctx, WH_NVM_FLAGS_NONMODIFIABLE, label, | ||
| sizeof(label), key, sizeof(key), &keyId); | ||
| if (ret != 0) { | ||
| WH_ERROR_PRINT("Failed to cache non-modifiable key: %d\n", ret); | ||
| return ret; | ||
| } | ||
|
|
||
| ret = wh_Client_KeyCommit(ctx, keyId); | ||
| if (ret != 0) { | ||
| WH_ERROR_PRINT("Failed first commit of non-modifiable key: %d\n", ret); | ||
| return ret; | ||
| } | ||
|
|
||
| /* Test 2: re-committing over the stored non-modifiable object is denied */ | ||
| ret = wh_Client_KeyCommit(ctx, keyId); | ||
| if (ret != WH_ERROR_ACCESS) { | ||
| WH_ERROR_PRINT("Non-modifiable key was re-committed unexpectedly: %d\n", | ||
| ret); | ||
| return -1; | ||
| } | ||
|
|
||
| WH_TEST_DEBUG_PRINT("Non-modifiable key re-commit correctly denied\n"); | ||
|
|
||
| /* Test 3: the denial left the stored object intact. Evicting is allowed | ||
| * because the key is committed, so the export below must freshen it back | ||
| * out of NVM rather than read the surviving cache slot. */ | ||
| ret = wh_Client_KeyEvict(ctx, keyId); | ||
| if (ret != 0) { | ||
| WH_ERROR_PRINT("Failed to evict committed non-modifiable key: %d\n", | ||
| ret); | ||
| return ret; | ||
| } | ||
|
|
||
| exportedKeySize = sizeof(exportedKey); | ||
| ret = wh_Client_KeyExport(ctx, keyId, exportedLabel, sizeof(exportedLabel), | ||
| exportedKey, &exportedKeySize); | ||
| if (ret != 0) { | ||
| WH_ERROR_PRINT("Failed to export stored non-modifiable key: %d\n", ret); | ||
| return ret; | ||
| } | ||
|
|
||
| if (exportedKeySize != sizeof(key) || | ||
| memcmp(key, exportedKey, exportedKeySize) != 0 || | ||
| memcmp(label, exportedLabel, sizeof(label)) != 0) { | ||
| WH_ERROR_PRINT("Denied commit altered the stored key\n"); | ||
| return -1; | ||
| } | ||
|
|
||
| WH_TEST_DEBUG_PRINT("Stored non-modifiable key unchanged after denial\n"); | ||
|
|
||
| /* The key cannot be erased: wh_Nvm_DestroyObjectsChecked refuses a | ||
|
Frauschi marked this conversation as resolved.
|
||
| * NONMODIFIABLE object, so only the cache slot is reclaimed here. */ | ||
| (void)wh_Client_KeyEvict(ctx, keyId); | ||
|
|
||
| /* Test 4: the denial does not depend on cache residency. With no slot | ||
| * left, the stored flags still decide, so commit reports ACCESS rather | ||
| * than the NOTFOUND raised by the missing slot. */ | ||
| ret = wh_Client_KeyCommit(ctx, keyId); | ||
| if (ret != WH_ERROR_ACCESS) { | ||
| WH_ERROR_PRINT("Uncached non-modifiable commit not denied: %d\n", ret); | ||
| return -1; | ||
| } | ||
|
|
||
| WH_TEST_DEBUG_PRINT("Uncached non-modifiable commit correctly denied\n"); | ||
|
|
||
| WH_TEST_PRINT("NON-MODIFIABLE COMMIT TEST SUCCESS\n"); | ||
| return 0; | ||
| } | ||
| #endif /* WOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS */ | ||
|
|
||
| static int _whTest_ModifiableRecommit(whClientContext* ctx) | ||
| { | ||
| int ret = 0; | ||
| whKeyId keyId = WH_KEYID_ERASED; | ||
| uint8_t key[WH_TEST_KEYSTORE_TEST_SZ] = { | ||
| 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, | ||
| 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, | ||
| 0x32, 0x10, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10}; | ||
| uint8_t label[WH_NVM_LABEL_LEN] = "ModifiableCommitKey"; | ||
|
|
||
| WH_TEST_PRINT("Testing modifiable commit is unaffected...\n"); | ||
|
|
||
| /* A key without the flag still commits repeatedly */ | ||
| ret = wh_Client_KeyCache(ctx, WH_NVM_FLAGS_NONE, label, sizeof(label), key, | ||
| sizeof(key), &keyId); | ||
| if (ret != 0) { | ||
| WH_ERROR_PRINT("Failed to cache modifiable key: %d\n", ret); | ||
| return ret; | ||
| } | ||
|
|
||
| ret = wh_Client_KeyCommit(ctx, keyId); | ||
| if (ret != 0) { | ||
| WH_ERROR_PRINT("Failed first commit of modifiable key: %d\n", ret); | ||
| return ret; | ||
| } | ||
|
|
||
| ret = wh_Client_KeyCommit(ctx, keyId); | ||
| if (ret != 0) { | ||
| WH_ERROR_PRINT("Failed repeat commit of modifiable key: %d\n", ret); | ||
| return ret; | ||
| } | ||
|
|
||
| WH_TEST_DEBUG_PRINT("Modifiable key repeat commit allowed\n"); | ||
|
|
||
| /* Clean up */ | ||
| (void)wh_Client_KeyErase(ctx, keyId); | ||
|
|
||
| WH_TEST_PRINT("MODIFIABLE COMMIT TEST SUCCESS\n"); | ||
| return 0; | ||
| } | ||
|
|
||
| int whTest_Crypto_Keystore(whClientContext* ctx) | ||
| { | ||
| /* A preceding suite may leave the DMA-preferred dispatch mode set; reset | ||
| * to the std path so this suite runs the same way in every config. */ | ||
| (void)wh_Client_SetDmaMode(ctx, 0); | ||
| WH_TEST_RETURN_ON_FAIL(_whTest_KeyCache(ctx)); | ||
| WH_TEST_RETURN_ON_FAIL(_whTest_NonExportableKeystore(ctx)); | ||
| #if defined(WOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS) | ||
| WH_TEST_RETURN_ON_FAIL(_whTest_NonModifiableCommit(ctx)); | ||
| #endif | ||
| WH_TEST_RETURN_ON_FAIL(_whTest_ModifiableRecommit(ctx)); | ||
| return 0; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -162,7 +162,19 @@ int wh_Server_KeystoreCommitKey(whServerContext* server, whNvmId keyId); | |
| /** | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [Medium] Client-facing header not updated with the new non-idempotent commit contract The PR correctly documents the behavior change - a repeat commit of a NONMODIFIABLE key now returns But that contract is written only on the server header. The client-visible entry point is Suggestion: Mirror the contract on * @return int Returns 0 on success, or a negative error code on failure.
* WH_ERROR_ACCESS if an NVM object already exists under keyId and is
* NONMODIFIABLE (or server-trusted). Commit is not idempotent for such
* keys: on retry after a lost response, treat WH_ERROR_ACCESS as
* "already committed".
*/ |
||
| * @brief Commit a cached key to NVM with policy enforcement | ||
| * | ||
| * Runs keystore policy checks before committing. | ||
| * Runs keystore policy checks before committing. The verdict comes from the | ||
| * flags of the stored NVM object, not the cache slot, so an unchecked cache | ||
| * path cannot launder them. | ||
| * | ||
| * @param[in] server Server context | ||
| * @param[in] keyId Key ID to commit | ||
| * @return WH_ERROR_OK on success | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 [Low] New WH_ERROR_NOTFOUND doc on wh_Server_KeystoreCommitKeyChecked is inaccurate, and the retry contract it documents is incomplete The newly added doc block states This matters because the same doc block introduces a client retry contract: "a client retrying after a lost response must treat WH_ERROR_ACCESS as 'already committed'." For a modifiable key that contract is under-specified in the opposite direction. Triggering sequence: cache key K with Recommendation: Correct the return doc and complete the retry guidance: * @return WH_ERROR_NOTFOUND if the key is not resident in the cache. Commit
* writes the cached copy, so this is returned even when an object
* already exists in NVM under keyId (for example after the cache slot
* of an already-committed key was reclaimed).
* @return WH_ERROR_ACCESS if an NVM object already exists under keyId and
* carries WH_NVM_FLAGS_NONMODIFIABLE or WH_NVM_FLAGS_TRUSTED. ...
* A client retrying after a lost response must therefore treat both
* WH_ERROR_ACCESS (immutable object already stored) and
* WH_ERROR_NOTFOUND (cache slot reclaimed after a successful commit)
* as possible outcomes of an already-completed commit, and confirm
* with an export or metadata read rather than re-caching. |
||
| * @return WH_ERROR_ACCESS if an NVM object already exists under keyId and | ||
| * carries WH_NVM_FLAGS_NONMODIFIABLE or WH_NVM_FLAGS_TRUSTED. Commit | ||
| * is therefore not idempotent for such keys: a repeat commit of | ||
| * unchanged bytes is refused, so a client retrying after a lost | ||
| * response must treat WH_ERROR_ACCESS as "already committed". | ||
| * @return WH_ERROR_NOTFOUND if the key is in neither cache nor NVM | ||
| */ | ||
| int wh_Server_KeystoreCommitKeyChecked(whServerContext* server, whNvmId keyId); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 [Medium] New CI step's CFLAGS_EXTRA override silently disables -Werror -Wall -Wextra for the only job that compiles the gated tests
💡 SUGGEST
bugThe new step passes the macro as
CFLAGS_EXTRA=-DWOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTSon the make command line.test-refactor/posix/Makefile:51-53defines that variable with a plain=followed by two+=lines:A command-line variable definition overrides every makefile assignment to that variable, and GNU make ignores subsequent
+=appends to a command-line-defined variable (GNU Make manual §6.6/§6.7). So this job compiles withCFLAGS = -std=c90 -DWOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS -fsanitize=addressand loses-Werror -Wall -Wextra -ffunction-sections -fdata-sections -MMD -MP.The macro does reach the compiler (so the gated tests run), but three concrete consequences follow:
_whTest_NonModifiableCommitand the revived_whTest_CryptoKeyRevocationAesCbc— code that could not compile at all before this PR — and it is the one config where warnings are not errors. The PR's stated verification ("clean under-std=c90 -Werror -Wall -Wextra") is not what CI enforces.test-refactor/posix/Makefile:248(wh_test_check_struct_padding.o: CFLAGS += -Wpadded -DWH_PADDING_CHECK) relies on-Werrorto turn padding violations into build failures. Without it, the struct-padding check degrades to a printed warning in this job.-MMD -MPmeans no.dfiles, so the followingmake run(invoked without the override, per the same shell line) has no header dependencies; any rebuild triggered there would silently produce objects compiled without the macro.Every other build knob in this Makefile is a named variable that appends to
DEF(SHE=1,AUTH=1,DMA=1,NOCRYPTO=1, …). The macro should follow that convention rather than hijacking the flag variable.Suggestion: Add a knob to test-refactor/posix/Makefile alongside the other options:
and change the workflow step to: