github.com/decred/dcrlnd@v0.7.6/macaroons/constraints.go (about)

     1  package macaroons
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"net"
     8  	"strings"
     9  	"time"
    10  
    11  	"google.golang.org/grpc/peer"
    12  
    13  	"gopkg.in/macaroon-bakery.v2/bakery/checkers"
    14  	macaroon "gopkg.in/macaroon.v2"
    15  )
    16  
    17  const (
    18  	// CondLndCustom is the first party caveat condition name that is used
    19  	// for all custom caveats in lnd. Every custom caveat entry will be
    20  	// encoded as the string
    21  	// "lnd-custom <custom-caveat-name> <custom-caveat-condition>"
    22  	// in the serialized macaroon. We choose a single space as the delimiter
    23  	// between the because that is also used by the macaroon bakery library.
    24  	CondLndCustom = "lnd-custom"
    25  )
    26  
    27  // CustomCaveatAcceptor is an interface that contains a single method for
    28  // checking whether a macaroon with the given custom caveat name should be
    29  // accepted or not.
    30  type CustomCaveatAcceptor interface {
    31  	// CustomCaveatSupported returns nil if a macaroon with the given custom
    32  	// caveat name can be validated by any component in lnd (for example an
    33  	// RPC middleware). If no component is registered to handle the given
    34  	// custom caveat then an error must be returned. This method only checks
    35  	// the availability of a validating component, not the validity of the
    36  	// macaroon itself.
    37  	CustomCaveatSupported(customCaveatName string) error
    38  }
    39  
    40  // Constraint type adds a layer of indirection over macaroon caveats.
    41  type Constraint func(*macaroon.Macaroon) error
    42  
    43  // Checker type adds a layer of indirection over macaroon checkers. A Checker
    44  // returns the name of the checker and the checker function; these are used to
    45  // register the function with the bakery service's compound checker.
    46  type Checker func() (string, checkers.Func)
    47  
    48  // AddConstraints returns new derived macaroon by applying every passed
    49  // constraint and tightening its restrictions.
    50  func AddConstraints(mac *macaroon.Macaroon,
    51  	cs ...Constraint) (*macaroon.Macaroon, error) {
    52  
    53  	// The macaroon library's Clone() method has a subtle bug that doesn't
    54  	// correctly clone all caveats. We need to use our own, safe clone
    55  	// function instead.
    56  	newMac, err := SafeCopyMacaroon(mac)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	for _, constraint := range cs {
    62  		if err := constraint(newMac); err != nil {
    63  			return nil, err
    64  		}
    65  	}
    66  	return newMac, nil
    67  }
    68  
    69  // Each *Constraint function is a functional option, which takes a pointer
    70  // to the macaroon and adds another restriction to it. For each *Constraint,
    71  // the corresponding *Checker is provided if not provided by default.
    72  
    73  // TimeoutConstraint restricts the lifetime of the macaroon
    74  // to the amount of seconds given.
    75  func TimeoutConstraint(seconds int64) func(*macaroon.Macaroon) error {
    76  	return func(mac *macaroon.Macaroon) error {
    77  		macaroonTimeout := time.Duration(seconds)
    78  		requestTimeout := time.Now().Add(time.Second * macaroonTimeout)
    79  		caveat := checkers.TimeBeforeCaveat(requestTimeout)
    80  		return mac.AddFirstPartyCaveat([]byte(caveat.Condition))
    81  	}
    82  }
    83  
    84  // IPLockConstraint locks macaroon to a specific IP address.
    85  // If address is an empty string, this constraint does nothing to
    86  // accommodate default value's desired behavior.
    87  func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error {
    88  	return func(mac *macaroon.Macaroon) error {
    89  		if ipAddr != "" {
    90  			macaroonIPAddr := net.ParseIP(ipAddr)
    91  			if macaroonIPAddr == nil {
    92  				return fmt.Errorf("incorrect macaroon IP-" +
    93  					"lock address")
    94  			}
    95  			caveat := checkers.Condition("ipaddr",
    96  				macaroonIPAddr.String())
    97  			return mac.AddFirstPartyCaveat([]byte(caveat))
    98  		}
    99  		return nil
   100  	}
   101  }
   102  
   103  // IPLockChecker accepts client IP from the validation context and compares it
   104  // with IP locked in the macaroon. It is of the `Checker` type.
   105  func IPLockChecker() (string, checkers.Func) {
   106  	return "ipaddr", func(ctx context.Context, cond, arg string) error {
   107  		// Get peer info and extract IP address from it for macaroon
   108  		// check.
   109  		pr, ok := peer.FromContext(ctx)
   110  		if !ok {
   111  			return fmt.Errorf("unable to get peer info from context")
   112  		}
   113  		peerAddr, _, err := net.SplitHostPort(pr.Addr.String())
   114  		if err != nil {
   115  			return fmt.Errorf("unable to parse peer address")
   116  		}
   117  
   118  		if !net.ParseIP(arg).Equal(net.ParseIP(peerAddr)) {
   119  			msg := "macaroon locked to different IP address"
   120  			return fmt.Errorf(msg)
   121  		}
   122  		return nil
   123  	}
   124  }
   125  
   126  // CustomConstraint returns a function that adds a custom caveat condition to
   127  // a macaroon.
   128  func CustomConstraint(name, condition string) func(*macaroon.Macaroon) error {
   129  	return func(mac *macaroon.Macaroon) error {
   130  		// We rely on a name being set for the interception, so don't
   131  		// allow creating a caveat without a name in the first place.
   132  		if name == "" {
   133  			return fmt.Errorf("name cannot be empty")
   134  		}
   135  
   136  		// The inner (custom) condition is optional.
   137  		outerCondition := fmt.Sprintf("%s %s", name, condition)
   138  		if condition == "" {
   139  			outerCondition = name
   140  		}
   141  
   142  		caveat := checkers.Condition(CondLndCustom, outerCondition)
   143  		return mac.AddFirstPartyCaveat([]byte(caveat))
   144  	}
   145  }
   146  
   147  // CustomChecker returns a Checker function that is used by the macaroon bakery
   148  // library to check whether a custom caveat is supported by lnd in general or
   149  // not. Support in this context means: An additional gRPC interceptor was set up
   150  // that validates the content (=condition) of the custom caveat. If such an
   151  // interceptor is in place then the acceptor should return a nil error. If no
   152  // interceptor exists for the custom caveat in the macaroon of a request context
   153  // then a non-nil error should be returned and the macaroon is rejected as a
   154  // whole.
   155  func CustomChecker(acceptor CustomCaveatAcceptor) Checker {
   156  	// We return the general name of all lnd custom macaroons and a function
   157  	// that splits the outer condition to extract the name of the custom
   158  	// condition and the condition itself. In the bakery library that's used
   159  	// here, a caveat always has the following form:
   160  	//
   161  	// <condition-name> <condition-value>
   162  	//
   163  	// Because a checker function needs to be bound to the condition name we
   164  	// have to choose a static name for the first part ("lnd-custom", see
   165  	// CondLndCustom. Otherwise we'd need to register a new Checker function
   166  	// for each custom caveat that's registered. To allow for a generic
   167  	// custom caveat handling, we just add another layer and expand the
   168  	// initial <condition-value> into
   169  	//
   170  	// "<custom-condition-name> <custom-condition-value>"
   171  	//
   172  	// The full caveat string entry of a macaroon that uses this generic
   173  	// mechanism would therefore look like this:
   174  	//
   175  	// "lnd-custom <custom-condition-name> <custom-condition-value>"
   176  	checker := func(_ context.Context, _, outerCondition string) error {
   177  		if outerCondition != strings.TrimSpace(outerCondition) {
   178  			return fmt.Errorf("unexpected white space found in " +
   179  				"caveat condition")
   180  		}
   181  		if outerCondition == "" {
   182  			return fmt.Errorf("expected custom caveat, got empty " +
   183  				"string")
   184  		}
   185  
   186  		// The condition part of the original caveat is now name and
   187  		// condition of the custom caveat (we add a layer of conditions
   188  		// to allow one custom checker to work for all custom lnd
   189  		// conditions that implement arbitrary business logic).
   190  		parts := strings.Split(outerCondition, " ")
   191  		customCaveatName := parts[0]
   192  
   193  		return acceptor.CustomCaveatSupported(customCaveatName)
   194  	}
   195  
   196  	return func() (string, checkers.Func) {
   197  		return CondLndCustom, checker
   198  	}
   199  }
   200  
   201  // HasCustomCaveat tests if the given macaroon has a custom caveat with the
   202  // given custom caveat name.
   203  func HasCustomCaveat(mac *macaroon.Macaroon, customCaveatName string) bool {
   204  	if mac == nil {
   205  		return false
   206  	}
   207  
   208  	caveatPrefix := []byte(fmt.Sprintf(
   209  		"%s %s", CondLndCustom, customCaveatName,
   210  	))
   211  	for _, caveat := range mac.Caveats() {
   212  		if bytes.HasPrefix(caveat.Id, caveatPrefix) {
   213  			return true
   214  		}
   215  	}
   216  
   217  	return false
   218  }
   219  
   220  // GetCustomCaveatCondition returns the custom caveat condition for the given
   221  // custom caveat name from the given macaroon.
   222  func GetCustomCaveatCondition(mac *macaroon.Macaroon,
   223  	customCaveatName string) string {
   224  
   225  	if mac == nil {
   226  		return ""
   227  	}
   228  
   229  	caveatPrefix := []byte(fmt.Sprintf(
   230  		"%s %s ", CondLndCustom, customCaveatName,
   231  	))
   232  	for _, caveat := range mac.Caveats() {
   233  
   234  		// The caveat id has a format of
   235  		// "lnd-custom [custom-caveat-name] [custom-caveat-condition]"
   236  		// and we only want the condition part. If we match the prefix
   237  		// part we return the condition that comes after the prefix.
   238  		if bytes.HasPrefix(caveat.Id, caveatPrefix) {
   239  			caveatSplit := strings.SplitN(
   240  				string(caveat.Id),
   241  				string(caveatPrefix),
   242  				2,
   243  			)
   244  			if len(caveatSplit) == 2 {
   245  				return caveatSplit[1]
   246  			}
   247  		}
   248  	}
   249  
   250  	// We didn't find a condition for the given custom caveat name.
   251  	return ""
   252  }