Delimit rangeproof cache key fields to prevent boundary collisions - #1601
Open
andycreed0x wants to merge 1 commit into
Open
Delimit rangeproof cache key fields to prevent boundary collisions#1601andycreed0x wants to merge 1 commit into
andycreed0x wants to merge 1 commit into
Conversation
ComputeEntryRangeProof hashed (proof, value commitment, asset commitment, scriptPubKey) into the cache key by raw concatenation, with no field delimiters. The two commitments are fixed 33-byte fields, but proof and scriptPubKey are variable length and sit at opposite ends of the stream, so the proof/script boundary can be shifted while leaving the concatenated bytes identical. Two distinct tuples then map to the same key. Because a positive cache hit returns true without verifying, a node that had cached one valid rangeproof would accept a different, unverified proof whose (proof, script) split differs but whose byte stream matches: the attacker primes the cache with a genuine proof over an OP_RETURN script carrying padding, then resubmits the same bytes re-split so the padding counts as proof and the script shrinks to a single byte. Prepend the four field lengths to the hashed key so the encoding is injective; no other partition of the same stream yields the same key. The key is process-local (salted per start, never serialized or compared across nodes), so the native byte order of the length array is fine. Add a regression test (blind_tests/rangeproof_cache_key_field_boundary) that primes the cache with a genuine proof over a 69-byte OP_RETURN script, then submits an attack tuple that shifts the boundary 68 bytes; the attack proof is invalid for its one-byte script and must be rejected.
Author
|
There isn't a GitHub Security Advisory to open a private PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ComputeEntryRangeProofbuilds the rangeproof cache key by concatenating four fields with no delimiters. Two of those fields are variable-length and sit at opposite ends of the stream, so two different(proof, value_commitment, asset_commitment, scriptPubKey)tuples can produce the same cache key. A positive cache hit returnstruewithout verifying, so a node that has cached one valid rangeproof can accept a different, unverified proof. This PR makes the key encoding injective by committing to the field lengths, and adds a regression test.The bug, in plain terms
The cache key is the hash of four pieces glued end-to-end with nothing between them. Picture the glued result as 8 letters, always in the same order:
Rule of the format: the two middle pieces are a fixed 2 letters each; the first and last piece can be any length.
A normal transaction cuts them like this:
The attacker re-cuts the same 8 letters like this:
Both cuts glue back to the identical byte string
ABCDEFGH. The key is the hash of the glued bytes, so both cuts hash to the same key. even though one is proofA/ scriptFGHand the other is proofABC/ scriptH. Nothing was moved to the front; only the invisible boundary between "proof" and "script" slid to the right. The hash saw the bytes but never the boundaries.This is not a SHA-256 collision. Two different logical inputs are made to produce the same byte string that gets hashed; identical input, identical output, as expected. The weakness is in how the pre-image is assembled, not in the hash.
Why it matters
A positive cache entry short-circuits verification:
rangeProofCache.Get(entry, !store)returnstrueandVerifyRangeProofreturns without ever checking the proof.An accepted-but-unverified rangeproof means a hidden output amount is taken on trust, which breaks the confidential-value balance guarantee.
The fix
Prepend the four field lengths to the hashed key:
Now the two cuts differ before the bytes even start:
Different lengths → different key → cache miss → the bogus proof is verified for real and rejected. The encoding is now injective: no other partition of the same stream yields the same key. The key is process-local (salted per start, never serialized or compared across nodes), so the length array's native byte order is fine.
Relationship to the earlier cache-key change
c26d719c29(bind the rangeproof cache key to the asset commitment and scriptPubKey) is necessary but not sufficient: it added those fields to the key but still concatenated everything without delimiters, leaving this boundary ambiguity in place. This PR closes it.Test
blind_tests/rangeproof_cache_key_field_boundaryprimes the cache with a genuine proof over a 69-byteOP_RETURNscript (6a 43 ‖ C(33) ‖ X(33) ‖ 6a), then submits the attack tuple with the boundary shifted 68 bytes; the attack proof is invalid for its 1-byte script and must be rejected.Verified by toggling only the fix: with the change the test passes; reverting just the
sigcache.cpphunk makes it fail (the bogus proof is wrongly accepted); restoring it passes again.Notes
The fix was based on the Bug B reported on https://gist.github.com/1440000bytes/211ac92dd4433bb1a2e674bf0ff7db2e
by @1440000bytes. And also an analysis over logs of an Elements node.