Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

itest: add tests for macaroon authentication #1152

Merged
merged 2 commits into from
Nov 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lntest/itest/lnd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
168 changes: 168 additions & 0 deletions lntest/itest/macaroons.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: newline above for some breathing room

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")
}
}
108 changes: 76 additions & 32 deletions lntest/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,22 @@ func (hn *HarnessNode) ChanBackupPath() string {
return hn.cfg.ChanBackupPath()
}

// 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 @@ -635,60 +651,88 @@ func (hn *HarnessNode) writePidFile() error {
return 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.
tlsTimeout := time.After(30 * time.Second)
for !fileExists(hn.cfg.TLSCertPath) {
// ReadMacaroon waits a given duration 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.
macTimeout := time.After(timeout)
for !fileExists(macPath) {
select {
case <-tlsTimeout:
return nil, fmt.Errorf("timeout waiting for TLS cert " +
"file to be created after 30 seconds")
case <-macTimeout:
return nil, fmt.Errorf("timeout waiting for macaroon "+
"file %s to be created after %d seconds",
macPath, timeout/time.Second)
case <-time.After(100 * time.Millisecond):
}
}

opts := []grpc.DialOption{
grpc.WithBlock(),
grpc.WithTimeout(time.Second * 20),
}

tlsCreds, err := credentials.NewClientTLSFromFile(hn.cfg.TLSCertPath, "")
// Now that we know the file exists, read it and return the macaroon.
macBytes, err := ioutil.ReadFile(macPath)
if err != nil {
return nil, err
}

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

if !useMacs {
return grpc.Dial(hn.cfg.RPCAddr(), opts...)
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, err
}
return mac, nil
}

macTimeout := time.After(30 * time.Second)
for !fileExists(hn.cfg.AdminMacPath) {
// 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(DefaultTimeout)
for !fileExists(hn.cfg.TLSCertPath) {
select {
case <-macTimeout:
return nil, fmt.Errorf("timeout waiting for admin " +
"macaroon file to be created after 30 seconds")
case <-tlsTimeout:
return nil, fmt.Errorf("timeout waiting for TLS cert " +
"file to be created")
case <-time.After(100 * time.Millisecond):
}
}

macBytes, err := ioutil.ReadFile(hn.cfg.AdminMacPath)
opts := []grpc.DialOption{grpc.WithBlock()}
tlsCreds, err := credentials.NewClientTLSFromFile(
hn.cfg.TLSCertPath, "",
)
if err != nil {
return nil, err
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, err
}
opts = append(opts, grpc.WithTransportCredentials(tlsCreds))

if mac == nil {
return grpc.Dial(hn.cfg.RPCAddr(), opts...)
}
macCred := macaroons.NewMacaroonCredential(mac)
opts = append(opts, grpc.WithPerRPCCredentials(macCred))

return grpc.Dial(hn.cfg.RPCAddr(), opts...)
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()
return grpc.DialContext(ctx, hn.cfg.RPCAddr(), opts...)
}

// 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) {
// If we don't want to use macaroons, just pass nil, the next method
// will handle it correctly.
if !useMacs {
return hn.ConnectRPCWithMacaroon(nil)
}

// If we should use a macaroon, always take the admin macaroon as a
// default.
mac, err := hn.ReadMacaroon(hn.cfg.AdminMacPath, DefaultTimeout)
if err != nil {
return nil, err
}
return hn.ConnectRPCWithMacaroon(mac)
}

// SetExtraArgs assigns the ExtraArgs field for the node's configuration. The
Expand Down