• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 11357847688

16 Oct 2024 02:36AM UTC coverage: 57.864% (-0.9%) from 58.779%
11357847688

Pull #9148

github

ProofOfKeags
lnwire: change DynPropose/DynCommit TLV numbers to align with spec
Pull Request #9148: DynComms [2/n]: lnwire: add authenticated wire messages for Dyn*

350 of 644 new or added lines in 12 files covered. (54.35%)

19831 existing lines in 241 files now uncovered.

99337 of 171674 relevant lines covered (57.86%)

38595.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/cmd/commands/cmd_macaroon.go
1
package commands
2

3
import (
4
        "bytes"
5
        "encoding/hex"
6
        "fmt"
7
        "net"
8
        "os"
9
        "strconv"
10
        "strings"
11
        "unicode"
12

13
        "github.com/lightningnetwork/lnd/lncfg"
14
        "github.com/lightningnetwork/lnd/lnrpc"
15
        "github.com/lightningnetwork/lnd/macaroons"
16
        "github.com/urfave/cli"
17
        "google.golang.org/protobuf/proto"
18
        "gopkg.in/macaroon-bakery.v2/bakery"
19
        "gopkg.in/macaroon.v2"
20
)
21

22
var (
23
        macTimeoutFlag = cli.Uint64Flag{
24
                Name: "timeout",
25
                Usage: "the number of seconds the macaroon will be " +
26
                        "valid before it times out",
27
        }
28
        macIPAddressFlag = cli.StringFlag{
29
                Name:  "ip_address",
30
                Usage: "the IP address the macaroon will be bound to",
31
        }
32
        macCustomCaveatNameFlag = cli.StringFlag{
33
                Name:  "custom_caveat_name",
34
                Usage: "the name of the custom caveat to add",
35
        }
36
        macCustomCaveatConditionFlag = cli.StringFlag{
37
                Name: "custom_caveat_condition",
38
                Usage: "the condition of the custom caveat to add, can be " +
39
                        "empty if custom caveat doesn't need a value",
40
        }
41
)
42

43
var bakeMacaroonCommand = cli.Command{
44
        Name:     "bakemacaroon",
45
        Category: "Macaroons",
46
        Usage: "Bakes a new macaroon with the provided list of permissions " +
47
                "and restrictions.",
48
        ArgsUsage: "[--save_to=] [--timeout=] [--ip_address=] " +
49
                "[--custom_caveat_name= [--custom_caveat_condition=]] " +
50
                "[--root_key_id=] [--allow_external_permissions] " +
51
                "permissions...",
52
        Description: `
53
        Bake a new macaroon that grants the provided permissions and
54
        optionally adds restrictions (timeout, IP address) to it.
55

56
        The new macaroon can either be shown on command line in hex serialized
57
        format or it can be saved directly to a file using the --save_to
58
        argument.
59

60
        A permission is a tuple of an entity and an action, separated by a
61
        colon. Multiple operations can be added as arguments, for example:
62

63
        lncli bakemacaroon info:read invoices:write foo:bar
64

65
        For even more fine-grained permission control, it is also possible to
66
        specify single RPC method URIs that are allowed to be accessed by a
67
        macaroon. This can be achieved by specifying "uri:<methodURI>" pairs,
68
        for example:
69

70
        lncli bakemacaroon uri:/lnrpc.Lightning/GetInfo uri:/verrpc.Versioner/GetVersion
71

72
        The macaroon created by this command would only be allowed to use the
73
        "lncli getinfo" and "lncli version" commands.
74

75
        To get a list of all available URIs and permissions, use the
76
        "lncli listpermissions" command.
77
        `,
78
        Flags: []cli.Flag{
79
                cli.StringFlag{
80
                        Name: "save_to",
81
                        Usage: "save the created macaroon to this file " +
82
                                "using the default binary format",
83
                },
84
                macTimeoutFlag,
85
                macIPAddressFlag,
86
                macCustomCaveatNameFlag,
87
                macCustomCaveatConditionFlag,
88
                cli.Uint64Flag{
89
                        Name: "root_key_id",
90
                        Usage: "the numerical root key ID used to create the " +
91
                                "macaroon",
92
                },
93
                cli.BoolFlag{
94
                        Name: "allow_external_permissions",
95
                        Usage: "whether permissions lnd is not familiar with " +
96
                                "are allowed",
97
                },
98
        },
99
        Action: actionDecorator(bakeMacaroon),
100
}
101

102
func bakeMacaroon(ctx *cli.Context) error {
103
        ctxc := getContext()
104
        client, cleanUp := getClient(ctx)
105
        defer cleanUp()
106

107
        // Show command help if no arguments.
108
        if ctx.NArg() == 0 {
109
                return cli.ShowCommandHelp(ctx, "bakemacaroon")
110
        }
111
        args := ctx.Args()
112

113
        var (
114
                savePath          string
115
                rootKeyID         uint64
116
                parsedPermissions []*lnrpc.MacaroonPermission
×
117
                err               error
×
118
        )
×
119

×
120
        if ctx.String("save_to") != "" {
×
121
                savePath = lncfg.CleanAndExpandPath(ctx.String("save_to"))
×
122
        }
×
UNCOV
123

×
124
        if ctx.IsSet("root_key_id") {
×
125
                rootKeyID = ctx.Uint64("root_key_id")
×
126
        }
×
UNCOV
127

×
UNCOV
128
        // A command line argument can't be an empty string. So we'll check each
×
UNCOV
129
        // entry if it's a valid entity:action tuple. The content itself is
×
UNCOV
130
        // validated server side. We just make sure we can parse it correctly.
×
131
        for _, permission := range args {
×
132
                tuple := strings.Split(permission, ":")
×
133
                if len(tuple) != 2 {
×
134
                        return fmt.Errorf("unable to parse "+
×
135
                                "permission tuple: %s", permission)
136
                }
×
137
                entity, action := tuple[0], tuple[1]
×
138
                if entity == "" {
×
139
                        return fmt.Errorf("invalid permission [%s]. entity "+
140
                                "cannot be empty", permission)
141
                }
142
                if action == "" {
143
                        return fmt.Errorf("invalid permission [%s]. action "+
×
144
                                "cannot be empty", permission)
×
145
                }
×
UNCOV
146

×
UNCOV
147
                // No we can assume that we have a formally valid entity:action
×
UNCOV
148
                // tuple. The rest of the validation happens server side.
×
149
                parsedPermissions = append(
×
150
                        parsedPermissions, &lnrpc.MacaroonPermission{
×
151
                                Entity: entity,
×
152
                                Action: action,
×
153
                        },
×
154
                )
×
UNCOV
155
        }
×
UNCOV
156

×
UNCOV
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
        }
168

UNCOV
169
        // Now we should have gotten a valid macaroon. Unmarshal it so we can
×
UNCOV
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
×
178
        }
×
179

UNCOV
180
        // Now apply the desired constraints to the macaroon. This will always
×
UNCOV
181
        // create a new macaroon object, even if no constraints are added.
×
182
        constrainedMac, err := applyMacaroonConstraints(ctx, unmarshalMac)
×
183
        if err != nil {
×
184
                return err
×
185
        }
×
186
        macBytes, err = constrainedMac.MarshalBinary()
187
        if err != nil {
×
188
                return err
×
189
        }
×
UNCOV
190

×
191
        // Now we can output the result. We either write it binary serialized to
UNCOV
192
        // a file or write to the standard output using hex encoding.
×
193
        switch {
×
194
        case savePath != "":
×
195
                err = os.WriteFile(savePath, macBytes, 0644)
×
196
                if err != nil {
×
197
                        return err
×
198
                }
×
199
                fmt.Printf("Macaroon saved to %s\n", savePath)
×
UNCOV
200

×
201
        default:
×
202
                fmt.Printf("%s\n", hex.EncodeToString(macBytes))
×
UNCOV
203
        }
×
UNCOV
204

×
205
        return nil
×
UNCOV
206
}
×
UNCOV
207

×
UNCOV
208
var listMacaroonIDsCommand = cli.Command{
×
209
        Name:     "listmacaroonids",
210
        Category: "Macaroons",
211
        Usage:    "List all macaroons root key IDs in use.",
UNCOV
212
        Action:   actionDecorator(listMacaroonIDs),
×
UNCOV
213
}
×
UNCOV
214

×
215
func listMacaroonIDs(ctx *cli.Context) error {
×
216
        ctxc := getContext()
×
217
        client, cleanUp := getClient(ctx)
×
218
        defer cleanUp()
×
219

×
220
        req := &lnrpc.ListMacaroonIDsRequest{}
221
        resp, err := client.ListMacaroonIDs(ctxc, req)
222
        if err != nil {
223
                return err
224
        }
×
UNCOV
225

×
226
        printRespJSON(resp)
×
227
        return nil
×
UNCOV
228
}
×
UNCOV
229

×
UNCOV
230
var deleteMacaroonIDCommand = cli.Command{
×
UNCOV
231
        Name:      "deletemacaroonid",
×
232
        Category:  "Macaroons",
233
        Usage:     "Delete a specific macaroon ID.",
234
        ArgsUsage: "root_key_id",
UNCOV
235
        Description: `
×
UNCOV
236
        Remove a macaroon ID using the specified root key ID. For example:
×
UNCOV
237

×
UNCOV
238
        lncli deletemacaroonid 1
×
UNCOV
239

×
UNCOV
240
        WARNING
×
UNCOV
241
        When the ID is deleted, all macaroons created from that root key will
×
242
        be invalidated.
UNCOV
243

×
UNCOV
244
        Note that the default root key ID 0 cannot be deleted.
×
245
        `,
246
        Action: actionDecorator(deleteMacaroonID),
UNCOV
247
}
×
248

249
func deleteMacaroonID(ctx *cli.Context) error {
250
        ctxc := getContext()
251
        client, cleanUp := getClient(ctx)
252
        defer cleanUp()
253

254
        // Validate args length. Only one argument is allowed.
255
        if ctx.NArg() != 1 {
256
                return cli.ShowCommandHelp(ctx, "deletemacaroonid")
257
        }
×
UNCOV
258

×
259
        rootKeyIDString := ctx.Args().First()
×
260

×
261
        // Convert string into uint64.
×
262
        rootKeyID, err := strconv.ParseUint(rootKeyIDString, 10, 64)
×
263
        if err != nil {
×
264
                return fmt.Errorf("root key ID must be a positive integer")
×
265
        }
×
UNCOV
266

×
267
        // Check that the value is not equal to DefaultRootKeyID. Note that the
UNCOV
268
        // server also validates the root key ID when removing it. However, we check
×
UNCOV
269
        // it here too so that we can give users a nice warning.
×
270
        if bytes.Equal([]byte(rootKeyIDString), macaroons.DefaultRootKeyID) {
271
                return fmt.Errorf("deleting the default root key ID 0 is not allowed")
272
        }
273

274
        // Make the actual RPC call.
275
        req := &lnrpc.DeleteMacaroonIDRequest{
276
                RootKeyId: rootKeyID,
277
        }
278
        resp, err := client.DeleteMacaroonID(ctxc, req)
279
        if err != nil {
280
                return err
281
        }
282

283
        printRespJSON(resp)
284
        return nil
285
}
286

287
var listPermissionsCommand = cli.Command{
288
        Name:     "listpermissions",
289
        Category: "Macaroons",
290
        Usage: "Lists all RPC method URIs and the macaroon permissions they " +
UNCOV
291
                "require to be invoked.",
×
UNCOV
292
        Action: actionDecorator(listPermissions),
×
UNCOV
293
}
×
UNCOV
294

×
295
func listPermissions(ctx *cli.Context) error {
×
296
        ctxc := getContext()
×
297
        client, cleanUp := getClient(ctx)
×
298
        defer cleanUp()
×
299

×
300
        request := &lnrpc.ListPermissionsRequest{}
301
        response, err := client.ListPermissions(ctxc, request)
×
302
        if err != nil {
×
303
                return err
×
304
        }
×
UNCOV
305

×
306
        printRespJSON(response)
×
307

×
308
        return nil
309
}
310

311
type macaroonContent struct {
UNCOV
312
        Version     uint16   `json:"version"`
×
UNCOV
313
        Location    string   `json:"location"`
×
UNCOV
314
        RootKeyID   string   `json:"root_key_id"`
×
315
        Permissions []string `json:"permissions"`
316
        Caveats     []string `json:"caveats"`
UNCOV
317
}
×
UNCOV
318

×
UNCOV
319
var printMacaroonCommand = cli.Command{
×
UNCOV
320
        Name:      "printmacaroon",
×
UNCOV
321
        Category:  "Macaroons",
×
UNCOV
322
        Usage:     "Print the content of a macaroon in a human readable format.",
×
UNCOV
323
        ArgsUsage: "[macaroon_content_hex]",
×
324
        Description: `
UNCOV
325
        Decode a macaroon and show its content in a more human readable format.
×
UNCOV
326
        The macaroon can either be passed as a hex encoded positional parameter
×
327
        or loaded from a file.
328
        `,
329
        Flags: []cli.Flag{
330
                cli.StringFlag{
331
                        Name: "macaroon_file",
332
                        Usage: "load the macaroon from a file instead of the " +
333
                                "command line directly",
334
                },
335
        },
336
        Action: actionDecorator(printMacaroon),
UNCOV
337
}
×
UNCOV
338

×
339
func printMacaroon(ctx *cli.Context) error {
×
340
        // Show command help if no arguments or flags are set.
×
341
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
342
                return cli.ShowCommandHelp(ctx, "printmacaroon")
×
343
        }
×
UNCOV
344

×
345
        var (
×
346
                macBytes []byte
×
347
                err      error
348
                args     = ctx.Args()
×
349
        )
×
350
        switch {
×
351
        case ctx.IsSet("macaroon_file"):
352
                macPath := lncfg.CleanAndExpandPath(ctx.String("macaroon_file"))
353

354
                // Load the specified macaroon file.
355
                macBytes, err = os.ReadFile(macPath)
356
                if err != nil {
357
                        return fmt.Errorf("unable to read macaroon path %v: %v",
358
                                macPath, err)
359
                }
360

361
        case args.Present():
362
                macBytes, err = hex.DecodeString(args.First())
363
                if err != nil {
364
                        return fmt.Errorf("unable to hex decode macaroon: %w",
365
                                err)
366
                }
367

368
        default:
369
                return fmt.Errorf("macaroon parameter missing")
370
        }
371

372
        // Decode the macaroon and its protobuf encoded internal identifier.
373
        mac := &macaroon.Macaroon{}
374
        if err = mac.UnmarshalBinary(macBytes); err != nil {
375
                return fmt.Errorf("unable to decode macaroon: %w", err)
376
        }
377
        rawID := mac.Id()
378
        if rawID[0] != byte(bakery.LatestVersion) {
379
                return fmt.Errorf("invalid macaroon version: %x", rawID)
380
        }
381
        decodedID := &lnrpc.MacaroonId{}
×
382
        idProto := rawID[1:]
×
383
        err = proto.Unmarshal(idProto, decodedID)
×
384
        if err != nil {
×
385
                return fmt.Errorf("unable to decode macaroon version: %w", err)
×
386
        }
UNCOV
387

×
UNCOV
388
        // Prepare everything to be printed in a more human-readable format.
×
389
        content := &macaroonContent{
×
390
                Version:     uint16(mac.Version()),
×
391
                Location:    mac.Location(),
×
392
                RootKeyID:   string(decodedID.StorageId),
×
393
                Permissions: nil,
×
394
                Caveats:     nil,
×
395
        }
×
396

×
397
        for _, caveat := range mac.Caveats() {
×
398
                content.Caveats = append(content.Caveats, string(caveat.Id))
×
399
        }
×
400
        for _, op := range decodedID.Ops {
×
401
                for _, action := range op.Actions {
×
402
                        permission := fmt.Sprintf("%s:%s", op.Entity, action)
403
                        content.Permissions = append(
×
404
                                content.Permissions, permission,
×
405
                        )
×
406
                }
×
UNCOV
407
        }
×
UNCOV
408

×
409
        printJSON(content)
410

×
411
        return nil
×
412
}
413

414
var constrainMacaroonCommand = cli.Command{
UNCOV
415
        Name:     "constrainmacaroon",
×
UNCOV
416
        Category: "Macaroons",
×
UNCOV
417
        Usage:    "Adds one or more restriction(s) to an existing macaroon",
×
UNCOV
418
        ArgsUsage: "[--timeout=] [--ip_address=] [--custom_caveat_name= " +
×
UNCOV
419
                "[--custom_caveat_condition=]] input-macaroon-file " +
×
UNCOV
420
                "constrained-macaroon-file",
×
UNCOV
421
        Description: `
×
UNCOV
422
        Add one or more first-party caveat(s) (a.k.a. constraints/restrictions)
×
UNCOV
423
        to an existing macaroon.
×
UNCOV
424
        `,
×
UNCOV
425
        Flags: []cli.Flag{
×
UNCOV
426
                macTimeoutFlag,
×
UNCOV
427
                macIPAddressFlag,
×
UNCOV
428
                macCustomCaveatNameFlag,
×
429
                macCustomCaveatConditionFlag,
430
        },
UNCOV
431
        Action: actionDecorator(constrainMacaroon),
×
UNCOV
432
}
×
UNCOV
433

×
434
func constrainMacaroon(ctx *cli.Context) error {
×
435
        // Show command help if not enough arguments.
×
436
        if ctx.NArg() != 2 {
×
437
                return cli.ShowCommandHelp(ctx, "constrainmacaroon")
×
438
        }
×
439
        args := ctx.Args()
×
440

×
441
        sourceMacFile := lncfg.CleanAndExpandPath(args.First())
×
442
        args = args.Tail()
×
443

×
444
        sourceMacBytes, err := os.ReadFile(sourceMacFile)
×
445
        if err != nil {
×
446
                return fmt.Errorf("error trying to read source macaroon file "+
×
447
                        "%s: %v", sourceMacFile, err)
×
448
        }
×
449

450
        destMacFile := lncfg.CleanAndExpandPath(args.First())
451

×
452
        // Now we should have gotten a valid macaroon. Unmarshal it so we can
×
453
        // add first-party caveats (if necessary) to it.
×
454
        sourceMac := &macaroon.Macaroon{}
455
        if err = sourceMac.UnmarshalBinary(sourceMacBytes); err != nil {
456
                return fmt.Errorf("error unmarshalling source macaroon file "+
457
                        "%s: %v", sourceMacFile, err)
458
        }
459

460
        // Now apply the desired constraints to the macaroon. This will always
461
        // create a new macaroon object, even if no constraints are added.
462
        constrainedMac, err := applyMacaroonConstraints(ctx, sourceMac)
463
        if err != nil {
464
                return err
465
        }
466

467
        destMacBytes, err := constrainedMac.MarshalBinary()
468
        if err != nil {
469
                return fmt.Errorf("error marshaling destination macaroon "+
470
                        "file: %v", err)
471
        }
472

473
        // Now we can output the result.
474
        err = os.WriteFile(destMacFile, destMacBytes, 0644)
475
        if err != nil {
476
                return fmt.Errorf("error writing destination macaroon file "+
×
477
                        "%s: %v", destMacFile, err)
×
478
        }
×
479
        fmt.Printf("Macaroon saved to %s\n", destMacFile)
×
480

×
481
        return nil
×
UNCOV
482
}
×
UNCOV
483

×
UNCOV
484
// applyMacaroonConstraints parses and applies all currently supported macaroon
×
UNCOV
485
// condition flags from the command line to the given macaroon and returns a new
×
UNCOV
486
// macaroon instance.
×
UNCOV
487
func applyMacaroonConstraints(ctx *cli.Context,
×
488
        mac *macaroon.Macaroon) (*macaroon.Macaroon, error) {
×
489

×
490
        macConstraints := make([]macaroons.Constraint, 0)
×
491

492
        if ctx.IsSet(macTimeoutFlag.Name) {
×
493
                timeout := ctx.Int64(macTimeoutFlag.Name)
×
494
                if timeout <= 0 {
×
495
                        return nil, fmt.Errorf("timeout must be greater than 0")
×
496
                }
×
497
                macConstraints = append(
×
498
                        macConstraints, macaroons.TimeoutConstraint(timeout),
×
499
                )
×
UNCOV
500
        }
×
501

502
        if ctx.IsSet(macIPAddressFlag.Name) {
503
                ipAddress := net.ParseIP(ctx.String(macIPAddressFlag.Name))
504
                if ipAddress == nil {
×
505
                        return nil, fmt.Errorf("unable to parse ip_address: %s",
×
506
                                ctx.String("ip_address"))
×
507
                }
×
508

509
                macConstraints = append(
×
510
                        macConstraints,
×
511
                        macaroons.IPLockConstraint(ipAddress.String()),
×
512
                )
×
UNCOV
513
        }
×
514

515
        if ctx.IsSet(macCustomCaveatNameFlag.Name) {
516
                customCaveatName := ctx.String(macCustomCaveatNameFlag.Name)
×
517
                if containsWhiteSpace(customCaveatName) {
×
518
                        return nil, fmt.Errorf("unexpected white space found " +
×
519
                                "in custom caveat name")
×
520
                }
×
521
                if customCaveatName == "" {
×
522
                        return nil, fmt.Errorf("invalid custom caveat name")
×
523
                }
×
524

525
                var customCaveatCond string
526
                if ctx.IsSet(macCustomCaveatConditionFlag.Name) {
527
                        customCaveatCond = ctx.String(
528
                                macCustomCaveatConditionFlag.Name,
529
                        )
530
                        if containsWhiteSpace(customCaveatCond) {
×
531
                                return nil, fmt.Errorf("unexpected white " +
×
532
                                        "space found in custom caveat " +
×
533
                                        "condition")
×
534
                        }
×
535
                        if customCaveatCond == "" {
×
536
                                return nil, fmt.Errorf("invalid custom " +
×
537
                                        "caveat condition")
×
538
                        }
×
UNCOV
539
                }
×
UNCOV
540

×
UNCOV
541
                // The custom caveat condition is optional, it could just be a
×
542
                // marker tag in the macaroon with just a name. The interceptor
543
                // itself doesn't care about the value anyway.
544
                macConstraints = append(
×
545
                        macConstraints, macaroons.CustomConstraint(
×
546
                                customCaveatName, customCaveatCond,
×
547
                        ),
×
548
                )
×
UNCOV
549
        }
×
550

551
        constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...)
×
552
        if err != nil {
×
553
                return nil, fmt.Errorf("error adding constraints: %w", err)
×
554
        }
×
555

556
        return constrainedMac, nil
UNCOV
557
}
×
UNCOV
558

×
UNCOV
559
// containsWhiteSpace returns true if the given string contains any character
×
UNCOV
560
// that is considered to be a white space or non-printable character such as
×
UNCOV
561
// space, tabulator, newline, carriage return and some more exotic ones.
×
562
func containsWhiteSpace(str string) bool {
×
563
        return strings.IndexFunc(str, unicode.IsSpace) >= 0
×
564
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc