From 0b78ea05fedc0602bf40fc5b95685de3fdcddb89 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 24 Feb 2025 13:09:21 +0100 Subject: [PATCH] macaroons: ip range constraint --- cmd/commands/cmd_macaroon.go | 17 +++++++++++ config_builder.go | 2 +- itest/lnd_macaroons_test.go | 43 ++++++++++++++++++++++++++ macaroons/constraints.go | 58 ++++++++++++++++++++++++++++++++++-- 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/cmd/commands/cmd_macaroon.go b/cmd/commands/cmd_macaroon.go index d7d6d5f9d..bfdc61fe7 100644 --- a/cmd/commands/cmd_macaroon.go +++ b/cmd/commands/cmd_macaroon.go @@ -30,6 +30,10 @@ var ( Name: "ip_address", Usage: "the IP address the macaroon will be bound to", } + macIPRangeFlag = cli.StringFlag{ + Name: "ip_range", + Usage: "the IP range the macaroon will be bound to", + } macCustomCaveatNameFlag = cli.StringFlag{ Name: "custom_caveat_name", Usage: "the name of the custom caveat to add", @@ -557,6 +561,19 @@ func applyMacaroonConstraints(ctx *cli.Context, ) } + if ctx.IsSet(macIPRangeFlag.Name) { + _, net, err := net.ParseCIDR(ctx.String(macIPRangeFlag.Name)) + if err != nil { + return nil, fmt.Errorf("unable to parse ip_range: %s", + ctx.String("ip_range")) + } + + macConstraints = append( + macConstraints, + macaroons.IPLockConstraint(net.String()), + ) + } + if ctx.IsSet(macCustomCaveatNameFlag.Name) { customCaveatName := ctx.String(macCustomCaveatNameFlag.Name) if containsWhiteSpace(customCaveatName) { diff --git a/config_builder.go b/config_builder.go index 43c9e4a68..bb9c1b320 100644 --- a/config_builder.go +++ b/config_builder.go @@ -472,7 +472,7 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context, } macaroonService, err = macaroons.NewService( rootKeyStore, "lnd", walletInitParams.StatelessInit, - macaroons.IPLockChecker, + macaroons.IPLockChecker, macaroons.IPRangeLockChecker, macaroons.CustomChecker(interceptorChain), ) if err != nil { diff --git a/itest/lnd_macaroons_test.go b/itest/lnd_macaroons_test.go index b896d455a..9bc35a3e9 100644 --- a/itest/lnd_macaroons_test.go +++ b/itest/lnd_macaroons_test.go @@ -148,6 +148,49 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) { require.NoError(t, err, "get new address") assert.Contains(t, res.Address, "bcrt1") }, + }, { + // Fifth test: Check first-party caveat with invalid IP range. + name: "invalid IP range macaroon", + run: func(ctxt context.Context, t *testing.T) { + readonlyMac, err := testNode.ReadMacaroon( + testNode.Cfg.ReadMacPath, defaultTimeout, + ) + require.NoError(t, err) + invalidIPRangeMac, err := macaroons.AddConstraints( + readonlyMac, macaroons.IPRangeLockConstraint( + "1.1.1.1/32", + ), + ) + require.NoError(t, err) + cleanup, client := macaroonClient( + t, testNode, invalidIPRangeMac, + ) + defer cleanup() + _, err = client.GetInfo(ctxt, infoReq) + require.Error(t, err) + require.Contains(t, err.Error(), "different IP range") + }, + }, { + // Sixth test: Make sure that if we do everything correct and + // send the admin macaroon with first-party caveats that we can + // satisfy, we get a correct answer. + name: "correct macaroon", + run: func(ctxt context.Context, t *testing.T) { + adminMac, err := testNode.ReadMacaroon( + testNode.Cfg.AdminMacPath, defaultTimeout, + ) + require.NoError(t, err) + adminMac, err = macaroons.AddConstraints( + adminMac, macaroons.TimeoutConstraint(30), + macaroons.IPRangeLockConstraint("127.0.0.1/32"), + ) + require.NoError(t, err) + cleanup, client := macaroonClient(t, testNode, adminMac) + defer cleanup() + res, err := client.NewAddress(ctxt, newAddrReq) + require.NoError(t, err, "get new address") + assert.Contains(t, res.Address, "bcrt1") + }, }, { // Seventh test: Bake a macaroon that can only access exactly // two RPCs and make sure it works as expected. diff --git a/macaroons/constraints.go b/macaroons/constraints.go index 642f8edcf..4f82918df 100644 --- a/macaroons/constraints.go +++ b/macaroons/constraints.go @@ -80,9 +80,9 @@ func TimeoutConstraint(seconds int64) func(*macaroon.Macaroon) error { } } -// IPLockConstraint locks macaroon to a specific IP address. -// If address is an empty string, this constraint does nothing to -// accommodate default value's desired behavior. +// IPLockConstraint locks a macaroon to a specific IP address. If ipAddr is an +// empty string, this constraint does nothing to accommodate default value's +// desired behavior. func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error { return func(mac *macaroon.Macaroon) error { if ipAddr != "" { @@ -93,8 +93,30 @@ func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error { } caveat := checkers.Condition("ipaddr", macaroonIPAddr.String()) + return mac.AddFirstPartyCaveat([]byte(caveat)) } + + return nil + } +} + +// IPRangeLockConstraint locks a macaroon to a specific IP address range. If +// ipRange is an empty string, this constraint does nothing to accommodate +// default value's desired behavior. +func IPRangeLockConstraint(ipRange string) func(*macaroon.Macaroon) error { + return func(mac *macaroon.Macaroon) error { + if ipRange != "" { + _, net, err := net.ParseCIDR(ipRange) + if err != nil { + return fmt.Errorf("incorrect macaroon IP " + + "range") + } + caveat := checkers.Condition("iprange", net.String()) + + return mac.AddFirstPartyCaveat([]byte(caveat)) + } + return nil } } @@ -122,6 +144,36 @@ func IPLockChecker() (string, checkers.Func) { } } +// IPRangeLockChecker accepts client IP range from the validation context and +// compares it with the IP range locked in the macaroon. It is of the `Checker` +// type. +func IPRangeLockChecker() (string, checkers.Func) { + return "iprange", func(ctx context.Context, cond, arg string) error { + // Get peer info and extract IP range from it for macaroon + // check. + pr, ok := peer.FromContext(ctx) + if !ok { + return fmt.Errorf("unable to get peer info from context") + } + peerAddr, _, err := net.SplitHostPort(pr.Addr.String()) + if err != nil { + return fmt.Errorf("unable to parse peer address") + } + + _, ipNet, err := net.ParseCIDR(arg) + if err != nil { + return fmt.Errorf("unable to parse macaroon IP range") + } + + if !ipNet.Contains(net.ParseIP(peerAddr)) { + msg := "macaroon locked to different IP range" + return fmt.Errorf(msg) + } + + return nil + } +} + // CustomConstraint returns a function that adds a custom caveat condition to // a macaroon. func CustomConstraint(name, condition string) func(*macaroon.Macaroon) error {