diff --git a/macaroons/constraints.go b/macaroons/constraints.go index 2ebea4252..58b369b2d 100644 --- a/macaroons/constraints.go +++ b/macaroons/constraints.go @@ -1,9 +1,11 @@ package macaroons import ( + "bytes" "context" "fmt" "net" + "strings" "time" "google.golang.org/grpc/peer" @@ -12,6 +14,29 @@ import ( macaroon "gopkg.in/macaroon.v2" ) +const ( + // CondLndCustom is the first party caveat condition name that is used + // for all custom caveats in lnd. Every custom caveat entry will be + // encoded as the string + // "lnd-custom " + // in the serialized macaroon. We choose a single space as the delimiter + // between the because that is also used by the macaroon bakery library. + CondLndCustom = "lnd-custom" +) + +// CustomCaveatAcceptor is an interface that contains a single method for +// checking whether a macaroon with the given custom caveat name should be +// accepted or not. +type CustomCaveatAcceptor interface { + // CustomCaveatSupported returns nil if a macaroon with the given custom + // caveat name can be validated by any component in lnd (for example an + // RPC middleware). If no component is registered to handle the given + // custom caveat then an error must be returned. This method only checks + // the availability of a validating component, not the validity of the + // macaroon itself. + CustomCaveatSupported(customCaveatName string) error +} + // Constraint type adds a layer of indirection over macaroon caveats. type Constraint func(*macaroon.Macaroon) error @@ -22,7 +47,9 @@ type Checker func() (string, checkers.Func) // AddConstraints returns new derived macaroon by applying every passed // constraint and tightening its restrictions. -func AddConstraints(mac *macaroon.Macaroon, cs ...Constraint) (*macaroon.Macaroon, error) { +func AddConstraints(mac *macaroon.Macaroon, + cs ...Constraint) (*macaroon.Macaroon, error) { + newMac := mac.Clone() for _, constraint := range cs { if err := constraint(newMac); err != nil { @@ -55,7 +82,8 @@ func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error { if ipAddr != "" { macaroonIPAddr := net.ParseIP(ipAddr) if macaroonIPAddr == nil { - return fmt.Errorf("incorrect macaroon IP-lock address") + return fmt.Errorf("incorrect macaroon IP-" + + "lock address") } caveat := checkers.Condition("ipaddr", macaroonIPAddr.String()) @@ -87,3 +115,97 @@ func IPLockChecker() (string, checkers.Func) { return nil } } + +// CustomConstraint returns a function that adds a custom caveat condition to +// a macaroon. +func CustomConstraint(name, condition string) func(*macaroon.Macaroon) error { + return func(mac *macaroon.Macaroon) error { + // We rely on a name being set for the interception, so don't + // allow creating a caveat without a name in the first place. + if name == "" { + return fmt.Errorf("name cannot be empty") + } + + // The inner (custom) condition is optional. + outerCondition := fmt.Sprintf("%s %s", name, condition) + if condition == "" { + outerCondition = name + } + + caveat := checkers.Condition(CondLndCustom, outerCondition) + return mac.AddFirstPartyCaveat([]byte(caveat)) + } +} + +// CustomChecker returns a Checker function that is used by the macaroon bakery +// library to check whether a custom caveat is supported by lnd in general or +// not. Support in this context means: An additional gRPC interceptor was set up +// that validates the content (=condition) of the custom caveat. If such an +// interceptor is in place then the acceptor should return a nil error. If no +// interceptor exists for the custom caveat in the macaroon of a request context +// then a non-nil error should be returned and the macaroon is rejected as a +// whole. +func CustomChecker(acceptor CustomCaveatAcceptor) Checker { + // We return the general name of all lnd custom macaroons and a function + // that splits the outer condition to extract the name of the custom + // condition and the condition itself. In the bakery library that's used + // here, a caveat always has the following form: + // + // + // + // Because a checker function needs to be bound to the condition name we + // have to choose a static name for the first part ("lnd-custom", see + // CondLndCustom. Otherwise we'd need to register a new Checker function + // for each custom caveat that's registered. To allow for a generic + // custom caveat handling, we just add another layer and expand the + // initial into + // + // " " + // + // The full caveat string entry of a macaroon that uses this generic + // mechanism would therefore look like this: + // + // "lnd-custom " + checker := func(_ context.Context, _, outerCondition string) error { + if outerCondition != strings.TrimSpace(outerCondition) { + return fmt.Errorf("unexpected white space found in " + + "caveat condition") + } + if outerCondition == "" { + return fmt.Errorf("expected custom caveat, got empty " + + "string") + } + + // The condition part of the original caveat is now name and + // condition of the custom caveat (we add a layer of conditions + // to allow one custom checker to work for all custom lnd + // conditions that implement arbitrary business logic). + parts := strings.Split(outerCondition, " ") + customCaveatName := parts[0] + + return acceptor.CustomCaveatSupported(customCaveatName) + } + + return func() (string, checkers.Func) { + return CondLndCustom, checker + } +} + +// HasCustomCaveat tests if the given macaroon has a custom caveat with the +// given custom caveat name. +func HasCustomCaveat(mac *macaroon.Macaroon, customCaveatName string) bool { + if mac == nil { + return false + } + + caveatPrefix := []byte(fmt.Sprintf( + "%s %s", CondLndCustom, customCaveatName, + )) + for _, caveat := range mac.Caveats() { + if bytes.HasPrefix(caveat.Id, caveatPrefix) { + return true + } + } + + return false +} diff --git a/macaroons/constraints_test.go b/macaroons/constraints_test.go index 44f78fff4..659b5d724 100644 --- a/macaroons/constraints_test.go +++ b/macaroons/constraints_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/lightningnetwork/lnd/macaroons" macaroon "gopkg.in/macaroon.v2" ) @@ -112,3 +114,34 @@ func TestIPLockBadIP(t *testing.T) { t.Fatalf("IPLockConstraint with bad IP should fail.") } } + +// TestCustomConstraint tests that a custom constraint with a name and value can +// be added to a macaroon. +func TestCustomConstraint(t *testing.T) { + // Test a custom caveat with a value first. + constraintFunc := macaroons.CustomConstraint("unit-test", "test-value") + testMacaroon := createDummyMacaroon(t) + require.NoError(t, constraintFunc(testMacaroon)) + + require.Equal( + t, []byte("lnd-custom unit-test test-value"), + testMacaroon.Caveats()[0].Id, + ) + require.True(t, macaroons.HasCustomCaveat(testMacaroon, "unit-test")) + require.False(t, macaroons.HasCustomCaveat(testMacaroon, "test-value")) + require.False(t, macaroons.HasCustomCaveat(testMacaroon, "something")) + require.False(t, macaroons.HasCustomCaveat(nil, "foo")) + + // Custom caveats don't necessarily need a value, just the name is fine + // too to create a tagged macaroon. + constraintFunc = macaroons.CustomConstraint("unit-test", "") + testMacaroon = createDummyMacaroon(t) + require.NoError(t, constraintFunc(testMacaroon)) + + require.Equal( + t, []byte("lnd-custom unit-test"), testMacaroon.Caveats()[0].Id, + ) + require.True(t, macaroons.HasCustomCaveat(testMacaroon, "unit-test")) + require.False(t, macaroons.HasCustomCaveat(testMacaroon, "test-value")) + require.False(t, macaroons.HasCustomCaveat(testMacaroon, "something")) +}