diff --git a/parser/block.go b/parser/block.go index de78f9c0..46499c02 100644 --- a/parser/block.go +++ b/parser/block.go @@ -72,6 +72,7 @@ func (b *Block) GetDisplayPrevHashString() string { // see https://github.com/zcash/lightwalletd/issues/17#issuecomment-467110828 const genesisTargetDifficulty = 520617983 +const minTransactionWireBytes = 5 // 4-byte header + at least one CompactSize byte // GetHeight extracts the block height from the coinbase transaction. See // BIP34. Returns block height on success, or -1 on error. @@ -140,6 +141,9 @@ func (b *Block) ParseFromSlice(data []byte) (rest []byte, err error) { if !s.ReadCompactSize(&txCount) { return nil, errors.New("could not read tx_count") } + if err := rejectCountExceedingRemaining("tx_count", txCount, len(s), minTransactionWireBytes); err != nil { + return nil, err + } data = []byte(s) vtx := make([]*Transaction, 0, txCount) diff --git a/parser/block_header.go b/parser/block_header.go index ea5a77fe..088146df 100644 --- a/parser/block_header.go +++ b/parser/block_header.go @@ -181,6 +181,9 @@ func (hdr *BlockHeader) ParseFromSlice(in []byte) (rest []byte, err error) { if !s.ReadCompactSize(&length) { return in, errors.New("could not read compact size of solution") } + if err := rejectCountExceedingRemaining("solution_length", length, len(s), 1); err != nil { + return in, err + } hdr.Solution = make([]byte, length) if !s.ReadBytes(&hdr.Solution, length) { return in, errors.New("could not read CompactSize-prefixed Equihash solution") diff --git a/parser/block_header_test.go b/parser/block_header_test.go index 95a9686f..8c1013b4 100644 --- a/parser/block_header_test.go +++ b/parser/block_header_test.go @@ -9,6 +9,7 @@ import ( "encoding/hex" "math/big" "os" + "strings" "testing" "github.com/zcash/lightwalletd/hash32" @@ -183,6 +184,24 @@ func TestBadBlockHeader(t *testing.T) { } } +func TestBlockHeaderRejectsSolutionLengthThatCannotFit(t *testing.T) { + // 140-byte block header prefix followed by solution_length=1 and no + // solution bytes. + blockData := make([]byte, 140) + blockData[0] = 0x04 + blockData = append(blockData, 0x01) + + blockHeader := NewBlockHeader() + _, err := blockHeader.ParseFromSlice(blockData) + if err == nil { + t.Fatal("expected error") + } + wantErr := "solution_length 1 exceeds remaining input length 0" + if !strings.Contains(err.Error(), wantErr) { + t.Fatalf("error mismatch:\nhave: %v\nwant substring: %s", err, wantErr) + } +} + var compactLengthPrefixedLenTests = []struct { length int returnLength int diff --git a/parser/block_test.go b/parser/block_test.go index 6d5b3740..b80a7e94 100644 --- a/parser/block_test.go +++ b/parser/block_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "testing" protobuf "github.com/golang/protobuf/proto" @@ -73,3 +74,22 @@ func TestCompactBlocks(t *testing.T) { } } + +func TestParseBlockRejectsTransactionCountThatCannotFit(t *testing.T) { + // 141-byte block header with version=4, zero-valued header fields, and an + // empty CompactSize-prefixed Equihash solution, followed by tx_count=1 and + // no transaction bytes. + blockData := make([]byte, 141) + blockData[0] = 0x04 + blockData = append(blockData, 0x01) + + block := NewBlock() + _, err := block.ParseFromSlice(blockData) + if err == nil { + t.Fatal("expected error") + } + wantErr := "tx_count 1 exceeds remaining input length 0" + if !strings.Contains(err.Error(), wantErr) { + t.Fatalf("error mismatch:\nhave: %v\nwant substring: %s", err, wantErr) + } +} diff --git a/parser/transaction.go b/parser/transaction.go index a45ba60e..89fd47b8 100644 --- a/parser/transaction.go +++ b/parser/transaction.go @@ -107,6 +107,32 @@ func (toutput *txOut) ToCompact() *walletrpc.TxOut { } } +const ( + minTxInWireBytes = 41 // 32-byte prevout hash + 4-byte index + 1-byte script length + 4-byte sequence + minTxOutWireBytes = 9 // 8-byte value + 1-byte script length + minSaplingV4SpendBytes = 384 // cv + anchor + nullifier + rk + zkproof + spendAuthSig + minSaplingV4OutputBytes = 948 // cv + cmu + ephemeralKey + encCiphertext + outCiphertext + zkproof + minSaplingV5SpendBytes = 96 // cv + nullifier + rk + minSaplingV5OutputBytes = 756 // cv + cmu + ephemeralKey + encCiphertext + outCiphertext + minOrchardActionBytes = 820 // cv + nullifier + rk + cmx + ephemeralKey + encCiphertext + outCiphertext + minJoinSplitGrothBytes = 1698 + minJoinSplitPHGRBytes = 1802 +) + +func rejectCountExceedingRemaining(label string, count int, remaining int, minElementBytes int) error { + if count > remaining/minElementBytes { + return fmt.Errorf("%s %d exceeds remaining input length %d", label, count, remaining) + } + return nil +} + +func minJoinSplitWireBytes(isGroth16Proof bool) int { + if isGroth16Proof { + return minJoinSplitGrothBytes + } + return minJoinSplitPHGRBytes +} + // parse the transparent parts of the transaction func (tx *Transaction) ParseTransparent(data []byte) ([]byte, error) { s := bytestring.String(data) @@ -114,6 +140,9 @@ func (tx *Transaction) ParseTransparent(data []byte) ([]byte, error) { if !s.ReadCompactSize(&txInCount) { return nil, errors.New("could not read tx_in_count") } + if err := rejectCountExceedingRemaining("tx_in_count", txInCount, len(s), minTxInWireBytes); err != nil { + return nil, err + } var err error tx.transparentInputs = make([]txIn, txInCount) for i := 0; i < txInCount; i++ { @@ -128,6 +157,9 @@ func (tx *Transaction) ParseTransparent(data []byte) ([]byte, error) { if !s.ReadCompactSize(&txOutCount) { return nil, errors.New("could not read tx_out_count") } + if err := rejectCountExceedingRemaining("tx_out_count", txOutCount, len(s), minTxOutWireBytes); err != nil { + return nil, err + } tx.transparentOutputs = make([]txOut, txOutCount) for i := 0; i < txOutCount; i++ { to := &tx.transparentOutputs[i] @@ -457,6 +489,9 @@ func (tx *Transaction) parsePreV5(data []byte) ([]byte, error) { if !s.ReadCompactSize(&spendCount) { return nil, errors.New("could not read nShieldedSpend") } + if err := rejectCountExceedingRemaining("nShieldedSpend", spendCount, len(s), minSaplingV4SpendBytes); err != nil { + return nil, err + } tx.shieldedSpends = make([]spend, spendCount) for i := 0; i < spendCount; i++ { newSpend := &tx.shieldedSpends[i] @@ -468,6 +503,9 @@ func (tx *Transaction) parsePreV5(data []byte) ([]byte, error) { if !s.ReadCompactSize(&outputCount) { return nil, errors.New("could not read nShieldedOutput") } + if err := rejectCountExceedingRemaining("nShieldedOutput", outputCount, len(s), minSaplingV4OutputBytes); err != nil { + return nil, err + } tx.shieldedOutputs = make([]output, outputCount) for i := 0; i < outputCount; i++ { newOutput := &tx.shieldedOutputs[i] @@ -482,6 +520,9 @@ func (tx *Transaction) parsePreV5(data []byte) ([]byte, error) { if !s.ReadCompactSize(&joinSplitCount) { return nil, errors.New("could not read nJoinSplit") } + if err := rejectCountExceedingRemaining("nJoinSplit", joinSplitCount, len(s), minJoinSplitWireBytes(tx.isGroth16Proof())); err != nil { + return nil, err + } tx.joinSplits = make([]joinSplit, joinSplitCount) if joinSplitCount > 0 { @@ -532,6 +573,9 @@ func (tx *Transaction) parseV5(data []byte) ([]byte, error) { if !s.ReadCompactSize(&spendCount) { return nil, errors.New("could not read nShieldedSpend") } + if err := rejectCountExceedingRemaining("nShieldedSpend", spendCount, len(s), minSaplingV5SpendBytes); err != nil { + return nil, err + } if spendCount >= (1 << 16) { return nil, fmt.Errorf("spentCount (%d) must be less than 2^16", spendCount) } @@ -546,6 +590,9 @@ func (tx *Transaction) parseV5(data []byte) ([]byte, error) { if !s.ReadCompactSize(&outputCount) { return nil, errors.New("could not read nShieldedOutput") } + if err := rejectCountExceedingRemaining("nShieldedOutput", outputCount, len(s), minSaplingV5OutputBytes); err != nil { + return nil, err + } if outputCount >= (1 << 16) { return nil, fmt.Errorf("outputCount (%d) must be less than 2^16", outputCount) } @@ -579,6 +626,9 @@ func (tx *Transaction) parseV5(data []byte) ([]byte, error) { if !s.ReadCompactSize(&actionsCount) { return nil, errors.New("could not read nActionsOrchard") } + if err := rejectCountExceedingRemaining("nActionsOrchard", actionsCount, len(s), minOrchardActionBytes); err != nil { + return nil, err + } if actionsCount >= (1 << 16) { return nil, fmt.Errorf("actionsCount (%d) must be less than 2^16", actionsCount) } diff --git a/parser/transaction_test.go b/parser/transaction_test.go index 86493e50..6d5abe11 100644 --- a/parser/transaction_test.go +++ b/parser/transaction_test.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "encoding/json" "os" + "strings" "testing" ) @@ -113,3 +114,138 @@ func TestV5TransactionParser(t *testing.T) { } } } + +func TestParseTransparentRejectsCountsThatCannotFit(t *testing.T) { + tests := []struct { + name string + data []byte + wantErr string + }{ + { + name: "transparent inputs", + data: []byte{0x01}, + wantErr: "tx_in_count 1 exceeds remaining input length 0", + }, + { + name: "transparent outputs", + data: []byte{0x00, 0x01}, + wantErr: "tx_out_count 1 exceeds remaining input length 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tx := NewTransaction() + _, err := tx.ParseTransparent(tt.data) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("error mismatch:\nhave: %v\nwant substring: %s", err, tt.wantErr) + } + }) + } +} + +func requireErrorContains(t *testing.T, err error, want string) { + t.Helper() + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), want) { + t.Fatalf("error mismatch:\nhave: %v\nwant substring: %s", err, want) + } +} + +func saplingV4Prefix() []byte { + return []byte{ + 0x00, // tx_in_count + 0x00, // tx_out_count + 0x00, 0x00, 0x00, 0x00, // nLockTime + 0x00, 0x00, 0x00, 0x00, // nExpiryHeight + 0x00, 0x00, 0x00, 0x00, // valueBalance + 0x00, 0x00, 0x00, 0x00, + } +} + +func TestParsePreV5RejectsCountsThatCannotFit(t *testing.T) { + tests := []struct { + name string + data []byte + wantErr string + }{ + { + name: "sapling spends", + data: append(saplingV4Prefix(), 0x01), + wantErr: "nShieldedSpend 1 exceeds remaining input length 0", + }, + { + name: "sapling outputs", + data: append(saplingV4Prefix(), 0x00, 0x01), + wantErr: "nShieldedOutput 1 exceeds remaining input length 0", + }, + { + name: "join splits", + data: append(saplingV4Prefix(), 0x00, 0x00, 0x01), + wantErr: "nJoinSplit 1 exceeds remaining input length 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tx := NewTransaction() + tx.fOverwintered = true + tx.version = SAPLING_TX_VERSION + tx.nVersionGroupID = SAPLING_VERSION_GROUP_ID + + _, err := tx.parsePreV5(tt.data) + requireErrorContains(t, err, tt.wantErr) + }) + } +} + +func zip225V5Prefix() []byte { + return []byte{ + 0x00, 0x00, 0x00, 0x00, // nConsensusBranchId + 0x00, 0x00, 0x00, 0x00, // nLockTime + 0x00, 0x00, 0x00, 0x00, // nExpiryHeight + 0x00, // tx_in_count + 0x00, // tx_out_count + } +} + +func TestParseV5RejectsCountsThatCannotFit(t *testing.T) { + tests := []struct { + name string + data []byte + wantErr string + }{ + { + name: "sapling spends", + data: append(zip225V5Prefix(), 0x01), + wantErr: "nShieldedSpend 1 exceeds remaining input length 0", + }, + { + name: "sapling outputs", + data: append(zip225V5Prefix(), 0x00, 0x01), + wantErr: "nShieldedOutput 1 exceeds remaining input length 0", + }, + { + name: "orchard actions", + data: append(zip225V5Prefix(), 0x00, 0x00, 0x01), + wantErr: "nActionsOrchard 1 exceeds remaining input length 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tx := NewTransaction() + tx.fOverwintered = true + tx.version = ZIP225_TX_VERSION + tx.nVersionGroupID = ZIP225_VERSION_GROUP_ID + + _, err := tx.parseV5(tt.data) + requireErrorContains(t, err, tt.wantErr) + }) + } +}