From 7742101417189e6e8bbd3142b0b5d78828ccd48a Mon Sep 17 00:00:00 2001 From: jalavosus Date: Sun, 10 Jan 2021 00:08:49 -0500 Subject: [PATCH] Implement macaroon-permission-based client loading This commit changes core behavior of LndServices; instead of relying on macaroons for all rpc clients to be available, it instead checks the permissions of loaded macaroon files, and determines which clients can be loaded from there. If a read-only macaroon is not available for client compatibility checking, or admin permissions are not available for a Lightning client, the initialization function will return an error. --- chainnotifier_client.go | 8 ++ invoices_client.go | 12 +++ lightning_client.go | 79 ++++++++++++++++++++ lnd_services.go | 160 +++++++++++++++++++++++++++++++--------- macaroon_permissions.go | 93 +++++++++++++++++++++++ router_client.go | 12 +++ signer_client.go | 12 +++ walletkit_client.go | 20 +++++ 8 files changed, 362 insertions(+), 34 deletions(-) create mode 100644 macaroon_permissions.go 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)