From 8e4e2bd889e3a3fc16f75808196f6e29a9570a4a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 27 Apr 2018 00:06:00 +0300 Subject: [PATCH] macaroons: add tests for service and constraints --- macaroons/constraints_test.go | 169 +++++++++++++++++----------------- macaroons/service_test.go | 134 +++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 85 deletions(-) create mode 100644 macaroons/service_test.go diff --git a/macaroons/constraints_test.go b/macaroons/constraints_test.go index 4bd9f552..694d7841 100644 --- a/macaroons/constraints_test.go +++ b/macaroons/constraints_test.go @@ -1,109 +1,108 @@ -package macaroons +package macaroons_test import ( - "errors" - "fmt" "testing" + "github.com/lightningnetwork/lnd/macaroons" + "gopkg.in/macaroon.v2" "time" - - "gopkg.in/macaroon-bakery.v1/bakery" - "gopkg.in/macaroon-bakery.v1/bakery/checkers" - macaroon "gopkg.in/macaroon.v1" + "strings" ) -type macError struct { - message string -} +var ( + testRootKey = []byte("dummyRootKey") + testId = []byte("dummyId") + testLocation = "lnd" + testVersion = macaroon.LatestVersion + expectedTimeCaveatSubstring = "time-before " + string(time.Now().Year()) +) -func (err macError) Error() string { - return err.message -} - -func testConstraint(constraint Constraint, ok checkers.Checker, - failFn func() checkers.Checker) error { - macParams := bakery.NewServiceParams{} - svc, err := bakery.NewService(macParams) +func createDummyMacaroon(t *testing.T) *macaroon.Macaroon { + dummyMacaroon, err := macaroon.New(testRootKey, testId, + testLocation, testVersion) if err != nil { - return errors.New("Failed to create a new service") + t.Fatalf("Error creating initial macaroon: %v", err) } - mac, err := svc.NewMacaroon("", nil, nil) - if err != nil { - return errors.New("Failed to create a new macaroon") - } - - mac, err = AddConstraints(mac, constraint) - if err != nil { - return errors.New("Failed to add macaroon constraint") - } - - okChecker := checkers.New(ok) - if err := svc.Check(macaroon.Slice{mac}, okChecker); err != nil { - msg := "Correct checker failed: %v" - return macError{fmt.Sprintf(msg, ok)} - } - - fail := failFn() - failChecker := checkers.New(fail) - if err := svc.Check(macaroon.Slice{mac}, failChecker); err == nil { - msg := "Incorrect checker succeeded: %v" - return macError{fmt.Sprintf(msg, fail)} - } - return nil + return dummyMacaroon } -func TestAllowConstraint(t *testing.T) { - if err := testConstraint( - AllowConstraint("op1", "op2", "op4"), - AllowChecker("op1"), - func() checkers.Checker { - return AllowChecker("op3") - }, - ); err != nil { - t.Fatalf(err.Error()) +// TestAddConstraints tests that constraints can be added to an existing +// macaroon and therefore tighten its restrictions. +func TestAddConstraints(t *testing.T) { + // We need a dummy macaroon to start with. Create one without + // a bakery, because we mock everything anyway. + initialMac := createDummyMacaroon(t) + + // Now add a constraint and make sure we have a cloned macaroon + // with the constraint applied instead of a mutated initial one. + newMac, err := macaroons.AddConstraints(initialMac, + macaroons.TimeoutConstraint(1)) + if err != nil { + t.Fatalf("Error adding constraint: %v", err) + } + if &newMac == &initialMac { + t.Fatalf("Initial macaroon has been changed, something " + + "went wrong!") + } + + // Finally, test that the constraint has been added. + if len(initialMac.Caveats()) == len(newMac.Caveats()) { + t.Fatalf("No caveat has been added to the macaroon when " + + "constraint was applied") } } +// TestTimeoutConstraint tests that a caveat for the lifetime of +// a macaroon is created. func TestTimeoutConstraint(t *testing.T) { - if err := testConstraint( - TimeoutConstraint(1), - TimeoutChecker(), - func() checkers.Checker { - time.Sleep(time.Second) - return TimeoutChecker() - }, - ); err != nil { - t.Fatalf(err.Error()) + // Get a configured version of the constraint function. + constraintFunc := macaroons.TimeoutConstraint(3) + + // Now we need a dummy macaroon that we can apply the constraint + // function to. + testMacaroon := createDummyMacaroon(t) + err := constraintFunc(testMacaroon) + if err != nil { + t.Fatalf("Error applying timeout constraint: %v", err) + } + + // Finally, check that the created caveat has an + // acceptable value + if strings.HasPrefix(string(testMacaroon.Caveats()[0].Id), + expectedTimeCaveatSubstring) { + t.Fatalf("Added caveat '%s' does not meet the expectations!", + testMacaroon.Caveats()[0].Id) } } -func TestIPLockConstraint(t *testing.T) { - if err := testConstraint( - IPLockConstraint("127.0.0.1"), - IPLockChecker("127.0.0.1"), - func() checkers.Checker { - return IPLockChecker("0.0.0.0") - }, - ); err != nil { - t.Fatalf(err.Error()) - } -} - -func TestIPLockEmptyIP(t *testing.T) { - if err := testConstraint( - IPLockConstraint(""), - IPLockChecker("127.0.0.1"), - func() checkers.Checker { - return IPLockChecker("0.0.0.0") - }, - ); err != nil { - if _, ok := err.(macError); !ok { - t.Fatalf("IPLock with an empty IP should always pass") - } +// TestTimeoutConstraint tests that a caveat for the lifetime of +// a macaroon is created. +func TestIpLockConstraint(t *testing.T) { + // Get a configured version of the constraint function. + constraintFunc := macaroons.IPLockConstraint("127.0.0.1") + + // Now we need a dummy macaroon that we can apply the constraint + // function to. + testMacaroon := createDummyMacaroon(t) + err := constraintFunc(testMacaroon) + if err != nil { + t.Fatalf("Error applying timeout constraint: %v", err) + } + + // Finally, check that the created caveat has an + // acceptable value + if string(testMacaroon.Caveats()[0].Id) != "ipaddr 127.0.0.1" { + t.Fatalf("Added caveat '%s' does not meet the expectations!", + testMacaroon.Caveats()[0].Id) } } +// TestIPLockBadIP tests that an IP constraint cannot be added if the +// provided string is not a valid IP address. func TestIPLockBadIP(t *testing.T) { - if err := IPLockConstraint("127.0.0/800"); err == nil { - t.Fatalf("IPLockConstraint with bad IP should fail") + constraintFunc := macaroons.IPLockConstraint("127.0.0/800"); + testMacaroon := createDummyMacaroon(t) + err := constraintFunc(testMacaroon) + if err == nil { + t.Fatalf("IPLockConstraint with bad IP should fail.") } } diff --git a/macaroons/service_test.go b/macaroons/service_test.go new file mode 100644 index 00000000..23f8b386 --- /dev/null +++ b/macaroons/service_test.go @@ -0,0 +1,134 @@ +package macaroons_test + +import ( + "testing" + "path" + "os" + "context" + "io/ioutil" + "encoding/hex" + + "github.com/coreos/bbolt" + "github.com/lightningnetwork/lnd/macaroons" + "gopkg.in/macaroon-bakery.v2/bakery/checkers" + "gopkg.in/macaroon-bakery.v2/bakery" + "google.golang.org/grpc/metadata" +) + +var ( + testOperation = bakery.Op{ + Entity: "testEntity", + Action: "read", + } + defaultPw = []byte("hello") +) + +// setupTestRootKeyStorage creates a dummy root key storage by +// creating a temporary macaroons.db and initializing it with the +// default password of 'hello'. Only the path to the temporary +// DB file is returned, because the service will open the file +// and read the store on its own. +func setupTestRootKeyStorage(t *testing.T) string { + tempDir, err := ioutil.TempDir("", "macaroonstore-") + if err != nil { + t.Fatalf("Error creating temp dir: %v", err) + } + db, err := bolt.Open(path.Join(tempDir, "macaroons.db"), 0600, + bolt.DefaultOptions) + if err != nil { + t.Fatalf("Error opening store DB: %v", err) + } + store, err := macaroons.NewRootKeyStorage(db) + if err != nil { + db.Close() + t.Fatalf("Error creating root key store: %v", err) + } + defer store.Close() + err = store.CreateUnlock(&defaultPw) + return tempDir +} + +// TestNewService tests the creation of the macaroon service. +func TestNewService(t *testing.T) { + // First, initialize a dummy DB file with a store that the service + // can read from. Make sure the file is removed in the end. + tempDir := setupTestRootKeyStorage(t) + defer os.RemoveAll(tempDir) + + // Second, create the new service instance, unlock it and pass in a + // checker that we expect it to add to the bakery. + service, err := macaroons.NewService(tempDir, macaroons.IPLockChecker) + defer service.Close() + if err != nil { + t.Fatalf("Error creating new service: %v", err) + } + err = service.CreateUnlock(&defaultPw) + if err != nil { + t.Fatalf("Error unlocking root key storage: %v", err) + } + + // Third, check if the created service can bake macaroons. + macaroon, err := service.Oven.NewMacaroon(nil, bakery.LatestVersion, + nil, testOperation) + if err != nil { + t.Fatalf("Error creating macaroon from service: %v", err) + } + if macaroon.Namespace().String() != "std:" { + t.Fatalf("The created macaroon has an invalid namespace: %s", + macaroon.Namespace().String()) + } + + // Finally, check if the service has been initialized correctly and + // the checker has been added. + var checkerFound = false + checker := service.Checker.FirstPartyCaveatChecker.(*checkers.Checker) + for _, info := range checker.Info() { + if info.Name == "ipaddr" && + info.Prefix == "" && + info.Namespace == "std" { + checkerFound = true + } + } + if !checkerFound { + t.Fatalf("Checker '%s' not found in service.", "ipaddr") + } +} + +// TestValidateMacaroon tests the validation of a macaroon that is in an +// incoming context. +func TestValidateMacaroon(t *testing.T) { + // First, initialize the service and unlock it + tempDir := setupTestRootKeyStorage(t) + defer os.RemoveAll(tempDir) + service, err := macaroons.NewService(tempDir, macaroons.IPLockChecker) + defer service.Close() + if err != nil { + t.Fatalf("Error creating new service: %v", err) + } + err = service.CreateUnlock(&defaultPw) + if err != nil { + t.Fatalf("Error unlocking root key storage: %v", err) + } + + // Then, create a new macaroon that we can serialize. + macaroon, err := service.Oven.NewMacaroon(nil, bakery.LatestVersion, + nil, testOperation) + if err != nil { + t.Fatalf("Error creating macaroon from service: %v", err) + } + macaroonBinary, err := macaroon.M().MarshalBinary() + if err != nil { + t.Fatalf("Error serializing macaroon: %v", err) + } + + // Because the macaroons are always passed in a context, we need to + // mock one that has just the serialized macaroon as a value. + md := metadata.New(map[string]string{"macaroon": hex.EncodeToString(macaroonBinary)}) + mockContext := metadata.NewIncomingContext(context.Background(), md) + + // Finally, validate the macaroon against the required permissions. + err = service.ValidateMacaroon(mockContext, []bakery.Op{testOperation}) + if err != nil { + t.Fatalf("Error validating the macaroon: %v", err) + } +}