Skip to content

Commit

Permalink
Merge 4b3408a into 5c42a88
Browse files Browse the repository at this point in the history
  • Loading branch information
guggero committed May 23, 2018
2 parents 5c42a88 + 4b3408a commit f0bfa91
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 24 deletions.
137 changes: 137 additions & 0 deletions lnd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ import (
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/roasbeef/btcd/chaincfg"
"github.com/roasbeef/btcd/chaincfg/chainhash"
"github.com/roasbeef/btcd/integration/rpctest"
"github.com/roasbeef/btcd/rpcclient"
"github.com/roasbeef/btcd/wire"
"github.com/roasbeef/btcutil"

"golang.org/x/net/context"
"google.golang.org/grpc"
"gopkg.in/macaroon.v2"
)

var (
Expand Down Expand Up @@ -9020,6 +9023,136 @@ func testQueryRoutes(net *lntest.NetworkHarness, t *harnessTest) {
}
}

// 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()
timeout = time.Duration(time.Second * 5)
ctxt, _ = context.WithTimeout(ctxb, timeout)
infoReq = &lnrpc.GetInfoRequest{}
newAddrReq = &lnrpc.NewWitnessAddressRequest{}
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()
noMacConnection := lnrpc.NewLightningClient(conn)
_, err = noMacConnection.GetInfo(ctxt, infoReq)
if err == nil || !strings.Contains(err.Error(), "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()
invalidMacConnection := lnrpc.NewLightningClient(conn)
_, err = invalidMacConnection.GetInfo(ctxt, infoReq)
if err == nil || !strings.Contains(err.Error(), "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(), 30)
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()
readonlyMacConnection := lnrpc.NewLightningClient(conn)
_, err = readonlyMacConnection.NewWitnessAddress(ctxt, newAddrReq)
if err == nil || !strings.Contains(err.Error(), "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()
timeoutMacConnection := lnrpc.NewLightningClient(conn)
_, err = timeoutMacConnection.GetInfo(ctxt, infoReq)
errMsg := err.Error()
if err == nil || !strings.Contains(errMsg, "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()
invalidIpAddrMacConnection := lnrpc.NewLightningClient(conn)
_, err = invalidIpAddrMacConnection.GetInfo(ctxt, infoReq)
errMsg = err.Error()
if err == nil || !strings.Contains(errMsg, "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(), 30)
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()
adminMacConnection := lnrpc.NewLightningClient(conn)
res, err := adminMacConnection.NewWitnessAddress(ctxt, newAddrReq)
if err != nil {
t.Fatalf("unable to get new address with valid macaroon: %v",
err)
}
if !strings.HasPrefix(res.Address, "r") {
t.Fatalf("returned address was not a regtest address")
}
}

type testCase struct {
name string
test func(net *lntest.NetworkHarness, t *harnessTest)
Expand Down Expand Up @@ -9195,6 +9328,10 @@ var testsCases = []*testCase{
name: "query routes",
test: testQueryRoutes,
},
{
name: "macaroon authentication",
test: testMacaroonAuthentication,
},
}

// TestLightningNetworkDaemon performs a series of integration tests amongst a
Expand Down
92 changes: 68 additions & 24 deletions lntest/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,22 @@ func (hn *HarnessNode) DBPath() string {
return hn.cfg.DBPath()
}

// AdminMacPath returns the filepath to the admin.macaroon file for this node.
func (hn *HarnessNode) AdminMacPath() string {
return hn.cfg.AdminMacPath
}

// ReadMacPath returns the filepath to the readonly.macaroon file for this node.
func (hn *HarnessNode) ReadMacPath() string {
return hn.cfg.ReadMacPath
}

// InvoiceMacPath returns the filepath to the invoice.macaroon file for this
// node.
func (hn *HarnessNode) InvoiceMacPath() string {
return hn.cfg.InvoiceMacPath
}

// Start launches a new process running lnd. Additionally, the PID of the
// launched process is saved in order to possibly kill the process forcibly
// later.
Expand Down Expand Up @@ -474,11 +490,57 @@ func (hn *HarnessNode) writePidFile() error {
return nil
}

// connectRPC uses the TLS certificate and admin macaroon files written by the
// ReadMacaroon waits a given number of seconds for the macaroon file to be
// created. If the file is readable within the timeout, its content is
// de-serialized as a macaroon and returned.
func (hn *HarnessNode) ReadMacaroon(macPath string,
timeout time.Duration) (*macaroon.Macaroon, error) {
// Wait until macaroon file is created before using it, up to 30 sec.
macTimeout := time.After(timeout * time.Second)
for !fileExists(macPath) {
select {
case <-macTimeout:
return nil, fmt.Errorf("timeout waiting for macaroon " +
"file %s to be created after 30 seconds",
macPath)
case <-time.After(100 * time.Millisecond):
}
}

macBytes, err := ioutil.ReadFile(macPath)
if err != nil {
return nil, err
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, err
}

return mac, nil
}

// ConnectRPC uses the TLS certificate and admin macaroon files written by the
// lnd node to create a gRPC client connection.
func (hn *HarnessNode) ConnectRPC(useMacs bool) (*grpc.ClientConn, error) {
// Wait until TLS certificate and admin macaroon are created before
// using them, up to 20 sec.
// If we don't want to use macaroons, just pass nil, the next method
// will handle it correctly.
if !useMacs {
return hn.ConnectRPCWithMacaroon(nil)
}

mac, err := hn.ReadMacaroon(hn.cfg.AdminMacPath, 30)
if err != nil {
return nil, err
}

return hn.ConnectRPCWithMacaroon(mac)
}

// ConnectRPCWithMacaroon uses the TLS certificate and given macaroon to
// create a gRPC client connection.
func (hn *HarnessNode) ConnectRPCWithMacaroon(
mac *macaroon.Macaroon) (*grpc.ClientConn, error) {
// Wait until TLS certificate is created before using it, up to 30 sec.
tlsTimeout := time.After(30 * time.Second)
for !fileExists(hn.cfg.TLSCertPath) {
select {
Expand All @@ -494,36 +556,18 @@ func (hn *HarnessNode) ConnectRPC(useMacs bool) (*grpc.ClientConn, error) {
grpc.WithTimeout(time.Second * 20),
}

tlsCreds, err := credentials.NewClientTLSFromFile(hn.cfg.TLSCertPath, "")
tlsCreds, err := credentials.NewClientTLSFromFile(hn.cfg.TLSCertPath,
"")
if err != nil {
return nil, err
}

opts = append(opts, grpc.WithTransportCredentials(tlsCreds))

if !useMacs {
if mac == nil {
return grpc.Dial(hn.cfg.RPCAddr(), opts...)
}

macTimeout := time.After(30 * time.Second)
for !fileExists(hn.cfg.AdminMacPath) {
select {
case <-macTimeout:
return nil, fmt.Errorf("timeout waiting for admin " +
"macaroon file to be created after 30 seconds")
case <-time.After(100 * time.Millisecond):
}
}

macBytes, err := ioutil.ReadFile(hn.cfg.AdminMacPath)
if err != nil {
return nil, err
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, err
}

macCred := macaroons.NewMacaroonCredential(mac)
opts = append(opts, grpc.WithPerRPCCredentials(macCred))

Expand Down

0 comments on commit f0bfa91

Please sign in to comment.