Skip to content

Commit 0cba26d

Browse files
authored
Merge pull request #2374 from ffranr/2372-fix-bip32-derivation-overflow-bug
psbt: overflow checks when computing Taproot BIP32 derivation min size
2 parents 253c36d + 565a7a8 commit 0cba26d

File tree

2 files changed

+151
-3
lines changed

2 files changed

+151
-3
lines changed

btcutil/psbt/psbt_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"encoding/base64"
1010
"encoding/binary"
1111
"encoding/hex"
12+
"math"
1213
"strings"
1314
"testing"
1415

@@ -1447,6 +1448,89 @@ func TestNonWitnessToWitness(t *testing.T) {
14471448
}
14481449
}
14491450

1451+
// TestPSBTNumberOfHashesOverflow tests the case where the number of hashes
1452+
// in the PSBT exceeds the maximum allowed value. This is a regression test
1453+
// for a bug that was fixed in the PSBT library.
1454+
func TestPSBTNumberOfHashesOverflow(t *testing.T) {
1455+
// This hex string represents a PSBT with an invalid number of hashes. The
1456+
// PSBT library should return an error when trying to parse this PSBT.
1457+
//
1458+
// TODO(ffranr): Is there a more minimal PSBT example?
1459+
hexString := "70736274ff01007374ff01030100000000002f0000002e2873007374" +
1460+
"ff01070100000000000000000000000000000000000000060680050000736274f" +
1461+
"f01000a0000000060c70006060000736274ff01000a0000010000010024070100" +
1462+
"00000000000000000000000000000000000006060000736274ff01000a0000000" +
1463+
"000010024c760002a707362c760000b0500000000000000060605000073626274" +
1464+
"ff01000a000000000001002421212121212121212121212121212121212121212" +
1465+
"12121212121212121212121212121212107010000000000000000000000000000" +
1466+
"000000000006060000736274ff01000a000eff000001000a0a040404040404040" +
1467+
"400"
1468+
1469+
// Convert hex string to byte slice
1470+
buffer, err := hex.DecodeString(hexString)
1471+
require.NoError(t, err)
1472+
1473+
// Attempt to parse the PSBT.
1474+
psbt, err := NewFromRawBytes(bytes.NewBuffer(buffer), false)
1475+
require.Nil(t, psbt)
1476+
require.ErrorIs(t, err, ErrInvalidPsbtFormat)
1477+
}
1478+
1479+
// TestMinTaprootBip32DerivationByteSize tests the
1480+
// minTaprootBip32DerivationByteSize function to ensure it correctly calculates
1481+
// the minimum byte size of the Taproot BIP32 derivation path.
1482+
func TestMinTaprootBip32DerivationByteSize(t *testing.T) {
1483+
tests := []struct {
1484+
label string
1485+
numHashes uint64
1486+
expectedSize uint64
1487+
expectErr bool
1488+
}{
1489+
{
1490+
label: "only compact size + fingerprint",
1491+
numHashes: 0,
1492+
expectedSize: 5,
1493+
expectErr: false,
1494+
},
1495+
{
1496+
label: "numHashes == 1, therefore: 1 * 32 + 5",
1497+
numHashes: 1,
1498+
expectedSize: 37,
1499+
expectErr: false,
1500+
},
1501+
{
1502+
label: "numHashes == 2, therefore: 2 * 32 + 5",
1503+
numHashes: 2,
1504+
expectedSize: 69,
1505+
expectErr: false,
1506+
},
1507+
{
1508+
label: "overflow expected",
1509+
numHashes: math.MaxUint64,
1510+
expectedSize: 0,
1511+
expectErr: true,
1512+
},
1513+
}
1514+
1515+
for _, tt := range tests {
1516+
actualSize, err := minTaprootBip32DerivationByteSize(tt.numHashes)
1517+
1518+
if (err != nil) != tt.expectErr {
1519+
t.Errorf(
1520+
"%s (numHashes=%d, unexpected_error=%v)", tt.label,
1521+
tt.numHashes, err,
1522+
)
1523+
continue
1524+
}
1525+
1526+
if err == nil && actualSize != tt.expectedSize {
1527+
t.Errorf("%s (numHashes=%d, actualSize=%d, expectedSize=%d)",
1528+
tt.label, tt.numHashes, actualSize, tt.expectedSize,
1529+
)
1530+
}
1531+
}
1532+
}
1533+
14501534
// TestEmptyInputSerialization tests the special serialization case for a wire
14511535
// transaction that has no inputs.
14521536
func TestEmptyInputSerialization(t *testing.T) {

btcutil/psbt/taproot.go

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package psbt
22

33
import (
44
"bytes"
5+
"math"
6+
"math/bits"
57

68
"github.com/btcsuite/btcd/btcec/v2/schnorr"
79
"github.com/btcsuite/btcd/txscript"
@@ -93,12 +95,61 @@ func (s *TaprootBip32Derivation) SortBefore(other *TaprootBip32Derivation) bool
9395
return bytes.Compare(s.XOnlyPubKey, other.XOnlyPubKey) < 0
9496
}
9597

98+
// minTaprootBip32DerivationByteSize returns the minimum number of bytes
99+
// required to encode a Taproot BIP32 derivation field, given the number of
100+
// leaf hashes.
101+
//
102+
// NOTE: This function does not account for the size of the BIP32 child indexes,
103+
// as we are only computing the minimum size (which occurs when the path is
104+
// empty). The bits package is used to safely detect and handle overflows.
105+
func minTaprootBip32DerivationByteSize(numHashes uint64) (uint64, error) {
106+
// The Taproot BIP32 derivation field is encoded as:
107+
// [compact size uint: number of leaf hashes]
108+
// [N × 32 bytes: leaf hashes]
109+
// [4 bytes: master key fingerprint]
110+
// [M × 4 bytes: BIP32 child indexes]
111+
//
112+
// To compute the minimum size given the number of hashes only, we assume:
113+
// - N = numHashes (provided)
114+
// - M = 0 (no child indexes)
115+
//
116+
// So the base byte size is:
117+
// 1 (leaf hash count) + (N × 32) + 4 (fingerprint)
118+
//
119+
// First, we calculate the total number of bytes for the leaf hashes.
120+
mulCarry, totalHashesBytes := bits.Mul64(numHashes, 32)
121+
if mulCarry != 0 {
122+
return 0, ErrInvalidPsbtFormat
123+
}
124+
125+
// Since we're computing the minimum possible size, we add a constant that
126+
// accounts for the fixed size fields:
127+
// * 1 byte for the compact size leaf hash count (assumes numHashes < 0xfd)
128+
// * 4 bytes for the master key fingerprint
129+
// Total: 5 bytes.
130+
// All other fields (e.g., BIP32 path) are assumed absent for minimum size
131+
// calculation.
132+
result, addCarry := bits.Add64(5, totalHashesBytes, 0)
133+
if addCarry != 0 {
134+
return 0, ErrInvalidPsbtFormat
135+
}
136+
137+
return result, nil
138+
}
139+
96140
// ReadTaprootBip32Derivation deserializes a byte slice containing the Taproot
97141
// BIP32 derivation info that consists of a list of leaf hashes as well as the
98142
// normal BIP32 derivation info.
99143
func ReadTaprootBip32Derivation(xOnlyPubKey,
100144
value []byte) (*TaprootBip32Derivation, error) {
101145

146+
// This function allocates additional memory while parsing the serialized
147+
// data. To prevent potential out-of-memory (OOM) issues, we must validate
148+
// the length of the value slice before proceeding.
149+
if len(value) > MaxPsbtValueLength {
150+
return nil, ErrInvalidPsbtFormat
151+
}
152+
102153
// The taproot key BIP 32 derivation path is defined as:
103154
// <hashes len> <leaf hash>* <4 byte fingerprint> <32-bit uint>*
104155
// So we get at least 5 bytes for the length and the 4 byte fingerprint.
@@ -113,9 +164,22 @@ func ReadTaprootBip32Derivation(xOnlyPubKey,
113164
return nil, ErrInvalidPsbtFormat
114165
}
115166

116-
// A hash is 32 bytes in size, so we need at least numHashes*32 + 5
117-
// bytes to be present.
118-
if len(value) < (int(numHashes)*32)+5 {
167+
// As a safety/sanity check, verify that the hash count fits in a `uint32`.
168+
// This isn’t mandated by BIP‑371, but it prevents overflow and limits
169+
// derivations to about 137 GiB of data.
170+
if numHashes > math.MaxUint32 {
171+
return nil, ErrInvalidPsbtFormat
172+
}
173+
174+
// Given the number of hashes, we can calculate the minimum byte size
175+
// of the taproot BIP32 derivation.
176+
minByteSize, err := minTaprootBip32DerivationByteSize(numHashes)
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
// Ensure that value is at least the minimum size.
182+
if uint64(len(value)) < minByteSize {
119183
return nil, ErrInvalidPsbtFormat
120184
}
121185

0 commit comments

Comments
 (0)