Repository navigation
Check internal node keys during sliced tree recovery - #118
Closed
Ergologica wants to merge 2 commits into
Closed
Ergologica wants to merge 2 commits into
Ergologica wants to merge 2 commits into
Conversation
Node labels hash the balance and the children labels, not the key, so a manifest or a subtree carries keys its own digest does not authenticate and both verify() methods took them on trust. The keys need not be trusted: an internal node's key is the smallest key of its right subtree, the invariant BatchAVLProver.checkTree enforces, so they can be recomputed from a materialized subtree. NodeKeyChecks does that, and it is applied in BatchAVLProverSubtree.verify (full check), BatchAVLProverManifest.verify (ordering only, its children are still labels there) and BatchAVLProverSerializer.combine (full check on the assembled tree). Serialization format unchanged. Closes input-output-hk#89
Four properties: that a label does not commit to an internal key (the fact the checks exist for), that a subtree and a manifest carrying a forged key still pass the digest comparison they used to be accepted on and are rejected anyway, and that combining a tree with a forged key fails. Each asserts the honest case passes too, so the checks cannot be satisfied by rejecting everything.
Author
|
Closing this in favour of ergoplatform#3. @kushti pointed out that this master is protected and that the Ergo side now works from ergoplatform/scrypto, which is where the recent work on Worth recording for anyone who finds this PR later: the problem it describes is still present on that fork. Matching subtrees by digest label, which the hardening there now does, does not cover it - |
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.
Closes #89.
The problem
InternalNode.computeLabel(BatchNode.scala:45-47) hashes the balance and the two children labels:The key is not in there.
InternalProverNode.getNewKeysays so outright — "label doesn't change when key of an internal node changes".So a manifest or a subtree carries keys its own digest does not authenticate, and both
verify()methods checked only labels. An adversary can hand over a subtree with altered internal keys and every check passes: the digest is unchanged,verify()returns true,combine()attaches it (the lookup is by label), and the resulting prover routes lookups down the wrong branch.nodesFromByteshas a localleft.key < key <= right.keyguard for materialized internal nodes (BatchAVLProverSerializer.scala:135-137), but it constrains the key only against its immediate children, and theProxyInternalNodebranch (:140-146) has no key check at all.The approach
The issue offers two routes — "avoid storing keys for internal nodes at all, or implement check". This implements the check, because dropping the keys changes the serialization format and downstream consumers would have to move in lockstep (ergo vendors its own copies of
ManifestSerializer/SubtreeSerializer). No wire format change.The keys do not need to be trusted, because they are recomputable. In this tree the key of an internal node is the smallest key of its right subtree — the invariant
BatchAVLProver.checkTreealready enforces atBatchAVLProver.scala:463:myRequire(minRight.key sameElements r.key, "min of right subtree doesn't match")So a materialized subtree can be checked against itself, with no extra data.
The change
New
NodeKeyChecks(package-private toserialization), applied at the three points where trust was being extended:BatchAVLProverSubtree.verify— full check. A subtree is fully materialized, so every internal key is recomputed from the leaves and compared. A subtree passing this cannot contain a forged internal key.BatchAVLProverManifest.verify— ordering check. A manifest ends atProxyInternalNodes whose children are only labels, so the keys below are not available yet and the full check is impossible here. What is checkable is the search-tree ordering: since every internal key is the smallest key of its right subtree, an in-order walk of the manifest's internal nodes must yield strictly increasing keys, terminal proxy nodes included. This is the check available "assuming bottom keys of subtrees are correct".BatchAVLProverSerializer.combine— full check on the assembled tree. This is what actually closes the hole: once manifest and subtrees are joined everything is materialized, so a forged key fails recovery even if a caller skippedverify().combinealready returnsTry, so the failure surfaces normally.Tests
Four new properties in
AVLBatchSerializationSpecification:a label does not commit to the key of an internal node— pins the underlying fact: same children, different key, identical label. It documents why the rest exists, and fails loudly if labels ever start committing to keys.subtree with a forged internal key does not verify— asserts the forged subtree still passes the digest comparison it used to be accepted on, then thatverifyrejects it anyway.manifest with an internal key out of order does not verify— same shape, via a root key smaller than every key in the tree.combining a tree with a forged internal key fails— end to end: the label is unchanged, so the forged subtree is still found and attached where the real one belongs, and recovery fails.Each asserts the honest case passes too, so the checks cannot be satisfied by rejecting everything.
sbt scryptoJVM/test— 139 tests green. Cross-compiled on 2.11, 2.12, 2.13 and 3.3.5.Two notes
SparseMerkleTreeSpecification's "Updated tree has valid proof" failed once for me across full-suite runs and passes otherwise. It is unrelated to this change — this PR touches nothing undermerkle/— but it looks genuinely flaky: the properties areforAll { (height: Byte) => ... }over an unconstrainedByte. Happy to open a separate issue.ergo vendors its own
avldb/.../ManifestSerializer.scalaandSubtreeSerializer.scala, which have no key check either. This PR does not touch ergo; say the word and I'll open the matching one there.