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 }