Merge pull request #1160 from guggero/new-macaroon

lncli: add command to create new macaroon
This commit is contained in:
Olaoluwa Osuntokun 2019-10-24 19:41:51 -07:00 committed by GitHub
commit b110a3a197
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1131 additions and 535 deletions

View File

@ -0,0 +1,182 @@
package main
import (
"context"
"encoding/hex"
"fmt"
"io/ioutil"
"net"
"strings"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/urfave/cli"
"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
`,
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",
},
},
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
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"))
}
}
// 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,
}
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
}

View File

@ -298,6 +298,7 @@ func main() {
exportChanBackupCommand,
verifyChanBackupCommand,
restoreChanBackupCommand,
bakeMacaroonCommand,
}
// Add any extra commands determined by build flags.

View File

@ -120,7 +120,13 @@ description):
enforced by the node globally for each channel.
* UpdateChannelPolicy
* Allows the caller to update the fee schedule and channel policies for all channels
globally, or a particular channel
globally, or a particular channel.
* ForwardingHistory
* ForwardingHistory allows the caller to query the htlcswitch for a
record of all HTLCs forwarded.
* BakeMacaroon
* Bakes a new macaroon with the provided list of permissions and
restrictions
## Service: WalletUnlocker

File diff suppressed because it is too large Load Diff

View File

@ -834,6 +834,19 @@ func request_Lightning_RestoreChannelBackups_0(ctx context.Context, marshaler ru
}
func request_Lightning_BakeMacaroon_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq BakeMacaroonRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.BakeMacaroon(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
// RegisterWalletUnlockerHandlerFromEndpoint is same as RegisterWalletUnlockerHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterWalletUnlockerHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
@ -2193,6 +2206,35 @@ func RegisterLightningHandler(ctx context.Context, mux *runtime.ServeMux, conn *
})
mux.Handle("POST", pattern_Lightning_BakeMacaroon_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Lightning_BakeMacaroon_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Lightning_BakeMacaroon_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@ -2276,6 +2318,8 @@ var (
pattern_Lightning_VerifyChanBackup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "channels", "backup", "verify"}, ""))
pattern_Lightning_RestoreChannelBackups_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "channels", "backup", "restore"}, ""))
pattern_Lightning_BakeMacaroon_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "macaroon"}, ""))
)
var (
@ -2358,4 +2402,6 @@ var (
forward_Lightning_VerifyChanBackup_0 = runtime.ForwardResponseMessage
forward_Lightning_RestoreChannelBackups_0 = runtime.ForwardResponseMessage
forward_Lightning_BakeMacaroon_0 = runtime.ForwardResponseMessage
)

View File

@ -777,6 +777,18 @@ service Lightning {
*/
rpc SubscribeChannelBackups(ChannelBackupSubscription) returns (stream ChanBackupSnapshot) {
};
/** lncli: `bakemacaroon`
BakeMacaroon allows the creation of a new macaroon with custom read and
write permissions. No first-party caveats are added since this can be done
offline.
*/
rpc BakeMacaroon(BakeMacaroonRequest) returns (BakeMacaroonResponse) {
option (google.api.http) = {
post: "/v1/macaroon"
body: "*"
};
};
}
message Utxo {
@ -2585,3 +2597,19 @@ message ChannelBackupSubscription {}
message VerifyChanBackupResponse {
}
message MacaroonPermission {
/// The entity a permission grants access to.
string entity = 1 [json_name = "entity"];
/// The action that is granted.
string action = 2 [json_name = "action"];
}
message BakeMacaroonRequest {
/// The list of permissions the new macaroon should grant.
repeated MacaroonPermission permissions = 1 [json_name = "permissions"];
}
message BakeMacaroonResponse {
/// The hex encoded macaroon, serialized in binary format.
string macaroon = 1 [json_name = "macaroon"];
}

View File

@ -917,6 +917,33 @@
]
}
},
"/v1/macaroon": {
"post": {
"summary": "* lncli: `bakemacaroon`\nBakeMacaroon allows the creation of a new macaroon with custom read and\nwrite permissions. No first-party caveats are added since this can be done\noffline.",
"operationId": "BakeMacaroon",
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/lnrpcBakeMacaroonResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/lnrpcBakeMacaroonRequest"
}
}
],
"tags": [
"Lightning"
]
}
},
"/v1/newaddress": {
"get": {
"summary": "* lncli: `newaddress`\nNewAddress creates a new address under control of the local wallet.",
@ -1512,6 +1539,27 @@
"description": "- `p2wkh`: Pay to witness key hash (`WITNESS_PUBKEY_HASH` = 0)\n- `np2wkh`: Pay to nested witness key hash (`NESTED_PUBKEY_HASH` = 1)",
"title": "* \n`AddressType` has to be one of:"
},
"lnrpcBakeMacaroonRequest": {
"type": "object",
"properties": {
"permissions": {
"type": "array",
"items": {
"$ref": "#/definitions/lnrpcMacaroonPermission"
},
"description": "/ The list of permissions the new macaroon should grant."
}
}
},
"lnrpcBakeMacaroonResponse": {
"type": "object",
"properties": {
"macaroon": {
"type": "string",
"description": "/ The hex encoded macaroon, serialized in binary format."
}
}
},
"lnrpcChain": {
"type": "object",
"properties": {
@ -2746,6 +2794,19 @@
}
}
},
"lnrpcMacaroonPermission": {
"type": "object",
"properties": {
"entity": {
"type": "string",
"description": "/ The entity a permission grants access to."
},
"action": {
"type": "string",
"description": "/ The action that is granted."
}
}
},
"lnrpcMultiChanBackup": {
"type": "object",
"properties": {

View File

@ -151,6 +151,10 @@ var (
Entity: "signer",
Action: "generate",
},
{
Entity: "macaroon",
Action: "generate",
},
}
// invoicePermissions is a slice of all the entities that allows a user
@ -178,8 +182,28 @@ var (
Action: "read",
},
}
// TODO(guggero): Refactor into constants that are used for all
// permissions in this file. Also expose the list of possible
// permissions in an RPC when per RPC permissions are
// implemented.
validActions = []string{"read", "write", "generate"}
validEntities = []string{
"onchain", "offchain", "address", "message",
"peers", "info", "invoices", "signer", "macaroon",
}
)
// stringInSlice returns true if a string is contained in the given slice.
func stringInSlice(a string, slice []string) bool {
for _, b := range slice {
if b == a {
return true
}
}
return false
}
// mainRPCServerPermissions returns a mapping of the main RPC server calls to
// the permissions they require.
func mainRPCServerPermissions() map[string][]bakery.Op {
@ -400,6 +424,10 @@ func mainRPCServerPermissions() map[string][]bakery.Op {
Entity: "offchain",
Action: "write",
}},
"/lnrpc.Lightning/BakeMacaroon": {{
Entity: "macaroon",
Action: "generate",
}},
}
}
@ -453,6 +481,10 @@ type rpcServer struct {
chanPredicate *chanacceptor.ChainedAcceptor
quit chan struct{}
// macService is the macaroon service that we need to mint new
// macaroons.
macService *macaroons.Service
}
// A compile time check to ensure that rpcServer fully implements the
@ -627,6 +659,7 @@ func newRPCServer(s *server, macService *macaroons.Service,
routerBackend: routerBackend,
chanPredicate: chanPredicate,
quit: make(chan struct{}, 1),
macService: macService,
}
lnrpc.RegisterLightningServer(grpcServer, rootRPCServer)
@ -5247,3 +5280,64 @@ func (r *rpcServer) ChannelAcceptor(stream lnrpc.Lightning_ChannelAcceptorServer
}
}
}
// BakeMacaroon allows the creation of a new macaroon with custom read and write
// permissions. No first-party caveats are added since this can be done offline.
func (r *rpcServer) BakeMacaroon(ctx context.Context,
req *lnrpc.BakeMacaroonRequest) (*lnrpc.BakeMacaroonResponse, error) {
rpcsLog.Debugf("[bakemacaroon]")
// If the --no-macaroons flag is used to start lnd, the macaroon service
// is not initialized. Therefore we can't bake new macaroons.
if r.macService == nil {
return nil, fmt.Errorf("macaroon authentication disabled, " +
"remove --no-macaroons flag to enable")
}
helpMsg := fmt.Sprintf("supported actions are %v, supported entities "+
"are %v", validActions, validEntities)
// Don't allow empty permission list as it doesn't make sense to have
// a macaroon that is not allowed to access any RPC.
if len(req.Permissions) == 0 {
return nil, fmt.Errorf("permission list cannot be empty. "+
"specify at least one action/entity pair. %s", helpMsg)
}
// Validate and map permission struct used by gRPC to the one used by
// the bakery.
requestedPermissions := make([]bakery.Op, len(req.Permissions))
for idx, op := range req.Permissions {
if !stringInSlice(op.Action, validActions) {
return nil, fmt.Errorf("invalid permission action. %s",
helpMsg)
}
if !stringInSlice(op.Entity, validEntities) {
return nil, fmt.Errorf("invalid permission entity. %s",
helpMsg)
}
requestedPermissions[idx] = bakery.Op{
Entity: op.Entity,
Action: op.Action,
}
}
// Bake new macaroon with the given permissions and send it binary
// serialized and hex encoded to the client.
newMac, err := r.macService.Oven.NewMacaroon(
ctx, bakery.LatestVersion, nil, requestedPermissions...,
)
if err != nil {
return nil, err
}
newMacBytes, err := newMac.M().MarshalBinary()
if err != nil {
return nil, err
}
resp := &lnrpc.BakeMacaroonResponse{}
resp.Macaroon = hex.EncodeToString(newMacBytes)
return resp, nil
}