From 50239ae779ef77a6308fd0f64c7f35b1d97c9d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Tigerstr=C3=B6m?= Date: Wed, 28 Aug 2024 12:02:58 +0200 Subject: [PATCH] itest: add testOutboundRSMacaroonEnforcement itest testOutboundRSMacaroonEnforcement tests that a valid macaroon including the `remotesigner` entity is required to connect to a watch-only node that uses an outbound remote signer, while the watch-only node is in the state (WalletState_ALLOW_REMOTE_SIGNER) where it waits for the signer to connect. --- itest/list_on_test.go | 4 ++ itest/lnd_remote_signer_test.go | 104 +++++++++++++++++++++++++++++++- lntest/harness.go | 6 +- 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 6c9001d5b7..be82bbcaaa 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -522,6 +522,10 @@ var allTestCases = []*lntest.TestCase{ Name: "outbound remote signer", TestFunc: testOutboundRemoteSigner, }, + { + Name: "outbound remote signer macaroon enforcement", + TestFunc: testOutboundRSMacaroonEnforcement, + }, { Name: "taproot coop close", TestFunc: testTaprootCoopClose, diff --git a/itest/lnd_remote_signer_test.go b/itest/lnd_remote_signer_test.go index ecff03d91b..865030f462 100644 --- a/itest/lnd_remote_signer_test.go +++ b/itest/lnd_remote_signer_test.go @@ -3,16 +3,20 @@ package itest import ( "fmt" "testing" + "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lnwallet/rpcwallet" "github.com/stretchr/testify/require" ) @@ -324,7 +328,7 @@ func testOutboundRemoteSigner(ht *lntest.HarnessTest) { "--remotesigner.timeout=30s", "--remotesigner.requesttimeout=30s", }, commitArgs...), - password, + password, true, ) // As the signer node will make an outbound connection to the @@ -412,6 +416,104 @@ func testOutboundRemoteSigner(ht *lntest.HarnessTest) { } } +// testOutboundRSMacaroonEnforcement tests that a valid macaroon including +// the `remotesigner` entity is required to connect to a watch-only node that +// uses an outbound remote signer, while the watch-only node is in the state +// where it waits for the signer to connect. +func testOutboundRSMacaroonEnforcement(ht *lntest.HarnessTest) { + // Ensure that the watch-only node uses a configuration that requires an + // outbound remote signer during startup. + watchOnlyArgs := []string{ + "--remotesigner.enable", + "--remotesigner.signertype=outbound", + "--remotesigner.timeout=15s", + "--remotesigner.requesttimeout=15s", + } + + // Create the watch-only node. Note that we require authentication for + // the watch-only node, as we want to test that the macaroon enforcement + // works as expected. + watchOnly := ht.CreateNewNode("WatchOnly", watchOnlyArgs, nil, false) + + startChan := make(chan error) + + // Start the watch-only node in a goroutine as it requires a remote + // signer to connect before it can fully start. + go func() { + startChan <- watchOnly.Start(ht.Context()) + }() + + // Wait and ensure that the watch-only node reaches the state where + // it waits for the remote signer to connect, as this is the state where + // we want to test the macaroon enforcement. + err := wait.Predicate(func() bool { + if watchOnly.RPC == nil { + return false + } + + state, err := watchOnly.RPC.State.GetState( + ht.Context(), &lnrpc.GetStateRequest{}, + ) + if err != nil { + return false + } + + return state.State == lnrpc.WalletState_ALLOW_REMOTE_SIGNER + }, 5*time.Second) + require.NoError(ht, err) + + // Set up a connection to the watch-only node. However, instead of using + // the watch-only node's admin macaroon, we'll use the invoice macaroon. + // The connection should not be allowed using this macaroon because it + // lacks the `remotesigner` entity required when the signer node + // connects to the watch-only node. + cfg := &lncfg.RemoteSigner{ + SignerType: lncfg.SignerClientType, + RPCHost: watchOnly.Cfg.RPCAddr(), + MacaroonPath: watchOnly.Cfg.InvoiceMacPath, + TLSCertPath: watchOnly.Cfg.TLSCertPath, + Timeout: 10 * time.Second, + } + streamFeeder := rpcwallet.NewStreamFeeder(cfg) + + stream, cleanup, err := streamFeeder.GetStream(ht.Context()) + require.NoError(ht, err) + + defer cleanup() + + // Since we're using an unauthorized macaroon, we should expect to be + // denied access to the watch-only node. + _, err = stream.Recv() + require.ErrorContains(ht, err, "permission denied") + + // Finally, connect a real signer to the watch-only node so that + // it can start up properly. + signerArgs := []string{ + "--remotesigner.signertype=signer", + "--remotesigner.timeout=30s", + "--remotesigner.requesttimeout=10s", + fmt.Sprintf( + "--remotesigner.rpchost=localhost:%d", + watchOnly.Cfg.RPCPort, + ), + fmt.Sprintf( + "--remotesigner.tlscertpath=%s", + watchOnly.Cfg.TLSCertPath, + ), + fmt.Sprintf( + "--remotesigner.macaroonpath=%s", + watchOnly.Cfg.AdminMacPath, // An authorized macaroon. + ), + } + + _ = ht.NewNode("Signer", signerArgs) + + // Finally, wait and ensure that the watch-only node is able to start + // up properly. + err = <-startChan + require.NoError(ht, err, "Shouldn't error on watch-only node startup") +} + // deriveCustomScopeAccounts derives the first 255 default accounts of the custom lnd // internal key scope. func deriveCustomScopeAccounts(t *testing.T) []*lnrpc.WatchOnlyAccount { diff --git a/lntest/harness.go b/lntest/harness.go index ba0bcebad3..329a2a9ec0 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -817,7 +817,7 @@ func (h *HarnessTest) NewNodeWithSeedEtcd(name string, etcdCfg *etcd.Config, func (h *HarnessTest) NewNodeWatchOnly(name string, extraArgs []string, password []byte, watchOnly *lnrpc.WatchOnly) *node.HarnessNode { - hn := h.CreateNewNode(name, extraArgs, password) + hn := h.CreateNewNode(name, extraArgs, password, true) h.StartWatchOnly(hn, name, password, watchOnly) @@ -827,9 +827,9 @@ func (h *HarnessTest) NewNodeWatchOnly(name string, extraArgs []string, // CreateNodeWatchOnly creates a new node and asserts its creation. The function // will only create the node and will not start it. func (h *HarnessTest) CreateNewNode(name string, extraArgs []string, - password []byte) *node.HarnessNode { + password []byte, noAuth bool) *node.HarnessNode { - hn, err := h.manager.newNode(h.T, name, extraArgs, password, true) + hn, err := h.manager.newNode(h.T, name, extraArgs, password, noAuth) require.NoErrorf(h, err, "unable to create new node for %s", name) return hn