719e32830d
We fix all linter issues except for the 'lostcontext' and 'unparam' ones as those are too numerous and would increase the diff even more. Therefore we silence them in the itest directory for now. Because the linter is still not build tag aware, we also have to silence the unused and deadcode sub linters to not get false positives.
515 lines
15 KiB
Go
515 lines
15 KiB
Go
package itest
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"sort"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/lntest"
|
|
"github.com/lightningnetwork/lnd/macaroons"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gopkg.in/macaroon.v2"
|
|
)
|
|
|
|
// testMacaroonAuthentication makes sure that if macaroon authentication is
|
|
// enabled on the gRPC interface, no requests with missing or invalid
|
|
// macaroons are allowed. Further, the specific access rights (read/write,
|
|
// entity based) and first-party caveats are tested as well.
|
|
func testMacaroonAuthentication(net *lntest.NetworkHarness, t *harnessTest) {
|
|
var (
|
|
infoReq = &lnrpc.GetInfoRequest{}
|
|
newAddrReq = &lnrpc.NewAddressRequest{
|
|
Type: AddrTypeWitnessPubkeyHash,
|
|
}
|
|
testNode = net.Alice
|
|
)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
run func(ctxt context.Context, t *testing.T)
|
|
}{{
|
|
// First test: Make sure we get an error if we use no macaroons
|
|
// but try to connect to a node that has macaroon authentication
|
|
// enabled.
|
|
name: "no macaroon",
|
|
run: func(ctxt context.Context, t *testing.T) {
|
|
conn, err := testNode.ConnectRPC(false)
|
|
require.NoError(t, err)
|
|
defer func() { _ = conn.Close() }()
|
|
client := lnrpc.NewLightningClient(conn)
|
|
_, err = client.GetInfo(ctxt, infoReq)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "expected 1 macaroon")
|
|
},
|
|
}, {
|
|
// Second test: Ensure that an invalid macaroon also triggers an
|
|
// error.
|
|
name: "invalid macaroon",
|
|
run: func(ctxt context.Context, t *testing.T) {
|
|
invalidMac, _ := macaroon.New(
|
|
[]byte("dummy_root_key"), []byte("0"), "itest",
|
|
macaroon.LatestVersion,
|
|
)
|
|
cleanup, client := macaroonClient(
|
|
t, testNode, invalidMac,
|
|
)
|
|
defer cleanup()
|
|
_, err := client.GetInfo(ctxt, infoReq)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "cannot get macaroon")
|
|
},
|
|
}, {
|
|
// Third test: Try to access a write method with read-only
|
|
// macaroon.
|
|
name: "read only macaroon",
|
|
run: func(ctxt context.Context, t *testing.T) {
|
|
readonlyMac, err := testNode.ReadMacaroon(
|
|
testNode.ReadMacPath(), defaultTimeout,
|
|
)
|
|
require.NoError(t, err)
|
|
cleanup, client := macaroonClient(
|
|
t, testNode, readonlyMac,
|
|
)
|
|
defer cleanup()
|
|
_, err = client.NewAddress(ctxt, newAddrReq)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "permission denied")
|
|
},
|
|
}, {
|
|
// Fourth test: Check first-party caveat with timeout that
|
|
// expired 30 seconds ago.
|
|
name: "expired macaroon",
|
|
run: func(ctxt context.Context, t *testing.T) {
|
|
readonlyMac, err := testNode.ReadMacaroon(
|
|
testNode.ReadMacPath(), defaultTimeout,
|
|
)
|
|
require.NoError(t, err)
|
|
timeoutMac, err := macaroons.AddConstraints(
|
|
readonlyMac, macaroons.TimeoutConstraint(-30),
|
|
)
|
|
require.NoError(t, err)
|
|
cleanup, client := macaroonClient(
|
|
t, testNode, timeoutMac,
|
|
)
|
|
defer cleanup()
|
|
_, err = client.GetInfo(ctxt, infoReq)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "macaroon has expired")
|
|
},
|
|
}, {
|
|
// Fifth test: Check first-party caveat with invalid IP address.
|
|
name: "invalid IP macaroon",
|
|
run: func(ctxt context.Context, t *testing.T) {
|
|
readonlyMac, err := testNode.ReadMacaroon(
|
|
testNode.ReadMacPath(), defaultTimeout,
|
|
)
|
|
require.NoError(t, err)
|
|
invalidIPAddrMac, err := macaroons.AddConstraints(
|
|
readonlyMac, macaroons.IPLockConstraint(
|
|
"1.1.1.1",
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
cleanup, client := macaroonClient(
|
|
t, testNode, invalidIPAddrMac,
|
|
)
|
|
defer cleanup()
|
|
_, err = client.GetInfo(ctxt, infoReq)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "different IP address")
|
|
},
|
|
}, {
|
|
// 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.AdminMacPath(), defaultTimeout,
|
|
)
|
|
require.NoError(t, err)
|
|
adminMac, err = macaroons.AddConstraints(
|
|
adminMac, macaroons.TimeoutConstraint(30),
|
|
macaroons.IPLockConstraint("127.0.0.1"),
|
|
)
|
|
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.
|
|
name: "custom URI permissions",
|
|
run: func(ctxt context.Context, t *testing.T) {
|
|
entity := macaroons.PermissionEntityCustomURI
|
|
req := &lnrpc.BakeMacaroonRequest{
|
|
Permissions: []*lnrpc.MacaroonPermission{{
|
|
Entity: entity,
|
|
Action: "/lnrpc.Lightning/GetInfo",
|
|
}, {
|
|
Entity: entity,
|
|
Action: "/lnrpc.Lightning/List" +
|
|
"Permissions",
|
|
}},
|
|
}
|
|
bakeRes, err := testNode.BakeMacaroon(ctxt, req)
|
|
require.NoError(t, err)
|
|
|
|
// Create a connection that uses the custom macaroon.
|
|
customMacBytes, err := hex.DecodeString(
|
|
bakeRes.Macaroon,
|
|
)
|
|
require.NoError(t, err)
|
|
customMac := &macaroon.Macaroon{}
|
|
err = customMac.UnmarshalBinary(customMacBytes)
|
|
require.NoError(t, err)
|
|
cleanup, client := macaroonClient(
|
|
t, testNode, customMac,
|
|
)
|
|
defer cleanup()
|
|
|
|
// Call GetInfo which should succeed.
|
|
_, err = client.GetInfo(ctxt, infoReq)
|
|
require.NoError(t, err)
|
|
|
|
// Call ListPermissions which should also succeed.
|
|
permReq := &lnrpc.ListPermissionsRequest{}
|
|
permRes, err := client.ListPermissions(ctxt, permReq)
|
|
require.NoError(t, err)
|
|
require.Greater(
|
|
t, len(permRes.MethodPermissions), 10,
|
|
"permissions",
|
|
)
|
|
|
|
// Try NewAddress which should be denied.
|
|
_, err = client.NewAddress(ctxt, newAddrReq)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "permission denied")
|
|
},
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctxt, cancel := context.WithTimeout(
|
|
context.Background(), defaultTimeout,
|
|
)
|
|
defer cancel()
|
|
|
|
tc.run(ctxt, t)
|
|
})
|
|
}
|
|
}
|
|
|
|
// testBakeMacaroon checks that when creating macaroons, the permissions param
|
|
// in the request must be set correctly, and the baked macaroon has the intended
|
|
// permissions.
|
|
func testBakeMacaroon(net *lntest.NetworkHarness, t *harnessTest) {
|
|
var testNode = net.Alice
|
|
|
|
testCases := []struct {
|
|
name string
|
|
run func(ctxt context.Context, t *testing.T,
|
|
adminClient lnrpc.LightningClient)
|
|
}{{
|
|
// First test: when the permission list is empty in the request,
|
|
// an error should be returned.
|
|
name: "no permission list",
|
|
run: func(ctxt context.Context, t *testing.T,
|
|
adminClient lnrpc.LightningClient) {
|
|
|
|
req := &lnrpc.BakeMacaroonRequest{}
|
|
_, err := adminClient.BakeMacaroon(ctxt, req)
|
|
require.Error(t, err)
|
|
assert.Contains(
|
|
t, err.Error(), "permission list cannot be "+
|
|
"empty",
|
|
)
|
|
},
|
|
}, {
|
|
// Second test: when the action in the permission list is not
|
|
// valid, an error should be returned.
|
|
name: "invalid permission list",
|
|
run: func(ctxt context.Context, t *testing.T,
|
|
adminClient lnrpc.LightningClient) {
|
|
|
|
req := &lnrpc.BakeMacaroonRequest{
|
|
Permissions: []*lnrpc.MacaroonPermission{{
|
|
Entity: "macaroon",
|
|
Action: "invalid123",
|
|
}},
|
|
}
|
|
_, err := adminClient.BakeMacaroon(ctxt, req)
|
|
require.Error(t, err)
|
|
assert.Contains(
|
|
t, err.Error(), "invalid permission action",
|
|
)
|
|
},
|
|
}, {
|
|
// Third test: when the entity in the permission list is not
|
|
// valid, an error should be returned.
|
|
name: "invalid permission entity",
|
|
run: func(ctxt context.Context, t *testing.T,
|
|
adminClient lnrpc.LightningClient) {
|
|
|
|
req := &lnrpc.BakeMacaroonRequest{
|
|
Permissions: []*lnrpc.MacaroonPermission{{
|
|
Entity: "invalid123",
|
|
Action: "read",
|
|
}},
|
|
}
|
|
_, err := adminClient.BakeMacaroon(ctxt, req)
|
|
require.Error(t, err)
|
|
assert.Contains(
|
|
t, err.Error(), "invalid permission entity",
|
|
)
|
|
},
|
|
}, {
|
|
// Fourth test: check that when no root key ID is specified, the
|
|
// default root keyID is used.
|
|
name: "default root key ID",
|
|
run: func(ctxt context.Context, t *testing.T,
|
|
adminClient lnrpc.LightningClient) {
|
|
|
|
req := &lnrpc.BakeMacaroonRequest{
|
|
Permissions: []*lnrpc.MacaroonPermission{{
|
|
Entity: "macaroon",
|
|
Action: "read",
|
|
}},
|
|
}
|
|
_, err := adminClient.BakeMacaroon(ctxt, req)
|
|
require.NoError(t, err)
|
|
|
|
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
|
resp, err := adminClient.ListMacaroonIDs(ctxt, listReq)
|
|
require.NoError(t, err)
|
|
require.Equal(t, resp.RootKeyIds[0], uint64(0))
|
|
},
|
|
}, {
|
|
// Fifth test: create a macaroon use a non-default root key ID.
|
|
name: "custom root key ID",
|
|
run: func(ctxt context.Context, t *testing.T,
|
|
adminClient lnrpc.LightningClient) {
|
|
|
|
rootKeyID := uint64(4200)
|
|
req := &lnrpc.BakeMacaroonRequest{
|
|
RootKeyId: rootKeyID,
|
|
Permissions: []*lnrpc.MacaroonPermission{{
|
|
Entity: "macaroon",
|
|
Action: "read",
|
|
}},
|
|
}
|
|
_, err := adminClient.BakeMacaroon(ctxt, req)
|
|
require.NoError(t, err)
|
|
|
|
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
|
resp, err := adminClient.ListMacaroonIDs(ctxt, listReq)
|
|
require.NoError(t, err)
|
|
|
|
// the ListMacaroonIDs should give a list of two IDs,
|
|
// the default ID 0, and the newly created ID. The
|
|
// returned response is sorted to guarantee the order so
|
|
// that we can compare them one by one.
|
|
sort.Slice(resp.RootKeyIds, func(i, j int) bool {
|
|
return resp.RootKeyIds[i] < resp.RootKeyIds[j]
|
|
})
|
|
require.Equal(t, resp.RootKeyIds[0], uint64(0))
|
|
require.Equal(t, resp.RootKeyIds[1], rootKeyID)
|
|
},
|
|
}, {
|
|
// Sixth test: check the baked macaroon has the intended
|
|
// permissions. It should succeed in reading, and fail to write
|
|
// a macaroon.
|
|
name: "custom macaroon permissions",
|
|
run: func(ctxt context.Context, t *testing.T,
|
|
adminClient lnrpc.LightningClient) {
|
|
|
|
rootKeyID := uint64(4200)
|
|
req := &lnrpc.BakeMacaroonRequest{
|
|
RootKeyId: rootKeyID,
|
|
Permissions: []*lnrpc.MacaroonPermission{{
|
|
Entity: "macaroon",
|
|
Action: "read",
|
|
}},
|
|
}
|
|
bakeResp, err := adminClient.BakeMacaroon(ctxt, req)
|
|
require.NoError(t, err)
|
|
|
|
newMac, err := readMacaroonFromHex(bakeResp.Macaroon)
|
|
require.NoError(t, err)
|
|
cleanup, readOnlyClient := macaroonClient(
|
|
t, testNode, newMac,
|
|
)
|
|
defer cleanup()
|
|
|
|
// BakeMacaroon requires a write permission, so this
|
|
// call should return an error.
|
|
_, err = readOnlyClient.BakeMacaroon(ctxt, req)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "permission denied")
|
|
|
|
// ListMacaroon requires a read permission, so this call
|
|
// should succeed.
|
|
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
|
_, err = readOnlyClient.ListMacaroonIDs(ctxt, listReq)
|
|
require.NoError(t, err)
|
|
|
|
// Current macaroon can only work on entity macaroon, so
|
|
// a GetInfo request will fail.
|
|
infoReq := &lnrpc.GetInfoRequest{}
|
|
_, err = readOnlyClient.GetInfo(ctxt, infoReq)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "permission denied")
|
|
},
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctxt, cancel := context.WithTimeout(
|
|
context.Background(), defaultTimeout,
|
|
)
|
|
defer cancel()
|
|
|
|
adminMac, err := testNode.ReadMacaroon(
|
|
testNode.AdminMacPath(), defaultTimeout,
|
|
)
|
|
require.NoError(t, err)
|
|
cleanup, client := macaroonClient(t, testNode, adminMac)
|
|
defer cleanup()
|
|
|
|
tc.run(ctxt, t, client)
|
|
})
|
|
}
|
|
}
|
|
|
|
// testDeleteMacaroonID checks that when deleting a macaroon ID, it removes the
|
|
// specified ID and invalidates all macaroons derived from the key with that ID.
|
|
// Also, it checks deleting the reserved marcaroon ID, DefaultRootKeyID or is
|
|
// forbidden.
|
|
func testDeleteMacaroonID(net *lntest.NetworkHarness, t *harnessTest) {
|
|
var (
|
|
ctxb = context.Background()
|
|
testNode = net.Alice
|
|
)
|
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
defer cancel()
|
|
|
|
// Use admin macaroon to create a connection.
|
|
adminMac, err := testNode.ReadMacaroon(
|
|
testNode.AdminMacPath(), defaultTimeout,
|
|
)
|
|
require.NoError(t.t, err)
|
|
cleanup, client := macaroonClient(t.t, testNode, adminMac)
|
|
defer cleanup()
|
|
|
|
// Record the number of macaroon IDs before creation.
|
|
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
|
listResp, err := client.ListMacaroonIDs(ctxt, listReq)
|
|
require.NoError(t.t, err)
|
|
numMacIDs := len(listResp.RootKeyIds)
|
|
|
|
// Create macaroons for testing.
|
|
rootKeyIDs := []uint64{1, 2, 3}
|
|
macList := make([]string, 0, len(rootKeyIDs))
|
|
for _, id := range rootKeyIDs {
|
|
req := &lnrpc.BakeMacaroonRequest{
|
|
RootKeyId: id,
|
|
Permissions: []*lnrpc.MacaroonPermission{{
|
|
Entity: "macaroon",
|
|
Action: "read",
|
|
}},
|
|
}
|
|
resp, err := client.BakeMacaroon(ctxt, req)
|
|
require.NoError(t.t, err)
|
|
macList = append(macList, resp.Macaroon)
|
|
}
|
|
|
|
// Check that the creation is successful.
|
|
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
|
listResp, err = client.ListMacaroonIDs(ctxt, listReq)
|
|
require.NoError(t.t, err)
|
|
|
|
// The number of macaroon IDs should be increased by len(rootKeyIDs).
|
|
require.Equal(t.t, numMacIDs+len(rootKeyIDs), len(listResp.RootKeyIds))
|
|
|
|
// First test: check deleting the DefaultRootKeyID returns an error.
|
|
defaultID, _ := strconv.ParseUint(
|
|
string(macaroons.DefaultRootKeyID), 10, 64,
|
|
)
|
|
req := &lnrpc.DeleteMacaroonIDRequest{
|
|
RootKeyId: defaultID,
|
|
}
|
|
_, err = client.DeleteMacaroonID(ctxt, req)
|
|
require.Error(t.t, err)
|
|
require.Contains(
|
|
t.t, err.Error(), macaroons.ErrDeletionForbidden.Error(),
|
|
)
|
|
|
|
// Second test: check deleting the customized ID returns success.
|
|
req = &lnrpc.DeleteMacaroonIDRequest{
|
|
RootKeyId: rootKeyIDs[0],
|
|
}
|
|
resp, err := client.DeleteMacaroonID(ctxt, req)
|
|
require.NoError(t.t, err)
|
|
require.True(t.t, resp.Deleted)
|
|
|
|
// Check that the deletion is successful.
|
|
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
|
listResp, err = client.ListMacaroonIDs(ctxt, listReq)
|
|
require.NoError(t.t, err)
|
|
|
|
// The number of macaroon IDs should be decreased by 1.
|
|
require.Equal(t.t, numMacIDs+len(rootKeyIDs)-1, len(listResp.RootKeyIds))
|
|
|
|
// Check that the deleted macaroon can no longer access macaroon:read.
|
|
deletedMac, err := readMacaroonFromHex(macList[0])
|
|
require.NoError(t.t, err)
|
|
cleanup, client = macaroonClient(t.t, testNode, deletedMac)
|
|
defer cleanup()
|
|
|
|
// Because the macaroon is deleted, it will be treated as an invalid one.
|
|
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
|
_, err = client.ListMacaroonIDs(ctxt, listReq)
|
|
require.Error(t.t, err)
|
|
require.Contains(t.t, err.Error(), "cannot get macaroon")
|
|
}
|
|
|
|
// readMacaroonFromHex loads a macaroon from a hex string.
|
|
func readMacaroonFromHex(macHex string) (*macaroon.Macaroon, error) {
|
|
macBytes, err := hex.DecodeString(macHex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mac := &macaroon.Macaroon{}
|
|
if err := mac.UnmarshalBinary(macBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
return mac, nil
|
|
}
|
|
|
|
func macaroonClient(t *testing.T, testNode *lntest.HarnessNode,
|
|
mac *macaroon.Macaroon) (func(), lnrpc.LightningClient) {
|
|
|
|
conn, err := testNode.ConnectRPCWithMacaroon(mac)
|
|
require.NoError(t, err, "connect to alice")
|
|
|
|
cleanup := func() {
|
|
err := conn.Close()
|
|
require.NoError(t, err, "close")
|
|
}
|
|
return cleanup, lnrpc.NewLightningClient(conn)
|
|
}
|