fix: prevent KV-to-KVValueHash proof value forgery attack (C1)#553
Conversation
Add value_hash_is_computed and is_reference_result fields to ProvedKeyOptionalValue and ProvedKeyValue structs. At the GroveDB verification layer, items whose value_hash was NOT independently computed by the merk verifier (and are not reference results) now have their value_hash independently verified against the provided value bytes. This prevents an attacker from substituting a KV proof node with KVValueHash to provide a fake value while preserving the valid Merkle root hash. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe changes enhance proof verification security by introducing detection logic for KV-to-KVValueHash tampering attacks, validate item element presence in KVValueHash nodes, and refactor tests to use dynamic proof generation instead of hardcoded proofs while maintaining equivalent verification coverage. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
❌ Your patch status has failed because the patch coverage (42.85%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## develop #553 +/- ##
============================================
+ Coverage 59.95% 90.73% +30.78%
============================================
Files 182 182
Lines 51082 51103 +21
============================================
+ Hits 30624 46367 +15743
+ Misses 20458 4736 -15722
🚀 New features to boost your workflow:
|
Merge the inner value_hash comparison into the outer if condition to reduce uncoverable lines in the security check blocks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Demonstrates the attack: an attacker replaces a KV node (tag 0x03) with KVValueHash (tag 0x04) using a fake value but the real value_hash. The Merkle root stays valid but the verifier returns the attacker's value. Without the fix, the tampered proof is accepted and returns "attacker_controlled" instead of "real_secret_value". With the fix, it's rejected with "item value hash mismatch". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of recomputing value_hash and comparing, simply assert that KVValueHash proof nodes (where value_hash is not computed by the verifier) must correspond to tree elements. Items should never arrive via KVValueHash since the verifier always computes their value_hash directly from KV nodes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The simplified "must be tree" check rejected legitimate proofs where items in aggregate trees arrive via KVValueHashFeatureType (e.g., SumItems in SumTrees from older proof formats). Restore the original approach: recompute value_hash for items and verify it matches, which allows legitimate proofs while still catching forgeries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@grovedb/src/operations/proof/verify.rs`:
- Around line 619-634: Reject any proof node where
!proved_key_value.value_hash_is_computed &&
!proved_key_value.is_reference_result unless you have verified and folded a
lower-layer hash into the combined hash; specifically, replace the current
exemption that checks element.is_any_tree() with logic that only allows
tree-encoded payloads when you recompute and validate the combined hash by
calling combine_hash(value_hash(value_bytes), lower_hash) (or equivalent) and
confirming it matches the node’s combined hash. Ensure the same fix is applied
in the other occurrence (the tree-target branch that currently skips recomputing
the combined hash and the duplicate check around include_empty_trees_in_result)
so that unauthenticated tree bytes cannot bypass verification or suppress
existing items.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4a7ff0b8-9ac1-4a3f-84bd-27362d967c69
📒 Files selected for processing (5)
grovedb/src/operations/proof/util.rsgrovedb/src/operations/proof/verify.rsgrovedb/src/tests/misc_coverage_tests.rsgrovedb/src/tests/proof_coverage_tests.rsmerk/src/proofs/query/verify.rs
Replace the boolean flag approach (value_hash_is_computed, is_reference_result) with a single element type discriminant check at the merk verification level. KVValueHash and KVValueHashFeatureType nodes now reject item elements (which use simple value hashes and must use KV/KVCount nodes instead). This prevents KV→KVValueHash substitution attacks without needing GroveDB-layer hash recomputation. Removes ~105 lines of code (boolean fields, GroveDB-layer checks, flag propagation through structs and conversions). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
merk/src/proofs/query/verify.rs (1)
323-337: Extract the item-element rejection into one helper.The
ElementType::from_serialized_value(...).has_simple_value_hash()guard is now security-critical and duplicated in bothKVValueHashbranches. Centralizing it would reduce the chance that one branch drifts from the other later.♻️ Suggested shape
+fn reject_item_in_value_hash_node(value: &[u8], node_name: &str) -> Result<(), Error> { + let element_type = ElementType::from_serialized_value(value).map_err(|e| { + Error::InvalidProofError(format!( + "cannot determine element type in {node_name}: {e}" + )) + })?; + if element_type.has_simple_value_hash() { + return Err(Error::InvalidProofError(format!( + "{node_name} must not contain an item element" + ))); + } + Ok(()) +} ... - let element_type = ElementType::from_serialized_value(value).map_err(|e| { - Error::InvalidProofError(format!( - "cannot determine element type in KVValueHash node: {e}" - )) - })?; - if element_type.has_simple_value_hash() { - return Err(Error::InvalidProofError( - "KVValueHash node must not contain an item element".to_string(), - )); - } + reject_item_in_value_hash_node(value, "KVValueHash node")?; ... - let element_type = ElementType::from_serialized_value(value).map_err(|e| { - Error::InvalidProofError(format!( - "cannot determine element type in KVValueHashFeatureType node: {e}" - )) - })?; - if element_type.has_simple_value_hash() { - return Err(Error::InvalidProofError( - "KVValueHashFeatureType node must not contain an item element" - .to_string(), - )); - } + reject_item_in_value_hash_node(value, "KVValueHashFeatureType node")?;Also applies to: 373-384
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@merk/src/proofs/query/verify.rs` around lines 323 - 337, Extract the duplicated guard into a small helper (e.g., validate_kv_valuehash_no_item or ensure_not_item_element_in_kv_value_hash) that takes the serialized value (the same input passed to ElementType::from_serialized_value) and returns Result<(), Error>; move the ElementType::from_serialized_value(...).has_simple_value_hash() check into that helper and map the error to the existing InvalidProofError messages, then call this helper from both KVValueHash branches (the blocks containing the current ElementType::from_serialized_value(...) check) to ensure a single canonical check is used everywhere.grovedb/src/tests/proof_coverage_tests.rs (1)
5919-6037: Add a matching regression forKVValueHashFeatureType.This test only drives the plain
KVValueHashpath, but the fix inmerk/src/proofs/query/verify.rsadds the same rejection logic forKVValueHashFeatureType. A second forged proof that hits that branch would keep the other security-sensitive path covered too.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@grovedb/src/tests/proof_coverage_tests.rs` around lines 5919 - 6037, Add a second regression in kv_to_kvvaluehash_forgery_is_detected that constructs and tampers a proof to hit the KVValueHashFeatureType branch: duplicate the tampering flow (using tamper_kv_to_kvvaluehash or a new helper) but replace the target merk node to be a KVValueHashFeatureType-encoded entry (keeping value_hash from the real element and swapping in a fake value) and then assert GroveDb::verify_query_raw returns an error mentioning the item rejection/value-hash mismatch; ensure the test invokes the same proof decode/modify/encode path so the new case covers the KVValueHashFeatureType branch in verify_query_raw.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@grovedb/src/tests/proof_coverage_tests.rs`:
- Around line 5919-6037: Add a second regression in
kv_to_kvvaluehash_forgery_is_detected that constructs and tampers a proof to hit
the KVValueHashFeatureType branch: duplicate the tampering flow (using
tamper_kv_to_kvvaluehash or a new helper) but replace the target merk node to be
a KVValueHashFeatureType-encoded entry (keeping value_hash from the real element
and swapping in a fake value) and then assert GroveDb::verify_query_raw returns
an error mentioning the item rejection/value-hash mismatch; ensure the test
invokes the same proof decode/modify/encode path so the new case covers the
KVValueHashFeatureType branch in verify_query_raw.
In `@merk/src/proofs/query/verify.rs`:
- Around line 323-337: Extract the duplicated guard into a small helper (e.g.,
validate_kv_valuehash_no_item or ensure_not_item_element_in_kv_value_hash) that
takes the serialized value (the same input passed to
ElementType::from_serialized_value) and returns Result<(), Error>; move the
ElementType::from_serialized_value(...).has_simple_value_hash() check into that
helper and map the error to the existing InvalidProofError messages, then call
this helper from both KVValueHash branches (the blocks containing the current
ElementType::from_serialized_value(...) check) to ensure a single canonical
check is used everywhere.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 111f69a6-07da-49a8-a87b-285e81596a02
📒 Files selected for processing (4)
grovedb/src/tests/proof_coverage_tests.rsgrovedb/src/tests/sum_tree_tests.rsmerk/src/proofs/query/merk_integration_tests.rsmerk/src/proofs/query/verify.rs
✅ Files skipped from review due to trivial changes (1)
- merk/src/proofs/query/merk_integration_tests.rs
…ValueHash Adds two tests demonstrating the KV→KVValueHash proof forgery attack vector and verifying that KV→KVRefValueHash is NOT exploitable. - kv_to_kvvaluehash_forgery_exploit: Confirms that replacing a KV node with KVValueHash (keeping the real value_hash but injecting a fake value) succeeds on the current codebase. The fix for this is in PR #553. - kv_to_kvrefvaluehash_forgery_exploit: Confirms that the same attack with KVRefValueHash fails because KVRefValueHash incorporates H(value) into the tree hash via combine_hash(node_value_hash, H(referenced_value)). Changing the value changes the root hash, so the forgery is detected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The hardcoded proof in test_verify_query_get_parent_tree_info was generated by an older version that used KVValueHash for item elements. The new proof format correctly rejects items in KVValueHash nodes to prevent forgery. Update the test to keep the original proof bytes but expect rejection, documenting the protocol change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ValueHash (#630) Adds two tests demonstrating the KV→KVValueHash proof forgery attack vector and verifying that KV→KVRefValueHash is NOT exploitable. - kv_to_kvvaluehash_forgery_exploit: Confirms that replacing a KV node with KVValueHash (keeping the real value_hash but injecting a fake value) succeeds on the current codebase. The fix for this is in PR #553. - kv_to_kvrefvaluehash_forgery_exploit: Confirms that the same attack with KVRefValueHash fails because KVRefValueHash incorporates H(value) into the tree hash via combine_hash(node_value_hash, H(referenced_value)). Changing the value changes the root hash, so the forgery is detected. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…-forgery-via-kvvaluehash Resolve conflict in proof_coverage_tests.rs: keep kv_to_kvvaluehash_forgery_is_detected test (expects rejection with fix) and add kv_to_kvrefvaluehash_forgery_is_rejected test from develop (proves KVRefValueHash is safe by construction). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
QuantumExplorer
left a comment
There was a problem hiding this comment.
Approved and heavily worked on.
…mpatibility The security check from da308f7 (PR #553) that rejects item elements in KVValueHash nodes is now gated on the proof version. V0 proofs pass `allow_items_in_value_hash_nodes = true` to skip the check, maintaining backwards compatibility with pre-existing proof fixtures. V1 proofs continue to enforce the check. This fixes 11 dash-sdk test failures caused by stale proof fixtures generated before the security fix was introduced. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: allow V0 proofs with items in KVValueHash nodes for backwards compatibility The security check from da308f7 (PR #553) that rejects item elements in KVValueHash nodes is now gated on the proof version. V0 proofs pass `allow_items_in_value_hash_nodes = true` to skip the check, maintaining backwards compatibility with pre-existing proof fixtures. V1 proofs continue to enforce the check. This fixes 11 dash-sdk test failures caused by stale proof fixtures generated before the security fix was introduced. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update V0 proof test to expect acceptance instead of rejection The test_verify_query_get_parent_tree_info test used a hardcoded V0 proof with items in KVValueHash nodes. Now that V0 proofs allow this for backwards compatibility, the test should expect success. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: replace bool parameter with proof_version u16 in execute_proof Change the `allow_items_in_value_hash_nodes: bool` parameter to `proof_version: u16` for clearer semantics: 0 = V0 lenient mode, ≥1 = strict mode that rejects items in value hash nodes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: add PROOF_VERSION_LATEST constant for execute_proof calls Replace magic number `1` with `PROOF_VERSION_LATEST` in all test and production code calling execute_proof. The constant is defined in merk::proofs::query::verify and re-exported from the query module. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Severity: Critical — Fixes a proof value forgery vulnerability in the merk proof verification layer.
The Attack
The merk proof system uses different node types to encode key-value data:
KV(key, value)— the verifier computesvalue_hash = blake3(value)and uses it to build the Merkle tree hashKVValueHash(key, value, value_hash)— the proof provides thevalue_hashdirectly, and the verifier trusts itThe hash computation for both is algebraically identical:
When
vh = value_hash(value), these produce the same Merkle root. An attacker can:KV(key, real_value)KVValueHash(key, fake_value, value_hash(real_value))value_hashhasn't changedfake_valueto the caller, believing it is authenticThe Fix
Two new boolean fields are added to
ProvedKeyOptionalValueandProvedKeyValue:value_hash_is_computed: bool—truewhen the merk verifier independently computed the value_hash from the value bytes (KV, KVCount nodes),falsewhen the hash was provided by the proof (KVValueHash, KVRefValueHash, etc.)is_reference_result: bool—truefor reference node results (KVRefValueHash, KVRefValueHashCount) where the value is the dereferenced target and the hash is the reference node's hash (secured viacombine_hashin tree.rs)At the GroveDB verification layer, for item elements where
value_hash_is_computed == false && is_reference_result == false, the verifier now independently computesvalue_hash(value_bytes)and checks it matches the proof's hash. A mismatch indicates proof tampering and returns an error.This check correctly excludes:
value_hashis acombine_hashof the element hash and subtree root, verified separatelyvalue_hashis the reference node's hash, with the dereferenced value secured viacombine_hashin tree.rsFiles Changed
merk/src/proofs/query/verify.rsvalue_hash_is_computedandis_reference_resultfields toProvedKeyOptionalValueandProvedKeyValue; updatedexecute_nodeclosure to propagate both flags; set flags correctly per node typegrovedb/src/operations/proof/verify.rsgrovedb/src/operations/proof/util.rsgrovedb/src/tests/proof_coverage_tests.rsgrovedb/src/tests/misc_coverage_tests.rsTest plan
cargo build— compiles cleanlycargo test -p grovedb --lib -- proof— all 198 proof tests passcargo test -p grovedb --lib -- reference— all 93 reference tests pass (verifies references aren't falsely rejected)cargo test -p grovedb-merk -- proof— all 155 merk proof tests passcargo test -p grovedb --lib -- coverage— all 367 coverage tests pass🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
Tests
Bug Fixes