client: reject malformed server public keys

Loop-in and loop-out responses carry compressed server public keys that are copied into fixed-size fields and later used for HTLC construction. Validate the length and parse each compressed key before storing it, and validate the MuSig2 loop-in receiver internal key as well.

This turns short or unparsable server keys into explicit errors instead of silently zero-padding short responses or accepting an invalid internal key. Update root test mocks to return size-correct MuSig2 signing data under the stricter checks.
This commit is contained in:
Slyghtning 2026-05-27 12:27:27 +02:00
parent 605e72a261
commit cc0392af3f
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
4 changed files with 80 additions and 16 deletions

View file

@ -6,12 +6,14 @@ import (
"testing" "testing"
"time" "time"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/input"
invpkg "github.com/lightningnetwork/lnd/invoices" invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@ -32,6 +34,13 @@ var (
testMaxSwapAmount = btcutil.Amount(1000000) testMaxSwapAmount = btcutil.Amount(1000000)
) )
// mockMuSig2SigningData returns size-correct placeholder data. Client tests
// only assert response handling, not MuSig2 cryptographic validity.
func mockMuSig2SigningData() ([]byte, []byte, error) {
return make([]byte, musig2.PubNonceSize),
make([]byte, input.MuSig2PartialSigSize), nil
}
// serverMock is used in client unit tests to simulate swap server behaviour. // serverMock is used in client unit tests to simulate swap server behaviour.
type serverMock struct { type serverMock struct {
expectedSwapAmt btcutil.Amount expectedSwapAmt btcutil.Amount
@ -276,7 +285,7 @@ func (s *serverMock) MuSig2SignSweep(_ context.Context, _ loopdb.ProtocolVersion
_ lntypes.Hash, _ [32]byte, _ []byte, _ []byte) ([]byte, _ lntypes.Hash, _ [32]byte, _ []byte, _ []byte) ([]byte,
[]byte, error) { []byte, error) {
return nil, nil, nil return mockMuSig2SigningData()
} }
func (s *serverMock) MultiMuSig2SignSweep(ctx context.Context, func (s *serverMock) MultiMuSig2SignSweep(ctx context.Context,
@ -285,7 +294,7 @@ func (s *serverMock) MultiMuSig2SignSweep(ctx context.Context,
prevoutMap map[wire.OutPoint]*wire.TxOut) ( prevoutMap map[wire.OutPoint]*wire.TxOut) (
[]byte, []byte, error) { []byte, []byte, error) {
return nil, nil, nil return mockMuSig2SigningData()
} }
func (s *serverMock) PushKey(_ context.Context, _ loopdb.ProtocolVersion, func (s *serverMock) PushKey(_ context.Context, _ loopdb.ProtocolVersion,

View file

@ -405,13 +405,9 @@ func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context,
return nil, err return nil, err
} }
var senderKey [33]byte senderKey, err := parseServerPubKey("sender key", swapResp.SenderKey)
copy(senderKey[:], swapResp.SenderKey)
// Validate sender key.
_, err = btcec.ParsePubKey(senderKey[:])
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid sender key: %v", err) return nil, err
} }
return &newLoopOutResponse{ return &newLoopOutResponse{
@ -470,14 +466,22 @@ func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context,
return nil, err return nil, err
} }
var receiverKey, receiverInternalKey [33]byte receiverKey, err := parseServerPubKey(
copy(receiverKey[:], swapResp.ReceiverKey) "receiver key", swapResp.ReceiverKey,
copy(receiverInternalKey[:], swapResp.ReceiverInternalPubkey) )
// Validate receiver key.
_, err = btcec.ParsePubKey(receiverKey[:])
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid sender key: %v", err) return nil, err
}
var receiverInternalKey [btcec.PubKeyBytesLenCompressed]byte
if loopdb.CurrentProtocolVersion() >= loopdb.ProtocolVersionMuSig2 {
receiverInternalKey, err = parseServerPubKey(
"receiver internal key",
swapResp.ReceiverInternalPubkey,
)
if err != nil {
return nil, err
}
} }
return &newLoopInResponse{ return &newLoopInResponse{
@ -488,6 +492,29 @@ func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context,
}, nil }, nil
} }
// parseServerPubKey validates that keyBytes is a well-formed compressed public
// key received from the server and returns it as a fixed-size array. The name
// argument is used to produce a descriptive error if validation fails.
func parseServerPubKey(name string,
keyBytes []byte) ([btcec.PubKeyBytesLenCompressed]byte, error) {
var key [btcec.PubKeyBytesLenCompressed]byte
if len(keyBytes) != btcec.PubKeyBytesLenCompressed {
return key, fmt.Errorf("invalid %s length: got %d, want %d",
name, len(keyBytes), btcec.PubKeyBytesLenCompressed)
}
_, err := btcec.ParsePubKey(keyBytes)
if err != nil {
return key, fmt.Errorf("invalid %s: %v", name, err)
}
copy(key[:], keyBytes)
return key, nil
}
// ServerUpdate summarizes an update from the swap server. // ServerUpdate summarizes an update from the swap server.
type ServerUpdate struct { type ServerUpdate struct {
// State is the state that the server has sent us. // State is the state that the server has sent us.

View file

@ -0,0 +1,28 @@
package loop
import (
"testing"
looptest "github.com/lightninglabs/loop/test"
"github.com/stretchr/testify/require"
)
// TestParseServerPubKey ensures that parseServerPubKey accepts a valid
// compressed public key and rejects keys with an invalid length or contents.
func TestParseServerPubKey(t *testing.T) {
t.Parallel()
_, pubKey := looptest.CreateKey(1)
pubKeyBytes := pubKey.SerializeCompressed()
parsedKey, err := parseServerPubKey("test key", pubKeyBytes)
require.NoError(t, err)
require.Equal(t, pubKeyBytes, parsedKey[:])
_, err = parseServerPubKey("test key", pubKeyBytes[:32])
require.ErrorContains(t, err, "invalid test key length")
invalidKey := make([]byte, 33)
_, err = parseServerPubKey("test key", invalidKey)
require.ErrorContains(t, err, "invalid test key")
}

View file

@ -67,7 +67,7 @@ func mockMuSig2SignSweep(ctx context.Context,
prevoutMap map[wire.OutPoint]*wire.TxOut) ( prevoutMap map[wire.OutPoint]*wire.TxOut) (
[]byte, []byte, error) { []byte, []byte, error) {
return nil, nil, nil return mockMuSig2SigningData()
} }
func newSwapClient(t *testing.T, config *clientConfig) *Client { func newSwapClient(t *testing.T, config *clientConfig) *Client {