package main import ( "bytes" "context" "encoding/hex" "fmt" "io/ioutil" "net" "strconv" "strings" "github.com/golang/protobuf/proto" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/macaroons" "github.com/urfave/cli" "gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon.v2" ) var bakeMacaroonCommand = cli.Command{ Name: "bakemacaroon", Category: "Macaroons", Usage: "Bakes a new macaroon with the provided list of permissions " + "and restrictions.", ArgsUsage: "[--save_to=] [--timeout=] [--ip_address=] permissions...", Description: ` Bake a new macaroon that grants the provided permissions and optionally adds restrictions (timeout, IP address) to it. The new macaroon can either be shown on command line in hex serialized format or it can be saved directly to a file using the --save_to argument. A permission is a tuple of an entity and an action, separated by a colon. Multiple operations can be added as arguments, for example: lncli bakemacaroon info:read invoices:write foo:bar For even more fine-grained permission control, it is also possible to specify single RPC method URIs that are allowed to be accessed by a macaroon. This can be achieved by specifying "uri:" pairs, for example: lncli bakemacaroon uri:/lnrpc.Lightning/GetInfo uri:/verrpc.Versioner/GetVersion The macaroon created by this command would only be allowed to use the "lncli getinfo" and "lncli version" commands. To get a list of all available URIs and permissions, use the "lncli listpermissions" command. `, Flags: []cli.Flag{ cli.StringFlag{ Name: "save_to", Usage: "save the created macaroon to this file " + "using the default binary format", }, cli.Uint64Flag{ Name: "timeout", Usage: "the number of seconds the macaroon will be " + "valid before it times out", }, cli.StringFlag{ Name: "ip_address", Usage: "the IP address the macaroon will be bound to", }, cli.Uint64Flag{ Name: "root_key_id", Usage: "the numerical root key ID used to create the macaroon", }, }, Action: actionDecorator(bakeMacaroon), } func bakeMacaroon(ctx *cli.Context) error { client, cleanUp := getClient(ctx) defer cleanUp() // Show command help if no arguments. if ctx.NArg() == 0 { return cli.ShowCommandHelp(ctx, "bakemacaroon") } args := ctx.Args() var ( savePath string timeout int64 ipAddress net.IP rootKeyID uint64 parsedPermissions []*lnrpc.MacaroonPermission err error ) if ctx.String("save_to") != "" { savePath = cleanAndExpandPath(ctx.String("save_to")) } if ctx.IsSet("timeout") { timeout = ctx.Int64("timeout") if timeout <= 0 { return fmt.Errorf("timeout must be greater than 0") } } if ctx.IsSet("ip_address") { ipAddress = net.ParseIP(ctx.String("ip_address")) if ipAddress == nil { return fmt.Errorf("unable to parse ip_address: %s", ctx.String("ip_address")) } } if ctx.IsSet("root_key_id") { rootKeyID = ctx.Uint64("root_key_id") } // A command line argument can't be an empty string. So we'll check each // entry if it's a valid entity:action tuple. The content itself is // validated server side. We just make sure we can parse it correctly. for _, permission := range args { tuple := strings.Split(permission, ":") if len(tuple) != 2 { return fmt.Errorf("unable to parse "+ "permission tuple: %s", permission) } entity, action := tuple[0], tuple[1] if entity == "" { return fmt.Errorf("invalid permission [%s]. entity "+ "cannot be empty", permission) } if action == "" { return fmt.Errorf("invalid permission [%s]. action "+ "cannot be empty", permission) } // No we can assume that we have a formally valid entity:action // tuple. The rest of the validation happens server side. parsedPermissions = append( parsedPermissions, &lnrpc.MacaroonPermission{ Entity: entity, Action: action, }, ) } // Now we have gathered all the input we need and can do the actual // RPC call. req := &lnrpc.BakeMacaroonRequest{ Permissions: parsedPermissions, RootKeyId: rootKeyID, } resp, err := client.BakeMacaroon(context.Background(), 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 } unmarshalMac := &macaroon.Macaroon{} if err = unmarshalMac.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. macConstraints := make([]macaroons.Constraint, 0) if timeout > 0 { macConstraints = append( macConstraints, macaroons.TimeoutConstraint(timeout), ) } if ipAddress != nil { macConstraints = append( macConstraints, macaroons.IPLockConstraint(ipAddress.String()), ) } constrainedMac, err := macaroons.AddConstraints( unmarshalMac, macConstraints..., ) if err != nil { return err } macBytes, err = constrainedMac.MarshalBinary() if err != nil { return err } // Now we can output the result. We either write it binary serialized to // a file or write to the standard output using hex encoding. switch { case savePath != "": err = ioutil.WriteFile(savePath, macBytes, 0644) if err != nil { return err } fmt.Printf("Macaroon saved to %s\n", savePath) default: fmt.Printf("%s\n", hex.EncodeToString(macBytes)) } return nil } var listMacaroonIDsCommand = cli.Command{ Name: "listmacaroonids", Category: "Macaroons", Usage: "List all macaroons root key IDs in use.", Action: actionDecorator(listMacaroonIDs), } func listMacaroonIDs(ctx *cli.Context) error { client, cleanUp := getClient(ctx) defer cleanUp() req := &lnrpc.ListMacaroonIDsRequest{} resp, err := client.ListMacaroonIDs(context.Background(), req) if err != nil { return err } printRespJSON(resp) return nil } var deleteMacaroonIDCommand = cli.Command{ Name: "deletemacaroonid", Category: "Macaroons", Usage: "Delete a specific macaroon ID.", ArgsUsage: "root_key_id", Description: ` Remove a macaroon ID using the specified root key ID. For example: lncli deletemacaroonid 1 WARNING When the ID is deleted, all macaroons created from that root key will be invalidated. Note that the default root key ID 0 cannot be deleted. `, Action: actionDecorator(deleteMacaroonID), } func deleteMacaroonID(ctx *cli.Context) error { client, cleanUp := getClient(ctx) defer cleanUp() // Validate args length. Only one argument is allowed. if ctx.NArg() != 1 { return cli.ShowCommandHelp(ctx, "deletemacaroonid") } rootKeyIDString := ctx.Args().First() // Convert string into uint64. rootKeyID, err := strconv.ParseUint(rootKeyIDString, 10, 64) if err != nil { return fmt.Errorf("root key ID must be a positive integer") } // Check that the value is not equal to DefaultRootKeyID. Note that the // server also validates the root key ID when removing it. However, we check // it here too so that we can give users a nice warning. if bytes.Equal([]byte(rootKeyIDString), macaroons.DefaultRootKeyID) { return fmt.Errorf("deleting the default root key ID 0 is not allowed") } // Make the actual RPC call. req := &lnrpc.DeleteMacaroonIDRequest{ RootKeyId: rootKeyID, } resp, err := client.DeleteMacaroonID(context.Background(), req) if err != nil { return err } printRespJSON(resp) return nil } var listPermissionsCommand = cli.Command{ Name: "listpermissions", Category: "Macaroons", Usage: "Lists all RPC method URIs and the macaroon permissions they " + "require to be invoked.", Action: actionDecorator(listPermissions), } func listPermissions(ctx *cli.Context) error { client, cleanUp := getClient(ctx) defer cleanUp() request := &lnrpc.ListPermissionsRequest{} response, err := client.ListPermissions(context.Background(), request) if err != nil { return err } printRespJSON(response) return nil } type macaroonContent struct { Version uint16 `json:"version"` Location string `json:"location"` RootKeyID string `json:"root_key_id"` Permissions []string `json:"permissions"` Caveats []string `json:"caveats"` } var printMacaroonCommand = cli.Command{ Name: "printmacaroon", Category: "Macaroons", Usage: "Print the content of a macaroon in a human readable format.", ArgsUsage: "[macaroon_content_hex]", Description: ` Decode a macaroon and show its content in a more human readable format. The macaroon can either be passed as a hex encoded positional parameter or loaded from a file. `, Flags: []cli.Flag{ cli.StringFlag{ Name: "macaroon_file", Usage: "load the macaroon from a file instead of the " + "command line directly", }, }, Action: actionDecorator(printMacaroon), } func printMacaroon(ctx *cli.Context) error { // Show command help if no arguments or flags are set. if ctx.NArg() == 0 && ctx.NumFlags() == 0 { return cli.ShowCommandHelp(ctx, "printmacaroon") } var ( macBytes []byte err error args = ctx.Args() ) switch { case ctx.IsSet("macaroon_file"): macPath := cleanAndExpandPath(ctx.String("macaroon_file")) // Load the specified macaroon file. macBytes, err = ioutil.ReadFile(macPath) if err != nil { return fmt.Errorf("unable to read macaroon path %v: %v", macPath, err) } case args.Present(): macBytes, err = hex.DecodeString(args.First()) if err != nil { return fmt.Errorf("unable to hex decode macaroon: %v", err) } default: return fmt.Errorf("macaroon parameter missing") } // Decode the macaroon and its protobuf encoded internal identifier. mac := &macaroon.Macaroon{} if err = mac.UnmarshalBinary(macBytes); err != nil { return fmt.Errorf("unable to decode macaroon: %v", err) } rawID := mac.Id() if rawID[0] != byte(bakery.LatestVersion) { return fmt.Errorf("invalid macaroon version: %x", rawID) } decodedID := &lnrpc.MacaroonId{} idProto := rawID[1:] err = proto.Unmarshal(idProto, decodedID) if err != nil { return fmt.Errorf("unable to decode macaroon version: %v", err) } // Prepare everything to be printed in a more human readable format. content := &macaroonContent{ Version: uint16(mac.Version()), Location: mac.Location(), RootKeyID: string(decodedID.StorageId), Permissions: nil, Caveats: nil, } for _, caveat := range mac.Caveats() { content.Caveats = append(content.Caveats, string(caveat.Id)) } for _, op := range decodedID.Ops { permission := fmt.Sprintf("%s:%s", op.Entity, op.Actions[0]) content.Permissions = append(content.Permissions, permission) } printJSON(content) return nil }