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