From fd37c6f90a99b32710a78070b2902b6c68df2037 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 9 Oct 2024 08:34:59 +0200 Subject: [PATCH 1/4] cmd/commands: add mac_root_key flag to unlock commands This commit adds a new --mac_root_key flag to both the lncli create and lncli createwatchonly commands that allows the user to specify the macaroon root key that should be used when creating the macaroon database on wallet initialization. This allows for deterministic wallet initialization and baking of macaroons before the wallet is initialized. --- cmd/commands/cmd_walletunlocker.go | 56 +++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/cmd/commands/cmd_walletunlocker.go b/cmd/commands/cmd_walletunlocker.go index a0a286047b..49a14b0e6c 100644 --- a/cmd/commands/cmd_walletunlocker.go +++ b/cmd/commands/cmd_walletunlocker.go @@ -12,6 +12,7 @@ import ( "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" + "github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/walletunlocker" "github.com/urfave/cli" ) @@ -26,6 +27,13 @@ var ( Name: "save_to", Usage: "save returned admin macaroon to this file", } + macRootKeyFlag = cli.StringFlag{ + Name: "mac_root_key", + Usage: "macaroon root key to use when initializing the " + + "macaroon store; allows for deterministic macaroon " + + "generation; if not set, a random one will be " + + "created", + } ) var createCommand = cli.Command{ @@ -81,6 +89,7 @@ var createCommand = cli.Command{ }, statelessInitFlag, saveToFlag, + macRootKeyFlag, }, Action: actionDecorator(create), } @@ -261,6 +270,7 @@ mnemonicCheck: extendedRootKey string extendedRootKeyBirthday uint64 recoveryWindow int32 + macRootKey []byte ) switch { // Use an existing cipher seed mnemonic in the aezeed format. @@ -366,6 +376,23 @@ mnemonicCheck: printCipherSeedWords(cipherSeedMnemonic) } + // Parse the macaroon root key if it was specified by the user. + if ctx.IsSet(macRootKeyFlag.Name) { + macRootKey, err = hex.DecodeString( + ctx.String(macRootKeyFlag.Name), + ) + if err != nil { + return fmt.Errorf("unable to parse macaroon root key: "+ + "%w", err) + } + + if len(macRootKey) != macaroons.RootKeyLen { + return fmt.Errorf("macaroon root key must be exactly "+ + "%v bytes, got %v", macaroons.RootKeyLen, + len(macRootKey)) + } + } + // With either the user's prior cipher seed, or a newly generated one, // we'll go ahead and initialize the wallet. req := &lnrpc.InitWalletRequest{ @@ -377,6 +404,7 @@ mnemonicCheck: RecoveryWindow: recoveryWindow, ChannelBackups: chanBackups, StatelessInit: statelessInit, + MacaroonRootKey: macRootKey, } response, err := client.InitWallet(ctxc, req) if err != nil { @@ -687,6 +715,7 @@ var createWatchOnlyCommand = cli.Command{ Flags: []cli.Flag{ statelessInitFlag, saveToFlag, + macRootKeyFlag, }, Action: actionDecorator(createWatchOnly), } @@ -764,11 +793,30 @@ func createWatchOnly(ctx *cli.Context) error { } } + // Parse the macaroon root key if it was specified by the user. + var macRootKey []byte + if ctx.IsSet(macRootKeyFlag.Name) { + macRootKey, err = hex.DecodeString( + ctx.String(macRootKeyFlag.Name), + ) + if err != nil { + return fmt.Errorf("unable to parse macaroon root key: "+ + "%w", err) + } + + if len(macRootKey) != macaroons.RootKeyLen { + return fmt.Errorf("macaroon root key must be exactly "+ + "%v bytes, got %v", macaroons.RootKeyLen, + len(macRootKey)) + } + } + initResp, err := client.InitWallet(ctxc, &lnrpc.InitWalletRequest{ - WalletPassword: walletPassword, - WatchOnly: rpcResp, - RecoveryWindow: recoveryWindow, - StatelessInit: statelessInit, + WalletPassword: walletPassword, + WatchOnly: rpcResp, + RecoveryWindow: recoveryWindow, + StatelessInit: statelessInit, + MacaroonRootKey: macRootKey, }) if err != nil { return err From 6e932c6bc866ea1078d1f1992b2828512ffd08b6 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 9 Oct 2024 08:54:16 +0200 Subject: [PATCH 2/4] macaroons: add BakeFromRootKey function --- macaroons/bake.go | 68 ++++++++++++++++++++++++++++++++++++++++++ macaroons/bake_test.go | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 macaroons/bake.go create mode 100644 macaroons/bake_test.go diff --git a/macaroons/bake.go b/macaroons/bake.go new file mode 100644 index 0000000000..29904d1515 --- /dev/null +++ b/macaroons/bake.go @@ -0,0 +1,68 @@ +package macaroons + +import ( + "bytes" + "fmt" + + "golang.org/x/net/context" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon.v2" +) + +// inMemoryRootKeyStore is a simple implementation of bakery.RootKeyStore that +// stores a single root key in memory. +type inMemoryRootKeyStore struct { + rootKey []byte +} + +// A compile-time check to ensure that inMemoryRootKeyStore implements +// bakery.RootKeyStore. +var _ bakery.RootKeyStore = (*inMemoryRootKeyStore)(nil) + +// Get returns the root key for the given id. If the item is not there, it +// returns ErrNotFound. +func (s *inMemoryRootKeyStore) Get(_ context.Context, id []byte) ([]byte, + error) { + + if !bytes.Equal(id, DefaultRootKeyID) { + return nil, bakery.ErrNotFound + } + + return s.rootKey, nil +} + +// RootKey returns the root key to be used for making a new macaroon, and an id +// that can be used to look it up later with the Get method. +func (s *inMemoryRootKeyStore) RootKey(context.Context) ([]byte, []byte, + error) { + + return s.rootKey, DefaultRootKeyID, nil +} + +// BakeFromRootKey creates a new macaroon that is derived from the given root +// key and permissions. +func BakeFromRootKey(rootKey []byte, + permissions []bakery.Op) (*macaroon.Macaroon, error) { + + if len(rootKey) != RootKeyLen { + return nil, fmt.Errorf("root key must be %d bytes, is %d", + RootKeyLen, len(rootKey)) + } + + rootKeyStore := &inMemoryRootKeyStore{ + rootKey: rootKey, + } + + service, err := NewService(rootKeyStore, "lnd", false) + if err != nil { + return nil, fmt.Errorf("unable to create service: %w", err) + } + + ctx := context.Background() + mac, err := service.NewMacaroon(ctx, DefaultRootKeyID, permissions...) + if err != nil { + return nil, fmt.Errorf("unable to create macaroon: %w", err) + } + + return mac.M(), nil +} diff --git a/macaroons/bake_test.go b/macaroons/bake_test.go new file mode 100644 index 0000000000..20c22b647b --- /dev/null +++ b/macaroons/bake_test.go @@ -0,0 +1,58 @@ +package macaroons_test + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/lightningnetwork/lnd/macaroons" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +// TestBakeFromRootKey tests that a macaroon can be baked from a root key +// directly without needing to create a store or service first. +func TestBakeFromRootKey(t *testing.T) { + // Create a test store and unlock it. + _, store := newTestStore(t) + + pw := []byte("weks") + err := store.CreateUnlock(&pw) + require.NoError(t, err) + + // Force the store to create a new random root key. + key, id, err := store.RootKey(defaultRootKeyIDContext) + require.NoError(t, err) + require.Len(t, key, 32) + + tmpKey, err := store.Get(defaultRootKeyIDContext, id) + require.NoError(t, err) + require.Equal(t, key, tmpKey) + + // Create a service that uses the root key store. + service, err := macaroons.NewService(store, "lnd", false) + require.NoError(t, err, "Error creating new service") + defer func() { + require.NoError(t, service.Close()) + }() + + // Call the BakeFromRootKey function that derives a macaroon directly + // from the root key. + perms := []bakery.Op{{Entity: "foo", Action: "bar"}} + mac, err := macaroons.BakeFromRootKey(key, perms) + require.NoError(t, err) + + macaroonBytes, err := mac.MarshalBinary() + require.NoError(t, err) + + md := metadata.New(map[string]string{ + "macaroon": hex.EncodeToString(macaroonBytes), + }) + macCtx := metadata.NewIncomingContext(context.Background(), md) + + // The macaroon should be valid for the service, since the root key was + // the same. + err = service.ValidateMacaroon(macCtx, nil, "baz") + require.NoError(t, err) +} From 882f25667ea7f9133f81a0c6ecec7da79a30c5fc Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 9 Oct 2024 09:06:23 +0200 Subject: [PATCH 3/4] cmd/commands: add root_key flag to bakemacaroon command --- cmd/commands/cmd_macaroon.go | 92 ++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/cmd/commands/cmd_macaroon.go b/cmd/commands/cmd_macaroon.go index 149c7db453..15c29380a7 100644 --- a/cmd/commands/cmd_macaroon.go +++ b/cmd/commands/cmd_macaroon.go @@ -10,6 +10,7 @@ import ( "strings" "unicode" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/macaroons" @@ -38,6 +39,12 @@ var ( Usage: "the condition of the custom caveat to add, can be " + "empty if custom caveat doesn't need a value", } + bakeFromRootKeyFlag = cli.StringFlag{ + Name: "root_key", + Usage: "if the root key is known, it can be passed directly " + + "as a hex encoded string, turning the command into " + + "an offline operation", + } ) var bakeMacaroonCommand = cli.Command{ @@ -48,7 +55,7 @@ var bakeMacaroonCommand = cli.Command{ ArgsUsage: "[--save_to=] [--timeout=] [--ip_address=] " + "[--custom_caveat_name= [--custom_caveat_condition=]] " + "[--root_key_id=] [--allow_external_permissions] " + - "permissions...", + "[--root_key=] permissions...", Description: ` Bake a new macaroon that grants the provided permissions and optionally adds restrictions (timeout, IP address) to it. @@ -74,6 +81,12 @@ var bakeMacaroonCommand = cli.Command{ To get a list of all available URIs and permissions, use the "lncli listpermissions" command. + + If the root key is known (for example because "lncli create" was used + with a custom --mac_root_key value), it can be passed directly as a + hex encoded string using the --root_key flag. This turns the command + into an offline operation and the macaroon will be created without + calling into the server's RPC endpoint. `, Flags: []cli.Flag{ cli.StringFlag{ @@ -95,14 +108,13 @@ var bakeMacaroonCommand = cli.Command{ Usage: "whether permissions lnd is not familiar with " + "are allowed", }, + bakeFromRootKeyFlag, }, Action: actionDecorator(bakeMacaroon), } func bakeMacaroon(ctx *cli.Context) error { ctxc := getContext() - client, cleanUp := getClient(ctx) - defer cleanUp() // Show command help if no arguments. if ctx.NArg() == 0 { @@ -154,36 +166,66 @@ func bakeMacaroon(ctx *cli.Context) error { ) } - // Now we have gathered all the input we need and can do the actual - // RPC call. - req := &lnrpc.BakeMacaroonRequest{ - Permissions: parsedPermissions, - RootKeyId: rootKeyID, - AllowExternalPermissions: ctx.Bool("allow_external_permissions"), - } - resp, err := client.BakeMacaroon(ctxc, req) - if err != nil { - return err - } + var rawMacaroon *macaroon.Macaroon + switch { + case ctx.IsSet(bakeFromRootKeyFlag.Name): + macRootKey, err := hex.DecodeString( + ctx.String(bakeFromRootKeyFlag.Name), + ) + if err != nil { + return fmt.Errorf("unable to parse macaroon root key: "+ + "%w", err) + } - // Now we should have gotten a valid macaroon. Unmarshal it so we can - // add first-party caveats (if necessary) to it. - macBytes, err := hex.DecodeString(resp.Macaroon) - if err != nil { - return err - } - unmarshalMac := &macaroon.Macaroon{} - if err = unmarshalMac.UnmarshalBinary(macBytes); err != nil { - return err + ops := fn.Map(func(p *lnrpc.MacaroonPermission) bakery.Op { + return bakery.Op{ + Entity: p.Entity, + Action: p.Action, + } + }, parsedPermissions) + + rawMacaroon, err = macaroons.BakeFromRootKey(macRootKey, ops) + if err != nil { + return fmt.Errorf("unable to bake macaroon: %w", err) + } + + default: + client, cleanUp := getClient(ctx) + defer cleanUp() + + // Now we have gathered all the input we need and can do the + // actual RPC call. + req := &lnrpc.BakeMacaroonRequest{ + Permissions: parsedPermissions, + RootKeyId: rootKeyID, + AllowExternalPermissions: ctx.Bool( + "allow_external_permissions", + ), + } + resp, err := client.BakeMacaroon(ctxc, req) + if err != nil { + return err + } + + // Now we should have gotten a valid macaroon. Unmarshal it so + // we can add first-party caveats (if necessary) to it. + macBytes, err := hex.DecodeString(resp.Macaroon) + if err != nil { + return err + } + rawMacaroon = &macaroon.Macaroon{} + if err = rawMacaroon.UnmarshalBinary(macBytes); err != nil { + return err + } } // Now apply the desired constraints to the macaroon. This will always // create a new macaroon object, even if no constraints are added. - constrainedMac, err := applyMacaroonConstraints(ctx, unmarshalMac) + constrainedMac, err := applyMacaroonConstraints(ctx, rawMacaroon) if err != nil { return err } - macBytes, err = constrainedMac.MarshalBinary() + macBytes, err := constrainedMac.MarshalBinary() if err != nil { return err } From fcb21dfc246de29488afe2c12faf15ffc6af96fd Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 11 Oct 2024 10:19:15 +0200 Subject: [PATCH 4/4] docs: document deterministic macaroons, add release notes --- docs/macaroons.md | 40 ++++++++++++++++++++++ docs/release-notes/release-notes-0.19.0.md | 4 +++ 2 files changed, 44 insertions(+) diff --git a/docs/macaroons.md b/docs/macaroons.md index c30eb2cf32..5cc0150b75 100644 --- a/docs/macaroons.md +++ b/docs/macaroons.md @@ -158,6 +158,46 @@ Examples: $ lncli --macaroonpath=/safe/location/admin.macaroon getinfo ``` +## Using deterministic/pre-generated macaroons + +All macaroons are derived from a secret root key (by default from the root key +with the ID `"0"`). That root key is randomly generated when the macaroon store +is first initialized (when the wallet is created) and is therefore not +deterministic by default. + +It can be useful to use a deterministic (or pre-generated) root key, which is +why the `InitWallet` RPC (or the `lncli create` or `lncli createwatchonly` +counterparts) allows a root key to be specified. + +Using a pre-generated root key can be useful for scenarios like: +* Testing: If a node is always initialized with the same root key for each test + run, then macaroons generated in one test run can be re-used in another run + and don't need to be re-derived. +* Remote signing setup: When using a remote signing setup where there are two + related `lnd` nodes (e.g. a watch-only and a signer pair), it can be useful + to generate a valid macaroon _before_ any of the nodes are even started up. + +**Example**: + +The following example shows how a valid macaroon can be generated before even +starting a node: + +```shell +# Randomly generate a 32-byte long secret root key and encode it as hex. +ROOT_KEY=$(cat /dev/urandom | head -c32 | xxd -p -c32) + +# Derive a read-only macaroon from that root key. +# NOTE: When using the --root_key flag, the `lncli bakemacaroon` command is +# fully offline and does not need to connect to any lnd node. +lncli bakemacaroon --root_key $ROOT_KEY --save_to /tmp/info.macaroon info:read + +# Create the lnd node now, using the same root key. +lncli create --mac_root_key $ROOT_KEY + +# Use the pre-generated macaroon for a call. +lncli --macaroonpath /tmp/info.macaroon getinfo +``` + ## Using Macaroons with GRPC clients When interacting with `lnd` using the GRPC interface, the macaroons are encoded diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index 204d25a54d..9d73311898 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -46,6 +46,10 @@ ## lncli Additions +* [A pre-generated macaroon root key can now be specified in `lncli create` and + `lncli createwatchonly`](https://github.com/lightningnetwork/lnd/pull/9172) to + allow for deterministic macaroon generation. + # Improvements ## Functional Updates