macaroons: add custom caveat contraint and checker

The way the macaroon bakery library lnd uses works is that one has to
register a Checker method for each caveat name that should be supported.
Since we want to allow fully customizable custom caveats we add another
layer of naming to the caveat by splitting the condition of the "outer"
caveat into two pieces, the custom caveat name and the actual custom
caveat condition.
The custom Checker function only checks that the format is correct and
that there is a handler available for a custom condition. It does not
check the condition itself, however. If the passed in acceptor signals
acceptance of a custom caveat then the bakery accepts the macaroon as a
whole (given its signature, standard caveats and permissions are all
correct) and assumes that another component down the line will make sure
the actual custom condition of a caveat is valid.
This commit is contained in:
Oliver Gugger 2021-08-12 16:07:16 +02:00
parent 96ea4bf05e
commit 538175f487
No known key found for this signature in database
GPG Key ID: 8E4256593F177720
2 changed files with 157 additions and 2 deletions

View File

@ -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 <custom-caveat-name> <custom-caveat-condition>"
// 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:
//
// <condition-name> <condition-value>
//
// 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 <condition-value> into
//
// "<custom-condition-name> <custom-condition-value>"
//
// The full caveat string entry of a macaroon that uses this generic
// mechanism would therefore look like this:
//
// "lnd-custom <custom-condition-name> <custom-condition-value>"
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
}

View File

@ -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"))
}