Skip to content

Commit de279bc

Browse files
authored
Merge pull request #1137 from hieblmi/param-validation
staticaddr: validate server address parameters
2 parents 7c3eb29 + 6aa6979 commit de279bc

2 files changed

Lines changed: 171 additions & 11 deletions

File tree

staticaddr/address/manager.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1212
"github.com/btcsuite/btcd/btcutil"
1313
"github.com/btcsuite/btcd/chaincfg"
14+
"github.com/btcsuite/btcd/wire"
1415
"github.com/lightninglabs/lndclient"
1516
"github.com/lightninglabs/loop/staticaddr/script"
1617
"github.com/lightninglabs/loop/staticaddr/version"
@@ -21,6 +22,13 @@ import (
2122
"github.com/lightningnetwork/lnd/lnwallet"
2223
)
2324

25+
const (
26+
// maxStaticAddressCSVExpiry is the maximum CSV delay that we accept
27+
// from the server for a static address timeout path: 200 days at 144
28+
// blocks per day.
29+
maxStaticAddressCSVExpiry = uint32(200 * 144)
30+
)
31+
2432
// ManagerConfig holds the configuration for the address manager.
2533
type ManagerConfig struct {
2634
// AddressClient is the client that communicates with the loop server
@@ -158,9 +166,16 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
158166
return nil, 0, err
159167
}
160168

169+
if resp == nil {
170+
return nil, 0, fmt.Errorf("missing server new address response")
171+
}
172+
161173
serverParams := resp.GetParams()
174+
if err := validateServerAddressParams(serverParams); err != nil {
175+
return nil, 0, err
176+
}
162177

163-
serverPubKey, err := btcec.ParsePubKey(serverParams.ServerKey)
178+
serverPubKey, err := btcec.ParsePubKey(serverParams.GetServerKey())
164179
if err != nil {
165180
return nil, 0, err
166181
}
@@ -222,6 +237,41 @@ func (m *Manager) NewAddress(ctx context.Context) (*btcutil.AddressTaproot,
222237
return address, int64(serverParams.Expiry), nil
223238
}
224239

240+
// validateServerAddressParams validates the server-controlled static address
241+
// parameters before they are committed into the address script or database.
242+
func validateServerAddressParams(
243+
params *staticaddressrpc.ServerAddressParameters) error {
244+
245+
if params == nil {
246+
return fmt.Errorf("missing server address parameters")
247+
}
248+
249+
serverKey := params.GetServerKey()
250+
if len(serverKey) == 0 {
251+
return fmt.Errorf("missing server public key")
252+
}
253+
if !btcec.IsCompressedPubKey(serverKey) {
254+
return fmt.Errorf("server public key is not a compressed " +
255+
"secp256k1 public key")
256+
}
257+
258+
expiry := params.GetExpiry()
259+
switch {
260+
case expiry == 0:
261+
return fmt.Errorf("static address CSV expiry must be non-zero")
262+
263+
case expiry&^wire.SequenceLockTimeMask != 0:
264+
return fmt.Errorf("static address expiry does not fit into "+
265+
"CSV: %x", expiry)
266+
267+
case expiry > maxStaticAddressCSVExpiry:
268+
return fmt.Errorf("static address CSV expiry %v exceeds "+
269+
"maximum %v", expiry, maxStaticAddressCSVExpiry)
270+
}
271+
272+
return nil
273+
}
274+
225275
// GetTaprootAddress returns a taproot address for the given client and server
226276
// public keys and expiry.
227277
func (m *Manager) GetTaprootAddress(clientPubkey, serverPubkey *btcec.PublicKey,

staticaddr/address/manager_test.go

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/btcsuite/btcd/btcec/v2"
99
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1010
"github.com/btcsuite/btcd/btcutil"
11+
"github.com/btcsuite/btcd/wire"
1112
"github.com/lightninglabs/loop/loopdb"
1213
"github.com/lightninglabs/loop/staticaddr/script"
1314
"github.com/lightninglabs/loop/swap"
@@ -93,8 +94,9 @@ func (m *mockStaticAddressClient) ServerNewAddress(ctx context.Context,
9394

9495
args := m.Called(ctx, in, opts)
9596

96-
return args.Get(0).(*swapserverrpc.ServerNewAddressResponse),
97-
args.Error(1)
97+
resp, _ := args.Get(0).(*swapserverrpc.ServerNewAddressResponse)
98+
99+
return resp, args.Error(1)
98100
}
99101

100102
// TestManager tests the static address manager generates the corerct static
@@ -128,6 +130,100 @@ func TestManager(t *testing.T) {
128130
require.EqualValues(t, defaultExpiry, expiry)
129131
}
130132

133+
// TestNewAddressValidatesServerResponse tests that the untrusted
134+
// ServerNewAddress response is validated before the address script is created.
135+
func TestNewAddressValidatesServerResponse(t *testing.T) {
136+
tests := []struct {
137+
name string
138+
resp *swapserverrpc.ServerNewAddressResponse
139+
expected string
140+
}{
141+
{
142+
name: "nil response",
143+
expected: "missing server new address response",
144+
},
145+
{
146+
name: "nil params",
147+
resp: &swapserverrpc.ServerNewAddressResponse{},
148+
expected: "missing server address parameters",
149+
},
150+
{
151+
name: "missing server key",
152+
resp: &swapserverrpc.ServerNewAddressResponse{
153+
Params: &swapserverrpc.ServerAddressParameters{
154+
Expiry: defaultExpiry,
155+
},
156+
},
157+
expected: "missing server public key",
158+
},
159+
{
160+
name: "uncompressed server key",
161+
resp: &swapserverrpc.ServerNewAddressResponse{
162+
Params: &swapserverrpc.ServerAddressParameters{
163+
ServerKey: []byte{0x04},
164+
Expiry: defaultExpiry,
165+
},
166+
},
167+
expected: "server public key is not a compressed",
168+
},
169+
{
170+
name: "zero expiry",
171+
resp: newServerNewAddressResponse(0),
172+
expected: "static address CSV expiry must be non-zero",
173+
},
174+
{
175+
name: "seconds flag",
176+
resp: newServerNewAddressResponse(
177+
wire.SequenceLockTimeIsSeconds | 1,
178+
),
179+
expected: "static address expiry does not fit into CSV",
180+
},
181+
{
182+
name: "disabled flag",
183+
resp: newServerNewAddressResponse(
184+
wire.SequenceLockTimeDisabled | 1,
185+
),
186+
expected: "static address expiry does not fit into CSV",
187+
},
188+
{
189+
name: "reserved flag",
190+
resp: newServerNewAddressResponse(
191+
wire.SequenceLockTimeMask + 1,
192+
),
193+
expected: "static address expiry does not fit into CSV",
194+
},
195+
{
196+
name: "too large",
197+
resp: newServerNewAddressResponse(
198+
maxStaticAddressCSVExpiry + 1,
199+
),
200+
expected: "exceeds maximum",
201+
},
202+
}
203+
204+
for _, test := range tests {
205+
t.Run(test.name, func(t *testing.T) {
206+
testContext := NewAddressManagerTestContextWithResponse(
207+
t, test.resp,
208+
)
209+
210+
_, _, err := testContext.manager.NewAddress(t.Context())
211+
require.ErrorContains(t, err, test.expected)
212+
})
213+
}
214+
}
215+
216+
// TestNewAddressAcceptsMaxCSVExpiry tests the upper valid CSV boundary.
217+
func TestNewAddressAcceptsMaxCSVExpiry(t *testing.T) {
218+
testContext := NewAddressManagerTestContextWithResponse(
219+
t, newServerNewAddressResponse(maxStaticAddressCSVExpiry),
220+
)
221+
222+
_, expiry, err := testContext.manager.NewAddress(t.Context())
223+
require.NoError(t, err)
224+
require.EqualValues(t, maxStaticAddressCSVExpiry, expiry)
225+
}
226+
131227
// GenerateExpectedTaprootAddress generates the expected taproot address that
132228
// the predefined parameters are supposed to generate.
133229
func GenerateExpectedTaprootAddress(t *ManagerTestContext) (
@@ -170,6 +266,16 @@ type ManagerTestContext struct {
170266
// NewAddressManagerTestContext creates a new test context for the static
171267
// address manager.
172268
func NewAddressManagerTestContext(t *testing.T) *ManagerTestContext {
269+
return NewAddressManagerTestContextWithResponse(
270+
t, newServerNewAddressResponse(defaultExpiry),
271+
)
272+
}
273+
274+
// NewAddressManagerTestContextWithResponse creates a new test context with a
275+
// custom ServerNewAddress response.
276+
func NewAddressManagerTestContextWithResponse(t *testing.T,
277+
resp *swapserverrpc.ServerNewAddressResponse) *ManagerTestContext {
278+
173279
ctxb, cancel := context.WithCancel(context.Background())
174280
defer cancel()
175281

@@ -184,14 +290,7 @@ func NewAddressManagerTestContext(t *testing.T) *ManagerTestContext {
184290

185291
mockStaticAddressClient.On(
186292
"ServerNewAddress", mock.Anything, mock.Anything, mock.Anything,
187-
).Return(
188-
&swapserverrpc.ServerNewAddressResponse{
189-
Params: &swapserverrpc.ServerAddressParameters{
190-
ServerKey: defaultServerPubkeyBytes,
191-
Expiry: defaultExpiry,
192-
},
193-
}, nil,
194-
)
293+
).Return(resp, nil)
195294

196295
cfg := &ManagerConfig{
197296
Store: store,
@@ -215,3 +314,14 @@ func NewAddressManagerTestContext(t *testing.T) *ManagerTestContext {
215314
mockStaticAddressClient: mockStaticAddressClient,
216315
}
217316
}
317+
318+
// newServerNewAddressResponse returns a valid server response with the given
319+
// CSV expiry.
320+
func newServerNewAddressResponse(expiry uint32) *swapserverrpc.ServerNewAddressResponse {
321+
return &swapserverrpc.ServerNewAddressResponse{
322+
Params: &swapserverrpc.ServerAddressParameters{
323+
ServerKey: defaultServerPubkeyBytes,
324+
Expiry: expiry,
325+
},
326+
}
327+
}

0 commit comments

Comments
 (0)