mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-06-13 10:21:37 +02:00
Merge pull request #1152 from guggero/macaroon-integrationtest
itest: add tests for macaroon authentication
This commit is contained in:
commit
b1e6d9c5cf
@ -14802,6 +14802,10 @@ var testsCases = []*testCase{
|
|||||||
name: "cpfp",
|
name: "cpfp",
|
||||||
test: testCPFP,
|
test: testCPFP,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "macaroon authentication",
|
||||||
|
test: testMacaroonAuthentication,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLightningNetworkDaemon performs a series of integration tests amongst a
|
// TestLightningNetworkDaemon performs a series of integration tests amongst a
|
||||||
|
168
lntest/itest/macaroons.go
Normal file
168
lntest/itest/macaroons.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
116
lntest/node.go
116
lntest/node.go
@ -336,6 +336,22 @@ func (hn *HarnessNode) ChanBackupPath() string {
|
|||||||
return hn.cfg.ChanBackupPath()
|
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
|
// Start launches a new process running lnd. Additionally, the PID of the
|
||||||
// launched process is saved in order to possibly kill the process forcibly
|
// launched process is saved in order to possibly kill the process forcibly
|
||||||
// later.
|
// later.
|
||||||
@ -635,48 +651,26 @@ func (hn *HarnessNode) writePidFile() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectRPC uses the TLS certificate and admin macaroon files written by the
|
// ReadMacaroon waits a given duration for the macaroon file to be created. If
|
||||||
// lnd node to create a gRPC client connection.
|
// the file is readable within the timeout, its content is de-serialized as a
|
||||||
func (hn *HarnessNode) ConnectRPC(useMacs bool) (*grpc.ClientConn, error) {
|
// macaroon and returned.
|
||||||
// Wait until TLS certificate and admin macaroon are created before
|
func (hn *HarnessNode) ReadMacaroon(macPath string, timeout time.Duration) (
|
||||||
// using them, up to 20 sec.
|
*macaroon.Macaroon, error) {
|
||||||
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):
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := []grpc.DialOption{
|
// Wait until macaroon file is created before using it.
|
||||||
grpc.WithBlock(),
|
macTimeout := time.After(timeout)
|
||||||
grpc.WithTimeout(time.Second * 20),
|
for !fileExists(macPath) {
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
select {
|
select {
|
||||||
case <-macTimeout:
|
case <-macTimeout:
|
||||||
return nil, fmt.Errorf("timeout waiting for admin " +
|
return nil, fmt.Errorf("timeout waiting for macaroon "+
|
||||||
"macaroon file to be created after 30 seconds")
|
"file %s to be created after %d seconds",
|
||||||
|
macPath, timeout/time.Second)
|
||||||
case <-time.After(100 * time.Millisecond):
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -684,11 +678,61 @@ func (hn *HarnessNode) ConnectRPC(useMacs bool) (*grpc.ClientConn, error) {
|
|||||||
if err = mac.UnmarshalBinary(macBytes); err != nil {
|
if err = mac.UnmarshalBinary(macBytes); err != nil {
|
||||||
return nil, err
|
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)
|
macCred := macaroons.NewMacaroonCredential(mac)
|
||||||
opts = append(opts, grpc.WithPerRPCCredentials(macCred))
|
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
|
// SetExtraArgs assigns the ExtraArgs field for the node's configuration. The
|
||||||
|
Loading…
x
Reference in New Issue
Block a user