Enforce NONMODIFIABLE and TRUSTED NVM policy on keystore key commit - #492
Enforce NONMODIFIABLE and TRUSTED NVM policy on keystore key commit#492yosuke-wolfssl wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request closes a defense-in-depth gap in the server keystore “gate” by ensuring WH_KEY_COMMIT cannot overwrite existing NVM objects that are marked NONMODIFIABLE or TRUSTED. The policy enforcement is implemented centrally in the keystore policy checker (rather than in the lower-level commit function), and is validated via a new server-side test suite.
Changes:
- Add a
WH_KS_OP_COMMITpolicy branch that consults the stored NVM object’s flags and denies overwrite whenNONMODIFIABLEorTRUSTED. - Register and add a new server test (
whTest_KeystoreCommitPolicy) covering overwrite-denial, first-commit allowance, normal commit round-trip, and the “uncached commit returns NOTFOUND” guard. - Extend the server test registry to include the new test.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/wh_server_keystore.c |
Enforces NONMODIFIABLE/TRUSTED overwrite denial for commit by checking stored NVM metadata when a cache slot is being committed. |
test-refactor/server/wh_test_keystore_policy.c |
Adds targeted server-side tests validating commit policy behavior and ensuring stored bytes/flags remain unchanged on denied overwrite. |
test-refactor/wh_test_list.c |
Registers the new keystore commit policy test in the server test group. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #492
Scan targets checked: wolfhsm-core-bugs, wolfhsm-src
No new issues found in the changed files. ✅
6041613 to
0a5c6c1
Compare
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: CONDITIONAL
Findings: 6 total — 3 posted, 3 skipped
Posted findings
- [Low] Repeat/idempotent commit of a NONMODIFIABLE key now fails with WH_ERROR_ACCESS (undeclared client-visible behavior change) —
src/wh_server_keystore.c:247-263 - [Info] The TRUSTED half of the new check is only reachable via unchecked cache paths; comment does not say so —
src/wh_server_keystore.c:248-257 - [Info] Commit-overwrite does not consider NONDESTROYABLE, unlike the evict gate —
src/wh_server_keystore.c:247-263
Skipped findings
- [Low] New access-control branch has no test coverage at any level, and the stated rationale for omitting tests does not hold
- [Info] Re-committing an already-committed NONMODIFIABLE key now returns WH_ERROR_ACCESS, breaking idempotent commit retries
- [Info] Commit path now hard-depends on the optional NVM GetMetadata callback
Review generated by Skoll via Claude/Codex
0a5c6c1 to
69ca15e
Compare
|
Hello @Frauschi , |
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: CONDITIONAL
Findings: 6 total — 2 posted, 4 skipped
Posted findings
- [Low] New test leaves an undeletable NONMODIFIABLE NVM object, bypassing the repo's WOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS convention —
test-refactor/client-server/wh_test_crypto_keystore.c:909-911 - [Low] Commit denial returns WH_ERROR_NOTFOUND instead of WH_ERROR_ACCESS when the cache slot is gone —
src/wh_server_keystore.c:247-263
Skipped findings
- [Medium] New keystore test permanently leaks an unerasable NVM object into the shared test fixture
- [Low] wh_Client_KeyCommit is no longer idempotent for NONMODIFIABLE keys, making commit retries a hard failure
- [Info] Fail-closed unreadable-metadata branch in the new commit policy has no test coverage and returns a non-ACCESS error code
- [Info] Revoke path still writes cached bytes to NVM unchecked, contradicting the new comment on the shared switch case
Review generated by Skoll via Claude/Codex
69ca15e to
69effbc
Compare
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: APPROVE
Findings: 10 total — 3 posted, 7 skipped
Posted findings
- [Medium] Fix guards only one of the two client-reachable commit paths; LMS/XMSS DMA keygen still commits unchecked —
src/wh_server_crypto.c:7816 - [Medium] Commit is no longer idempotent for NONMODIFIABLE keys, and the contract is undocumented —
src/wh_server_keystore.c:247 - [Medium] New NONMODIFIABLE commit regression test never compiles or runs in any buildable configuration —
test-refactor/client-server/wh_test_crypto_keystore.c:842
Skipped findings
- [Medium] New commit-denial test is compiled out in every supported configuration
- [Low] Discarded evict return lets the uncached-denial test pass for the wrong reason
- [Low] Fail-closed comment does not cover the server->nvm == NULL path
- [Low] COMMIT branch duplicates the NVM metadata lookup already performed above the switch
- [Low] Discarded evict return leaves Test 4's uncached-commit premise unverified
- [Info] Identical 32-byte key literal duplicated across the two new tests
- [Info] _KeystoreCheckPolicy now carries two divergent notions of "the key's flags"
Review generated by Skoll via Claude/Codex
| @@ -245,8 +245,28 @@ static int _KeystoreCheckPolicy(whServerContext* server, whKsOp op, | |||
| break; | |||
|
|
|||
| case WH_KS_OP_COMMIT: | |||
There was a problem hiding this comment.
🟡 [Medium] Commit is no longer idempotent for NONMODIFIABLE keys, and the contract is undocumented
💡 SUGGEST api
The new WH_KS_OP_COMMIT policy branch makes a byte-identical re-commit of a NONMODIFIABLE key return WH_ERROR_ACCESS where it previously returned WH_ERROR_OK. This is a deliberate, defensible change — docs/src/5-Features.md:211 already documents NONMODIFIABLE as "the object cannot be overwritten or destroyed through the policy-checked APIs", and _NvmCheckPolicy/WH_NVM_OP_ADD already refuses a no-op rewrite — so the fix aligns code with documented intent.
What is missing is the contract update. The doxygen for wh_Server_KeystoreCommitKeyChecked in wolfhsm/wh_server_keystore.h:163-167 still says only "Runs keystore policy checks before committing", with no @return block and no mention of WH_ERROR_ACCESS. The practical consequence the PR body itself identifies — a client retrying a commit after a dropped response now sees a hard failure on a commit that actually succeeded — is a client-visible behavior change that integrators need to be told about.
Suggestion:
| case WH_KS_OP_COMMIT: | |
| /** | |
| * @brief Commit a cached key to NVM with policy enforcement | |
| * | |
| * Runs keystore policy checks before committing. The verdict is taken 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 | |
| * @return WH_ERROR_ACCESS if an NVM object already exists under keyId and | |
| * carries WH_NVM_FLAGS_NONMODIFIABLE or WH_NVM_FLAGS_TRUSTED. Note | |
| * this makes commit non-idempotent for such keys: a repeated 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); |
| return 0; | ||
| } | ||
|
|
||
| #if defined(WOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS) |
There was a problem hiding this comment.
🟡 [Medium] New NONMODIFIABLE commit regression test never compiles or runs in any buildable configuration
💡 SUGGEST
The only test that exercises the behavior this PR adds (_whTest_NonModifiableCommit) is compiled out in every configuration that currently builds, so the security fix ships with no automated coverage of its deny path. Verified empirically in this worktree:
- Default build (
make check WOLFSSL_DIR=...): 45 passed / 26 skipped / 0 failed, andtest-refactor/posix/test-suite.logcontains onlyMODIFIABLE COMMIT TEST SUCCESS— theTesting non-modifiable commit enforcement...banner never appears. - Forcing the gate on (
CFLAGS_EXTRA=... -DWOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS, aftermake clean) fails the build attest-refactor/client-server/wh_test_crypto_keypolicy.c:527: error: use of undeclared identifier 'client'(pre-existing onmain, and correctly disclosed in the PR description). Because the macro is a whole-build-D, not a per-file define, there is no way to enable the new test without hitting that error.
The consequence is not merely "the test is skipped": nothing in CI compiles this function, so it can silently rot (unused statics, renamed APIs, changed error codes) and no regression of _KeystoreCheckPolicy's WH_KS_OP_COMMIT branch will be caught. The other new test, _whTest_ModifiableRecommit, is ungated but only exercises the allow path — deleting the entire WH_KS_OP_COMMIT block from src/wh_server_keystore.c still leaves it passing. So the net test delta that actually executes cannot detect a regression of the fix.
I confirmed the test itself is sound: after locally repairing the keypolicy.c:527 blocker, the gated build runs both new tests and passes (NON-MODIFIABLE COMMIT TEST SUCCESS), and a negative control that reverts only the WH_KS_OP_COMMIT branch produces whTest_Crypto_Keystore test FAILED (rc=-1). The test works — it just never runs.
Recommendation: Either (a) fix the one-line blocker in the same PR so the gate becomes usable — whTest_RevocationTryAESEncrypt needs the client context threaded in:
static int whTest_RevocationTryAESEncrypt(whClientContext* client,
whKeyId keyId, WC_RNG* rng,
int* encryptRes)
...
ret = whTest_RevocationTryAESEncrypt(client, keyId, rng, &encryptRes);
(verified: this makes the gated build compile and both new tests pass); or (b) restructure _whTest_NonModifiableCommit so it does not need the gate. The stored-flags denial can be exercised without stranding an undeletable NVM object by making the cached slot NONMODIFIABLE only, e.g. commit a plain key, then re-cache it and verify the denial comes from NVM metadata — no permanent artifact, so the test runs in the default config. If neither is done, at minimum add an ungated deny-path assertion so the fix has some executing coverage.
There was a problem hiding this comment.
As already mentioned above, I'd be fine if this is handled in a follow-up PR.
Problem
wh_Server_KeystoreCommitKeywrites the cached key to NVM through theunchecked
wh_Nvm_AddObjectWithReclaim, so nothing consulted the storedobject's flags. A client could overwrite an existing
NONMODIFIABLEorTRUSTEDNVM object — including a trusted KEK — by caching a key under its idand committing. Closes f-4225.
Fix (
src/wh_server_keystore.c)Added a
WH_KS_OP_COMMITbranch to_KeystoreCheckPolicythat reads thestored object's metadata and denies the overwrite:
(unwrap-and-cache, SHE) cannot launder the policy by populating a slot whose
flags do not reflect the stored object.
NONMODIFIABLE | TRUSTED— the same pair aswh_Nvm_AddObjectChecked,the correct model for an add-shaped operation.
NONDESTROYABLEgatesdestroy, not overwrite.
nvmMetawhether or not the cache slot survived, so a denied commit always reports
WH_ERROR_ACCESSinstead of theWH_ERROR_NOTFOUNDraised by a missing slot.GetMetadatacallbackcannot be policed, so the commit is denied rather than blind.
Client-visible behavior change: re-committing a cached key whose stored
object is
NONMODIFIABLEnow returnsWH_ERROR_ACCESSwhere a byte-identicalrewrite previously returned
WH_ERROR_OK. This makes commit consistent withevery other immutable-write path —
_NvmCheckPolicy(WH_NVM_OP_ADD) alreadyrefuses a no-op rewrite over a
NONMODIFIABLEobject, and commit was the onlyexception. A client retrying a commit after a dropped response will now see a
hard failure. Revoke is unaffected; it keeps its own "already revoked and
committed" short-circuit.
Tests
Added to
test-refactor/client-server/wh_test_crypto_keystore.c, driven onlyby
wh_Client_*:_whTest_NonModifiableCommit— first commit succeeds, repeat commit returnsWH_ERROR_ACCESS, the denial still holds once the slot is evicted, and thestored bytes and label survive the denial (evict + export freshens from NVM).
Gated behind
WOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS, since acommitted
NONMODIFIABLEobject cannot be erased — same convention as thekeypolicy revocation test.
_whTest_ModifiableRecommit— ungated; a key without the flag still commitsrepeatedly.
Separately,
test-refactordoes not currently build with that macro defined:wh_test_crypto_keypolicy.c:527references an undeclaredclient. Pre-existingon
mainand out of scope here, but it means the gate presently implies"never compiled".
Not covered: overwriting with differing content is not client-reachable (the
cache gate denies re-caching over a
NONMODIFIABLEid), and theunreadable-flags path needs a mock NVM backend.
Verification
-std=c90 -Werror -Wall -Wextra. Legacytest/suite exits 0.executing.
-2104(WH_ERROR_NOTFOUND); with the fix,-2101(WH_ERROR_ACCESS).