Skip to content

Commit e0e1da5

Browse files
authored
Merge pull request lightninglabs#1148 from hieblmi/harden-musig2-handling
multi: validate server-provided signing data and clean up comments
2 parents aa772fa + cc0392a commit e0e1da5

16 files changed

Lines changed: 389 additions & 44 deletions

liquidity/autoloop_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func TestAutoLoopDisabled(t *testing.T) {
7878
c.stop()
7979
}
8080

81-
// TestAutoLoopEnabled tests enabling the liquidity manger's autolooper. To keep
81+
// TestAutoLoopEnabled tests enabling the liquidity manager's autolooper. To keep
8282
// the test simple, we do not update actual lnd channel balances, but rather
8383
// run our mock with two channels that will always require a loop out according
8484
// to our rules. This allows us to test the other restrictions placed on the

liquidity/liquidity.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ func (m *Manager) Run(ctx context.Context) error {
340340
}
341341
}
342342

343-
// Try to automatically dispach an asset auto-loop.
343+
// Try to automatically dispatch an asset auto-loop.
344344
for assetID := range m.params.AssetAutoloopParams {
345345
err = m.easyAssetAutoloop(ctx, assetID)
346346
if err != nil {

loopdb/postgres.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type PostgresConfig struct {
3939
RequireSSL bool `long:"requiressl" description:"Whether to require using SSL (mode: require) when connecting to the server."`
4040
}
4141

42-
// DSN returns the dns to connect to the database.
42+
// DSN returns the data source name used to connect to the database.
4343
func (s *PostgresConfig) DSN(hidePassword bool) string {
4444
var sslMode = "disable"
4545
if s.RequireSSL {

loopdb/protocol_version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const (
2424
ProtocolVersionSegwitLoopIn ProtocolVersion = 2
2525

2626
// ProtocolVersionPreimagePush indicates that the client will push loop
27-
// out preimages to the sever to speed up claim.
27+
// out preimages to the server to speed up claim.
2828
ProtocolVersionPreimagePush ProtocolVersion = 3
2929

3030
// ProtocolVersionUserExpiryLoopOut indicates that the client will

loopout_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ func testCustomSweepConfTarget(t *testing.T) {
478478
require.Equal(t, swap.Preimage, preimage)
479479
}
480480

481-
// Now that we have pushed our preimage to the sever, we send an update
481+
// Now that we have pushed our preimage to the server, we send an update
482482
// indicating that our off chain htlc is settled. We do this so that
483483
// we don't have to keep consuming preimage pushes from our server mock
484484
// for every sweep attempt.
@@ -779,22 +779,22 @@ func testPreimagePush(t *testing.T) {
779779
require.NoError(t, <-errChan)
780780
}
781781

782-
// TestFailedOffChainCancelation tests sending of a cancelation message to
782+
// TestFailedOffChainCancellation tests sending of a cancellation message to
783783
// the server when a swap fails due to off-chain routing.
784-
func TestFailedOffChainCancelation(t *testing.T) {
784+
func TestFailedOffChainCancellation(t *testing.T) {
785785
t.Run("stable protocol", func(t *testing.T) {
786-
testFailedOffChainCancelation(t)
786+
testFailedOffChainCancellation(t)
787787
})
788788

789789
t.Run("experimental protocol", func(t *testing.T) {
790790
loopdb.EnableExperimentalProtocol()
791791
defer loopdb.ResetCurrentProtocolVersion()
792792

793-
testFailedOffChainCancelation(t)
793+
testFailedOffChainCancellation(t)
794794
})
795795
}
796796

797-
func testFailedOffChainCancelation(t *testing.T) {
797+
func testFailedOffChainCancellation(t *testing.T) {
798798
defer test.Guard(t)()
799799

800800
lnd := test.NewMockLnd()
@@ -873,7 +873,7 @@ func testFailedOffChainCancelation(t *testing.T) {
873873
FailureSourceIndex: 1,
874874
},
875875
},
876-
// Add one htlc that failed in the network at wide.
876+
// Add one htlc that failed in the network at large.
877877
{
878878
Status: lnrpc.HTLCAttempt_FAILED,
879879
Route: &lnrpc.Route{
@@ -892,7 +892,7 @@ func testFailedOffChainCancelation(t *testing.T) {
892892
State: lnrpc.Payment_SUCCEEDED,
893893
}
894894

895-
// We want to fail our swap payment and succeed the prepush, so we send
895+
// We want to fail our swap payment and succeed the prepayment, so we send
896896
// a failure update to the payment that has the larger amount.
897897
if pmt1.Amount > pmt2.Amount {
898898
pmt1.TrackPaymentMessage.Updates <- failUpdate
@@ -908,7 +908,7 @@ func testFailedOffChainCancelation(t *testing.T) {
908908
require.NoError(t, err)
909909

910910
payAddr := invoice.PaymentAddr.UnwrapOrFail(t)
911-
swapCancelation := &outCancelDetails{
911+
swapCancellation := &outCancelDetails{
912912
hash: swap.hash,
913913
paymentAddr: payAddr,
914914
metadata: routeCancelMetadata{
@@ -920,7 +920,7 @@ func testFailedOffChainCancelation(t *testing.T) {
920920
},
921921
},
922922
}
923-
server.assertSwapCanceled(t, swapCancelation)
923+
server.assertSwapCanceled(t, swapCancellation)
924924

925925
// Finally, the swap should be recorded with failed off chain timeout.
926926
cfg.store.(*loopdb.StoreMock).AssertLoopOutState(

server_mock_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import (
66
"testing"
77
"time"
88

9+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
910
"github.com/btcsuite/btcd/btcutil"
1011
"github.com/btcsuite/btcd/chaincfg"
1112
"github.com/btcsuite/btcd/wire"
1213
"github.com/lightninglabs/lndclient"
1314
"github.com/lightninglabs/loop/loopdb"
1415
"github.com/lightninglabs/loop/test"
16+
"github.com/lightningnetwork/lnd/input"
1517
invpkg "github.com/lightningnetwork/lnd/invoices"
1618
"github.com/lightningnetwork/lnd/lntypes"
1719
"github.com/lightningnetwork/lnd/lnwire"
@@ -32,6 +34,13 @@ var (
3234
testMaxSwapAmount = btcutil.Amount(1000000)
3335
)
3436

37+
// mockMuSig2SigningData returns size-correct placeholder data. Client tests
38+
// only assert response handling, not MuSig2 cryptographic validity.
39+
func mockMuSig2SigningData() ([]byte, []byte, error) {
40+
return make([]byte, musig2.PubNonceSize),
41+
make([]byte, input.MuSig2PartialSigSize), nil
42+
}
43+
3544
// serverMock is used in client unit tests to simulate swap server behaviour.
3645
type serverMock struct {
3746
expectedSwapAmt btcutil.Amount
@@ -276,7 +285,7 @@ func (s *serverMock) MuSig2SignSweep(_ context.Context, _ loopdb.ProtocolVersion
276285
_ lntypes.Hash, _ [32]byte, _ []byte, _ []byte) ([]byte,
277286
[]byte, error) {
278287

279-
return nil, nil, nil
288+
return mockMuSig2SigningData()
280289
}
281290

282291
func (s *serverMock) MultiMuSig2SignSweep(ctx context.Context,
@@ -285,7 +294,7 @@ func (s *serverMock) MultiMuSig2SignSweep(ctx context.Context,
285294
prevoutMap map[wire.OutPoint]*wire.TxOut) (
286295
[]byte, []byte, error) {
287296

288-
return nil, nil, nil
297+
return mockMuSig2SigningData()
289298
}
290299

291300
func (s *serverMock) PushKey(_ context.Context, _ loopdb.ProtocolVersion,

staticaddr/loopin/manager.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -379,12 +379,14 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
379379
return err
380380
}
381381

382-
var (
383-
serverNonce [musig2.PubNonceSize]byte
384-
sigHash [32]byte
385-
)
382+
var sigHash [32]byte
383+
384+
serverNonce, err := byteSliceTo66ByteSlice(nonce)
385+
if err != nil {
386+
return fmt.Errorf("invalid server nonce for "+
387+
"deposit %v: %w", depositOutpoint, err)
388+
}
386389

387-
copy(serverNonce[:], nonce)
388390
musig2Session, err := staticutil.CreateMusig2Session(
389391
ctx, m.cfg.Signer, loopIn.AddressParams, loopIn.Address,
390392
)

staticaddr/loopin/manager_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package loopin
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"testing"
78

9+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
810
"github.com/btcsuite/btcd/btcutil"
11+
"github.com/btcsuite/btcd/btcutil/psbt"
912
"github.com/btcsuite/btcd/chaincfg/chainhash"
1013
"github.com/btcsuite/btcd/wire"
1114
"github.com/lightninglabs/loop"
@@ -14,6 +17,7 @@ import (
1417
"github.com/lightninglabs/loop/staticaddr/deposit"
1518
"github.com/lightninglabs/loop/staticaddr/script"
1619
"github.com/lightninglabs/loop/swap"
20+
"github.com/lightninglabs/loop/swapserverrpc"
1721
"github.com/lightningnetwork/lnd/lntypes"
1822
"github.com/lightningnetwork/lnd/routing/route"
1923
"github.com/lightningnetwork/lnd/zpay32"
@@ -220,6 +224,80 @@ func TestInitiateLoopInAllowsReservedAutoloopLabel(t *testing.T) {
220224
require.Equal(t, selectedDeposit.Value, quoteGetter.amount)
221225
}
222226

227+
// TestHandleLoopInSweepReqRejectsInvalidServerNonce ensures that a malformed
228+
// MuSig2 nonce returned by the server is rejected before it reaches the signer.
229+
func TestHandleLoopInSweepReqRejectsInvalidServerNonce(t *testing.T) {
230+
ctx := t.Context()
231+
232+
changeAddr := &script.Parameters{
233+
PkScript: []byte{0xaa, 0xbb},
234+
}
235+
236+
const confirmationHeight = 0
237+
dep := makeDeposit(7, 0, 10_000, confirmationHeight)
238+
depOutpoint := outpointString(dep)
239+
240+
swapHash := lntypes.Hash{9}
241+
loopIn := &StaticAddressLoopIn{
242+
SwapHash: swapHash,
243+
DepositOutpoints: []string{depOutpoint},
244+
SelectedAmount: dep.Value,
245+
}
246+
loopIn.SetState(Succeeded)
247+
248+
sweepTx := makeSweepTx(
249+
[]wire.OutPoint{dep.OutPoint},
250+
[]*wire.TxOut{{
251+
Value: int64(dep.Value),
252+
PkScript: []byte{0xcc, 0xdd},
253+
}},
254+
)
255+
sweepPacket, err := psbt.NewFromUnsignedTx(sweepTx)
256+
require.NoError(t, err)
257+
258+
var psbtBuf bytes.Buffer
259+
require.NoError(t, sweepPacket.Serialize(&psbtBuf))
260+
261+
mgr := &Manager{
262+
cfg: &Config{
263+
AddressManager: &mockAddressManager{
264+
params: changeAddr,
265+
},
266+
DepositManager: &mockDepositManager{
267+
byOutpoint: map[string]*deposit.Deposit{
268+
depOutpoint: dep,
269+
},
270+
},
271+
Store: &mockStore{
272+
loopIns: map[lntypes.Hash]*StaticAddressLoopIn{
273+
swapHash: loopIn,
274+
},
275+
mapIDs: map[lntypes.Hash][]deposit.ID{
276+
swapHash: {dep.ID},
277+
},
278+
},
279+
},
280+
}
281+
282+
req := &swapserverrpc.ServerStaticLoopInSweepNotification{
283+
SweepTxPsbt: psbtBuf.Bytes(),
284+
SwapHash: swapHash[:],
285+
DepositToNonces: map[string][]byte{
286+
depOutpoint: make([]byte, musig2.PubNonceSize-1),
287+
},
288+
PrevoutInfo: []*swapserverrpc.PrevoutInfo{{
289+
Value: uint64(dep.Value),
290+
PkScript: changeAddr.PkScript,
291+
TxidBytes: dep.Hash[:],
292+
OutputIndex: dep.Index,
293+
}},
294+
}
295+
296+
err = mgr.handleLoopInSweepReq(ctx, req)
297+
require.ErrorContains(t, err, "invalid server nonce")
298+
require.ErrorContains(t, err, depOutpoint)
299+
}
300+
223301
// mockDepositManager implements DepositManager for tests.
224302
type mockDepositManager struct {
225303
// activeDeposits is the set returned by GetActiveDepositsInState.

staticaddr/script/parameters.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ type Parameters struct {
1818
// used for the 2-of-2 funding output.
1919
ServerPubkey *btcec.PublicKey
2020

21-
// Expiry is the CSV timout value at which the client can claim the
22-
// static address's timout path.
21+
// Expiry is the CSV timeout value at which the client can claim the
22+
// static address's timeout path.
2323
Expiry uint32
2424

2525
// PkScript is the unique static address's output script.

staticaddr/withdraw/manager.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,13 +793,32 @@ func (m *Manager) signMusig2Tx(ctx context.Context,
793793

794794
// We'll now add the nonce to our session and sign the tx.
795795
for deposit, sigAndNonce := range sigInfo {
796+
if sigAndNonce == nil {
797+
return nil, fmt.Errorf("missing signing info for "+
798+
"deposit %v", deposit)
799+
}
800+
796801
session, ok := sessions[deposit]
797802
if !ok {
798803
return nil, errors.New("session not found")
799804
}
800805

801-
nonce := [musig2.PubNonceSize]byte{}
806+
if len(sigAndNonce.Nonce) != musig2.PubNonceSize {
807+
return nil, fmt.Errorf("invalid nonce length for "+
808+
"deposit %v: got %d, want %d", deposit,
809+
len(sigAndNonce.Nonce), musig2.PubNonceSize)
810+
}
811+
812+
if len(sigAndNonce.Sig) != input.MuSig2PartialSigSize {
813+
return nil, fmt.Errorf("invalid partial signature "+
814+
"length for deposit %v: got %d, want %d",
815+
deposit, len(sigAndNonce.Sig),
816+
input.MuSig2PartialSigSize)
817+
}
818+
819+
var nonce [musig2.PubNonceSize]byte
802820
copy(nonce[:], sigAndNonce.Nonce)
821+
803822
haveAllNonces, err := signer.MuSig2RegisterNonces(
804823
ctx, session.SessionID,
805824
[][musig2.PubNonceSize]byte{nonce},

0 commit comments

Comments
 (0)