sweepbatcher: reject malformed MuSig2 cosign data

The cooperative batch sweep path receives a server nonce and partial signature before constructing a keyspend witness. Validate both byte slice lengths before registering the nonce or combining signatures, so malformed server responses fail explicitly instead of being zero-padded into fixed-size MuSig2 buffers.

Update batcher test helpers to return size-correct placeholder signing data under the stricter validation.
This commit is contained in:
Slyghtning 2026-05-27 12:23:36 +02:00
parent db9bd06629
commit 605e72a261
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
2 changed files with 88 additions and 6 deletions

View file

@ -1882,6 +1882,12 @@ func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep,
return nil, err
}
if err := validateServerMuSig2SigningData(
serverNonce, serverSig,
); err != nil {
return nil, err
}
var serverPublicNonce [musig2.PubNonceSize]byte
copy(serverPublicNonce[:], serverNonce)
@ -1934,6 +1940,26 @@ func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep,
return finalSig, nil
}
// validateServerMuSig2SigningData rejects malformed MuSig2 cosigning data
// received from the server by checking that the nonce and partial signature
// have the expected lengths before they are passed to the signer.
func validateServerMuSig2SigningData(serverNonce,
serverSig []byte) error {
if len(serverNonce) != musig2.PubNonceSize {
return fmt.Errorf("invalid server nonce length: got %d, "+
"want %d", len(serverNonce), musig2.PubNonceSize)
}
if len(serverSig) != input.MuSig2PartialSigSize {
return fmt.Errorf("invalid server partial signature "+
"length: got %d, want %d", len(serverSig),
input.MuSig2PartialSigSize)
}
return nil
}
// updateRbfRate updates the fee rate we should use for the new batch
// transaction. This fee rate does not guarantee RBF success, but the continuous
// increase leads to an eventual successful RBF replacement.

View file

@ -15,6 +15,7 @@ import (
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@ -87,7 +88,62 @@ func testMuSig2SignSweep(ctx context.Context,
prevoutMap map[wire.OutPoint]*wire.TxOut) (
[]byte, []byte, error) {
return nil, nil, nil
return testMuSig2SigningData()
}
// testMuSig2SigningData returns size-correct placeholder data. These tests
// only exercise control flow around the signing response, not cryptographic
// validity.
func testMuSig2SigningData() ([]byte, []byte, error) {
return make([]byte, musig2.PubNonceSize),
make([]byte, input.MuSig2PartialSigSize), nil
}
// TestValidateServerMuSig2SigningData ensures that MuSig2 cosigning data from
// the server is accepted when well-formed and rejected when the nonce or
// partial signature has an unexpected length.
func TestValidateServerMuSig2SigningData(t *testing.T) {
validNonce, validSig, err := testMuSig2SigningData()
require.NoError(t, err)
testCases := []struct {
name string
serverNonce []byte
serverSig []byte
errContains string
}{
{
name: "valid signing data",
serverNonce: validNonce,
serverSig: validSig,
},
{
name: "invalid nonce length",
serverNonce: validNonce[:musig2.PubNonceSize-1],
serverSig: validSig,
errContains: "invalid server nonce length",
},
{
name: "invalid partial signature length",
serverNonce: validNonce,
serverSig: validSig[:input.MuSig2PartialSigSize-1],
errContains: "invalid server partial signature length",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := validateServerMuSig2SigningData(
tc.serverNonce, tc.serverSig,
)
if tc.errContains == "" {
require.NoError(t, err)
return
}
require.ErrorContains(t, err, tc.errContains)
})
}
}
var customSignature = func() []byte {
@ -5024,7 +5080,7 @@ func testWithMixedBatch(t *testing.T, store testStore,
[]byte, []byte, error) {
if swapHash == swapHashes[2] {
return nil, nil, nil
return testMuSig2SigningData()
} else {
return nil, nil, fmt.Errorf("test error")
}
@ -5377,14 +5433,14 @@ func testWithMixedBatchLarge(t *testing.T, store testStore,
} else {
swapHash2Used = true
return nil, nil, nil
return testMuSig2SigningData()
}
case swapHash == preimages[5].Hash():
return nil, nil, nil
return testMuSig2SigningData()
case swapHash == preimages[8].Hash():
return nil, nil, nil
return testMuSig2SigningData()
default:
return nil, nil, fmt.Errorf("test error")
@ -5431,7 +5487,7 @@ func testWithMixedBatchCoopOnly(t *testing.T, store testStore,
prevoutMap map[wire.OutPoint]*wire.TxOut) (
[]byte, []byte, error) {
return nil, nil, nil
return testMuSig2SigningData()
}
// All the sweeps are cooperative.