Merge pull request #9172 from guggero/initwallet-mac-root-key
cmd: allow deterministic macaroon derivation with `lncli`
This commit is contained in:
6 files changed
+289
-29
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user