Merge pull request #5304 from orbitalturtle/check-macaroon-rpcs

rpc: Bake and validate macaroons with external permissions
This commit is contained in:
Oliver Gugger 2021-09-15 09:42:38 +02:00 committed by GitHub
commit 08c9d3fbdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1319 additions and 728 deletions

View file

@ -23,7 +23,7 @@ var bakeMacaroonCommand = cli.Command{
Category: "Macaroons",
Usage: "Bakes a new macaroon with the provided list of permissions " +
"and restrictions.",
ArgsUsage: "[--save_to=] [--timeout=] [--ip_address=] permissions...",
ArgsUsage: "[--save_to=] [--timeout=] [--ip_address=] [--allow_external_permissions] permissions...",
Description: `
Bake a new macaroon that grants the provided permissions and
optionally adds restrictions (timeout, IP address) to it.
@ -69,6 +69,10 @@ var bakeMacaroonCommand = cli.Command{
Name: "root_key_id",
Usage: "the numerical root key ID used to create the macaroon",
},
cli.BoolFlag{
Name: "allow_external_permissions",
Usage: "whether permissions lnd is not familiar with are allowed",
},
},
Action: actionDecorator(bakeMacaroon),
}
@ -148,8 +152,9 @@ func bakeMacaroon(ctx *cli.Context) error {
// Now we have gathered all the input we need and can do the actual
// RPC call.
req := &lnrpc.BakeMacaroonRequest{
Permissions: parsedPermissions,
RootKeyId: rootKeyID,
Permissions: parsedPermissions,
RootKeyId: rootKeyID,
AllowExternalPermissions: ctx.Bool("allow_external_permissions"),
}
resp, err := client.BakeMacaroon(ctxc, req)
if err != nil {

View file

@ -74,6 +74,8 @@ proposed channel type is used.
is added to the state server. This state indicates whether the `lnd` server
and all its subservers have been fully started or not.
* [Adds an option to the BakeMacaroon rpc "allow-external-permissions,"](https://github.com/lightningnetwork/lnd/pull/5304) which makes it possible to bake a macaroon with external permissions. That way, the baked macaroons can be used for services beyond LND. Also adds a new CheckMacaroonPermissions rpc that checks that the macaroon permissions and other restrictions are being followed. It can also check permissions not native to LND.
### Batched channel funding
[Multiple channels can now be opened in a single

File diff suppressed because it is too large Load diff

View file

@ -2217,6 +2217,40 @@ func local_request_Lightning_ListPermissions_0(ctx context.Context, marshaler ru
}
func request_Lightning_CheckMacaroonPermissions_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CheckMacPermRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.CheckMacaroonPermissions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Lightning_CheckMacaroonPermissions_0(ctx context.Context, marshaler runtime.Marshaler, server LightningServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CheckMacPermRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.CheckMacaroonPermissions(ctx, &protoReq)
return msg, metadata, err
}
// RegisterLightningHandlerServer registers the http handlers for service Lightning to "mux".
// UnaryRPC :call LightningServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@ -3443,6 +3477,29 @@ func RegisterLightningHandlerServer(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_Lightning_CheckMacaroonPermissions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/lnrpc.Lightning/CheckMacaroonPermissions", runtime.WithHTTPPathPattern("/v1/macaroon/checkpermissions"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Lightning_CheckMacaroonPermissions_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Lightning_CheckMacaroonPermissions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@ -4684,6 +4741,26 @@ func RegisterLightningHandlerClient(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_Lightning_CheckMacaroonPermissions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/lnrpc.Lightning/CheckMacaroonPermissions", runtime.WithHTTPPathPattern("/v1/macaroon/checkpermissions"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Lightning_CheckMacaroonPermissions_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_CheckMacaroonPermissions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@ -4807,6 +4884,8 @@ var (
pattern_Lightning_DeleteMacaroonID_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "macaroon", "root_key_id"}, ""))
pattern_Lightning_ListPermissions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "macaroon", "permissions"}, ""))
pattern_Lightning_CheckMacaroonPermissions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "macaroon", "checkpermissions"}, ""))
)
var (
@ -4929,4 +5008,6 @@ var (
forward_Lightning_DeleteMacaroonID_0 = runtime.ForwardResponseMessage
forward_Lightning_ListPermissions_0 = runtime.ForwardResponseMessage
forward_Lightning_CheckMacaroonPermissions_0 = runtime.ForwardResponseMessage
)

View file

@ -1608,4 +1608,29 @@ func RegisterLightningJSONCallbacks(registry map[string]func(ctx context.Context
}
callback(string(respBytes), nil)
}
registry["lnrpc.Lightning.CheckMacaroonPermissions"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &CheckMacPermRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewLightningClient(conn)
resp, err := client.CheckMacaroonPermissions(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
}

View file

@ -532,6 +532,14 @@ service Lightning {
*/
rpc ListPermissions (ListPermissionsRequest)
returns (ListPermissionsResponse);
/*
CheckMacaroonPermissions checks whether a request follows the constraints
imposed on the macaroon and that the macaroon is authorized to follow the
provided permissions.
*/
rpc CheckMacaroonPermissions (CheckMacPermRequest)
returns (CheckMacPermResponse);
}
message Utxo {
@ -3795,6 +3803,12 @@ message BakeMacaroonRequest {
// The root key ID used to create the macaroon, must be a positive integer.
uint64 root_key_id = 2;
/*
Informs the RPC on whether to allow external permissions that LND is not
aware of.
*/
bool allow_external_permissions = 3;
}
message BakeMacaroonResponse {
// The hex encoded macaroon, serialized in binary format.
@ -4006,3 +4020,13 @@ message Op {
string entity = 1;
repeated string actions = 2;
}
message CheckMacPermRequest {
bytes macaroon = 1;
repeated MacaroonPermission permissions = 2;
string fullMethod = 3;
}
message CheckMacPermResponse {
bool valid = 1;
}

View file

@ -1563,6 +1563,39 @@
]
}
},
"/v1/macaroon/checkpermissions": {
"post": {
"summary": "CheckMacaroonPermissions checks whether a request follows the constraints\nimposed on the macaroon and that the macaroon is authorized to follow the\nprovided permissions.",
"operationId": "Lightning_CheckMacaroonPermissions",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/lnrpcCheckMacPermResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/lnrpcCheckMacPermRequest"
}
}
],
"tags": [
"Lightning"
]
}
},
"/v1/macaroon/ids": {
"get": {
"summary": "lncli: `listmacaroonids`\nListMacaroonIDs returns all root key IDs that are in use.",
@ -2787,6 +2820,10 @@
"type": "string",
"format": "uint64",
"description": "The root key ID used to create the macaroon, must be a positive integer."
},
"allow_external_permissions": {
"type": "boolean",
"description": "Informs the RPC on whether to allow external permissions that LND is not\naware of."
}
}
},
@ -3622,6 +3659,32 @@
}
}
},
"lnrpcCheckMacPermRequest": {
"type": "object",
"properties": {
"macaroon": {
"type": "string",
"format": "byte"
},
"permissions": {
"type": "array",
"items": {
"$ref": "#/definitions/lnrpcMacaroonPermission"
}
},
"fullMethod": {
"type": "string"
}
}
},
"lnrpcCheckMacPermResponse": {
"type": "object",
"properties": {
"valid": {
"type": "boolean"
}
}
},
"lnrpcCloseStatusUpdate": {
"type": "object",
"properties": {

View file

@ -150,3 +150,7 @@ http:
delete: "/v1/macaroon/{root_key_id}"
- selector: lnrpc.Lightning.ListPermissions
get: "/v1/macaroon/permissions"
- selector: lnrpc.Lightning.CheckMacaroonPermissions
post: "/v1/macaroon/checkpermissions"
body: "*"

View file

@ -384,6 +384,11 @@ type LightningClient interface {
//ListPermissions lists all RPC method URIs and their required macaroon
//permissions to access them.
ListPermissions(ctx context.Context, in *ListPermissionsRequest, opts ...grpc.CallOption) (*ListPermissionsResponse, error)
//
//CheckMacaroonPermissions checks whether a request follows the constraints
//imposed on the macaroon and that the macaroon is authorized to follow the
//provided permissions.
CheckMacaroonPermissions(ctx context.Context, in *CheckMacPermRequest, opts ...grpc.CallOption) (*CheckMacPermResponse, error)
}
type lightningClient struct {
@ -1195,6 +1200,15 @@ func (c *lightningClient) ListPermissions(ctx context.Context, in *ListPermissio
return out, nil
}
func (c *lightningClient) CheckMacaroonPermissions(ctx context.Context, in *CheckMacPermRequest, opts ...grpc.CallOption) (*CheckMacPermResponse, error) {
out := new(CheckMacPermResponse)
err := c.cc.Invoke(ctx, "/lnrpc.Lightning/CheckMacaroonPermissions", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// LightningServer is the server API for Lightning service.
// All implementations must embed UnimplementedLightningServer
// for forward compatibility
@ -1565,6 +1579,11 @@ type LightningServer interface {
//ListPermissions lists all RPC method URIs and their required macaroon
//permissions to access them.
ListPermissions(context.Context, *ListPermissionsRequest) (*ListPermissionsResponse, error)
//
//CheckMacaroonPermissions checks whether a request follows the constraints
//imposed on the macaroon and that the macaroon is authorized to follow the
//provided permissions.
CheckMacaroonPermissions(context.Context, *CheckMacPermRequest) (*CheckMacPermResponse, error)
mustEmbedUnimplementedLightningServer()
}
@ -1755,6 +1774,9 @@ func (UnimplementedLightningServer) DeleteMacaroonID(context.Context, *DeleteMac
func (UnimplementedLightningServer) ListPermissions(context.Context, *ListPermissionsRequest) (*ListPermissionsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListPermissions not implemented")
}
func (UnimplementedLightningServer) CheckMacaroonPermissions(context.Context, *CheckMacPermRequest) (*CheckMacPermResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckMacaroonPermissions not implemented")
}
func (UnimplementedLightningServer) mustEmbedUnimplementedLightningServer() {}
// UnsafeLightningServer may be embedded to opt out of forward compatibility for this service.
@ -2914,6 +2936,24 @@ func _Lightning_ListPermissions_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler)
}
func _Lightning_CheckMacaroonPermissions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CheckMacPermRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LightningServer).CheckMacaroonPermissions(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/lnrpc.Lightning/CheckMacaroonPermissions",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LightningServer).CheckMacaroonPermissions(ctx, req.(*CheckMacPermRequest))
}
return interceptor(ctx, in, info, handler)
}
// Lightning_ServiceDesc is the grpc.ServiceDesc for Lightning service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -3121,6 +3161,10 @@ var Lightning_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListPermissions",
Handler: _Lightning_ListPermissions_Handler,
},
{
MethodName: "CheckMacaroonPermissions",
Handler: _Lightning_CheckMacaroonPermissions_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View file

@ -10,6 +10,7 @@ import (
"strings"
"testing"
"github.com/golang/protobuf/proto"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/macaroons"
@ -196,6 +197,84 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) {
require.Error(t, err)
require.Contains(t, err.Error(), "permission denied")
},
}, {
// Eighth test: check that with the CheckMacaroonPermissions
// RPC, we can check that a macaroon follows (or doesn't)
// permissions and constraints.
name: "unknown permissions",
run: func(ctxt context.Context, t *testing.T) {
// A test macaroon created with permissions from pool,
// to make sure CheckMacaroonPermissions RPC accepts
// them.
rootKeyID := uint64(4200)
req := &lnrpc.BakeMacaroonRequest{
RootKeyId: rootKeyID,
Permissions: []*lnrpc.MacaroonPermission{{
Entity: "account",
Action: "read",
}, {
Entity: "recommendation",
Action: "read",
}},
AllowExternalPermissions: true,
}
bakeResp, err := testNode.BakeMacaroon(ctxt, req)
require.NoError(t, err)
macBytes, err := hex.DecodeString(bakeResp.Macaroon)
require.NoError(t, err)
checkReq := &lnrpc.CheckMacPermRequest{
Macaroon: macBytes,
Permissions: req.Permissions,
}
// Test that CheckMacaroonPermissions accurately
// characterizes macaroon as valid, even if the
// permissions are not native to LND.
checkResp, err := testNode.CheckMacaroonPermissions(
ctxt, checkReq,
)
require.NoError(t, err)
require.Equal(t, checkResp.Valid, true)
mac, err := readMacaroonFromHex(bakeResp.Macaroon)
require.NoError(t, err)
// Test that CheckMacaroonPermissions responds that the
// macaroon is invalid if timed out.
timeoutMac, err := macaroons.AddConstraints(
mac, macaroons.TimeoutConstraint(-30),
)
require.NoError(t, err)
timeoutMacBytes, err := timeoutMac.MarshalBinary()
require.NoError(t, err)
checkReq.Macaroon = timeoutMacBytes
_, err = testNode.CheckMacaroonPermissions(
ctxt, checkReq,
)
require.Error(t, err)
require.Contains(t, err.Error(), "macaroon has expired")
// Test that CheckMacaroonPermissions labels macaroon
// input with wrong permissions as invalid.
wrongPermissions := []*lnrpc.MacaroonPermission{{
Entity: "invoice",
Action: "read",
}}
checkReq.Permissions = wrongPermissions
checkReq.Macaroon = macBytes
_, err = testNode.CheckMacaroonPermissions(
ctxt, checkReq,
)
require.Error(t, err)
require.Contains(t, err.Error(), "permission denied")
},
}}
for _, tc := range testCases {
@ -371,6 +450,47 @@ func testBakeMacaroon(net *lntest.NetworkHarness, t *harnessTest) {
require.Error(t, err)
require.Contains(t, err.Error(), "permission denied")
},
}, {
// Seventh test: check that if the allow_external_permissions
// flag is set, we are able to feed BakeMacaroons permissions
// that LND is not familiar with.
name: "allow external macaroon permissions",
run: func(ctxt context.Context, t *testing.T,
adminClient lnrpc.LightningClient) {
// We'll try permissions from Pool to test that the
// allow_external_permissions flag properly allows it.
rootKeyID := uint64(4200)
req := &lnrpc.BakeMacaroonRequest{
RootKeyId: rootKeyID,
Permissions: []*lnrpc.MacaroonPermission{{
Entity: "account",
Action: "read",
}},
AllowExternalPermissions: true,
}
resp, err := adminClient.BakeMacaroon(ctxt, req)
require.NoError(t, err)
// We'll also check that the external permission was
// successfully added to the macaroon.
macBytes, err := hex.DecodeString(resp.Macaroon)
require.NoError(t, err)
mac := &macaroon.Macaroon{}
err = mac.UnmarshalBinary(macBytes)
require.NoError(t, err)
rawID := mac.Id()
decodedID := &lnrpc.MacaroonId{}
idProto := rawID[1:]
err = proto.Unmarshal(idProto, decodedID)
require.NoError(t, err)
require.Equal(t, "account", decodedID.Ops[0].Entity)
require.Equal(t, "read", decodedID.Ops[0].Actions[0])
},
}}
for _, tc := range testCases {

View file

@ -161,10 +161,20 @@ func (svc *Service) ValidateMacaroon(ctx context.Context,
len(md["macaroon"]))
}
return svc.CheckMacAuth(
ctx, md["macaroon"][0], requiredPermissions, fullMethod,
)
}
// CheckMacAuth checks that the macaroon is not disobeying any caveats and is
// authorized to perform the operation the user wants to perform.
func (svc *Service) CheckMacAuth(ctx context.Context, macStr string,
requiredPermissions []bakery.Op, fullMethod string) error {
// With the macaroon obtained, we'll now decode the hex-string
// encoding, then unmarshal it from binary into its concrete struct
// representation.
macBytes, err := hex.DecodeString(md["macaroon"][0])
macBytes, err := hex.DecodeString(macStr)
if err != nil {
return err
}

View file

@ -513,6 +513,10 @@ func MainRPCServerPermissions() map[string][]bakery.Op {
Entity: "info",
Action: "read",
}},
"/lnrpc.Lightning/CheckMacaroonPermissions": {{
Entity: "macaroon",
Action: "read",
}},
"/lnrpc.Lightning/SubscribePeerEvents": {{
Entity: "peers",
Action: "read",
@ -6795,6 +6799,8 @@ 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.
// If the --allow-external-permissions flag is set, the RPC will allow
// external permissions that LND is not aware of.
func (r *rpcServer) BakeMacaroon(ctx context.Context,
req *lnrpc.BakeMacaroonRequest) (*lnrpc.BakeMacaroonResponse, error) {
@ -6817,9 +6823,18 @@ func (r *rpcServer) BakeMacaroon(ctx context.Context,
}
// Validate and map permission struct used by gRPC to the one used by
// the bakery.
// the bakery. If the --allow-external-permissions flag is set, we
// will not validate, but map.
requestedPermissions := make([]bakery.Op, len(req.Permissions))
for idx, op := range req.Permissions {
if req.AllowExternalPermissions {
requestedPermissions[idx] = bakery.Op{
Entity: op.Entity,
Action: op.Action,
}
continue
}
if !stringInSlice(op.Entity, validEntities) {
return nil, fmt.Errorf("invalid permission entity. %s",
helpMsg)
@ -6962,6 +6977,33 @@ func (r *rpcServer) ListPermissions(_ context.Context,
}, nil
}
// CheckMacaroonPermissions checks the caveats and permissions of a macaroon.
func (r *rpcServer) CheckMacaroonPermissions(ctx context.Context,
req *lnrpc.CheckMacPermRequest) (*lnrpc.CheckMacPermResponse, error) {
// Turn grpc macaroon permission into bakery.Op for the server to
// process.
permissions := make([]bakery.Op, len(req.Permissions))
for idx, perm := range req.Permissions {
permissions[idx] = bakery.Op{
Entity: perm.Entity,
Action: perm.Action,
}
}
err := r.macService.CheckMacAuth(
ctx, hex.EncodeToString(req.Macaroon), permissions,
req.FullMethod,
)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
return &lnrpc.CheckMacPermResponse{
Valid: true,
}, nil
}
// FundingStateStep is an advanced funding related call that allows the caller
// to either execute some preparatory steps for a funding workflow, or manually
// progress a funding workflow. The primary way a funding flow is identified is