diff --git a/chainnotifier_client.go b/chainnotifier_client.go index 7626473..ad7c4bc 100644 --- a/chainnotifier_client.go +++ b/chainnotifier_client.go @@ -3,6 +3,7 @@ package lndclient import ( "context" "fmt" + "gopkg.in/macaroon-bakery.v2/bakery" "sync" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -26,6 +27,13 @@ type ChainNotifierClient interface { chan *chainntnfs.SpendDetail, chan error, error) } +var chainNotifierRequiredPermissions = []bakery.Op{ + { + Entity: "onchain", + Action: "read", + }, +} + type chainNotifierClient struct { client chainrpc.ChainNotifierClient chainMac serializedMacaroon diff --git a/invoices_client.go b/invoices_client.go index 1e9f73d..ce3ec64 100644 --- a/invoices_client.go +++ b/invoices_client.go @@ -3,6 +3,7 @@ package lndclient import ( "context" "errors" + "gopkg.in/macaroon-bakery.v2/bakery" "sync" "github.com/btcsuite/btcutil" @@ -32,6 +33,17 @@ type InvoiceUpdate struct { AmtPaid btcutil.Amount } +var invoicesRequiredPermissions = []bakery.Op{ + { + Entity: "invoices", + Action: "write", + }, + { + Entity: "invoices", + Action: "read", + }, +} + type invoicesClient struct { client invoicesrpc.InvoicesClient invoiceMac serializedMacaroon diff --git a/lightning_client.go b/lightning_client.go index 83e15d9..3169756 100644 --- a/lightning_client.go +++ b/lightning_client.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "errors" "fmt" + "gopkg.in/macaroon-bakery.v2/bakery" "io" "sync" "time" @@ -827,6 +828,84 @@ type lightningClient struct { adminMac serializedMacaroon } +var lightningRequiredPermissions = []bakery.Op{ + { + Entity: "address", + Action: "read", + }, + { + Entity: "address", + Action: "write", + }, + { + Entity: "message", + Action: "read", + }, + { + Entity: "message", + Action: "write", + }, + { + Entity: "peers", + Action: "read", + }, + { + Entity: "peers", + Action: "write", + }, + { + Entity: "onchain", + Action: "read", + }, + { + Entity: "onchain", + Action: "write", + }, + { + Entity: "invoices", + Action: "read", + }, + { + Entity: "invoices", + Action: "write", + }, + { + Entity: "info", + Action: "read", + }, + { + Entity: "info", + Action: "write", + }, + { + Entity: "offchain", + Action: "read", + }, + { + Entity: "offchain", + Action: "write", + }, + { + Entity: "macaroon", + Action: "read", + }, + { + Entity: "macaroon", + Action: "write", + }, + { + Entity: "macaroon", + Action: "generate", + }, +} + +var readOnlyRequiredPermssions = []bakery.Op{ + { + Entity: "info", + Action: "read", + }, +} + func newLightningClient(conn *grpc.ClientConn, params *chaincfg.Params, adminMac serializedMacaroon) *lightningClient { diff --git a/lnd_services.go b/lnd_services.go index b26e8b8..4f3ca31 100644 --- a/lnd_services.go +++ b/lnd_services.go @@ -108,6 +108,19 @@ type LndServicesConfig struct { // DialerFunc is a function that is used as grpc.WithContextDialer(). type DialerFunc func(context.Context, string) (net.Conn, error) +// availablePermissions contains any/all available permissions +// for clients and subclients. If a field is set to false, +// that client/subclient cannot be used. +type availablePermissions struct { + lightning bool + walletKit bool + chainNotifier bool + signer bool + invoices bool + router bool + readOnly bool +} + // LndServices constitutes a set of required services. type LndServices struct { Client LightningClient @@ -124,6 +137,8 @@ type LndServices struct { Version *verrpc.Version macaroons *macaroonPouch + + permissions *availablePermissions } // GrpcLndServices constitutes a set of required RPC services. @@ -216,6 +231,14 @@ func NewLndServices(cfg *LndServicesConfig) (*GrpcLndServices, error) { if err != nil { return nil, err } + + // check that our provided macaroon(s) can perform the readonly + // operations necessary for initializing the client + if !checkMacaroonPermissions(readonlyMac, readOnlyRequiredPermssions) { + return nil, fmt.Errorf("permissions needed for readonly operations " + + "not found in provided macaroon(s)") + } + nodeAlias, nodeKey, version, err := checkLndCompatibility( conn, chainParams, readonlyMac, cfg.Network, cfg.CheckVersion, ) @@ -230,56 +253,88 @@ func NewLndServices(cfg *LndServicesConfig) (*GrpcLndServices, error) { return nil, fmt.Errorf("unable to obtain macaroons: %v", err) } + // Check which clients our macaroon(s) can access + // and add those clients to lndServices accordingly + permissions := loadAvailablePermissions(macaroons) + var cleanupFuncs []func() + + var lndServices = LndServices{ + ChainParams: chainParams, + NodeAlias: nodeAlias, + NodePubkey: nodeKey, + Version: version, + macaroons: macaroons, + permissions: permissions, + } + // With the macaroons loaded and the version checked, we can now create // the real lightning client which uses the admin macaroon. - lightningClient := newLightningClient( - conn, chainParams, macaroons.adminMac, - ) + if permissions.lightning { + lightningClient := newLightningClient(conn, chainParams, macaroons.adminMac) + lndServices.Client = lightningClient + + cleanupFuncs = append(cleanupFuncs, func() { + log.Debugf("Wait for client to shut down") + lightningClient.WaitForFinished() + }) + } else { + return nil, fmt.Errorf("required permissions for main lightning client " + + "not available, please use a different macaroon") + } // With the network check passed, we'll now initialize the rest of the // sub-server connections, giving each of them their specific macaroon. - notifierClient := newChainNotifierClient(conn, macaroons.chainMac) - signerClient := newSignerClient(conn, macaroons.signerMac) - walletKitClient := newWalletKitClient(conn, macaroons.walletKitMac) - invoicesClient := newInvoicesClient(conn, macaroons.invoiceMac) - routerClient := newRouterClient(conn, macaroons.routerMac) - versionerClient := newVersionerClient(conn, macaroons.readonlyMac) + lndServices.Versioner = newVersionerClient(conn, macaroons.readonlyMac) + + if permissions.chainNotifier { + notifierClient := newChainNotifierClient(conn, macaroons.chainMac) + lndServices.ChainNotifier = notifierClient + + cleanupFuncs = append(cleanupFuncs, func() { + log.Debugf("Wait for chain notifier client to shut down") + notifierClient.WaitForFinished() + }) + } + + if permissions.invoices { + invoicesClient := newInvoicesClient(conn, macaroons.invoiceMac) + lndServices.Invoices = invoicesClient + + cleanupFuncs = append(cleanupFuncs, func() { + log.Debugf("Wait for invoices client to shut down") + invoicesClient.WaitForFinished() + }) + } + + if permissions.signer { + lndServices.Signer = newSignerClient(conn, macaroons.signerMac) + } + + if permissions.walletKit { + lndServices.WalletKit = newWalletKitClient(conn, macaroons.walletKitMac) + } + + if permissions.router { + lndServices.Router = newRouterClient(conn, macaroons.routerMac) + } cleanup := func() { log.Debugf("Closing lnd connection") - err := conn.Close() - if err != nil { + + if err := conn.Close(); err != nil { log.Errorf("Error closing client connection: %v", err) } - log.Debugf("Wait for client to finish") - lightningClient.WaitForFinished() - - log.Debugf("Wait for chain notifier to finish") - notifierClient.WaitForFinished() - - log.Debugf("Wait for invoices to finish") - invoicesClient.WaitForFinished() + for _, cleanupFunc := range cleanupFuncs { + cleanupFunc() + } log.Debugf("Lnd services finished") } services := &GrpcLndServices{ - LndServices: LndServices{ - Client: lightningClient, - WalletKit: walletKitClient, - ChainNotifier: notifierClient, - Signer: signerClient, - Invoices: invoicesClient, - Router: routerClient, - Versioner: versionerClient, - ChainParams: chainParams, - NodeAlias: nodeAlias, - NodePubkey: nodeKey, - Version: version, - macaroons: macaroons, - }, - cleanup: cleanup, + LndServices: lndServices, + cleanup: cleanup, } log.Infof("Using network %v", cfg.Network) @@ -313,6 +368,43 @@ func (s *GrpcLndServices) Close() { log.Debugf("Lnd services finished") } +// GetAvailableClients returns a string slice containing +// the names of all available clients, based the permissions found +// in any loaded macaroons. +func (s *GrpcLndServices) GetAvailableClients() []string { + var availablePerms []string + + if s.permissions.lightning { + availablePerms = append(availablePerms, "lightning") + } + + if s.permissions.walletKit { + availablePerms = append(availablePerms, "walletkit") + } + + if s.permissions.router { + availablePerms = append(availablePerms, "router") + } + + if s.permissions.signer { + availablePerms = append(availablePerms, "signer") + } + + if s.permissions.invoices { + availablePerms = append(availablePerms, "invoices") + } + + if s.permissions.chainNotifier { + availablePerms = append(availablePerms, "chainnotifier") + } + + if s.permissions.readOnly { + availablePerms = append(availablePerms, "readonly") + } + + return availablePerms +} + // waitForChainSync waits and blocks until the connected lnd node is fully // synced to its chain backend. This could theoretically take hours if the // initial block download is still in progress. diff --git a/macaroon_permissions.go b/macaroon_permissions.go new file mode 100644 index 0000000..6711d6c --- /dev/null +++ b/macaroon_permissions.go @@ -0,0 +1,93 @@ +package lndclient + +import ( + "context" + "encoding/hex" + "fmt" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon.v2" +) + +func loadAvailablePermissions(macPouch *macaroonPouch) *availablePermissions { + return &availablePermissions{ + lightning: checkMacaroonPermissions(macPouch.adminMac, lightningRequiredPermissions), + walletKit: checkMacaroonPermissions(macPouch.walletKitMac, walletKitRequiredPermissions), + invoices: checkMacaroonPermissions(macPouch.invoiceMac, invoicesRequiredPermissions), + signer: checkMacaroonPermissions(macPouch.signerMac, signerRequiredPermissions), + chainNotifier: checkMacaroonPermissions(macPouch.chainMac, chainNotifierRequiredPermissions), + router: checkMacaroonPermissions(macPouch.routerMac, routerRequiredPermissions), + readOnly: checkMacaroonPermissions(macPouch.readonlyMac, readOnlyRequiredPermssions), + } +} + +// checkMacaroonPermissions takes a serializedMacaroon +// and checks that it has all of the required permissions for +// a given client. +// Returns false and an error if an error occurs while loading +// the macaroon data or creating a bakery.Oven. +func checkMacaroonPermissions(mac serializedMacaroon, + requiredPermissions []bakery.Op) bool { + + m, err := unmarshalMacaroon(mac) + if err != nil { + log.Error(err) + return false + } + + macOven := bakery.NewOven(bakery.OvenParams{}) + + ops, _, err := macOven.VerifyMacaroon(context.Background(), []*macaroon.Macaroon{m}) + if err != nil { + log.Error(err) + return false + } + + macOpsMap := convertMacOpsToMap(ops) + requiredPermsMap := convertMacOpsToMap(requiredPermissions) + permissionsMap := make(map[string]bool) + + // appends all matched permissions in macOpsMap + // to permissionsMap + for permName := range requiredPermsMap { + if _, ok := macOpsMap[permName]; ok { + permissionsMap[permName] = true + } + } + + hasAllPermissions := len(permissionsMap) == len(requiredPermissions) + + return hasAllPermissions +} + +// convertMacOpsToMap converts a slice of bakery.Op into a map[string]bool +// by concatenating the entity name and action into a single string; +// for example, { Entity: "address", Action: "read" } becomes "address.read". +func convertMacOpsToMap(ops []bakery.Op) map[string]bool { + opsMap := make(map[string]bool) + + for _, op := range ops { + macOp := fmt.Sprintf("%[1]s.%[2]s", op.Entity, op.Action) + + _, ok := opsMap[macOp] + if !ok { + opsMap[macOp] = true + } + } + + return opsMap +} + +func unmarshalMacaroon(mac serializedMacaroon) (*macaroon.Macaroon, error) { + var m = &macaroon.Macaroon{} + + deserializedMac, err := hex.DecodeString(string(mac)) + if err != nil { + return nil, fmt.Errorf("could not deserialize macaroon: %[1]v", err) + } + + if err := m.UnmarshalBinary(deserializedMac); err != nil { + return nil, fmt.Errorf("could not unmarshal macaroon data: %[1]v", err) + } + + return m, nil +} diff --git a/router_client.go b/router_client.go index 33c487a..d34a6cc 100644 --- a/router_client.go +++ b/router_client.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "fmt" + "gopkg.in/macaroon-bakery.v2/bakery" "io" "time" @@ -131,6 +132,17 @@ type SendPaymentRequest struct { AllowSelfPayment bool } +var routerRequiredPermissions = []bakery.Op{ + { + Entity: "offchain", + Action: "read", + }, + { + Entity: "offchain", + Action: "write", + }, +} + // routerClient is a wrapper around the generated routerrpc proxy. type routerClient struct { client routerrpc.RouterClient diff --git a/signer_client.go b/signer_client.go index b133398..a215928 100644 --- a/signer_client.go +++ b/signer_client.go @@ -2,6 +2,7 @@ package lndclient import ( "context" + "gopkg.in/macaroon-bakery.v2/bakery" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/txscript" @@ -107,6 +108,17 @@ type SignDescriptor struct { InputIndex int } +var signerRequiredPermissions = []bakery.Op{ + { + Entity: "signer", + Action: "generate", + }, + { + Entity: "signer", + Action: "read", + }, +} + type signerClient struct { client signrpc.SignerClient signerMac serializedMacaroon diff --git a/walletkit_client.go b/walletkit_client.go index 62bef44..bcd2216 100644 --- a/walletkit_client.go +++ b/walletkit_client.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "fmt" + "gopkg.in/macaroon-bakery.v2/bakery" "time" "github.com/btcsuite/btcd/btcec" @@ -77,6 +78,25 @@ type walletKitClient struct { walletKitMac serializedMacaroon } +var walletKitRequiredPermissions = []bakery.Op{ + { + Entity: "address", + Action: "write", + }, + { + Entity: "address", + Action: "read", + }, + { + Entity: "onchain", + Action: "write", + }, + { + Entity: "onchain", + Action: "read", + }, +} + // A compile-time constraint to ensure walletKitclient satisfies the // WalletKitClient interface. var _ WalletKitClient = (*walletKitClient)(nil)