Skip to content
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

Bug: Proofs of Absence can be Forged #86

Closed
preston-evans98 opened this issue Dec 14, 2022 · 2 comments · Fixed by #90
Closed

Bug: Proofs of Absence can be Forged #86

preston-evans98 opened this issue Dec 14, 2022 · 2 comments · Fixed by #90
Assignees
Labels
bug Something isn't working

Comments

@preston-evans98
Copy link
Contributor

Bug: Proofs of Absence can be Forged

Problem

The namespaced merkle tree exposes a VerifyNamespace method whose documentation states that it "verifies that the namespace is complete and no leaf of that namespace was left out in the proof."

This API allow consumers to write code like the following:

proof, namespaceData := getUntrustedProofFor(namespaceId)
// check that the namespaceData was all returned
if proof.VerifyNamespace(sha256.New(), namespaceId, namespaceData, n.Root()) {
    process(namespaceData)
} else {
    return errors.New("prover lied about namespace data")
}

Unfortunately, this code is vulnerable. A malicious prover can falsely claim that the namespace is empty, and, as long as he also provides an empty proof, the claim will be accepted.

Test Case

A simple test case which reproduces the issue is as follows:

func TestNMT_forgeNamespaceEmptinessProof(t *testing.T) {
	data := [][]byte{
		append(namespace.ID{0}, []byte("leaf_0")...),
		append(namespace.ID{0}, []byte("leaf_1")...),
		append(namespace.ID{1}, []byte("leaf_2")...),
		append(namespace.ID{1}, []byte("leaf_3")...)}
	// Init a tree with the namespace size as well as
	// the underlying hash function:
	tree := New(sha256.New(), NamespaceIDSize(1))
	for _, d := range data {
		if err := tree.Push(d); err != nil {
			panic(fmt.Sprintf("unexpected error: %v", err))
		}
	}

	root := tree.Root()
	actualLeaves := tree.Get(namespace.ID{0})
	if len(actualLeaves) == 0 {
		t.Fatalf("Get(namespace.ID{0}) should have returned two leaves but returned none.")
	}

	forgedProof := Proof{
		start:                   0,
		end:                     0,
		nodes:                   [][]byte{},
		leafHash:                []byte{},
		isMaxNamespaceIDIgnored: true,
	}

	forged_proof_succes := forgedProof.VerifyNamespace(sha256.New(), namespace.ID{0}, [][]byte{}, root)
	if forged_proof_succes {
		t.Fatalf("Successfully verified proof that non-empty namespace was empty")
	}
}

Root Cause

This behavior is introduced by the following condition here:

// empty range, proof, and data: always checks out
if len(data) == 0 && isEmptyRange && len(proof.nodes) == 0 {
    return true
}

Suggested Solution

Currently, the handling of empty range proofs is not well defined. I propose to reject all proofs that do not contain at least one node in either the proof.nodes or data arguments, unless the tree is the empty. (Without this special case, it's impossible to check range proofs against the empty tree). This will change the semantics of the NMT, causing some existing test cases to fail.

@liamsi
Copy link
Member

liamsi commented Dec 15, 2022

Regarding the root cause, mentioned above, I think changing that if condition to this should fix it:

if len(data) == 0 && isEmptyRange && len(proof.nodes) == 0 {
	// empty range, proof, and data and nID < min or nID > max: always checks out
	if nID.Less(min) || max.Less(nID) {
		return true
	}
	return false
}

This would ensure that an empty range is only valid if nID < min or nID > max in the root. Need to think a bit further about the empty-tree case though. Note that an empty tree is defined as this (or base-case here) but I think that would also work with the above.

@preston-evans98
Copy link
Contributor Author

I think this change is good. I would just add a special case for the empty root since its purported min and max namespaces aren't accurate.

if len(data) == 0 && isEmptyRange && len(proof.nodes) == 0 {
	// empty range, proof, and data and nID < min or nID > max: always checks out
	if nID.Less(min) || max.Less(nID) || bytes.Equal(root, nth.EmptyRoot())  {
		return true
	}
	return false
}

The extra check is necessary - otherwise, it's impossible to verify a proof of absence for namespace 0 in the empty tree

func TestNMT_absenceProofInEmptyTree(t *testing.T) {
	tree := New(sha256.New(), NamespaceIDSize(1))
	root := tree.Root()
	emptyleaves, proof, err := tree.GetWithProof(namespace.ID{0})
	if err != nil {
		t.Fatalf("GetWithProof()  could not get namespace{0}. err: %v ", err)
	}
	if len(emptyleaves) != 0 {
		t.Fatalf("Get(namespace.ID{0}) should have returned no leaves but returned %v", emptyleaves)
	}
	if !proof.VerifyNamespace(sha256.New(), namespace.ID{0}, emptyleaves, root) {
		t.Fatalf("Could not verify proof of absence of namespace zero in empty tree")
	}
}

preston-evans98 added a commit to preston-evans98/nmt that referenced this issue Dec 15, 2022
- Reject empty "proofs" of absence, except in the special case where the
  root doesn't cover the namespace being proved
- Add test case to catch regression
- Add test case to ensure that the absence of the zero namespace can be
  proved against the empty tree
preston-evans98 added a commit to preston-evans98/nmt that referenced this issue Dec 16, 2022
- Reject empty "proofs" of absence, except in the special case where the
  root doesn't cover the namespace being proved
- Add test case to catch regression
- Add test case to ensure that the absence of the zero namespace can be
  proved against the empty tree
@liamsi liamsi linked a pull request Dec 16, 2022 that will close this issue
5 tasks
liamsi added a commit that referenced this issue Dec 16, 2022
- Reject empty "proofs" of absence, except in the special case where the
root doesn't cover the namespace being proved
- Add test case to catch regression
- Add test case to ensure that the absence of the zero namespace can be
proved against the empty tree

<!--
Please read and fill out this form before submitting your PR.

Please make sure you have reviewed our contributors guide before
submitting your
first PR.
-->

closes #86

## Overview

<!-- 
Please provide an explanation of the PR, including the appropriate
context,
background, goal, and rationale. If there is an issue with this
information,
please provide a tl;dr and link the issue. 
-->

## Checklist

<!-- 
Please complete the checklist to ensure that the PR is ready to be
reviewed.

IMPORTANT:
PRs should be left in Draft until the below checklist is completed.
-->

- [x] New and updated code has appropriate documentation
- [x] New and updated code has new and/or updated testing
- [x] Required CI checks are passing
- [x] Visual proof for any user facing features like CLI or
documentation updates
- [x] Linked issues closed with keywords

Co-authored-by: Ismail Khoffi <[email protected]>
@rootulp rootulp added the bug Something isn't working label Mar 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants