From e8df2757aef48ce04e0d7094e3515bac4886eb42 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sat, 26 May 2018 10:04:09 +0200 Subject: [PATCH 1/2] lntest: allow client to connect with specific macaroon --- lntest/node.go | 116 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 80 insertions(+), 36 deletions(-) diff --git a/lntest/node.go b/lntest/node.go index 9b4256c3..26a5e053 100644 --- a/lntest/node.go +++ b/lntest/node.go @@ -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. @@ -635,48 +651,26 @@ 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) { - select { - case <-tlsTimeout: - return nil, fmt.Errorf("timeout waiting for TLS cert " + - "file to be created after 30 seconds") - case <-time.After(100 * time.Millisecond): - } - } +// 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) { - opts := []grpc.DialOption{ - grpc.WithBlock(), - grpc.WithTimeout(time.Second * 20), - } - - tlsCreds, err := credentials.NewClientTLSFromFile(hn.cfg.TLSCertPath, "") - if err != nil { - return nil, err - } - - opts = append(opts, grpc.WithTransportCredentials(tlsCreds)) - - if !useMacs { - return grpc.Dial(hn.cfg.RPCAddr(), opts...) - } - - macTimeout := time.After(30 * time.Second) - for !fileExists(hn.cfg.AdminMacPath) { + // Wait until macaroon file is created before using it. + macTimeout := time.After(timeout) + for !fileExists(macPath) { select { case <-macTimeout: - return nil, fmt.Errorf("timeout waiting for admin " + - "macaroon file to be created after 30 seconds") + 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): } } - macBytes, err := ioutil.ReadFile(hn.cfg.AdminMacPath) + // Now that we know the file exists, read it and return the macaroon. + macBytes, err := ioutil.ReadFile(macPath) if err != nil { return nil, err } @@ -684,11 +678,61 @@ func (hn *HarnessNode) ConnectRPC(useMacs bool) (*grpc.ClientConn, error) { if err = mac.UnmarshalBinary(macBytes); err != nil { return nil, err } + return mac, nil +} +// 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 <-tlsTimeout: + return nil, fmt.Errorf("timeout waiting for TLS cert " + + "file to be created") + case <-time.After(100 * time.Millisecond): + } + } + + opts := []grpc.DialOption{grpc.WithBlock()} + tlsCreds, err := credentials.NewClientTLSFromFile( + hn.cfg.TLSCertPath, "", + ) + if 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 From 543b258e30b65da983ed040a2dd6f9793ecc07a6 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sat, 26 May 2018 10:04:28 +0200 Subject: [PATCH 2/2] lnd_test: add integration test for macaroon authentication --- lntest/itest/lnd_test.go | 4 + lntest/itest/macaroons.go | 168 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 lntest/itest/macaroons.go 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") + } +}