Skip to content

Commit

Permalink
Merge pull request #9172 from guggero/initwallet-mac-root-key
Browse files Browse the repository at this point in the history
cmd: allow deterministic macaroon derivation with `lncli`
  • Loading branch information
guggero authored Oct 15, 2024
2 parents 7bf9b59 + fcb21df commit 5f86e25
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 29 deletions.
92 changes: 67 additions & 25 deletions cmd/commands/cmd_macaroon.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand All @@ -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.
Expand All @@ -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{
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
56 changes: 52 additions & 4 deletions cmd/commands/cmd_walletunlocker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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{
Expand Down Expand Up @@ -81,6 +89,7 @@ var createCommand = cli.Command{
},
statelessInitFlag,
saveToFlag,
macRootKeyFlag,
},
Action: actionDecorator(create),
}
Expand Down Expand Up @@ -261,6 +270,7 @@ mnemonicCheck:
extendedRootKey string
extendedRootKeyBirthday uint64
recoveryWindow int32
macRootKey []byte
)
switch {
// Use an existing cipher seed mnemonic in the aezeed format.
Expand Down Expand Up @@ -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{
Expand All @@ -377,6 +404,7 @@ mnemonicCheck:
RecoveryWindow: recoveryWindow,
ChannelBackups: chanBackups,
StatelessInit: statelessInit,
MacaroonRootKey: macRootKey,
}
response, err := client.InitWallet(ctxc, req)
if err != nil {
Expand Down Expand Up @@ -687,6 +715,7 @@ var createWatchOnlyCommand = cli.Command{
Flags: []cli.Flag{
statelessInitFlag,
saveToFlag,
macRootKeyFlag,
},
Action: actionDecorator(createWatchOnly),
}
Expand Down Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions docs/macaroons.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/release-notes/release-notes-0.19.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
68 changes: 68 additions & 0 deletions macaroons/bake.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 5f86e25

Please sign in to comment.