Skip to content

Commit 5f86e25

Browse files
authored
Merge pull request #9172 from guggero/initwallet-mac-root-key
cmd: allow deterministic macaroon derivation with `lncli`
2 parents 7bf9b59 + fcb21df commit 5f86e25

File tree

6 files changed

+289
-29
lines changed

6 files changed

+289
-29
lines changed

cmd/commands/cmd_macaroon.go

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"unicode"
1212

13+
"github.com/lightningnetwork/lnd/fn"
1314
"github.com/lightningnetwork/lnd/lncfg"
1415
"github.com/lightningnetwork/lnd/lnrpc"
1516
"github.com/lightningnetwork/lnd/macaroons"
@@ -38,6 +39,12 @@ var (
3839
Usage: "the condition of the custom caveat to add, can be " +
3940
"empty if custom caveat doesn't need a value",
4041
}
42+
bakeFromRootKeyFlag = cli.StringFlag{
43+
Name: "root_key",
44+
Usage: "if the root key is known, it can be passed directly " +
45+
"as a hex encoded string, turning the command into " +
46+
"an offline operation",
47+
}
4148
)
4249

4350
var bakeMacaroonCommand = cli.Command{
@@ -48,7 +55,7 @@ var bakeMacaroonCommand = cli.Command{
4855
ArgsUsage: "[--save_to=] [--timeout=] [--ip_address=] " +
4956
"[--custom_caveat_name= [--custom_caveat_condition=]] " +
5057
"[--root_key_id=] [--allow_external_permissions] " +
51-
"permissions...",
58+
"[--root_key=] permissions...",
5259
Description: `
5360
Bake a new macaroon that grants the provided permissions and
5461
optionally adds restrictions (timeout, IP address) to it.
@@ -74,6 +81,12 @@ var bakeMacaroonCommand = cli.Command{
7481
7582
To get a list of all available URIs and permissions, use the
7683
"lncli listpermissions" command.
84+
85+
If the root key is known (for example because "lncli create" was used
86+
with a custom --mac_root_key value), it can be passed directly as a
87+
hex encoded string using the --root_key flag. This turns the command
88+
into an offline operation and the macaroon will be created without
89+
calling into the server's RPC endpoint.
7790
`,
7891
Flags: []cli.Flag{
7992
cli.StringFlag{
@@ -95,14 +108,13 @@ var bakeMacaroonCommand = cli.Command{
95108
Usage: "whether permissions lnd is not familiar with " +
96109
"are allowed",
97110
},
111+
bakeFromRootKeyFlag,
98112
},
99113
Action: actionDecorator(bakeMacaroon),
100114
}
101115

102116
func bakeMacaroon(ctx *cli.Context) error {
103117
ctxc := getContext()
104-
client, cleanUp := getClient(ctx)
105-
defer cleanUp()
106118

107119
// Show command help if no arguments.
108120
if ctx.NArg() == 0 {
@@ -154,36 +166,66 @@ func bakeMacaroon(ctx *cli.Context) error {
154166
)
155167
}
156168

157-
// Now we have gathered all the input we need and can do the actual
158-
// RPC call.
159-
req := &lnrpc.BakeMacaroonRequest{
160-
Permissions: parsedPermissions,
161-
RootKeyId: rootKeyID,
162-
AllowExternalPermissions: ctx.Bool("allow_external_permissions"),
163-
}
164-
resp, err := client.BakeMacaroon(ctxc, req)
165-
if err != nil {
166-
return err
167-
}
169+
var rawMacaroon *macaroon.Macaroon
170+
switch {
171+
case ctx.IsSet(bakeFromRootKeyFlag.Name):
172+
macRootKey, err := hex.DecodeString(
173+
ctx.String(bakeFromRootKeyFlag.Name),
174+
)
175+
if err != nil {
176+
return fmt.Errorf("unable to parse macaroon root key: "+
177+
"%w", err)
178+
}
168179

169-
// Now we should have gotten a valid macaroon. Unmarshal it so we can
170-
// add first-party caveats (if necessary) to it.
171-
macBytes, err := hex.DecodeString(resp.Macaroon)
172-
if err != nil {
173-
return err
174-
}
175-
unmarshalMac := &macaroon.Macaroon{}
176-
if err = unmarshalMac.UnmarshalBinary(macBytes); err != nil {
177-
return err
180+
ops := fn.Map(func(p *lnrpc.MacaroonPermission) bakery.Op {
181+
return bakery.Op{
182+
Entity: p.Entity,
183+
Action: p.Action,
184+
}
185+
}, parsedPermissions)
186+
187+
rawMacaroon, err = macaroons.BakeFromRootKey(macRootKey, ops)
188+
if err != nil {
189+
return fmt.Errorf("unable to bake macaroon: %w", err)
190+
}
191+
192+
default:
193+
client, cleanUp := getClient(ctx)
194+
defer cleanUp()
195+
196+
// Now we have gathered all the input we need and can do the
197+
// actual RPC call.
198+
req := &lnrpc.BakeMacaroonRequest{
199+
Permissions: parsedPermissions,
200+
RootKeyId: rootKeyID,
201+
AllowExternalPermissions: ctx.Bool(
202+
"allow_external_permissions",
203+
),
204+
}
205+
resp, err := client.BakeMacaroon(ctxc, req)
206+
if err != nil {
207+
return err
208+
}
209+
210+
// Now we should have gotten a valid macaroon. Unmarshal it so
211+
// we can add first-party caveats (if necessary) to it.
212+
macBytes, err := hex.DecodeString(resp.Macaroon)
213+
if err != nil {
214+
return err
215+
}
216+
rawMacaroon = &macaroon.Macaroon{}
217+
if err = rawMacaroon.UnmarshalBinary(macBytes); err != nil {
218+
return err
219+
}
178220
}
179221

180222
// Now apply the desired constraints to the macaroon. This will always
181223
// create a new macaroon object, even if no constraints are added.
182-
constrainedMac, err := applyMacaroonConstraints(ctx, unmarshalMac)
224+
constrainedMac, err := applyMacaroonConstraints(ctx, rawMacaroon)
183225
if err != nil {
184226
return err
185227
}
186-
macBytes, err = constrainedMac.MarshalBinary()
228+
macBytes, err := constrainedMac.MarshalBinary()
187229
if err != nil {
188230
return err
189231
}

cmd/commands/cmd_walletunlocker.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/lightningnetwork/lnd/lncfg"
1313
"github.com/lightningnetwork/lnd/lnrpc"
1414
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
15+
"github.com/lightningnetwork/lnd/macaroons"
1516
"github.com/lightningnetwork/lnd/walletunlocker"
1617
"github.com/urfave/cli"
1718
)
@@ -26,6 +27,13 @@ var (
2627
Name: "save_to",
2728
Usage: "save returned admin macaroon to this file",
2829
}
30+
macRootKeyFlag = cli.StringFlag{
31+
Name: "mac_root_key",
32+
Usage: "macaroon root key to use when initializing the " +
33+
"macaroon store; allows for deterministic macaroon " +
34+
"generation; if not set, a random one will be " +
35+
"created",
36+
}
2937
)
3038

3139
var createCommand = cli.Command{
@@ -81,6 +89,7 @@ var createCommand = cli.Command{
8189
},
8290
statelessInitFlag,
8391
saveToFlag,
92+
macRootKeyFlag,
8493
},
8594
Action: actionDecorator(create),
8695
}
@@ -261,6 +270,7 @@ mnemonicCheck:
261270
extendedRootKey string
262271
extendedRootKeyBirthday uint64
263272
recoveryWindow int32
273+
macRootKey []byte
264274
)
265275
switch {
266276
// Use an existing cipher seed mnemonic in the aezeed format.
@@ -366,6 +376,23 @@ mnemonicCheck:
366376
printCipherSeedWords(cipherSeedMnemonic)
367377
}
368378

379+
// Parse the macaroon root key if it was specified by the user.
380+
if ctx.IsSet(macRootKeyFlag.Name) {
381+
macRootKey, err = hex.DecodeString(
382+
ctx.String(macRootKeyFlag.Name),
383+
)
384+
if err != nil {
385+
return fmt.Errorf("unable to parse macaroon root key: "+
386+
"%w", err)
387+
}
388+
389+
if len(macRootKey) != macaroons.RootKeyLen {
390+
return fmt.Errorf("macaroon root key must be exactly "+
391+
"%v bytes, got %v", macaroons.RootKeyLen,
392+
len(macRootKey))
393+
}
394+
}
395+
369396
// With either the user's prior cipher seed, or a newly generated one,
370397
// we'll go ahead and initialize the wallet.
371398
req := &lnrpc.InitWalletRequest{
@@ -377,6 +404,7 @@ mnemonicCheck:
377404
RecoveryWindow: recoveryWindow,
378405
ChannelBackups: chanBackups,
379406
StatelessInit: statelessInit,
407+
MacaroonRootKey: macRootKey,
380408
}
381409
response, err := client.InitWallet(ctxc, req)
382410
if err != nil {
@@ -687,6 +715,7 @@ var createWatchOnlyCommand = cli.Command{
687715
Flags: []cli.Flag{
688716
statelessInitFlag,
689717
saveToFlag,
718+
macRootKeyFlag,
690719
},
691720
Action: actionDecorator(createWatchOnly),
692721
}
@@ -764,11 +793,30 @@ func createWatchOnly(ctx *cli.Context) error {
764793
}
765794
}
766795

796+
// Parse the macaroon root key if it was specified by the user.
797+
var macRootKey []byte
798+
if ctx.IsSet(macRootKeyFlag.Name) {
799+
macRootKey, err = hex.DecodeString(
800+
ctx.String(macRootKeyFlag.Name),
801+
)
802+
if err != nil {
803+
return fmt.Errorf("unable to parse macaroon root key: "+
804+
"%w", err)
805+
}
806+
807+
if len(macRootKey) != macaroons.RootKeyLen {
808+
return fmt.Errorf("macaroon root key must be exactly "+
809+
"%v bytes, got %v", macaroons.RootKeyLen,
810+
len(macRootKey))
811+
}
812+
}
813+
767814
initResp, err := client.InitWallet(ctxc, &lnrpc.InitWalletRequest{
768-
WalletPassword: walletPassword,
769-
WatchOnly: rpcResp,
770-
RecoveryWindow: recoveryWindow,
771-
StatelessInit: statelessInit,
815+
WalletPassword: walletPassword,
816+
WatchOnly: rpcResp,
817+
RecoveryWindow: recoveryWindow,
818+
StatelessInit: statelessInit,
819+
MacaroonRootKey: macRootKey,
772820
})
773821
if err != nil {
774822
return err

docs/macaroons.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,46 @@ Examples:
158158
$ lncli --macaroonpath=/safe/location/admin.macaroon getinfo
159159
```
160160

161+
## Using deterministic/pre-generated macaroons
162+
163+
All macaroons are derived from a secret root key (by default from the root key
164+
with the ID `"0"`). That root key is randomly generated when the macaroon store
165+
is first initialized (when the wallet is created) and is therefore not
166+
deterministic by default.
167+
168+
It can be useful to use a deterministic (or pre-generated) root key, which is
169+
why the `InitWallet` RPC (or the `lncli create` or `lncli createwatchonly`
170+
counterparts) allows a root key to be specified.
171+
172+
Using a pre-generated root key can be useful for scenarios like:
173+
* Testing: If a node is always initialized with the same root key for each test
174+
run, then macaroons generated in one test run can be re-used in another run
175+
and don't need to be re-derived.
176+
* Remote signing setup: When using a remote signing setup where there are two
177+
related `lnd` nodes (e.g. a watch-only and a signer pair), it can be useful
178+
to generate a valid macaroon _before_ any of the nodes are even started up.
179+
180+
**Example**:
181+
182+
The following example shows how a valid macaroon can be generated before even
183+
starting a node:
184+
185+
```shell
186+
# Randomly generate a 32-byte long secret root key and encode it as hex.
187+
ROOT_KEY=$(cat /dev/urandom | head -c32 | xxd -p -c32)
188+
189+
# Derive a read-only macaroon from that root key.
190+
# NOTE: When using the --root_key flag, the `lncli bakemacaroon` command is
191+
# fully offline and does not need to connect to any lnd node.
192+
lncli bakemacaroon --root_key $ROOT_KEY --save_to /tmp/info.macaroon info:read
193+
194+
# Create the lnd node now, using the same root key.
195+
lncli create --mac_root_key $ROOT_KEY
196+
197+
# Use the pre-generated macaroon for a call.
198+
lncli --macaroonpath /tmp/info.macaroon getinfo
199+
```
200+
161201
## Using Macaroons with GRPC clients
162202

163203
When interacting with `lnd` using the GRPC interface, the macaroons are encoded

docs/release-notes/release-notes-0.19.0.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646

4747
## lncli Additions
4848

49+
* [A pre-generated macaroon root key can now be specified in `lncli create` and
50+
`lncli createwatchonly`](https://github.com/lightningnetwork/lnd/pull/9172) to
51+
allow for deterministic macaroon generation.
52+
4953
# Improvements
5054
## Functional Updates
5155

macaroons/bake.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package macaroons
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
7+
"golang.org/x/net/context"
8+
"gopkg.in/macaroon-bakery.v2/bakery"
9+
"gopkg.in/macaroon.v2"
10+
)
11+
12+
// inMemoryRootKeyStore is a simple implementation of bakery.RootKeyStore that
13+
// stores a single root key in memory.
14+
type inMemoryRootKeyStore struct {
15+
rootKey []byte
16+
}
17+
18+
// A compile-time check to ensure that inMemoryRootKeyStore implements
19+
// bakery.RootKeyStore.
20+
var _ bakery.RootKeyStore = (*inMemoryRootKeyStore)(nil)
21+
22+
// Get returns the root key for the given id. If the item is not there, it
23+
// returns ErrNotFound.
24+
func (s *inMemoryRootKeyStore) Get(_ context.Context, id []byte) ([]byte,
25+
error) {
26+
27+
if !bytes.Equal(id, DefaultRootKeyID) {
28+
return nil, bakery.ErrNotFound
29+
}
30+
31+
return s.rootKey, nil
32+
}
33+
34+
// RootKey returns the root key to be used for making a new macaroon, and an id
35+
// that can be used to look it up later with the Get method.
36+
func (s *inMemoryRootKeyStore) RootKey(context.Context) ([]byte, []byte,
37+
error) {
38+
39+
return s.rootKey, DefaultRootKeyID, nil
40+
}
41+
42+
// BakeFromRootKey creates a new macaroon that is derived from the given root
43+
// key and permissions.
44+
func BakeFromRootKey(rootKey []byte,
45+
permissions []bakery.Op) (*macaroon.Macaroon, error) {
46+
47+
if len(rootKey) != RootKeyLen {
48+
return nil, fmt.Errorf("root key must be %d bytes, is %d",
49+
RootKeyLen, len(rootKey))
50+
}
51+
52+
rootKeyStore := &inMemoryRootKeyStore{
53+
rootKey: rootKey,
54+
}
55+
56+
service, err := NewService(rootKeyStore, "lnd", false)
57+
if err != nil {
58+
return nil, fmt.Errorf("unable to create service: %w", err)
59+
}
60+
61+
ctx := context.Background()
62+
mac, err := service.NewMacaroon(ctx, DefaultRootKeyID, permissions...)
63+
if err != nil {
64+
return nil, fmt.Errorf("unable to create macaroon: %w", err)
65+
}
66+
67+
return mac.M(), nil
68+
}

0 commit comments

Comments
 (0)