Skip to content

Commit 53618d9

Browse files
authored
Merge pull request #308 from ellemouton/macaroonService
multi: add macaroon service and remove ui password use in litcli
2 parents 6630f46 + 6ebab93 commit 53618d9

File tree

10 files changed

+378
-311
lines changed

10 files changed

+378
-311
lines changed

cmd/litcli/main.go

Lines changed: 98 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
package main
22

33
import (
4-
"context"
5-
"encoding/base64"
6-
"encoding/hex"
74
"fmt"
5+
"io/ioutil"
86
"os"
97
"path/filepath"
108
"strings"
11-
"syscall"
129

1310
terminal "github.com/lightninglabs/lightning-terminal"
1411
"github.com/lightninglabs/lightning-terminal/litrpc"
@@ -17,17 +14,17 @@ import (
1714
"github.com/lightninglabs/protobuf-hex-display/proto"
1815
"github.com/lightningnetwork/lnd"
1916
"github.com/lightningnetwork/lnd/lncfg"
17+
"github.com/lightningnetwork/lnd/macaroons"
2018
"github.com/urfave/cli"
21-
"golang.org/x/term"
2219
"google.golang.org/grpc"
2320
"google.golang.org/grpc/credentials"
24-
"google.golang.org/grpc/metadata"
21+
"gopkg.in/macaroon.v2"
2522
)
2623

2724
const (
28-
// uiPasswordEnvName is the name of the environment variable under which
29-
// we look for the UI password for litcli.
30-
uiPasswordEnvName = "UI_PASSWORD"
25+
// defaultMacaroonTimeout is the default macaroon timeout in seconds
26+
// that we set when sending it over the line.
27+
defaultMacaroonTimeout int64 = 60
3128
)
3229

3330
var (
@@ -61,12 +58,10 @@ var (
6158
Usage: "path to lnd's TLS certificate",
6259
Value: lnd.DefaultConfig().TLSCertPath,
6360
}
64-
uiPasswordFlag = cli.StringFlag{
65-
Name: "uipassword",
66-
Usage: "the UI password for authenticating against LiT; if " +
67-
"not specified will read from environment variable " +
68-
uiPasswordEnvName + " or prompt on terminal if both " +
69-
"values are empty",
61+
macaroonPathFlag = cli.StringFlag{
62+
Name: "macaroonpath",
63+
Usage: "path to lit's macaroon file",
64+
Value: terminal.DefaultMacaroonPath,
7065
}
7166
)
7267

@@ -87,7 +82,7 @@ func main() {
8782
lndMode,
8883
tlsCertFlag,
8984
lndTlsCertFlag,
90-
uiPasswordFlag,
85+
macaroonPathFlag,
9186
}
9287
app.Commands = append(app.Commands, sessionCommands...)
9388

@@ -104,11 +99,11 @@ func fatal(err error) {
10499

105100
func getClient(ctx *cli.Context) (litrpc.SessionsClient, func(), error) {
106101
rpcServer := ctx.GlobalString("rpcserver")
107-
tlsCertPath, err := extractPathArgs(ctx)
102+
tlsCertPath, macPath, err := extractPathArgs(ctx)
108103
if err != nil {
109104
return nil, nil, err
110105
}
111-
conn, err := getClientConn(rpcServer, tlsCertPath)
106+
conn, err := getClientConn(rpcServer, tlsCertPath, macPath)
112107
if err != nil {
113108
return nil, nil, err
114109
}
@@ -118,9 +113,18 @@ func getClient(ctx *cli.Context) (litrpc.SessionsClient, func(), error) {
118113
return sessionsClient, cleanup, nil
119114
}
120115

121-
func getClientConn(address, tlsCertPath string) (*grpc.ClientConn, error) {
116+
func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn,
117+
error) {
118+
119+
// We always need to send a macaroon.
120+
macOption, err := readMacaroon(macaroonPath)
121+
if err != nil {
122+
return nil, err
123+
}
124+
122125
opts := []grpc.DialOption{
123126
grpc.WithDefaultCallOptions(maxMsgRecvSize),
127+
macOption,
124128
}
125129

126130
// 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) {
140144
return conn, nil
141145
}
142146

143-
// extractPathArgs parses the TLS certificate from the command.
144-
func extractPathArgs(ctx *cli.Context) (string, error) {
147+
// extractPathArgs parses the TLS certificate and macaroon paths from the
148+
// command.
149+
func extractPathArgs(ctx *cli.Context) (string, string, error) {
145150
// We'll start off by parsing the network. This is needed to determine
146151
// the correct path to the TLS certificate and macaroon when not
147152
// specified.
148153
networkStr := strings.ToLower(ctx.GlobalString("network"))
149154
_, err := lndclient.Network(networkStr).ChainParams()
150155
if err != nil {
151-
return "", err
156+
return "", "", err
152157
}
153158

154-
// We'll now fetch the basedir so we can make a decision on how to
155-
// properly read the cert. This will either be the default,
156-
// or will have been overwritten by the end user.
159+
// Get the base dir so that we can reconstruct the default tls and
160+
// macaroon paths if needed.
157161
baseDir := lncfg.CleanAndExpandPath(ctx.GlobalString(baseDirFlag.Name))
158-
lndmode := strings.ToLower(ctx.GlobalString(lndMode.Name))
159162

163+
macaroonPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
164+
macaroonPathFlag.Name,
165+
))
166+
167+
// If the macaroon path flag has not been set to a custom value,
168+
// then reconstruct it with the possibly new base dir and network
169+
// values.
170+
if macaroonPath == terminal.DefaultMacaroonPath {
171+
macaroonPath = filepath.Join(
172+
baseDir, networkStr, terminal.DefaultMacaroonFilename,
173+
)
174+
}
175+
176+
// Get the LND mode. If Lit is in integrated LND mode, then LND's tls
177+
// cert is used directly. Otherwise, Lit's own tls cert is used.
178+
lndmode := strings.ToLower(ctx.GlobalString(lndMode.Name))
160179
if lndmode == terminal.ModeIntegrated {
161180
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
162181
lndTlsCertFlag.Name,
163182
))
164183

165-
return tlsCertPath, nil
184+
return tlsCertPath, macaroonPath, nil
166185
}
167186

187+
// Lit is in remote LND mode. So we need Lit's tls cert.
168188
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
169189
tlsCertFlag.Name,
170190
))
171191

192+
// If a custom TLS path was set, use it as is.
193+
if tlsCertPath != terminal.DefaultTLSCertPath {
194+
return tlsCertPath, macaroonPath, nil
195+
}
196+
172197
// If a custom base directory was set, we'll also check if custom paths
173198
// for the TLS cert file was set as well. If not, we'll override the
174199
// paths so they can be found within the custom base directory set.
175200
// This allows us to set a custom base directory, along with custom
176201
// paths to the TLS cert file.
177-
if baseDir != terminal.DefaultLitDir || networkStr != terminal.DefaultNetwork {
202+
if baseDir != terminal.DefaultLitDir {
178203
tlsCertPath = filepath.Join(
179-
baseDir, networkStr, terminal.DefaultTLSCertFilename,
204+
baseDir, terminal.DefaultTLSCertFilename,
180205
)
181206
}
182207

183-
return tlsCertPath, nil
208+
return tlsCertPath, macaroonPath, nil
184209
}
185210

186-
func printRespJSON(resp proto.Message) { // nolint
187-
jsonMarshaler := &jsonpb.Marshaler{
188-
EmitDefaults: true,
189-
OrigName: true,
190-
Indent: "\t", // Matches indentation of printJSON.
211+
// readMacaroon tries to read the macaroon file at the specified path and create
212+
// gRPC dial options from it.
213+
func readMacaroon(macPath string) (grpc.DialOption, error) {
214+
// Load the specified macaroon file.
215+
macBytes, err := ioutil.ReadFile(macPath)
216+
if err != nil {
217+
return nil, fmt.Errorf("unable to read macaroon path : %v", err)
191218
}
192219

193-
jsonStr, err := jsonMarshaler.MarshalToString(resp)
194-
if err != nil {
195-
fmt.Println("unable to decode response: ", err)
196-
return
220+
mac := &macaroon.Macaroon{}
221+
if err = mac.UnmarshalBinary(macBytes); err != nil {
222+
return nil, fmt.Errorf("unable to decode macaroon: %v", err)
197223
}
198224

199-
fmt.Println(jsonStr)
200-
}
225+
macConstraints := []macaroons.Constraint{
226+
// We add a time-based constraint to prevent replay of the
227+
// macaroon. It's good for 60 seconds by default to make up for
228+
// any discrepancy between client and server clocks, but leaking
229+
// the macaroon before it becomes invalid makes it possible for
230+
// an attacker to reuse the macaroon. In addition, the validity
231+
// time of the macaroon is extended by the time the server clock
232+
// is behind the client clock, or shortened by the time the
233+
// server clock is ahead of the client clock (or invalid
234+
// altogether if, in the latter case, this time is more than 60
235+
// seconds).
236+
macaroons.TimeoutConstraint(defaultMacaroonTimeout),
237+
}
201238

202-
func getAuthContext(cliCtx *cli.Context) context.Context {
203-
uiPassword, err := getUIPassword(cliCtx)
239+
// Apply constraints to the macaroon.
240+
constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...)
204241
if err != nil {
205-
fatal(err)
242+
return nil, err
206243
}
207244

208-
basicAuth := base64.StdEncoding.EncodeToString(
209-
[]byte(fmt.Sprintf("%s:%s", uiPassword, uiPassword)),
210-
)
211-
212-
ctxb := context.Background()
213-
md := metadata.MD{}
214-
215-
md.Set("macaroon", hex.EncodeToString(terminal.EmptyMacaroonBytes))
216-
md.Set("authorization", fmt.Sprintf("Basic %s", basicAuth))
217-
218-
return metadata.NewOutgoingContext(ctxb, md)
219-
}
220-
221-
func getUIPassword(ctx *cli.Context) (string, error) {
222-
// The command line flag has precedence.
223-
uiPassword := strings.TrimSpace(ctx.GlobalString(uiPasswordFlag.Name))
224-
225-
// To automate things with litcli, we also offer reading the password
226-
// from environment variables if the flag wasn't specified.
227-
if uiPassword == "" {
228-
uiPassword = strings.TrimSpace(os.Getenv(uiPasswordEnvName))
245+
// Now we append the macaroon credentials to the dial options.
246+
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
247+
if err != nil {
248+
return nil, fmt.Errorf("error creating macaroon credential: %v",
249+
err)
229250
}
251+
return grpc.WithPerRPCCredentials(cred), nil
252+
}
230253

231-
if uiPassword == "" {
232-
// If there's no value in the environment, we'll now prompt the
233-
// user to enter their password on the terminal.
234-
fmt.Printf("Input your LiT UI password: ")
235-
236-
// The variable syscall.Stdin is of a different type in the
237-
// Windows API that's why we need the explicit cast. And of
238-
// course the linter doesn't like it either.
239-
pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert
240-
fmt.Println()
241-
242-
if err != nil {
243-
return "", err
244-
}
245-
uiPassword = strings.TrimSpace(string(pw))
254+
func printRespJSON(resp proto.Message) { // nolint
255+
jsonMarshaler := &jsonpb.Marshaler{
256+
EmitDefaults: true,
257+
OrigName: true,
258+
Indent: "\t", // Matches indentation of printJSON.
246259
}
247260

248-
if uiPassword == "" {
249-
return "", fmt.Errorf("no UI password provided")
261+
jsonStr, err := jsonMarshaler.MarshalToString(resp)
262+
if err != nil {
263+
fmt.Println("unable to decode response: ", err)
264+
return
250265
}
251266

252-
return uiPassword, nil
267+
fmt.Println(jsonStr)
253268
}

cmd/litcli/sessions.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"encoding/hex"
56
"fmt"
67
"time"
@@ -88,8 +89,9 @@ func addSession(ctx *cli.Context) error {
8889
sessionLength := time.Second * time.Duration(ctx.Uint64("expiry"))
8990
sessionExpiry := time.Now().Add(sessionLength).Unix()
9091

92+
ctxb := context.Background()
9193
resp, err := client.AddSession(
92-
getAuthContext(ctx), &litrpc.AddSessionRequest{
94+
ctxb, &litrpc.AddSessionRequest{
9395
Label: label,
9496
SessionType: sessType,
9597
ExpiryTimestampSeconds: uint64(sessionExpiry),
@@ -196,8 +198,9 @@ func listSessions(filter sessionFilter) func(ctx *cli.Context) error {
196198
}
197199
defer cleanup()
198200

201+
ctxb := context.Background()
199202
resp, err := client.ListSessions(
200-
getAuthContext(ctx), &litrpc.ListSessionsRequest{},
203+
ctxb, &litrpc.ListSessionsRequest{},
201204
)
202205
if err != nil {
203206
return err
@@ -248,8 +251,9 @@ func revokeSession(ctx *cli.Context) error {
248251
return err
249252
}
250253

254+
ctxb := context.Background()
251255
resp, err := client.RevokeSession(
252-
getAuthContext(ctx), &litrpc.RevokeSessionRequest{
256+
ctxb, &litrpc.RevokeSessionRequest{
253257
LocalPublicKey: pubkey,
254258
},
255259
)

config.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ const (
7373
// certificate. The value corresponds to 14 months
7474
// (14 months * 30 days * 24 hours).
7575
DefaultAutogenValidity = 14 * 30 * 24 * time.Hour
76+
77+
// DefaultMacaroonFilename is the default file name for the
78+
// autogenerated lit macaroon.
79+
DefaultMacaroonFilename = "lit.macaroon"
7680
)
7781

7882
var (
@@ -119,6 +123,12 @@ var (
119123
lndDefaultConfig.DataDir, defaultLndChainSubDir,
120124
defaultLndChain, DefaultNetwork, defaultLndMacaroon,
121125
)
126+
127+
// DefaultMacaroonPath is the default full path of the base lit
128+
// macaroon.
129+
DefaultMacaroonPath = filepath.Join(
130+
DefaultLitDir, DefaultNetwork, DefaultMacaroonFilename,
131+
)
122132
)
123133

124134
// Config is the main configuration struct of lightning-terminal. It contains
@@ -141,6 +151,8 @@ type Config struct {
141151
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."`
142152
ConfigFile string `long:"configfile" description:"Path to LiT's configuration file."`
143153

154+
MacaroonPath string `long:"macaroonpath" description:"Path to write the macaroon for litd's RPC and REST services if it doesn't exist."`
155+
144156
// Network is the Bitcoin network we're running on. This will be parsed
145157
// before the configuration is loaded and will set the correct flag on
146158
// `lnd.bitcoin.mainnet|testnet|regtest` and also for the other daemons.
@@ -296,6 +308,7 @@ func defaultConfig() *Config {
296308
LitDir: DefaultLitDir,
297309
LetsEncryptListen: defaultLetsEncryptListen,
298310
LetsEncryptDir: defaultLetsEncryptDir,
311+
MacaroonPath: DefaultMacaroonPath,
299312
ConfigFile: defaultConfigFile,
300313
FaradayMode: defaultFaradayMode,
301314
Faraday: &faradayDefaultConfig,
@@ -394,6 +407,14 @@ func loadAndValidateConfig(interceptor signal.Interceptor) (*Config, error) {
394407
"UI, at least %d characters long", uiPasswordMinLength)
395408
}
396409

410+
if cfg.Network != DefaultNetwork {
411+
if cfg.MacaroonPath == DefaultMacaroonPath {
412+
cfg.MacaroonPath = filepath.Join(
413+
litDir, cfg.Network, DefaultMacaroonFilename,
414+
)
415+
}
416+
}
417+
397418
// Initiate our listeners. For now, we only support listening on one
398419
// port at a time because we can only pass in one pre-configured RPC
399420
// listener into lnd.

0 commit comments

Comments
 (0)