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 }