diff --git a/cmd/litcli/main.go b/cmd/litcli/main.go index 9fe71cf89..53872d619 100644 --- a/cmd/litcli/main.go +++ b/cmd/litcli/main.go @@ -1,14 +1,11 @@ package main import ( - "context" - "encoding/base64" - "encoding/hex" "fmt" + "io/ioutil" "os" "path/filepath" "strings" - "syscall" terminal "github.com/lightninglabs/lightning-terminal" "github.com/lightninglabs/lightning-terminal/litrpc" @@ -17,17 +14,17 @@ import ( "github.com/lightninglabs/protobuf-hex-display/proto" "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/macaroons" "github.com/urfave/cli" - "golang.org/x/term" "google.golang.org/grpc" "google.golang.org/grpc/credentials" - "google.golang.org/grpc/metadata" + "gopkg.in/macaroon.v2" ) const ( - // uiPasswordEnvName is the name of the environment variable under which - // we look for the UI password for litcli. - uiPasswordEnvName = "UI_PASSWORD" + // defaultMacaroonTimeout is the default macaroon timeout in seconds + // that we set when sending it over the line. + defaultMacaroonTimeout int64 = 60 ) var ( @@ -61,12 +58,10 @@ var ( Usage: "path to lnd's TLS certificate", Value: lnd.DefaultConfig().TLSCertPath, } - uiPasswordFlag = cli.StringFlag{ - Name: "uipassword", - Usage: "the UI password for authenticating against LiT; if " + - "not specified will read from environment variable " + - uiPasswordEnvName + " or prompt on terminal if both " + - "values are empty", + macaroonPathFlag = cli.StringFlag{ + Name: "macaroonpath", + Usage: "path to lit's macaroon file", + Value: terminal.DefaultMacaroonPath, } ) @@ -87,7 +82,7 @@ func main() { lndMode, tlsCertFlag, lndTlsCertFlag, - uiPasswordFlag, + macaroonPathFlag, } app.Commands = append(app.Commands, sessionCommands...) @@ -104,11 +99,11 @@ func fatal(err error) { func getClient(ctx *cli.Context) (litrpc.SessionsClient, func(), error) { rpcServer := ctx.GlobalString("rpcserver") - tlsCertPath, err := extractPathArgs(ctx) + tlsCertPath, macPath, err := extractPathArgs(ctx) if err != nil { return nil, nil, err } - conn, err := getClientConn(rpcServer, tlsCertPath) + conn, err := getClientConn(rpcServer, tlsCertPath, macPath) if err != nil { return nil, nil, err } @@ -118,9 +113,18 @@ func getClient(ctx *cli.Context) (litrpc.SessionsClient, func(), error) { return sessionsClient, cleanup, nil } -func getClientConn(address, tlsCertPath string) (*grpc.ClientConn, error) { +func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn, + error) { + + // We always need to send a macaroon. + macOption, err := readMacaroon(macaroonPath) + if err != nil { + return nil, err + } + opts := []grpc.DialOption{ grpc.WithDefaultCallOptions(maxMsgRecvSize), + macOption, } // TLS cannot be disabled, we'll always have a cert file to read. @@ -140,114 +144,125 @@ func getClientConn(address, tlsCertPath string) (*grpc.ClientConn, error) { return conn, nil } -// extractPathArgs parses the TLS certificate from the command. -func extractPathArgs(ctx *cli.Context) (string, error) { +// extractPathArgs parses the TLS certificate and macaroon paths from the +// command. +func extractPathArgs(ctx *cli.Context) (string, string, error) { // We'll start off by parsing the network. This is needed to determine // the correct path to the TLS certificate and macaroon when not // specified. networkStr := strings.ToLower(ctx.GlobalString("network")) _, err := lndclient.Network(networkStr).ChainParams() if err != nil { - return "", err + return "", "", err } - // We'll now fetch the basedir so we can make a decision on how to - // properly read the cert. This will either be the default, - // or will have been overwritten by the end user. + // Get the base dir so that we can reconstruct the default tls and + // macaroon paths if needed. baseDir := lncfg.CleanAndExpandPath(ctx.GlobalString(baseDirFlag.Name)) - lndmode := strings.ToLower(ctx.GlobalString(lndMode.Name)) + macaroonPath := lncfg.CleanAndExpandPath(ctx.GlobalString( + macaroonPathFlag.Name, + )) + + // If the macaroon path flag has not been set to a custom value, + // then reconstruct it with the possibly new base dir and network + // values. + if macaroonPath == terminal.DefaultMacaroonPath { + macaroonPath = filepath.Join( + baseDir, networkStr, terminal.DefaultMacaroonFilename, + ) + } + + // Get the LND mode. If Lit is in integrated LND mode, then LND's tls + // cert is used directly. Otherwise, Lit's own tls cert is used. + lndmode := strings.ToLower(ctx.GlobalString(lndMode.Name)) if lndmode == terminal.ModeIntegrated { tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString( lndTlsCertFlag.Name, )) - return tlsCertPath, nil + return tlsCertPath, macaroonPath, nil } + // Lit is in remote LND mode. So we need Lit's tls cert. tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString( tlsCertFlag.Name, )) + // If a custom TLS path was set, use it as is. + if tlsCertPath != terminal.DefaultTLSCertPath { + return tlsCertPath, macaroonPath, nil + } + // If a custom base directory was set, we'll also check if custom paths // for the TLS cert file was set as well. If not, we'll override the // paths so they can be found within the custom base directory set. // This allows us to set a custom base directory, along with custom // paths to the TLS cert file. - if baseDir != terminal.DefaultLitDir || networkStr != terminal.DefaultNetwork { + if baseDir != terminal.DefaultLitDir { tlsCertPath = filepath.Join( - baseDir, networkStr, terminal.DefaultTLSCertFilename, + baseDir, terminal.DefaultTLSCertFilename, ) } - return tlsCertPath, nil + return tlsCertPath, macaroonPath, nil } -func printRespJSON(resp proto.Message) { // nolint - jsonMarshaler := &jsonpb.Marshaler{ - EmitDefaults: true, - OrigName: true, - Indent: "\t", // Matches indentation of printJSON. +// readMacaroon tries to read the macaroon file at the specified path and create +// gRPC dial options from it. +func readMacaroon(macPath string) (grpc.DialOption, error) { + // Load the specified macaroon file. + macBytes, err := ioutil.ReadFile(macPath) + if err != nil { + return nil, fmt.Errorf("unable to read macaroon path : %v", err) } - jsonStr, err := jsonMarshaler.MarshalToString(resp) - if err != nil { - fmt.Println("unable to decode response: ", err) - return + mac := &macaroon.Macaroon{} + if err = mac.UnmarshalBinary(macBytes); err != nil { + return nil, fmt.Errorf("unable to decode macaroon: %v", err) } - fmt.Println(jsonStr) -} + macConstraints := []macaroons.Constraint{ + // We add a time-based constraint to prevent replay of the + // macaroon. It's good for 60 seconds by default to make up for + // any discrepancy between client and server clocks, but leaking + // the macaroon before it becomes invalid makes it possible for + // an attacker to reuse the macaroon. In addition, the validity + // time of the macaroon is extended by the time the server clock + // is behind the client clock, or shortened by the time the + // server clock is ahead of the client clock (or invalid + // altogether if, in the latter case, this time is more than 60 + // seconds). + macaroons.TimeoutConstraint(defaultMacaroonTimeout), + } -func getAuthContext(cliCtx *cli.Context) context.Context { - uiPassword, err := getUIPassword(cliCtx) + // Apply constraints to the macaroon. + constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...) if err != nil { - fatal(err) + return nil, err } - basicAuth := base64.StdEncoding.EncodeToString( - []byte(fmt.Sprintf("%s:%s", uiPassword, uiPassword)), - ) - - ctxb := context.Background() - md := metadata.MD{} - - md.Set("macaroon", hex.EncodeToString(terminal.EmptyMacaroonBytes)) - md.Set("authorization", fmt.Sprintf("Basic %s", basicAuth)) - - return metadata.NewOutgoingContext(ctxb, md) -} - -func getUIPassword(ctx *cli.Context) (string, error) { - // The command line flag has precedence. - uiPassword := strings.TrimSpace(ctx.GlobalString(uiPasswordFlag.Name)) - - // To automate things with litcli, we also offer reading the password - // from environment variables if the flag wasn't specified. - if uiPassword == "" { - uiPassword = strings.TrimSpace(os.Getenv(uiPasswordEnvName)) + // Now we append the macaroon credentials to the dial options. + cred, err := macaroons.NewMacaroonCredential(constrainedMac) + if err != nil { + return nil, fmt.Errorf("error creating macaroon credential: %v", + err) } + return grpc.WithPerRPCCredentials(cred), nil +} - if uiPassword == "" { - // If there's no value in the environment, we'll now prompt the - // user to enter their password on the terminal. - fmt.Printf("Input your LiT UI password: ") - - // The variable syscall.Stdin is of a different type in the - // Windows API that's why we need the explicit cast. And of - // course the linter doesn't like it either. - pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert - fmt.Println() - - if err != nil { - return "", err - } - uiPassword = strings.TrimSpace(string(pw)) +func printRespJSON(resp proto.Message) { // nolint + jsonMarshaler := &jsonpb.Marshaler{ + EmitDefaults: true, + OrigName: true, + Indent: "\t", // Matches indentation of printJSON. } - if uiPassword == "" { - return "", fmt.Errorf("no UI password provided") + jsonStr, err := jsonMarshaler.MarshalToString(resp) + if err != nil { + fmt.Println("unable to decode response: ", err) + return } - return uiPassword, nil + fmt.Println(jsonStr) } diff --git a/cmd/litcli/sessions.go b/cmd/litcli/sessions.go index 6648ead94..2b0ea65ec 100644 --- a/cmd/litcli/sessions.go +++ b/cmd/litcli/sessions.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/hex" "fmt" "time" @@ -88,8 +89,9 @@ func addSession(ctx *cli.Context) error { sessionLength := time.Second * time.Duration(ctx.Uint64("expiry")) sessionExpiry := time.Now().Add(sessionLength).Unix() + ctxb := context.Background() resp, err := client.AddSession( - getAuthContext(ctx), &litrpc.AddSessionRequest{ + ctxb, &litrpc.AddSessionRequest{ Label: label, SessionType: sessType, ExpiryTimestampSeconds: uint64(sessionExpiry), @@ -196,8 +198,9 @@ func listSessions(filter sessionFilter) func(ctx *cli.Context) error { } defer cleanup() + ctxb := context.Background() resp, err := client.ListSessions( - getAuthContext(ctx), &litrpc.ListSessionsRequest{}, + ctxb, &litrpc.ListSessionsRequest{}, ) if err != nil { return err @@ -248,8 +251,9 @@ func revokeSession(ctx *cli.Context) error { return err } + ctxb := context.Background() resp, err := client.RevokeSession( - getAuthContext(ctx), &litrpc.RevokeSessionRequest{ + ctxb, &litrpc.RevokeSessionRequest{ LocalPublicKey: pubkey, }, ) diff --git a/config.go b/config.go index 4f8f3f1fd..264cc5064 100644 --- a/config.go +++ b/config.go @@ -73,6 +73,10 @@ const ( // certificate. The value corresponds to 14 months // (14 months * 30 days * 24 hours). DefaultAutogenValidity = 14 * 30 * 24 * time.Hour + + // DefaultMacaroonFilename is the default file name for the + // autogenerated lit macaroon. + DefaultMacaroonFilename = "lit.macaroon" ) var ( @@ -119,6 +123,12 @@ var ( lndDefaultConfig.DataDir, defaultLndChainSubDir, defaultLndChain, DefaultNetwork, defaultLndMacaroon, ) + + // DefaultMacaroonPath is the default full path of the base lit + // macaroon. + DefaultMacaroonPath = filepath.Join( + DefaultLitDir, DefaultNetwork, DefaultMacaroonFilename, + ) ) // Config is the main configuration struct of lightning-terminal. It contains @@ -141,6 +151,8 @@ type Config struct { LitDir string `long:"lit-dir" description:"The main directory where LiT looks for its configuration file. If LiT is running in 'remote' lnd mode, this is also the directory where the TLS certificates and log files are stored by default."` ConfigFile string `long:"configfile" description:"Path to LiT's configuration file."` + MacaroonPath string `long:"macaroonpath" description:"Path to write the macaroon for litd's RPC and REST services if it doesn't exist."` + // Network is the Bitcoin network we're running on. This will be parsed // before the configuration is loaded and will set the correct flag on // `lnd.bitcoin.mainnet|testnet|regtest` and also for the other daemons. @@ -296,6 +308,7 @@ func defaultConfig() *Config { LitDir: DefaultLitDir, LetsEncryptListen: defaultLetsEncryptListen, LetsEncryptDir: defaultLetsEncryptDir, + MacaroonPath: DefaultMacaroonPath, ConfigFile: defaultConfigFile, FaradayMode: defaultFaradayMode, Faraday: &faradayDefaultConfig, @@ -394,6 +407,14 @@ func loadAndValidateConfig(interceptor signal.Interceptor) (*Config, error) { "UI, at least %d characters long", uiPasswordMinLength) } + if cfg.Network != DefaultNetwork { + if cfg.MacaroonPath == DefaultMacaroonPath { + cfg.MacaroonPath = filepath.Join( + litDir, cfg.Network, DefaultMacaroonFilename, + ) + } + } + // Initiate our listeners. For now, we only support listening on one // port at a time because we can only pass in one pre-configured RPC // listener into lnd. diff --git a/itest/litd_mode_integrated_test.go b/itest/litd_mode_integrated_test.go index 0769f3266..4e8baadb9 100644 --- a/itest/litd_mode_integrated_test.go +++ b/itest/litd_mode_integrated_test.go @@ -134,81 +134,60 @@ var ( ctx, &litrpc.ListSessionsRequest{}, ) } + litMacaroonFn = func(cfg *LitNodeConfig) string { + return cfg.LitMacPath + } endpoints = []struct { - name string - macaroonFn macaroonFn - requestFn requestFn - successPattern string - supportsMacAuthOnLndPort bool - supportsMacAuthOnLitPort bool - supportsUIPasswordOnLndPort bool - supportsUIPasswordOnLitPort bool - allowedThroughLNC bool - grpcWebURI string - restWebURI string + name string + macaroonFn macaroonFn + requestFn requestFn + successPattern string + allowedThroughLNC bool + grpcWebURI string + restWebURI string }{{ - name: "lnrpc", - macaroonFn: lndMacaroonFn, - requestFn: lndRequestFn, - successPattern: "\"identity_pubkey\":\"0", - supportsMacAuthOnLndPort: true, - supportsMacAuthOnLitPort: true, - supportsUIPasswordOnLndPort: false, - supportsUIPasswordOnLitPort: true, - allowedThroughLNC: true, - grpcWebURI: "/lnrpc.Lightning/GetInfo", - restWebURI: "/v1/getinfo", + name: "lnrpc", + macaroonFn: lndMacaroonFn, + requestFn: lndRequestFn, + successPattern: "\"identity_pubkey\":\"0", + allowedThroughLNC: true, + grpcWebURI: "/lnrpc.Lightning/GetInfo", + restWebURI: "/v1/getinfo", }, { - name: "frdrpc", - macaroonFn: faradayMacaroonFn, - requestFn: faradayRequestFn, - successPattern: "\"reports\":[]", - supportsMacAuthOnLndPort: true, - supportsMacAuthOnLitPort: true, - supportsUIPasswordOnLndPort: false, - supportsUIPasswordOnLitPort: true, - allowedThroughLNC: true, - grpcWebURI: "/frdrpc.FaradayServer/RevenueReport", - restWebURI: "/v1/faraday/revenue", + name: "frdrpc", + macaroonFn: faradayMacaroonFn, + requestFn: faradayRequestFn, + successPattern: "\"reports\":[]", + allowedThroughLNC: true, + grpcWebURI: "/frdrpc.FaradayServer/RevenueReport", + restWebURI: "/v1/faraday/revenue", }, { - name: "looprpc", - macaroonFn: loopMacaroonFn, - requestFn: loopRequestFn, - successPattern: "\"swaps\":[]", - supportsMacAuthOnLndPort: true, - supportsMacAuthOnLitPort: true, - supportsUIPasswordOnLndPort: false, - supportsUIPasswordOnLitPort: true, - allowedThroughLNC: true, - grpcWebURI: "/looprpc.SwapClient/ListSwaps", - restWebURI: "/v1/loop/swaps", + name: "looprpc", + macaroonFn: loopMacaroonFn, + requestFn: loopRequestFn, + successPattern: "\"swaps\":[]", + allowedThroughLNC: true, + grpcWebURI: "/looprpc.SwapClient/ListSwaps", + restWebURI: "/v1/loop/swaps", }, { - name: "poolrpc", - macaroonFn: poolMacaroonFn, - requestFn: poolRequestFn, - successPattern: "\"accounts_active\":0", - supportsMacAuthOnLndPort: true, - supportsMacAuthOnLitPort: true, - supportsUIPasswordOnLndPort: false, - supportsUIPasswordOnLitPort: true, - allowedThroughLNC: true, - grpcWebURI: "/poolrpc.Trader/GetInfo", - restWebURI: "/v1/pool/info", + name: "poolrpc", + macaroonFn: poolMacaroonFn, + requestFn: poolRequestFn, + successPattern: "\"accounts_active\":0", + allowedThroughLNC: true, + grpcWebURI: "/poolrpc.Trader/GetInfo", + restWebURI: "/v1/pool/info", }, { name: "litrpc", - macaroonFn: nil, + macaroonFn: litMacaroonFn, requestFn: litRequestFn, // In some test cases we actually expect some sessions, so we // don't explicitly check for an empty array but just the // existence of the array in the response. - successPattern: "\"sessions\":[", - supportsMacAuthOnLndPort: false, - supportsMacAuthOnLitPort: false, - supportsUIPasswordOnLndPort: true, - supportsUIPasswordOnLitPort: true, - allowedThroughLNC: false, - grpcWebURI: "/litrpc.Sessions/ListSessions", + successPattern: "\"sessions\":[", + allowedThroughLNC: false, + grpcWebURI: "/litrpc.Sessions/ListSessions", }} ) @@ -236,10 +215,6 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { for _, endpoint := range endpoints { endpoint := endpoint tt.Run(endpoint.name+" lnd port", func(ttt *testing.T) { - if !endpoint.supportsMacAuthOnLndPort { - return - } - runGRPCAuthTest( ttt, cfg.RPCAddr(), cfg.TLSCertPath, endpoint.macaroonFn(cfg), @@ -249,10 +224,6 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { }) tt.Run(endpoint.name+" lit port", func(ttt *testing.T) { - if !endpoint.supportsMacAuthOnLitPort { - return - } - runGRPCAuthTest( ttt, cfg.LitAddr(), cfg.TLSCertPath, endpoint.macaroonFn(cfg), @@ -271,20 +242,16 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { tt.Run(endpoint.name+" lnd port", func(ttt *testing.T) { runUIPasswordCheck( ttt, cfg.RPCAddr(), cfg.TLSCertPath, - cfg.UIPassword, - endpoint.requestFn, true, - !endpoint.supportsUIPasswordOnLndPort, - endpoint.successPattern, + cfg.UIPassword, endpoint.requestFn, + true, endpoint.successPattern, ) }) tt.Run(endpoint.name+" lit port", func(ttt *testing.T) { runUIPasswordCheck( ttt, cfg.LitAddr(), cfg.TLSCertPath, - cfg.UIPassword, - endpoint.requestFn, false, - !endpoint.supportsUIPasswordOnLitPort, - endpoint.successPattern, + cfg.UIPassword, endpoint.requestFn, + false, endpoint.successPattern, ) }) } @@ -321,10 +288,6 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { for _, endpoint := range endpoints { endpoint := endpoint tt.Run(endpoint.name+" lnd port", func(ttt *testing.T) { - if !endpoint.supportsMacAuthOnLndPort { - return - } - runGRPCAuthTest( ttt, cfg.RPCAddr(), cfg.TLSCertPath, superMacFile, @@ -334,10 +297,6 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { }) tt.Run(endpoint.name+" lit port", func(ttt *testing.T) { - if !endpoint.supportsMacAuthOnLitPort { - return - } - runGRPCAuthTest( ttt, cfg.LitAddr(), cfg.TLSCertPath, superMacFile, @@ -376,9 +335,8 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) { endpoint := endpoint tt.Run(endpoint.name+" lit port", func(ttt *testing.T) { runLNCAuthTest( - ttt, cfg.LitAddr(), cfg.UIPassword, - cfg.TLSCertPath, - endpoint.requestFn, + ttt, cfg.LitAddr(), cfg.TLSCertPath, + cfg.LitMacPath, endpoint.requestFn, endpoint.successPattern, endpoint.allowedThroughLNC, ) @@ -454,8 +412,8 @@ func runGRPCAuthTest(t *testing.T, hostPort, tlsCertPath, macPath string, // runUIPasswordCheck tests UI password authentication. func runUIPasswordCheck(t *testing.T, hostPort, tlsCertPath, uiPassword string, - makeRequest requestFn, shouldFailWithoutMacaroon, - shouldFailWithDummyMacaroon bool, successContent string) { + makeRequest requestFn, shouldFailWithoutMacaroon bool, + successContent string) { ctxb := context.Background() ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) @@ -503,13 +461,11 @@ func runUIPasswordCheck(t *testing.T, hostPort, tlsCertPath, uiPassword string, ctxm = uiPasswordContext(ctxt, uiPassword, true) resp, err = makeRequest(ctxm, rawConn) - if shouldFailWithDummyMacaroon { - require.Error(t, err) - require.Contains( - t, err.Error(), "cannot get macaroon: root", - ) - return - } + require.Error(t, err) + require.Contains( + t, err.Error(), "cannot get macaroon: root", + ) + return } // We expect the call to succeed. @@ -626,7 +582,7 @@ func runRESTAuthTest(t *testing.T, hostPort, uiPassword, macaroonPath, restURI, // runLNCAuthTest tests authentication of the given interface when connecting // through Lightning Node Connect. -func runLNCAuthTest(t *testing.T, hostPort, uiPassword, tlsCertPath string, +func runLNCAuthTest(t *testing.T, hostPort, tlsCertPath, macPath string, makeRequest requestFn, successContent string, callAllowed bool) { ctxb := context.Background() @@ -636,9 +592,12 @@ func runLNCAuthTest(t *testing.T, hostPort, uiPassword, tlsCertPath string, rawConn, err := connectRPC(ctxt, hostPort, tlsCertPath) require.NoError(t, err) + macBytes, err := ioutil.ReadFile(macPath) + require.NoError(t, err) + ctxm := macaroonContext(ctxt, macBytes) + // We first need to create an LNC session that we can use to connect. // We use the UI password to create the session. - ctxm := uiPasswordContext(ctxt, uiPassword, true) litClient := litrpc.NewSessionsClient(rawConn) sessResp, err := litClient.AddSession(ctxm, &litrpc.AddSessionRequest{ Label: "integration-test", @@ -661,7 +620,7 @@ func runLNCAuthTest(t *testing.T, hostPort, uiPassword, tlsCertPath string, // endpoint, unless it is explicitly disallowed (we currently don't want // to support creating more sessions through LNC until we have all // macaroon permissions properly set up). - resp, err := makeRequest(ctxm, rawLNCConn) + resp, err := makeRequest(ctxt, rawLNCConn) // Is this a disallowed call? if !callAllowed { @@ -787,6 +746,7 @@ func connectMailbox(ctx context.Context, grpc.WithContextDialer(transportConn.Dial), grpc.WithTransportCredentials(noiseConn), grpc.WithPerRPCCredentials(noiseConn), + grpc.WithBlock(), } return grpc.DialContext(ctx, mailboxServerAddr, dialOpts...) diff --git a/itest/litd_mode_remote_test.go b/itest/litd_mode_remote_test.go index b3f0b2f73..4fcdcc91f 100644 --- a/itest/litd_mode_remote_test.go +++ b/itest/litd_mode_remote_test.go @@ -43,10 +43,6 @@ func testModeRemote(net *NetworkHarness, t *harnessTest) { for _, endpoint := range endpoints { endpoint := endpoint tt.Run(endpoint.name+" lit port", func(ttt *testing.T) { - if !endpoint.supportsMacAuthOnLitPort { - return - } - runGRPCAuthTest( ttt, cfg.LitAddr(), cfg.LitTLSCertPath, endpoint.macaroonFn(cfg), @@ -65,10 +61,8 @@ func testModeRemote(net *NetworkHarness, t *harnessTest) { tt.Run(endpoint.name+" lit port", func(ttt *testing.T) { runUIPasswordCheck( ttt, cfg.LitAddr(), cfg.LitTLSCertPath, - cfg.UIPassword, - endpoint.requestFn, false, - !endpoint.supportsUIPasswordOnLitPort, - endpoint.successPattern, + cfg.UIPassword, endpoint.requestFn, + false, endpoint.successPattern, ) }) } @@ -105,10 +99,6 @@ func testModeRemote(net *NetworkHarness, t *harnessTest) { for _, endpoint := range endpoints { endpoint := endpoint tt.Run(endpoint.name+" lit port", func(ttt *testing.T) { - if !endpoint.supportsMacAuthOnLitPort { - return - } - runGRPCAuthTest( ttt, cfg.LitAddr(), cfg.LitTLSCertPath, superMacFile, @@ -147,9 +137,8 @@ func testModeRemote(net *NetworkHarness, t *harnessTest) { endpoint := endpoint tt.Run(endpoint.name+" lit port", func(ttt *testing.T) { runLNCAuthTest( - ttt, cfg.LitAddr(), cfg.UIPassword, - cfg.LitTLSCertPath, - endpoint.requestFn, + ttt, cfg.LitAddr(), cfg.LitTLSCertPath, + cfg.LitMacPath, endpoint.requestFn, endpoint.successPattern, endpoint.allowedThroughLNC, ) diff --git a/itest/litd_node.go b/itest/litd_node.go index 44cb03a82..c6d5f2e96 100644 --- a/itest/litd_node.go +++ b/itest/litd_node.go @@ -67,6 +67,7 @@ type LitNodeConfig struct { LoopMacPath string PoolMacPath string LitTLSCertPath string + LitMacPath string UIPassword string LitDir string @@ -279,6 +280,9 @@ func newNode(cfg *LitNodeConfig, harness *NetworkHarness) (*HarnessNode, error) cfg.PoolMacPath = filepath.Join( cfg.PoolDir, cfg.NetParams.Name, "pool.macaroon", ) + cfg.LitMacPath = filepath.Join( + cfg.LitDir, cfg.NetParams.Name, "lit.macaroon", + ) cfg.LitTLSCertPath = filepath.Join(cfg.LitDir, "tls.cert") cfg.GenerateListeningPorts() diff --git a/rpc_proxy.go b/rpc_proxy.go index 7352d6ad5..1662da772 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -36,15 +36,6 @@ const ( HeaderMacaroon = "Macaroon" ) -var ( - // EmptyMacaroonBytes is the byte representation of an empty but - // formally valid macaroon. - EmptyMacaroonBytes, _ = hex.DecodeString( - "020205656d7074790000062062083e2ea599285ac29350abb4ea21fd7c5a" + - "15aca8b4c0d38e6c058829369e50", - ) -) - // proxyErr is an error type that adds more context to an error occurring in the // proxy. type proxyErr struct { @@ -537,7 +528,7 @@ func (p *rpcProxy) basicAuthToMacaroon(basicAuth, requestURI string, } case isLitURI(requestURI): - return EmptyMacaroonBytes, nil + macPath = p.cfg.MacaroonPath default: return nil, fmt.Errorf("unknown gRPC web request: %v", diff --git a/session_rpcserver.go b/session_rpcserver.go index e78f5d880..6f48c60a5 100644 --- a/session_rpcserver.go +++ b/session_rpcserver.go @@ -11,31 +11,98 @@ import ( "github.com/lightninglabs/lightning-node-connect/mailbox" "github.com/lightninglabs/lightning-terminal/litrpc" "github.com/lightninglabs/lightning-terminal/session" + "google.golang.org/grpc" ) // sessionRpcServer is the gRPC server for the Session RPC interface. type sessionRpcServer struct { litrpc.UnimplementedSessionsServer - basicAuth string - + cfg *sessionRpcServerConfig db *session.DB sessionServer *session.Server - superMacBaker func(ctx context.Context, rootKeyID uint64, - recipe *session.MacaroonRecipe) (string, error) - quit chan struct{} wg sync.WaitGroup stopOnce sync.Once } +// sessionRpcServerConfig holds the values used to configure the +// sessionRpcServer. +type sessionRpcServerConfig struct { + basicAuth string + dbDir string + grpcOptions []grpc.ServerOption + registerGrpcServers func(server *grpc.Server) + superMacBaker func(ctx context.Context, rootKeyID uint64, + recipe *session.MacaroonRecipe) (string, error) +} + +// newSessionRPCServer creates a new sessionRpcServer using the passed config. +func newSessionRPCServer(cfg *sessionRpcServerConfig) (*sessionRpcServer, + error) { + + // Create an instance of the local Terminal Connect session store DB. + db, err := session.NewDB(cfg.dbDir, session.DBFilename) + if err != nil { + return nil, fmt.Errorf("error creating session DB: %v", err) + } + + // Create the gRPC server that handles adding/removing sessions and the + // actual mailbox server that spins up the Terminal Connect server + // interface. + server := session.NewServer( + func(opts ...grpc.ServerOption) *grpc.Server { + allOpts := append(cfg.grpcOptions, opts...) + grpcServer := grpc.NewServer(allOpts...) + + cfg.registerGrpcServers(grpcServer) + + return grpcServer + }, + ) + + return &sessionRpcServer{ + cfg: cfg, + db: db, + sessionServer: server, + quit: make(chan struct{}), + }, nil +} + +// start all the components necessary for the sessionRpcServer to start serving +// requests. This includes starting the macaroon service and resuming all +// non-revoked sessions. +func (s *sessionRpcServer) start() error { + // Start up all previously created sessions. + sessions, err := s.db.ListSessions() + if err != nil { + return fmt.Errorf("error listing sessions: %v", err) + } + for _, sess := range sessions { + if err := s.resumeSession(sess); err != nil { + return fmt.Errorf("error resuming sesion: %v", err) + } + } + + return nil +} + // stop cleans up any sessionRpcServer resources. -func (s *sessionRpcServer) stop() { +func (s *sessionRpcServer) stop() error { + var returnErr error s.stopOnce.Do(func() { + if err := s.db.Close(); err != nil { + log.Errorf("Error closing session DB: %v", err) + returnErr = err + } + s.sessionServer.Stop() + close(s.quit) s.wg.Wait() }) + + return returnErr } // AddSession adds and starts a new Terminal Connect session. @@ -52,12 +119,11 @@ func (s *sessionRpcServer) AddSession(_ context.Context, return nil, err } - if typ != session.TypeUIPassword && typ != session.TypeMacaroonAdmin && + if typ != session.TypeMacaroonAdmin && typ != session.TypeMacaroonReadonly { - return nil, fmt.Errorf("invalid session type, only UI " + - "password, admin and readonly macaroon types " + - "supported in LiT") + return nil, fmt.Errorf("invalid session type, only admin " + + "and readonly macaroon types supported in LiT") } sess, err := session.NewSession( @@ -114,33 +180,29 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error { return nil } - var authData []byte - switch sess.Type { - case session.TypeUIPassword: - authData = []byte("Authorization: Basic " + s.basicAuth) - - case session.TypeMacaroonAdmin, session.TypeMacaroonReadonly: - ctx := context.Background() - readOnly := sess.Type == session.TypeMacaroonReadonly - mac, err := s.superMacBaker( - ctx, sess.MacaroonRootKey, &session.MacaroonRecipe{ - Permissions: GetAllPermissions(readOnly), - }, - ) - if err != nil { - log.Debugf("Not resuming session %x. Could not bake"+ - "the necessary macaroon: %w", pubKeyBytes, err) - return nil - } - - authData = []byte(fmt.Sprintf("%s: %s", HeaderMacaroon, mac)) + if sess.Type != session.TypeMacaroonAdmin && + sess.Type != session.TypeMacaroonReadonly { - default: log.Debugf("Not resuming session %x with type %d", pubKeyBytes, sess.Type) return nil } + readOnly := sess.Type == session.TypeMacaroonReadonly + mac, err := s.cfg.superMacBaker( + context.Background(), sess.MacaroonRootKey, + &session.MacaroonRecipe{ + Permissions: GetAllPermissions(readOnly), + }, + ) + if err != nil { + log.Debugf("Not resuming session %x. Could not bake "+ + "the necessary macaroon: %w", pubKeyBytes, err) + return nil + } + + authData := []byte(fmt.Sprintf("%s: %s", HeaderMacaroon, mac)) + sessionClosedSub, err := s.sessionServer.StartSession(sess, authData) if err != nil { return err diff --git a/subserver_permissions.go b/subserver_permissions.go index 427f4bef4..4d4a2896a 100644 --- a/subserver_permissions.go +++ b/subserver_permissions.go @@ -12,9 +12,18 @@ var ( // litPermissions is a map of all LiT RPC methods and their required // macaroon permissions to access the session service. litPermissions = map[string][]bakery.Op{ - "/litrpc.Sessions/AddSession": {{}}, - "/litrpc.Sessions/ListSessions": {{}}, - "/litrpc.Sessions/RevokeSession": {{}}, + "/litrpc.Sessions/AddSession": {{ + Entity: "sessions", + Action: "write", + }}, + "/litrpc.Sessions/ListSessions": {{ + Entity: "sessions", + Action: "read", + }}, + "/litrpc.Sessions/RevokeSession": {{ + Entity: "sessions", + Action: "write", + }}, } // whiteListedMethods is a map of all lnd RPC methods that don't require diff --git a/terminal.go b/terminal.go index ea1d89ee9..fa7a3fac2 100644 --- a/terminal.go +++ b/terminal.go @@ -156,9 +156,11 @@ type LightningTerminal struct { rpcProxy *rpcProxy httpServer *http.Server - sessionDB *session.DB - sessionServer *session.Server - sessionRpcServer *sessionRpcServer + sessionRpcServer *sessionRpcServer + sessionRpcServerStarted bool + + macaroonService *lndclient.MacaroonService + macaroonServiceStarted bool restHandler http.Handler restCancel func() @@ -200,46 +202,27 @@ func (g *LightningTerminal) Run() error { g.cfg, g, g.validateSuperMacaroon, getAllMethodPermissions(), bufRpcListener, ) - - // Create an instance of the local Terminal Connect session store DB. - networkDir := path.Join(g.cfg.LitDir, g.cfg.Network) - g.sessionDB, err = session.NewDB(networkDir, session.DBFilename) - if err != nil { - return fmt.Errorf("error creating session DB: %v", err) - } - - // Create the gRPC server that handles adding/removing sessions and the - // actual mailbox server that spins up the Terminal Connect server - // interface. - g.sessionServer = session.NewServer( - func(opts ...grpc.ServerOption) *grpc.Server { - allOpts := []grpc.ServerOption{ - grpc.CustomCodec(grpcProxy.Codec()), // nolint: staticcheck, - grpc.ChainStreamInterceptor( - g.rpcProxy.StreamServerInterceptor, - ), - grpc.ChainUnaryInterceptor( - g.rpcProxy.UnaryServerInterceptor, - ), - grpc.UnknownServiceHandler( - grpcProxy.TransparentHandler( - // Don't allow calls to litrpc. - g.rpcProxy.makeDirector(false), - ), + g.sessionRpcServer, err = newSessionRPCServer(&sessionRpcServerConfig{ + basicAuth: g.rpcProxy.basicAuth, + dbDir: path.Join(g.cfg.LitDir, g.cfg.Network), + grpcOptions: []grpc.ServerOption{ + grpc.CustomCodec(grpcProxy.Codec()), // nolint: staticcheck, + grpc.ChainStreamInterceptor( + g.rpcProxy.StreamServerInterceptor, + ), + grpc.ChainUnaryInterceptor( + g.rpcProxy.UnaryServerInterceptor, + ), + grpc.UnknownServiceHandler( + grpcProxy.TransparentHandler( + // Don't allow calls to litrpc. + g.rpcProxy.makeDirector(false), ), - } - allOpts = append(allOpts, opts...) - grpcServer := grpc.NewServer(allOpts...) - g.registerSubDaemonGrpcServers(grpcServer, false) - - return grpcServer + ), + }, + registerGrpcServers: func(server *grpc.Server) { + g.registerSubDaemonGrpcServers(server, false) }, - ) - g.sessionRpcServer = &sessionRpcServer{ - basicAuth: g.rpcProxy.basicAuth, - db: g.sessionDB, - sessionServer: g.sessionServer, - quit: make(chan struct{}), superMacBaker: func(ctx context.Context, rootKeyID uint64, recipe *session.MacaroonRecipe) (string, error) { @@ -248,6 +231,10 @@ func (g *LightningTerminal) Run() error { recipe.Permissions, recipe.Caveats, ) }, + }) + if err != nil { + return fmt.Errorf("could not create new session rpc "+ + "server: %v", err) } // Overwrite the loop and pool daemon's user agent name so it sends @@ -387,20 +374,6 @@ func (g *LightningTerminal) Run() error { return err } - // Now start up all previously created sessions. Since the sessions - // require a lnd connection in order to bake macaroons, we can only - // start up the sessions once the connection to lnd has been - // established. - sessions, err := g.sessionDB.ListSessions() - if err != nil { - return fmt.Errorf("error listing sessions: %v", err) - } - for _, sess := range sessions { - if err := g.sessionRpcServer.resumeSession(sess); err != nil { - return fmt.Errorf("error resuming sesion: %v", err) - } - } - // Now block until we receive an error or the main shutdown signal. select { case err := <-g.loopServer.ErrChan: @@ -578,6 +551,33 @@ func (g *LightningTerminal) startSubservers() error { g.poolStarted = true } + g.macaroonService, err = lndclient.NewMacaroonService( + &lndclient.MacaroonServiceConfig{ + DBPath: path.Join(g.cfg.LitDir, g.cfg.Network), + MacaroonLocation: "litd", + StatelessInit: !createDefaultMacaroons, + RequiredPerms: litPermissions, + LndClient: &g.lndClient.LndServices, + EphemeralKey: lndclient.SharedKeyNUMS, + KeyLocator: lndclient.SharedKeyLocator, + MacaroonPath: g.cfg.MacaroonPath, + }, + ) + if err != nil { + log.Errorf("Could not create a new macaroon service: %v", err) + return err + } + + if err := g.macaroonService.Start(); err != nil { + return fmt.Errorf("could not start macaroon service: %v", err) + } + g.macaroonServiceStarted = true + + if err = g.sessionRpcServer.start(); err != nil { + return err + } + g.sessionRpcServerStarted = true + return nil } @@ -775,12 +775,17 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, } case isLitURI(fullMethod): - wrap := fmt.Errorf("invalid basic auth") - _, err := g.rpcProxy.convertBasicAuth(ctx, fullMethod, wrap) - if err != nil { + if !g.macaroonServiceStarted { + return fmt.Errorf("the macaroon service has not " + + "started yet") + } + + if err := g.macaroonService.ValidateMacaroon( + ctx, requiredPermissions, fullMethod, + ); err != nil { return &proxyErr{ proxyContext: "lit", - wrapped: fmt.Errorf("invalid auth: %v", + wrapped: fmt.Errorf("invalid macaroon: %w", err), } } @@ -847,12 +852,19 @@ func (g *LightningTerminal) shutdown() error { } } - g.sessionRpcServer.stop() - if err := g.sessionDB.Close(); err != nil { - log.Errorf("Error closing session DB: %v", err) - returnErr = err + if g.sessionRpcServerStarted { + if err := g.sessionRpcServer.stop(); err != nil { + log.Errorf("Error closing session DB: %v", err) + returnErr = err + } + } + + if g.macaroonServiceStarted { + if err := g.macaroonService.Stop(); err != nil { + log.Errorf("Error stopping macaroon service: %v", err) + returnErr = err + } } - g.sessionServer.Stop() if g.lndClient != nil { g.lndClient.Close()