@@ -3,6 +3,7 @@ package itest
33import (
44 "bytes"
55 "context"
6+ "crypto/sha512"
67 "crypto/tls"
78 "crypto/x509"
89 "encoding/base64"
@@ -15,13 +16,16 @@ import (
1516 "testing"
1617 "time"
1718
19+ "github.com/btcsuite/btcd/btcec"
1820 "github.com/btcsuite/btcutil"
1921 "github.com/lightninglabs/faraday/frdrpc"
22+ "github.com/lightninglabs/lightning-node-connect/mailbox"
2023 terminal "github.com/lightninglabs/lightning-terminal"
2124 "github.com/lightninglabs/lightning-terminal/litrpc"
2225 "github.com/lightninglabs/lightning-terminal/session"
2326 "github.com/lightninglabs/loop/looprpc"
2427 "github.com/lightninglabs/pool/poolrpc"
28+ "github.com/lightningnetwork/lnd/keychain"
2529 "github.com/lightningnetwork/lnd/lnrpc"
2630 "github.com/stretchr/testify/require"
2731 "golang.org/x/net/http2"
@@ -39,6 +43,10 @@ const (
3943 // file of the main UI. This is created by Webpack and should be fairly
4044 // stable.
4145 indexHtmlMarker = "webpackJsonplightning-terminal"
46+
47+ // mailboxServerAddr is the address of the mailbox server to use during
48+ // integration tests.
49+ mailboxServerAddr = "mailbox.testnet.lightningcluster.com:443"
4250)
4351
4452// requestFn is a function type for a helper function that makes a daemon
@@ -137,6 +145,7 @@ var (
137145 supportsMacAuthOnLitPort bool
138146 supportsUIPasswordOnLndPort bool
139147 supportsUIPasswordOnLitPort bool
148+ allowedThroughLNC bool
140149 grpcWebURI string
141150 restWebURI string
142151 }{{
@@ -148,6 +157,7 @@ var (
148157 supportsMacAuthOnLitPort : true ,
149158 supportsUIPasswordOnLndPort : false ,
150159 supportsUIPasswordOnLitPort : true ,
160+ allowedThroughLNC : true ,
151161 grpcWebURI : "/lnrpc.Lightning/GetInfo" ,
152162 restWebURI : "/v1/getinfo" ,
153163 }, {
@@ -159,6 +169,7 @@ var (
159169 supportsMacAuthOnLitPort : true ,
160170 supportsUIPasswordOnLndPort : false ,
161171 supportsUIPasswordOnLitPort : true ,
172+ allowedThroughLNC : true ,
162173 grpcWebURI : "/frdrpc.FaradayServer/RevenueReport" ,
163174 restWebURI : "/v1/faraday/revenue" ,
164175 }, {
@@ -170,6 +181,7 @@ var (
170181 supportsMacAuthOnLitPort : true ,
171182 supportsUIPasswordOnLndPort : false ,
172183 supportsUIPasswordOnLitPort : true ,
184+ allowedThroughLNC : true ,
173185 grpcWebURI : "/looprpc.SwapClient/ListSwaps" ,
174186 restWebURI : "/v1/loop/swaps" ,
175187 }, {
@@ -181,17 +193,22 @@ var (
181193 supportsMacAuthOnLitPort : true ,
182194 supportsUIPasswordOnLndPort : false ,
183195 supportsUIPasswordOnLitPort : true ,
196+ allowedThroughLNC : true ,
184197 grpcWebURI : "/poolrpc.Trader/GetInfo" ,
185198 restWebURI : "/v1/pool/info" ,
186199 }, {
187- name : "litrpc" ,
188- macaroonFn : nil ,
189- requestFn : litRequestFn ,
190- successPattern : "\" sessions\" :[]" ,
200+ name : "litrpc" ,
201+ macaroonFn : nil ,
202+ requestFn : litRequestFn ,
203+ // In some test cases we actually expect some sessions, so we
204+ // don't explicitly check for an empty array but just the
205+ // existence of the array in the response.
206+ successPattern : "\" sessions\" :[" ,
191207 supportsMacAuthOnLndPort : false ,
192208 supportsMacAuthOnLitPort : false ,
193209 supportsUIPasswordOnLndPort : true ,
194210 supportsUIPasswordOnLitPort : true ,
211+ allowedThroughLNC : false ,
195212 grpcWebURI : "/litrpc.Sessions/ListSessions" ,
196213 }}
197214)
@@ -352,6 +369,23 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) {
352369 })
353370 }
354371 })
372+
373+ t .t .Run ("lnc auth" , func (tt * testing.T ) {
374+ cfg := net .Alice .Cfg
375+
376+ for _ , endpoint := range endpoints {
377+ endpoint := endpoint
378+ tt .Run (endpoint .name + " lit port" , func (ttt * testing.T ) {
379+ runLNCAuthTest (
380+ ttt , cfg .LitAddr (), cfg .UIPassword ,
381+ cfg .TLSCertPath ,
382+ endpoint .requestFn ,
383+ endpoint .successPattern ,
384+ endpoint .allowedThroughLNC ,
385+ )
386+ })
387+ }
388+ })
355389}
356390
357391// runCertificateCheck checks that the TLS certificates presented to clients are
@@ -591,6 +625,61 @@ func runRESTAuthTest(t *testing.T, hostPort, uiPassword, macaroonPath, restURI,
591625 require .Contains (t , body , successPattern )
592626}
593627
628+ // runLNCAuthTest tests authentication of the given interface when connecting
629+ // through Lightning Node Connect.
630+ func runLNCAuthTest (t * testing.T , hostPort , uiPassword , tlsCertPath string ,
631+ makeRequest requestFn , successContent string , callAllowed bool ) {
632+
633+ ctxb := context .Background ()
634+ ctxt , cancel := context .WithTimeout (ctxb , defaultTimeout )
635+ defer cancel ()
636+
637+ rawConn , err := connectRPC (ctxt , hostPort , tlsCertPath )
638+ require .NoError (t , err )
639+
640+ // We first need to create an LNC session that we can use to connect.
641+ // We use the UI password to create the session.
642+ ctxm := uiPasswordContext (ctxt , uiPassword , true )
643+ litClient := litrpc .NewSessionsClient (rawConn )
644+ sessResp , err := litClient .AddSession (ctxm , & litrpc.AddSessionRequest {
645+ Label : "integration-test" ,
646+ SessionType : litrpc .SessionType_TYPE_MACAROON_READONLY ,
647+ ExpiryTimestampSeconds : uint64 (
648+ time .Now ().Add (5 * time .Minute ).Unix (),
649+ ),
650+ MailboxServerAddr : mailboxServerAddr ,
651+ })
652+ require .NoError (t , err )
653+
654+ // Try the LNC connection now.
655+ connectPhrase := strings .Split (
656+ sessResp .Session .PairingSecretMnemonic , " " ,
657+ )
658+ rawLNCConn , err := connectMailbox (ctxt , connectPhrase )
659+ require .NoError (t , err )
660+
661+ // We should be able to make a request via LNC to the given RPC
662+ // endpoint, unless it is explicitly disallowed (we currently don't want
663+ // to support creating more sessions through LNC until we have all
664+ // macaroon permissions properly set up).
665+ resp , err := makeRequest (ctxm , rawLNCConn )
666+
667+ // Is this a disallowed call?
668+ if ! callAllowed {
669+ require .Error (t , err )
670+ require .Contains (t , err .Error (), "unknown service" )
671+
672+ return
673+ }
674+
675+ // The call should be allowed, so we expect no error.
676+ require .NoError (t , err )
677+
678+ json , err := marshalOptions .Marshal (resp )
679+ require .NoError (t , err )
680+ require .Contains (t , string (json ), successContent )
681+ }
682+
594683// getURL retrieves the body of a given URL, ignoring any TLS certificate the
595684// server might present.
596685func getURL (url string ) (string , error ) {
@@ -671,6 +760,39 @@ func getServerCertificates(hostPort string) ([]*x509.Certificate, error) {
671760 return conn .ConnectionState ().PeerCertificates , nil
672761}
673762
763+ // connectMailbox tries to establish a connection through LNC using the given
764+ // connect phrase and the test mailbox server.
765+ func connectMailbox (ctx context.Context ,
766+ connectPhrase []string ) (grpc.ClientConnInterface , error ) {
767+
768+ var mnemonicWords [mailbox .NumPasswordWords ]string
769+ copy (mnemonicWords [:], connectPhrase )
770+ password := mailbox .PasswordMnemonicToEntropy (mnemonicWords )
771+
772+ sid := sha512 .Sum512 (password [:])
773+
774+ privKey , err := btcec .NewPrivateKey (btcec .S256 ())
775+ if err != nil {
776+ return nil , err
777+ }
778+ ecdh := & keychain.PrivKeyECDH {PrivKey : privKey }
779+
780+ transportConn , err := mailbox .NewClient (ctx , sid )
781+ if err != nil {
782+ return nil , err
783+ }
784+
785+ noiseConn := mailbox .NewNoiseGrpcConn (ecdh , nil , password [:])
786+
787+ dialOpts := []grpc.DialOption {
788+ grpc .WithContextDialer (transportConn .Dial ),
789+ grpc .WithTransportCredentials (noiseConn ),
790+ grpc .WithPerRPCCredentials (noiseConn ),
791+ }
792+
793+ return grpc .DialContext (ctx , mailboxServerAddr , dialOpts ... )
794+ }
795+
674796func macaroonContext (ctx context.Context , macBytes []byte ) context.Context {
675797 md := metadata.MD {}
676798 if len (macBytes ) > 0 {
0 commit comments