From ff17553f3bb31faaa790283a1e04aca45193b43b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 3 Sep 2020 14:35:39 +0200 Subject: [PATCH 1/5] mod+loopd: update to lnd 0.11.1 We update to the newest version of lnd so we can use the updated macaroon service. NOTE: This is a compile time dependency update only, no RPC level update is required. --- go.mod | 5 ++++- go.sum | 8 ++++---- loopd/run.go | 4 +++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 7102d55..b2e4ab3 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,10 @@ require ( github.com/jessevdk/go-flags v1.4.0 github.com/lightninglabs/lndclient v0.11.0-0 github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d - github.com/lightningnetwork/lnd v0.11.0-beta + + // TODO(guggero): Bump lnd to the final v0.11.1-beta version once it's + // released. + github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20200911014924-bc6e52888763 github.com/lightningnetwork/lnd/cert v1.0.3 github.com/lightningnetwork/lnd/queue v1.0.4 github.com/stretchr/testify v1.5.1 diff --git a/go.sum b/go.sum index 5381885..117478a 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,8 @@ github.com/btcsuite/btcwallet/walletdb v1.3.3 h1:u6e7vRIKBF++cJy+hOHaMGg+88ZTwvp github.com/btcsuite/btcwallet/walletdb v1.3.3/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY= github.com/btcsuite/btcwallet/wtxmgr v1.2.0 h1:ZUYPsSv8GjF9KK7lboB2OVHF0uYEcHxgrCfFWqPd9NA= +github.com/btcsuite/btcwallet/wtxmgr v1.2.0 h1:ZUYPsSv8GjF9KK7lboB2OVHF0uYEcHxgrCfFWqPd9NA= +github.com/btcsuite/btcwallet/wtxmgr v1.2.0/go.mod h1:h8hkcKUE3X7lMPzTUoGnNiw5g7VhGrKEW3KpR2r0VnY= github.com/btcsuite/btcwallet/wtxmgr v1.2.0/go.mod h1:h8hkcKUE3X7lMPzTUoGnNiw5g7VhGrKEW3KpR2r0VnY= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= @@ -179,14 +181,14 @@ github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce7 github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d/go.mod h1:KDb67YMzoh4eudnzClmvs2FbiLG9vxISmLApUkCa4uI= github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea h1:oCj48NQ8u7Vz+MmzHqt0db6mxcFZo3Ho7M5gCJauY/k= github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4= -github.com/lightningnetwork/lnd v0.11.0-beta h1:pUAT7FMHqS+iarNxyRtgj96XKCGAWwmb6ZdiUBy78ts= github.com/lightningnetwork/lnd v0.11.0-beta/go.mod h1:CzArvT7NFDLhVyW06+NJWSuWFmE6Ea+AjjA3txUBqTM= +github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20200911014924-bc6e52888763 h1:OUWOTo2BAcsnEaMQIf4gLktU3zGytx6pXrmjUNpZpdg= +github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20200911014924-bc6e52888763/go.mod h1:IvrqVCc5tN2on6E7IHhrwyiM7FCHZ92LphZD+v88LXY= github.com/lightningnetwork/lnd/cert v1.0.2/go.mod h1:fmtemlSMf5t4hsQmcprSoOykypAPp+9c+0d0iqTScMo= github.com/lightningnetwork/lnd/cert v1.0.3 h1:/K2gjzLgVI8we2IIPKc0ztWTEa85uds5sWXi1K6mOT0= github.com/lightningnetwork/lnd/cert v1.0.3/go.mod h1:3MWXVLLPI0Mg0XETm9fT4N9Vyy/8qQLmaM5589bEggM= github.com/lightningnetwork/lnd/clock v1.0.1 h1:QQod8+m3KgqHdvVMV+2DRNNZS1GRFir8mHZYA+Z2hFo= github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= -github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQR5sqtjdv2R0= github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms= github.com/lightningnetwork/lnd/queue v1.0.4 h1:8Dq3vxAFSACPy+pKN88oPFhuCpCoAAChPBwa4BJxH4k= github.com/lightningnetwork/lnd/queue v1.0.4/go.mod h1:YTkTVZCxz8tAYreH27EO3s8572ODumWrNdYW2E/YKxg= @@ -246,7 +248,6 @@ github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -259,7 +260,6 @@ github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50 h1:ASw9n1EHMftwnP3Az4XW6e308+gNsrHzmdhd0Olz9Hs= go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= diff --git a/loopd/run.go b/loopd/run.go index 199a5ca..434e19d 100644 --- a/loopd/run.go +++ b/loopd/run.go @@ -208,7 +208,9 @@ func Run(rpcCfg RPCConfig) error { // we are running. When our command tries to get a lnd connection, it // blocks until lnd is synced. We listen for interrupts so that we can // shutdown the daemon while waiting for sync to complete. - signal.Intercept() + if err := signal.Intercept(); err != nil { + return err + } // Execute command. if parser.Active == nil { From 751c70e6c7571261e6c95c454bcef1154d849f66 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 3 Sep 2020 14:35:41 +0200 Subject: [PATCH 2/5] loopd: add macaroon authentication to the daemon's server connection To secure access to loop's RPC server, we add a macaroon authentication service and its gRPC interceptors to the daemon's server connection. --- go.mod | 1 + loopd/config.go | 26 +++++- loopd/daemon.go | 34 ++++++-- loopd/macaroons.go | 192 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 244 insertions(+), 9 deletions(-) create mode 100644 loopd/macaroons.go diff --git a/go.mod b/go.mod index b2e4ab3..2b43d44 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c google.golang.org/grpc v1.24.0 + gopkg.in/macaroon-bakery.v2 v2.0.1 gopkg.in/macaroon.v2 v2.1.0 ) diff --git a/loopd/config.go b/loopd/config.go index ae062be..2e711af 100644 --- a/loopd/config.go +++ b/loopd/config.go @@ -57,6 +57,16 @@ var ( DefaultTLSKeyPath = filepath.Join( LoopDirBase, DefaultNetwork, DefaultTLSKeyFilename, ) + + // DefaultMacaroonFilename is the default file name for the + // autogenerated loop macaroon. + DefaultMacaroonFilename = "loop.macaroon" + + // DefaultMacaroonPath is the default full path of the base loop + // macaroon. + DefaultMacaroonPath = filepath.Join( + LoopDirBase, DefaultNetwork, DefaultMacaroonFilename, + ) ) type lndConfig struct { @@ -82,7 +92,7 @@ type Config struct { RESTListen string `long:"restlisten" description:"Address to listen on for REST clients"` CORSOrigin string `long:"corsorigin" description:"The value to send in the Access-Control-Allow-Origin header. Header will be omitted if empty."` - LoopDir string `long:"loopdir" description:"The directory for all of loop's data. If set, this option overwrites --datadir, --logdir, --tlscertpath and --tlskeypath."` + LoopDir string `long:"loopdir" description:"The directory for all of loop's data. If set, this option overwrites --datadir, --logdir, --tlscertpath, --tlskeypath and --macaroonpath."` ConfigFile string `long:"configfile" description:"Path to configuration file."` DataDir string `long:"datadir" description:"Directory for loopdb."` @@ -93,6 +103,8 @@ type Config struct { TLSAutoRefresh bool `long:"tlsautorefresh" description:"Re-generate TLS certificate and key if the IPs or domains are changed."` TLSDisableAutofill bool `long:"tlsdisableautofill" description:"Do not include the interface IPs or the system hostname in TLS certificate, use first --tlsextradomain as Common Name instead, if set."` + MacaroonPath string `long:"macaroonpath" description:"Path to write the macaroon for loop's RPC and REST services if it doesn't exist."` + LogDir string `long:"logdir" description:"Directory to log output."` MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)."` MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB."` @@ -133,6 +145,7 @@ func DefaultConfig() Config { DebugLevel: defaultLogLevel, TLSCertPath: DefaultTLSCertPath, TLSKeyPath: DefaultTLSKeyPath, + MacaroonPath: DefaultMacaroonPath, MaxLSATCost: lsat.DefaultMaxCostSats, MaxLSATFee: lsat.DefaultMaxRoutingFeeSats, LoopOutMaxParts: defaultLoopOutMaxParts, @@ -193,9 +206,9 @@ func Validate(cfg *Config) error { cfg.DataDir = filepath.Join(cfg.DataDir, cfg.Network) cfg.LogDir = filepath.Join(cfg.LogDir, cfg.Network) - // We want the TLS files to also be in the "namespaced" sub directory. - // Replace the default values with actual values in case the user - // specified either loopdir or datadir. + // We want the TLS and macaroon files to also be in the "namespaced" sub + // directory. Replace the default values with actual values in case the + // user specified either loopdir or datadir. if cfg.TLSCertPath == DefaultTLSCertPath { cfg.TLSCertPath = filepath.Join( cfg.DataDir, DefaultTLSCertFilename, @@ -206,6 +219,11 @@ func Validate(cfg *Config) error { cfg.DataDir, DefaultTLSKeyFilename, ) } + if cfg.MacaroonPath == DefaultMacaroonPath { + cfg.MacaroonPath = filepath.Join( + cfg.DataDir, DefaultMacaroonFilename, + ) + } // If either of these directories do not exist, create them. if err := os.MkdirAll(cfg.DataDir, os.ModePerm); err != nil { diff --git a/loopd/daemon.go b/loopd/daemon.go index 63794e7..608c810 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -16,6 +16,7 @@ import ( "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/looprpc" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" ) @@ -79,6 +80,8 @@ type Daemon struct { restServer *http.Server restListener net.Listener restCtxCancel func() + + macaroonService *macaroons.Service } // New creates a new instance of the loop client daemon. @@ -172,8 +175,9 @@ func (d *Daemon) startWebServers() error { var err error // With our client created, let's now finish setting up and start our - // RPC server. - serverOpts := []grpc.ServerOption{} + // RPC server. First we add the security interceptor to our gRPC server + // options that checks the macaroons for validity. + serverOpts := d.macaroonInterceptor() d.grpcServer = grpc.NewServer(serverOpts...) looprpc.RegisterSwapClientServer(d.grpcServer, d) @@ -322,6 +326,17 @@ func (d *Daemon) initialize() error { // stop on main context cancel. So we create it early and pass it down. d.mainCtx, d.mainCtxCancel = context.WithCancel(context.Background()) + // Start the macaroon service and let it create its default macaroon in + // case it doesn't exist yet. + err = d.startMacaroonService() + if err != nil { + // The client is the only thing we started yet, so if we clean + // up its connection now, nothing else needs to be shut down at + // this point. + clientCleanup() + return err + } + // Now finally fully initialize the swap client RPC server instance. d.swapClientServer = swapClientServer{ impl: swapclient, @@ -336,9 +351,13 @@ func (d *Daemon) initialize() error { // Retrieve all currently existing swaps from the database. swapsList, err := d.impl.FetchSwaps() if err != nil { - // The client is the only thing we started yet, so if we clean - // up its connection now, nothing else needs to be shut down at - // this point. + // The client and the macaroon service are the only things we + // started yet, so if we clean that up now, nothing else needs + // to be shut down at this point. + if err := d.stopMacaroonService(); err != nil { + log.Errorf("Error shutting down macaroon service: %v", + err) + } clientCleanup() return err } @@ -443,6 +462,11 @@ func (d *Daemon) stop() { d.restCtxCancel() } + err := d.macaroonService.Close() + if err != nil { + log.Errorf("Error stopping macaroon service: %v", err) + } + // Next, shut down the connections to lnd and the swap server. if d.lnd != nil { d.lnd.Close() diff --git a/loopd/macaroons.go b/loopd/macaroons.go new file mode 100644 index 0000000..07c4e93 --- /dev/null +++ b/loopd/macaroons.go @@ -0,0 +1,192 @@ +package loopd + +import ( + "context" + "fmt" + "io/ioutil" + "os" + + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/macaroons" + "google.golang.org/grpc" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +const ( + // loopMacaroonLocation is the value we use for the loopd macaroons' + // "Location" field when baking them. + loopMacaroonLocation = "loop" +) + +var ( + // RequiredPermissions is a map of all loop RPC methods and their + // required macaroon permissions to access loopd. + RequiredPermissions = map[string][]bakery.Op{ + "/looprpc.SwapClient/LoopOut": {{ + Entity: "swap", + Action: "execute", + }, { + Entity: "loop", + Action: "out", + }}, + "/looprpc.SwapClient/LoopIn": {{ + Entity: "swap", + Action: "execute", + }, { + Entity: "loop", + Action: "in", + }}, + "/looprpc.SwapClient/Monitor": {{ + Entity: "swap", + Action: "read", + }}, + "/looprpc.SwapClient/ListSwaps": {{ + Entity: "swap", + Action: "read", + }}, + "/looprpc.SwapClient/SwapInfo": {{ + Entity: "swap", + Action: "read", + }}, + "/looprpc.SwapClient/LoopOutTerms": {{ + Entity: "terms", + Action: "read", + }, { + Entity: "loop", + Action: "out", + }}, + "/looprpc.SwapClient/LoopOutQuote": {{ + Entity: "swap", + Action: "read", + }, { + Entity: "loop", + Action: "out", + }}, + "/looprpc.SwapClient/GetLoopInTerms": {{ + Entity: "terms", + Action: "read", + }, { + Entity: "loop", + Action: "in", + }}, + "/looprpc.SwapClient/GetLoopInQuote": {{ + Entity: "swap", + Action: "read", + }, { + Entity: "loop", + Action: "in", + }}, + "/looprpc.SwapClient/GetLsatTokens": {{ + Entity: "auth", + Action: "read", + }}, + } + + // allPermissions is the list of all existing permissions that exist + // for loopd's RPC. The default macaroon that is created on startup + // contains all these permissions and is therefore equivalent to lnd's + // admin.macaroon but for loop. + allPermissions = []bakery.Op{{ + Entity: "loop", + Action: "out", + }, { + Entity: "loop", + Action: "in", + }, { + Entity: "swap", + Action: "execute", + }, { + Entity: "swap", + Action: "read", + }, { + Entity: "terms", + Action: "read", + }, { + Entity: "auth", + Action: "read", + }} + + // macDbDefaultPw is the default encryption password used to encrypt the + // loop macaroon database. The macaroon service requires us to set a + // non-nil password so we set it to an empty string. This will cause the + // keys to be encrypted on disk but won't provide any security at all as + // the password is known to anyone. + // + // TODO(guggero): Allow the password to be specified by the user. Needs + // create/unlock calls in the RPC. Using a password should be optional + // though. + macDbDefaultPw = []byte("") +) + +// startMacaroonService starts the macaroon validation service, creates or +// unlocks the macaroon database and creates the default macaroon if it doesn't +// exist yet. If macaroons are disabled in general in the configuration, none of +// these actions are taken. +func (d *Daemon) startMacaroonService() error { + // Create the macaroon authentication/authorization service. + var err error + d.macaroonService, err = macaroons.NewService( + d.cfg.DataDir, loopMacaroonLocation, macaroons.IPLockChecker, + ) + if err != nil { + return fmt.Errorf("unable to set up macaroon authentication: "+ + "%v", err) + } + + // Try to unlock the macaroon store with the private password. + err = d.macaroonService.CreateUnlock(&macDbDefaultPw) + if err != nil { + return fmt.Errorf("unable to unlock macaroon DB: %v", err) + } + + // Create macaroon files for loop CLI to use if they don't exist. + if !lnrpc.FileExists(d.cfg.MacaroonPath) { + ctx := context.Background() + + // We only generate one default macaroon that contains all + // existing permissions (equivalent to the admin.macaroon in + // lnd). Custom macaroons can be created through the bakery + // RPC. + loopMac, err := d.macaroonService.NewMacaroon( + ctx, macaroons.DefaultRootKeyID, + allPermissions..., + ) + if err != nil { + return err + } + loopMacBytes, err := loopMac.M().MarshalBinary() + if err != nil { + return err + } + err = ioutil.WriteFile(d.cfg.MacaroonPath, loopMacBytes, 0644) + if err != nil { + if err := os.Remove(d.cfg.MacaroonPath); err != nil { + log.Errorf("Unable to remove %s: %v", + d.cfg.MacaroonPath, err) + } + return err + } + } + + return nil +} + +// stopMacaroonService closes the macaroon database. +func (d *Daemon) stopMacaroonService() error { + return d.macaroonService.Close() +} + +// macaroonInterceptor creates gRPC server options with the macaroon security +// interceptors. +func (d *Daemon) macaroonInterceptor() []grpc.ServerOption { + unaryInterceptor := d.macaroonService.UnaryServerInterceptor( + RequiredPermissions, + ) + streamInterceptor := d.macaroonService.StreamServerInterceptor( + RequiredPermissions, + ) + return []grpc.ServerOption{ + grpc.UnaryInterceptor(unaryInterceptor), + grpc.StreamInterceptor(streamInterceptor), + } +} From 8ecd9673f2ab9535bf180a93e0e9a0395d7a79f4 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 3 Sep 2020 14:35:42 +0200 Subject: [PATCH 3/5] cmd/loop: add macaroon params to CLI --- cmd/loop/main.go | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 6847975..97d33cd 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -66,9 +66,9 @@ var ( Value: loopd.DefaultTLSCertPath, } macaroonPathFlag = cli.StringFlag{ - Name: "macaroonpath", - Usage: "path to macaroon file, only needed if loop runs " + - "in the same process as lnd", + Name: "macaroonpath", + Usage: "path to macaroon file", + Value: loopd.DefaultMacaroonPath, } ) @@ -171,25 +171,31 @@ func extractPathArgs(ctx *cli.Context) (string, string, error) { } // We'll now fetch the loopdir 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. + // properly read the macaroons and also the cert. This will either be + // the default, or will have been overwritten by the end user. loopDir := lncfg.CleanAndExpandPath(ctx.GlobalString(loopDirFlag.Name)) tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString( tlsCertFlag.Name, )) + macPath := lncfg.CleanAndExpandPath(ctx.GlobalString( + macaroonPathFlag.Name, + )) - // If a custom lnd directory was set, we'll also check if custom paths - // for the TLS cert file were set as well. If not, we'll override their - // paths so they can be found within the custom loop directory set. This - // allows us to set a custom lnd directory, along with custom paths to - // the TLS cert file. + // If a custom loop directory was set, we'll also check if custom paths + // for the TLS cert and macaroon file were set as well. If not, we'll + // override their paths so they can be found within the custom loop + // directory set. This allows us to set a custom loop directory, along + // with custom paths to the TLS cert and macaroon file. if loopDir != loopd.LoopDirBase || networkStr != loopd.DefaultNetwork { tlsCertPath = filepath.Join( loopDir, networkStr, loopd.DefaultTLSCertFilename, ) + macPath = filepath.Join( + loopDir, networkStr, loopd.DefaultMacaroonFilename, + ) } - return tlsCertPath, ctx.GlobalString(macaroonPathFlag.Name), nil + return tlsCertPath, macPath, nil } type inLimits struct { @@ -373,8 +379,15 @@ func logSwap(swap *looprpc.SwapStatus) { 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. @@ -383,15 +396,6 @@ func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn, return nil, err } - // Macaroons are not yet enabled by default. - if macaroonPath != "" { - macOption, err := readMacaroon(macaroonPath) - if err != nil { - return nil, err - } - opts = append(opts, macOption) - } - opts = append(opts, grpc.WithTransportCredentials(creds)) conn, err := grpc.Dial(address, opts...) From 617964e25f105a255ae7daf180685b048c91a9b9 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 3 Sep 2020 14:35:43 +0200 Subject: [PATCH 4/5] loopd: allow loopd to be used as external subserver When loopd runs in the same process as lnd (in LiT), it hooks itself into lnd's RPC server as an external subserver. But because the user should still be able to use the default loop macaroon, the loop daemon must be able to validate its own macaroons as lnd's macaroon service doesn't know the root key for it. --- loopd/daemon.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/loopd/daemon.go b/loopd/daemon.go index 608c810..f806788 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -18,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" + "gopkg.in/macaroon-bakery.v2/bakery" ) var ( @@ -170,6 +171,21 @@ func (d *Daemon) StartAsSubserver(lndGrpc *lndclient.GrpcLndServices) error { return d.initialize() } +// ValidateMacaroon extracts the macaroon from the context's gRPC metadata, +// checks its signature, makes sure all specified permissions for the called +// method are contained within and finally ensures all caveat conditions are +// met. A non-nil error is returned if any of the checks fail. This method is +// needed to enable loopd running as an external subserver in the same process +// as lnd but still validate its own macaroons. +func (d *Daemon) ValidateMacaroon(ctx context.Context, + requiredPermissions []bakery.Op, fullMethod string) error { + + // Delegate the call to loop's own macaroon validator service. + return d.macaroonService.ValidateMacaroon( + ctx, requiredPermissions, fullMethod, + ) +} + // startWebServers starts the gRPC and REST servers in goroutines. func (d *Daemon) startWebServers() error { var err error From 9adbd59a34743ec5ab035fe5889dfc3a99bb47ba Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 3 Sep 2020 14:35:45 +0200 Subject: [PATCH 5/5] README: mention macaroons --- README.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5a1e0ea..05e7eca 100644 --- a/README.md +++ b/README.md @@ -333,18 +333,25 @@ pending swaps after a restart. Information about pending swaps is stored persistently in the swap database. Its location is `~/.loopd//loop.db`. -## Transport security +## Authentication and transport security -The gRPC and REST connections of `loopd` are encrypted with TLS the same way -`lnd` is. +The gRPC and REST connections of `loopd` are encrypted with TLS and secured with +macaroon authentication the same way `lnd` is. If no custom loop directory is set then the TLS certificate is stored in -`~/.loopd//tls.cert`. +`~/.loop//tls.cert` and the base macaroon in +`~/.loop//loop.macaroon`. -The `loop` command will pick up the file automatically on mainnet if no custom +The `loop` command will pick up these file automatically on mainnet if no custom loop directory is used. For other networks it should be sufficient to add the `--network` flag to tell the CLI in what sub directory to look for the files. +For more information on macaroons, +[see the macaroon documentation of lnd.](https://github.com/lightningnetwork/lnd/blob/master/docs/macaroons.md) + +**NOTE**: Loop's macaroons are independent from `lnd`'s. The same macaroon +cannot be used for both `loopd` and `lnd`. + ## Multiple Simultaneous Swaps It is possible to execute multiple swaps simultaneously. Just keep loopd