github.com/decred/dcrlnd@v0.7.6/cmd/dcrlncli/cmd_macaroon.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "encoding/hex" 6 "fmt" 7 "io/ioutil" 8 "net" 9 "strconv" 10 "strings" 11 "unicode" 12 13 "github.com/decred/dcrlnd/lncfg" 14 "github.com/decred/dcrlnd/lnrpc" 15 "github.com/decred/dcrlnd/macaroons" 16 "github.com/golang/protobuf/proto" 17 "github.com/urfave/cli" 18 "gopkg.in/macaroon-bakery.v2/bakery" 19 "gopkg.in/macaroon.v2" 20 ) 21 22 var bakeMacaroonCommand = cli.Command{ 23 Name: "bakemacaroon", 24 Category: "Macaroons", 25 Usage: "Bakes a new macaroon with the provided list of permissions " + 26 "and restrictions.", 27 ArgsUsage: "[--save_to=] [--timeout=] [--ip_address=] [--allow_external_permissions] permissions...", 28 Description: ` 29 Bake a new macaroon that grants the provided permissions and 30 optionally adds restrictions (timeout, IP address) to it. 31 32 The new macaroon can either be shown on command line in hex serialized 33 format or it can be saved directly to a file using the --save_to 34 argument. 35 36 A permission is a tuple of an entity and an action, separated by a 37 colon. Multiple operations can be added as arguments, for example: 38 39 lncli bakemacaroon info:read invoices:write foo:bar 40 41 For even more fine-grained permission control, it is also possible to 42 specify single RPC method URIs that are allowed to be accessed by a 43 macaroon. This can be achieved by specifying "uri:<methodURI>" pairs, 44 for example: 45 46 lncli bakemacaroon uri:/lnrpc.Lightning/GetInfo uri:/verrpc.Versioner/GetVersion 47 48 The macaroon created by this command would only be allowed to use the 49 "lncli getinfo" and "lncli version" commands. 50 51 To get a list of all available URIs and permissions, use the 52 "lncli listpermissions" command. 53 `, 54 Flags: []cli.Flag{ 55 cli.StringFlag{ 56 Name: "save_to", 57 Usage: "save the created macaroon to this file " + 58 "using the default binary format", 59 }, 60 cli.Uint64Flag{ 61 Name: "timeout", 62 Usage: "the number of seconds the macaroon will be " + 63 "valid before it times out", 64 }, 65 cli.StringFlag{ 66 Name: "ip_address", 67 Usage: "the IP address the macaroon will be bound to", 68 }, 69 cli.StringFlag{ 70 Name: "custom_caveat_name", 71 Usage: "the name of the custom caveat to add", 72 }, 73 cli.StringFlag{ 74 Name: "custom_caveat_condition", 75 Usage: "the condition of the custom caveat to add, " + 76 "can be empty if custom caveat doesn't need " + 77 "a value", 78 }, 79 cli.Uint64Flag{ 80 Name: "root_key_id", 81 Usage: "the numerical root key ID used to create the macaroon", 82 }, 83 cli.BoolFlag{ 84 Name: "allow_external_permissions", 85 Usage: "whether permissions lnd is not familiar with are allowed", 86 }, 87 }, 88 Action: actionDecorator(bakeMacaroon), 89 } 90 91 func bakeMacaroon(ctx *cli.Context) error { 92 ctxc := getContext() 93 client, cleanUp := getClient(ctx) 94 defer cleanUp() 95 96 // Show command help if no arguments. 97 if ctx.NArg() == 0 { 98 return cli.ShowCommandHelp(ctx, "bakemacaroon") 99 } 100 args := ctx.Args() 101 102 var ( 103 savePath string 104 timeout int64 105 ipAddress net.IP 106 customCaveatName string 107 customCaveatCond string 108 rootKeyID uint64 109 parsedPermissions []*lnrpc.MacaroonPermission 110 err error 111 ) 112 113 if ctx.String("save_to") != "" { 114 savePath = lncfg.CleanAndExpandPath(ctx.String("save_to")) 115 } 116 117 if ctx.IsSet("timeout") { 118 timeout = ctx.Int64("timeout") 119 if timeout <= 0 { 120 return fmt.Errorf("timeout must be greater than 0") 121 } 122 } 123 124 if ctx.IsSet("ip_address") { 125 ipAddress = net.ParseIP(ctx.String("ip_address")) 126 if ipAddress == nil { 127 return fmt.Errorf("unable to parse ip_address: %s", 128 ctx.String("ip_address")) 129 } 130 } 131 132 if ctx.IsSet("custom_caveat_name") { 133 customCaveatName = ctx.String("custom_caveat_name") 134 if containsWhiteSpace(customCaveatName) { 135 return fmt.Errorf("unexpected white space found in " + 136 "custom caveat name") 137 } 138 if customCaveatName == "" { 139 return fmt.Errorf("invalid custom caveat name") 140 } 141 } 142 143 if ctx.IsSet("custom_caveat_condition") { 144 customCaveatCond = ctx.String("custom_caveat_condition") 145 if containsWhiteSpace(customCaveatCond) { 146 return fmt.Errorf("unexpected white space found in " + 147 "custom caveat condition") 148 } 149 if customCaveatCond == "" { 150 return fmt.Errorf("invalid custom caveat condition") 151 } 152 if customCaveatCond != "" && customCaveatName == "" { 153 return fmt.Errorf("cannot set custom caveat " + 154 "condition without custom caveat name") 155 } 156 } 157 158 if ctx.IsSet("root_key_id") { 159 rootKeyID = ctx.Uint64("root_key_id") 160 } 161 162 // A command line argument can't be an empty string. So we'll check each 163 // entry if it's a valid entity:action tuple. The content itself is 164 // validated server side. We just make sure we can parse it correctly. 165 for _, permission := range args { 166 tuple := strings.Split(permission, ":") 167 if len(tuple) != 2 { 168 return fmt.Errorf("unable to parse "+ 169 "permission tuple: %s", permission) 170 } 171 entity, action := tuple[0], tuple[1] 172 if entity == "" { 173 return fmt.Errorf("invalid permission [%s]. entity "+ 174 "cannot be empty", permission) 175 } 176 if action == "" { 177 return fmt.Errorf("invalid permission [%s]. action "+ 178 "cannot be empty", permission) 179 } 180 181 // No we can assume that we have a formally valid entity:action 182 // tuple. The rest of the validation happens server side. 183 parsedPermissions = append( 184 parsedPermissions, &lnrpc.MacaroonPermission{ 185 Entity: entity, 186 Action: action, 187 }, 188 ) 189 } 190 191 // Now we have gathered all the input we need and can do the actual 192 // RPC call. 193 req := &lnrpc.BakeMacaroonRequest{ 194 Permissions: parsedPermissions, 195 RootKeyId: rootKeyID, 196 AllowExternalPermissions: ctx.Bool("allow_external_permissions"), 197 } 198 resp, err := client.BakeMacaroon(ctxc, req) 199 if err != nil { 200 return err 201 } 202 203 // Now we should have gotten a valid macaroon. Unmarshal it so we can 204 // add first-party caveats (if necessary) to it. 205 macBytes, err := hex.DecodeString(resp.Macaroon) 206 if err != nil { 207 return err 208 } 209 unmarshalMac := &macaroon.Macaroon{} 210 if err = unmarshalMac.UnmarshalBinary(macBytes); err != nil { 211 return err 212 } 213 214 // Now apply the desired constraints to the macaroon. This will always 215 // create a new macaroon object, even if no constraints are added. 216 macConstraints := make([]macaroons.Constraint, 0) 217 if timeout > 0 { 218 macConstraints = append( 219 macConstraints, macaroons.TimeoutConstraint(timeout), 220 ) 221 } 222 if ipAddress != nil { 223 macConstraints = append( 224 macConstraints, 225 macaroons.IPLockConstraint(ipAddress.String()), 226 ) 227 } 228 229 // The custom caveat condition is optional, it could just be a marker 230 // tag in the macaroon with just a name. The interceptor itself doesn't 231 // care about the value anyway. 232 if customCaveatName != "" { 233 macConstraints = append( 234 macConstraints, macaroons.CustomConstraint( 235 customCaveatName, customCaveatCond, 236 ), 237 ) 238 } 239 constrainedMac, err := macaroons.AddConstraints( 240 unmarshalMac, macConstraints..., 241 ) 242 if err != nil { 243 return err 244 } 245 macBytes, err = constrainedMac.MarshalBinary() 246 if err != nil { 247 return err 248 } 249 250 // Now we can output the result. We either write it binary serialized to 251 // a file or write to the standard output using hex encoding. 252 switch { 253 case savePath != "": 254 err = ioutil.WriteFile(savePath, macBytes, 0644) 255 if err != nil { 256 return err 257 } 258 fmt.Printf("Macaroon saved to %s\n", savePath) 259 260 default: 261 fmt.Printf("%s\n", hex.EncodeToString(macBytes)) 262 } 263 264 return nil 265 } 266 267 var listMacaroonIDsCommand = cli.Command{ 268 Name: "listmacaroonids", 269 Category: "Macaroons", 270 Usage: "List all macaroons root key IDs in use.", 271 Action: actionDecorator(listMacaroonIDs), 272 } 273 274 func listMacaroonIDs(ctx *cli.Context) error { 275 ctxc := getContext() 276 client, cleanUp := getClient(ctx) 277 defer cleanUp() 278 279 req := &lnrpc.ListMacaroonIDsRequest{} 280 resp, err := client.ListMacaroonIDs(ctxc, req) 281 if err != nil { 282 return err 283 } 284 285 printRespJSON(resp) 286 return nil 287 } 288 289 var deleteMacaroonIDCommand = cli.Command{ 290 Name: "deletemacaroonid", 291 Category: "Macaroons", 292 Usage: "Delete a specific macaroon ID.", 293 ArgsUsage: "root_key_id", 294 Description: ` 295 Remove a macaroon ID using the specified root key ID. For example: 296 297 lncli deletemacaroonid 1 298 299 WARNING 300 When the ID is deleted, all macaroons created from that root key will 301 be invalidated. 302 303 Note that the default root key ID 0 cannot be deleted. 304 `, 305 Action: actionDecorator(deleteMacaroonID), 306 } 307 308 func deleteMacaroonID(ctx *cli.Context) error { 309 ctxc := getContext() 310 client, cleanUp := getClient(ctx) 311 defer cleanUp() 312 313 // Validate args length. Only one argument is allowed. 314 if ctx.NArg() != 1 { 315 return cli.ShowCommandHelp(ctx, "deletemacaroonid") 316 } 317 318 rootKeyIDString := ctx.Args().First() 319 320 // Convert string into uint64. 321 rootKeyID, err := strconv.ParseUint(rootKeyIDString, 10, 64) 322 if err != nil { 323 return fmt.Errorf("root key ID must be a positive integer") 324 } 325 326 // Check that the value is not equal to DefaultRootKeyID. Note that the 327 // server also validates the root key ID when removing it. However, we check 328 // it here too so that we can give users a nice warning. 329 if bytes.Equal([]byte(rootKeyIDString), macaroons.DefaultRootKeyID) { 330 return fmt.Errorf("deleting the default root key ID 0 is not allowed") 331 } 332 333 // Make the actual RPC call. 334 req := &lnrpc.DeleteMacaroonIDRequest{ 335 RootKeyId: rootKeyID, 336 } 337 resp, err := client.DeleteMacaroonID(ctxc, req) 338 if err != nil { 339 return err 340 } 341 342 printRespJSON(resp) 343 return nil 344 } 345 346 var listPermissionsCommand = cli.Command{ 347 Name: "listpermissions", 348 Category: "Macaroons", 349 Usage: "Lists all RPC method URIs and the macaroon permissions they " + 350 "require to be invoked.", 351 Action: actionDecorator(listPermissions), 352 } 353 354 func listPermissions(ctx *cli.Context) error { 355 ctxc := getContext() 356 client, cleanUp := getClient(ctx) 357 defer cleanUp() 358 359 request := &lnrpc.ListPermissionsRequest{} 360 response, err := client.ListPermissions(ctxc, request) 361 if err != nil { 362 return err 363 } 364 365 printRespJSON(response) 366 367 return nil 368 } 369 370 type macaroonContent struct { 371 Version uint16 `json:"version"` 372 Location string `json:"location"` 373 RootKeyID string `json:"root_key_id"` 374 Permissions []string `json:"permissions"` 375 Caveats []string `json:"caveats"` 376 } 377 378 var printMacaroonCommand = cli.Command{ 379 Name: "printmacaroon", 380 Category: "Macaroons", 381 Usage: "Print the content of a macaroon in a human readable format.", 382 ArgsUsage: "[macaroon_content_hex]", 383 Description: ` 384 Decode a macaroon and show its content in a more human readable format. 385 The macaroon can either be passed as a hex encoded positional parameter 386 or loaded from a file. 387 `, 388 Flags: []cli.Flag{ 389 cli.StringFlag{ 390 Name: "macaroon_file", 391 Usage: "load the macaroon from a file instead of the " + 392 "command line directly", 393 }, 394 }, 395 Action: actionDecorator(printMacaroon), 396 } 397 398 func printMacaroon(ctx *cli.Context) error { 399 // Show command help if no arguments or flags are set. 400 if ctx.NArg() == 0 && ctx.NumFlags() == 0 { 401 return cli.ShowCommandHelp(ctx, "printmacaroon") 402 } 403 404 var ( 405 macBytes []byte 406 err error 407 args = ctx.Args() 408 ) 409 switch { 410 case ctx.IsSet("macaroon_file"): 411 macPath := lncfg.CleanAndExpandPath(ctx.String("macaroon_file")) 412 413 // Load the specified macaroon file. 414 macBytes, err = ioutil.ReadFile(macPath) 415 if err != nil { 416 return fmt.Errorf("unable to read macaroon path %v: %v", 417 macPath, err) 418 } 419 420 case args.Present(): 421 macBytes, err = hex.DecodeString(args.First()) 422 if err != nil { 423 return fmt.Errorf("unable to hex decode macaroon: %v", 424 err) 425 } 426 427 default: 428 return fmt.Errorf("macaroon parameter missing") 429 } 430 431 // Decode the macaroon and its protobuf encoded internal identifier. 432 mac := &macaroon.Macaroon{} 433 if err = mac.UnmarshalBinary(macBytes); err != nil { 434 return fmt.Errorf("unable to decode macaroon: %v", err) 435 } 436 rawID := mac.Id() 437 if rawID[0] != byte(bakery.LatestVersion) { 438 return fmt.Errorf("invalid macaroon version: %x", rawID) 439 } 440 decodedID := &lnrpc.MacaroonId{} 441 idProto := rawID[1:] 442 err = proto.Unmarshal(idProto, decodedID) 443 if err != nil { 444 return fmt.Errorf("unable to decode macaroon version: %v", err) 445 } 446 447 // Prepare everything to be printed in a more human readable format. 448 content := &macaroonContent{ 449 Version: uint16(mac.Version()), 450 Location: mac.Location(), 451 RootKeyID: string(decodedID.StorageId), 452 Permissions: nil, 453 Caveats: nil, 454 } 455 456 for _, caveat := range mac.Caveats() { 457 content.Caveats = append(content.Caveats, string(caveat.Id)) 458 } 459 for _, op := range decodedID.Ops { 460 for _, action := range op.Actions { 461 permission := fmt.Sprintf("%s:%s", op.Entity, action) 462 content.Permissions = append( 463 content.Permissions, permission, 464 ) 465 } 466 } 467 468 printJSON(content) 469 470 return nil 471 } 472 473 // containsWhiteSpace returns true if the given string contains any character 474 // that is considered to be a white space or non-printable character such as 475 // space, tabulator, newline, carriage return and some more exotic ones. 476 func containsWhiteSpace(str string) bool { 477 return strings.IndexFunc(str, unicode.IsSpace) >= 0 478 }