Skip to content

Commit

Permalink
fix(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. To ensure that the fuzzers can run alright,
added -short to any invocations of "go test".

Fixes #241
Fixes #242
Fixes #243
  • Loading branch information
odeke-em committed Dec 12, 2023
1 parent e18fc4d commit e43e4a9
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 37 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
go-version: "1.21"
- name: Run coverage
working-directory: ./go
run: go test -race -coverprofile=coverage.txt -covermode=atomic
run: go test -short -race -coverprofile=coverage.txt -covermode=atomic
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
Expand Down
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
20 changes: 20 additions & 0 deletions go/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,13 @@ 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 {
if op == nil || spec == nil {
return errors.New("op and spec must be non-nil")
}

Check warning on line 99 in go/ops.go

View check run for this annotation

Codecov / codecov/patch

go/ops.go#L98-L99

Added lines #L98 - L99 were not covered by tests
lspec := spec.LeafSpec
if lspec == nil {
return errors.New("spec.LeafSpec must be non-nil")
}

Check warning on line 103 in go/ops.go

View check run for this annotation

Codecov / codecov/patch

go/ops.go#L102-L103

Added lines #L102 - L103 were not covered by tests

if validateSpec(spec) {
err := validateIavlOps(op, 0)
Expand Down Expand Up @@ -123,6 +129,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")
}

Check warning on line 134 in go/ops.go

View check run for this annotation

Codecov / codecov/patch

go/ops.go#L133-L134

Added lines #L133 - L134 were not covered by tests
if spec.InnerSpec == nil {
return errors.New("spec.InnerSpec must be non-nil")
}

Check warning on line 137 in go/ops.go

View check run for this annotation

Codecov / codecov/patch

go/ops.go#L136-L137

Added lines #L136 - L137 were not covered by tests
if spec.LeafSpec == nil {
return errors.New("spec.LeafSpec must be non-nil")
}

Check warning on line 140 in go/ops.go

View check run for this annotation

Codecov / codecov/patch

go/ops.go#L139-L140

Added lines #L139 - L140 were not covered by tests

if op.Hash != spec.InnerSpec.Hash {
return fmt.Errorf("unexpected HashOp: %d", op.Hash)
}
Expand All @@ -146,6 +162,10 @@ func (op *InnerOp) CheckAgainstSpec(spec *ProofSpec, b int) error {
return fmt.Errorf("innerOp prefix too long (%d)", len(op.Prefix))
}

if spec.InnerSpec.ChildSize <= 0 {
return errors.New("spec.InnerSpec.ChildSize must be >= 1")
}

Check warning on line 167 in go/ops.go

View check run for this annotation

Codecov / codecov/patch

go/ops.go#L166-L167

Added lines #L166 - L167 were not covered by tests

// ensures soundness, with suffix having to be of correct length
if len(op.Suffix)%int(spec.InnerSpec.ChildSize) != 0 {
return fmt.Errorf("InnerOp suffix malformed")
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

Check warning on line 458 in go/proof.go

View check run for this annotation

Codecov / codecov/patch

go/proof.go#L457-L458

Added lines #L457 - L458 were not covered by tests

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.

Check warning on line 469 in go/proof.go

View check run for this annotation

Codecov / codecov/patch

go/proof.go#L469

Added line #L469 was not covered by tests
}

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

Check warning on line 475 in go/proof.go

View check run for this annotation

Codecov / codecov/patch

go/proof.go#L474-L475

Added lines #L474 - L475 were not covered by tests

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.

Check warning on line 487 in go/proof.go

View check run for this annotation

Codecov / codecov/patch

go/proof.go#L487

Added line #L487 was not covered by tests
}

// 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 e43e4a9

Please sign in to comment.