Skip to content

Commit

Permalink
go: bullet-proof against nil dereferences + more fuzzers
Browse files Browse the repository at this point in the history
This change fixes a bunch of issues identified by Orijtech Inc's audit
of ics23 which is a critical cosmos-sdk dependency and as per reports
about the Dragonberry & Elderberry vulnerability reports, this package
was put back on our radar to further audit and voila that uncovered
some issues, some of which have beenfixed in this change. While here
also added more fuzzers.

Fixes #241
Fixes #242
Fixes #243
  • Loading branch information
odeke-em committed Dec 5, 2023
1 parent d583e12 commit e80f323
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 36 deletions.
79 changes: 79 additions & 0 deletions go/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ics23

import (
"bytes"
"encoding/json"
"os"
"path/filepath"
Expand Down Expand Up @@ -40,6 +41,38 @@ func FuzzExistenceProofCalculate(f *testing.F) {
})
}

func FuzzExistenceProofCheckAgainstSpec(f *testing.F) {
if testing.Short() {
f.Skip("in -short mode")
}

seedDataMap := CheckAgainstSpecTestData(f)
for _, seed := range seedDataMap {
if seed.IsErr {
// Erroneous data, skip it.
continue
}
if blob, err := json.Marshal(seed); err == nil {
f.Add(blob)
}
}

// 2. Now run the fuzzer.
f.Fuzz(func(t *testing.T, fJSON []byte) {
pvs := new(CheckAgainstSpecTestStruct)
if err := json.Unmarshal(fJSON, pvs); err != nil {
return
}
if pvs.Proof == nil {
// No need checking from a nil ExistenceProof.
return
}

ep, spec := pvs.Proof, pvs.Spec
_ = ep.CheckAgainstSpec(spec)
})
}

var batchVectorDataSeeds []*BatchVectorData

func init() {
Expand Down Expand Up @@ -122,3 +155,49 @@ func FuzzCombineProofs(f *testing.F) {
_, _ = CombineProofs(proofs)
})
}

type verifyJSON struct {
Proof *CommitmentProof
Ref *RefData
Spec *ProofSpec
}

func FuzzVerifyMembership(f *testing.F) {
seeds := VectorsTestData()

// VerifyMembership requires:
// Spec, ExistenceProof, CommitmentRootref,
for _, seed := range seeds {
proof, ref := LoadFile(f, seed.Dir, seed.Filename)
root, err := proof.Calculate()
if err != nil {
continue
}
if !bytes.Equal(ref.RootHash, root) {
continue
}

if ref.Value == nil {
continue
}

// Now serialize this value as a seed.
blob, err := json.Marshal(&verifyJSON{Proof: proof, Ref: ref, Spec: seed.Spec})
if err == nil {
f.Add(blob)
}
}

// 2. Now let's run the fuzzer.
f.Fuzz(func(t *testing.T, input []byte) {
var con verifyJSON
if err := json.Unmarshal(input, &con); err != nil {
return
}
spec, ref, proof := con.Spec, con.Ref, con.Proof
if ref == nil {
return
}
_ = VerifyMembership(spec, ref.RootHash, proof, ref.Key, ref.Value)
})
}
4 changes: 4 additions & 0 deletions go/ics23.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ func CombineProofs(proofs []*CommitmentProof) (*CommitmentProof, error) {
}

func getExistProofForKey(proof *CommitmentProof, key []byte) *ExistenceProof {
if proof == nil {
return nil
}

switch p := proof.Proof.(type) {
case *CommitmentProof_Exist:
ep := p.Exist
Expand Down
13 changes: 13 additions & 0 deletions go/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ func (op *InnerOp) Apply(child []byte) ([]byte, error) {
// CheckAgainstSpec will verify the LeafOp is in the format defined in spec
func (op *LeafOp) CheckAgainstSpec(spec *ProofSpec) error {
lspec := spec.LeafSpec
if lspec == nil {
return errors.New("spec.LeafSpec must be non-nil")
}

if validateSpec(spec) {
err := validateIavlOps(op, 0)
Expand Down Expand Up @@ -123,6 +126,16 @@ func (op *LeafOp) CheckAgainstSpec(spec *ProofSpec) error {

// CheckAgainstSpec will verify the InnerOp is in the format defined in spec
func (op *InnerOp) CheckAgainstSpec(spec *ProofSpec, b int) error {
if spec == nil || op == nil {
return errors.New("op and spec must be both non-nil")
}
if spec.InnerSpec == nil {
return errors.New("spec.InnerSpec must be non-nil")
}
if spec.LeafSpec == nil {
return errors.New("spec.LeafSpec must be non-nil")
}

if op.Hash != spec.InnerSpec.Hash {
return fmt.Errorf("unexpected HashOp: %d", op.Hash)
}
Expand Down
52 changes: 40 additions & 12 deletions go/proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,11 @@ func (p *NonExistenceProof) Calculate() (CommitmentRoot, error) {

// CheckAgainstSpec will verify the leaf and all path steps are in the format defined in spec
func (p *ExistenceProof) CheckAgainstSpec(spec *ProofSpec) error {
if p.GetLeaf() == nil {
leaf := p.GetLeaf()
if leaf == nil {
return errors.New("existence Proof needs defined LeafOp")
}
err := p.Leaf.CheckAgainstSpec(spec)
if err != nil {
if err := leaf.CheckAgainstSpec(spec); err != nil {
return fmt.Errorf("leaf, %w", err)
}
if spec.MinDepth > 0 && len(p.Path) < int(spec.MinDepth) {
Expand Down Expand Up @@ -452,13 +452,41 @@ func orderFromPadding(spec *InnerSpec, inner *InnerOp) (int32, error) {

// over-declares equality, which we cosnider fine for now.
func (p *ProofSpec) SpecEquals(spec *ProofSpec) bool {
return p.LeafSpec.Hash == spec.LeafSpec.Hash &&
p.LeafSpec.PrehashKey == spec.LeafSpec.PrehashKey &&
p.LeafSpec.PrehashValue == spec.LeafSpec.PrehashValue &&
p.LeafSpec.Length == spec.LeafSpec.Length &&
p.InnerSpec.Hash == spec.InnerSpec.Hash &&
p.InnerSpec.MinPrefixLength == spec.InnerSpec.MinPrefixLength &&
p.InnerSpec.MaxPrefixLength == spec.InnerSpec.MaxPrefixLength &&
p.InnerSpec.ChildSize == spec.InnerSpec.ChildSize &&
len(p.InnerSpec.ChildOrder) == len(spec.InnerSpec.ChildOrder)
// 1. Compare LeafSpecs values.
switch {
case (p.LeafSpec == nil) != (spec.LeafSpec == nil): // One of them is nil.
return false

case p.LeafSpec != nil && spec.LeafSpec != nil:
ok := p.LeafSpec.Hash == spec.LeafSpec.Hash &&
p.LeafSpec.PrehashKey == spec.LeafSpec.PrehashKey &&
p.LeafSpec.PrehashValue == spec.LeafSpec.PrehashValue &&
p.LeafSpec.Length == spec.LeafSpec.Length
if !ok {
return false
}

default: // Both are nil, hence LeafSpec values are equal.
}

// 2. Compare InnerSpec values.
switch {
case (p.InnerSpec == nil) != (spec.InnerSpec == nil): // One of them is not nil.
return false

case p.InnerSpec != nil && spec.InnerSpec == nil: // Both are non-nil
ok := p.InnerSpec.Hash == spec.InnerSpec.Hash &&
p.InnerSpec.MinPrefixLength == spec.InnerSpec.MinPrefixLength &&
p.InnerSpec.MaxPrefixLength == spec.InnerSpec.MaxPrefixLength &&
p.InnerSpec.ChildSize == spec.InnerSpec.ChildSize &&
len(p.InnerSpec.ChildOrder) == len(spec.InnerSpec.ChildOrder)
if !ok {
return false
}

default: // Both are nil, hence InnerSpec values are equal.
}

// By this point all the above conditions pass so they are equal.
return true
}
24 changes: 12 additions & 12 deletions go/proof_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ type ExistenceProofTestStruct struct {
Expected []byte
}

func ExistenceProofTestData(t *testing.T) map[string]ExistenceProofTestStruct {
t.Helper()
func ExistenceProofTestData(tb testing.TB) map[string]ExistenceProofTestStruct {
tb.Helper()
fname := filepath.Join("..", "testdata", "TestExistenceProofData.json")
ffile, err := os.Open(fname)
if err != nil {
t.Fatal(err)
tb.Fatal(err)
}
var cases map[string]ExistenceProofTestStruct
jsonDecoder := json.NewDecoder(ffile)
err = jsonDecoder.Decode(&cases)
if err != nil {
t.Fatal(err)
tb.Fatal(err)
}
return cases
}
Expand All @@ -35,18 +35,18 @@ type CheckLeafTestStruct struct {
IsErr bool
}

func CheckLeafTestData(t *testing.T) map[string]CheckLeafTestStruct {
t.Helper()
func CheckLeafTestData(tb testing.TB) map[string]CheckLeafTestStruct {
tb.Helper()
fname := filepath.Join("..", "testdata", "TestCheckLeafData.json")
ffile, err := os.Open(fname)
if err != nil {
t.Fatal(err)
tb.Fatal(err)
}
var cases map[string]CheckLeafTestStruct
jsonDecoder := json.NewDecoder(ffile)
err = jsonDecoder.Decode(&cases)
if err != nil {
t.Fatal(err)
tb.Fatal(err)
}
return cases
}
Expand All @@ -57,18 +57,18 @@ type CheckAgainstSpecTestStruct struct {
IsErr bool
}

func CheckAgainstSpecTestData(t *testing.T) map[string]CheckAgainstSpecTestStruct {
t.Helper()
func CheckAgainstSpecTestData(tb testing.TB) map[string]CheckAgainstSpecTestStruct {
tb.Helper()
fname := filepath.Join("..", "testdata", "TestCheckAgainstSpecData.json")
ffile, err := os.Open(fname)
if err != nil {
t.Fatal(err)
tb.Fatal(err)
}
var cases map[string]CheckAgainstSpecTestStruct
jsonDecoder := json.NewDecoder(ffile)
err = jsonDecoder.Decode(&cases)
if err != nil {
t.Fatal(err)
tb.Fatal(err)
}
return cases
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"Proof\":{\"leAf\":{\"prehAsh_vAlue\":1,\"length\":1,\"prefiX\":\"AA00\"},\"pAth\":[{\"hAsh\":1}]},\"SpeC\":{\"leAf_speC\":{\"prehAsh_vAlue\":1,\"length\":1,\"prefiX\":\"AA00\"},\"inner_speC\":{\"hAsh\":1}}}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"Proof\":{\"leAf\":{},\"pAth\":[{}]},\"SpeC\":{\"leAf_speC\":{}}}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"Proof\":{\"leAf\":{}},\"SpeC\":{}}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"Proof\":{\"leAf\":{}}}")
2 changes: 2 additions & 0 deletions go/testdata/fuzz/FuzzVerifyMembership/8093511184ad3e25
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{}")
2 changes: 2 additions & 0 deletions go/testdata/fuzz/FuzzVerifyMembership/99dd1125ca292163
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("{\"Ref\":{}}")
24 changes: 12 additions & 12 deletions go/vectors_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,42 +165,42 @@ func DecompressBatchVectorsTestData(t *testing.T) map[string]*CommitmentProof {
}
}

func LoadFile(t *testing.T, dir string, filename string) (*CommitmentProof, *RefData) {
t.Helper()
func LoadFile(tb testing.TB, dir string, filename string) (*CommitmentProof, *RefData) {
tb.Helper()
// load the file into a json struct
name := filepath.Join(dir, filename)
bz, err := os.ReadFile(name)
if err != nil {
t.Fatalf("Read file: %+v", err)
tb.Fatalf("Read file: %+v", err)
}
var data TestVector
err = json.Unmarshal(bz, &data)
if err != nil {
t.Fatalf("Unmarshal json: %+v", err)
tb.Fatalf("Unmarshal json: %+v", err)
}
// parse the protobuf object
var proof CommitmentProof
err = proof.Unmarshal(mustHex(t, data.Proof))
err = proof.Unmarshal(mustHex(tb, data.Proof))
if err != nil {
t.Fatalf("Unmarshal protobuf: %+v", err)
tb.Fatalf("Unmarshal protobuf: %+v", err)
}
var ref RefData
ref.RootHash = CommitmentRoot(mustHex(t, data.RootHash))
ref.Key = mustHex(t, data.Key)
ref.RootHash = CommitmentRoot(mustHex(tb, data.RootHash))
ref.Key = mustHex(tb, data.Key)
if data.Value != "" {
ref.Value = mustHex(t, data.Value)
ref.Value = mustHex(tb, data.Value)
}
return &proof, &ref
}

func mustHex(t *testing.T, data string) []byte {
t.Helper()
func mustHex(tb testing.TB, data string) []byte {
tb.Helper()
if data == "" {
return nil
}
res, err := hex.DecodeString(data)
if err != nil {
t.Fatalf("decoding hex: %v", err)
tb.Fatalf("decoding hex: %v", err)
}
return res
}
Expand Down

0 comments on commit e80f323

Please sign in to comment.