diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 5817e85d..b7a4b1a6 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -14761,6 +14761,10 @@ var testsCases = []*testCase{ name: "cpfp", test: testCPFP, }, + { + name: "macaroon authentication", + test: testMacaroonAuthentication, + }, } // TestLightningNetworkDaemon performs a series of integration tests amongst a diff --git a/lntest/itest/macaroons.go b/lntest/itest/macaroons.go new file mode 100644 index 00000000..bfade118 --- /dev/null +++ b/lntest/itest/macaroons.go @@ -0,0 +1,168 @@ +// +build rpctest + +package itest + +import ( + "context" + "strings" + + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/macaroons" + "gopkg.in/macaroon.v2" +) + +// errContains is a helper function that returns true if a string is contained +// in the message of an error. +func errContains(err error, str string) bool { + return strings.Contains(err.Error(), str) +} + +// 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 ( + ctxb = context.Background() + infoReq = &lnrpc.GetInfoRequest{} + newAddrReq = &lnrpc.NewAddressRequest{ + Type: AddrTypeWitnessPubkeyHash, + } + testNode = net.Alice + ) + + // 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. + conn, err := testNode.ConnectRPC(false) + if err != nil { + t.Fatalf("unable to connect to alice: %v", err) + } + defer conn.Close() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + noMacConnection := lnrpc.NewLightningClient(conn) + _, err = noMacConnection.GetInfo(ctxt, infoReq) + if err == nil || !errContains(err, "expected 1 macaroon") { + t.Fatalf("expected to get an error when connecting without " + + "macaroons") + } + + // Second test: Ensure that an invalid macaroon also triggers an error. + invalidMac, _ := macaroon.New( + []byte("dummy_root_key"), []byte("0"), "itest", + macaroon.LatestVersion, + ) + conn, err = testNode.ConnectRPCWithMacaroon(invalidMac) + if err != nil { + t.Fatalf("unable to connect to alice: %v", err) + } + defer conn.Close() + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + invalidMacConnection := lnrpc.NewLightningClient(conn) + _, err = invalidMacConnection.GetInfo(ctxt, infoReq) + if err == nil || !errContains(err, "cannot get macaroon") { + t.Fatalf("expected to get an error when connecting with an " + + "invalid macaroon") + } + + // Third test: Try to access a write method with read-only macaroon. + readonlyMac, err := testNode.ReadMacaroon( + testNode.ReadMacPath(), defaultTimeout, + ) + if err != nil { + t.Fatalf("unable to read readonly.macaroon from node: %v", err) + } + conn, err = testNode.ConnectRPCWithMacaroon(readonlyMac) + if err != nil { + t.Fatalf("unable to connect to alice: %v", err) + } + defer conn.Close() + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + readonlyMacConnection := lnrpc.NewLightningClient(conn) + _, err = readonlyMacConnection.NewAddress(ctxt, newAddrReq) + if err == nil || !errContains(err, "permission denied") { + t.Fatalf("expected to get an error when connecting to " + + "write method with read-only macaroon") + } + + // Fourth test: Check first-party caveat with timeout that expired + // 30 seconds ago. + timeoutMac, err := macaroons.AddConstraints( + readonlyMac, macaroons.TimeoutConstraint(-30), + ) + if err != nil { + t.Fatalf("unable to add constraint to readonly macaroon: %v", + err) + } + conn, err = testNode.ConnectRPCWithMacaroon(timeoutMac) + if err != nil { + t.Fatalf("unable to connect to alice: %v", err) + } + defer conn.Close() + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + timeoutMacConnection := lnrpc.NewLightningClient(conn) + _, err = timeoutMacConnection.GetInfo(ctxt, infoReq) + if err == nil || !errContains(err, "macaroon has expired") { + t.Fatalf("expected to get an error when connecting with an " + + "invalid macaroon") + } + + // Fifth test: Check first-party caveat with invalid IP address. + invalidIpAddrMac, err := macaroons.AddConstraints( + readonlyMac, macaroons.IPLockConstraint("1.1.1.1"), + ) + if err != nil { + t.Fatalf("unable to add constraint to readonly macaroon: %v", + err) + } + conn, err = testNode.ConnectRPCWithMacaroon(invalidIpAddrMac) + if err != nil { + t.Fatalf("unable to connect to alice: %v", err) + } + defer conn.Close() + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + invalidIpAddrMacConnection := lnrpc.NewLightningClient(conn) + _, err = invalidIpAddrMacConnection.GetInfo(ctxt, infoReq) + if err == nil || !errContains(err, "different IP address") { + t.Fatalf("expected to get an error when connecting with an " + + "invalid macaroon") + } + + // 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. + adminMac, err := testNode.ReadMacaroon( + testNode.AdminMacPath(), defaultTimeout, + ) + if err != nil { + t.Fatalf("unable to read admin.macaroon from node: %v", err) + } + adminMac, err = macaroons.AddConstraints( + adminMac, macaroons.TimeoutConstraint(30), + macaroons.IPLockConstraint("127.0.0.1"), + ) + if err != nil { + t.Fatalf("unable to add constraints to admin macaroon: %v", err) + } + conn, err = testNode.ConnectRPCWithMacaroon(adminMac) + if err != nil { + t.Fatalf("unable to connect to alice: %v", err) + } + defer conn.Close() + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + adminMacConnection := lnrpc.NewLightningClient(conn) + res, err := adminMacConnection.NewAddress(ctxt, newAddrReq) + if err != nil { + t.Fatalf("unable to get new address with valid macaroon: %v", + err) + } + if !strings.HasPrefix(res.Address, "bcrt1") { + t.Fatalf("returned address was not a regtest address") + } +}