-
Notifications
You must be signed in to change notification settings - Fork 2k
/
service.go
384 lines (317 loc) · 12.2 KB
/
service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
package macaroons
import (
"context"
"encoding/hex"
"fmt"
"google.golang.org/grpc/metadata"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
macaroon "gopkg.in/macaroon.v2"
)
var (
// ErrMissingRootKeyID specifies the root key ID is missing.
ErrMissingRootKeyID = fmt.Errorf("missing root key ID")
// ErrDeletionForbidden is used when attempting to delete the
// DefaultRootKeyID or the encryptedKeyID.
ErrDeletionForbidden = fmt.Errorf("the specified ID cannot be deleted")
// PermissionEntityCustomURI is a special entity name for a permission
// that does not describe an entity:action pair but instead specifies a
// specific URI that needs to be granted access to. This can be used for
// more fine-grained permissions where a macaroon only grants access to
// certain methods instead of a whole list of methods that define the
// same entity:action pairs. For example: uri:/lnrpc.Lightning/GetInfo
// only gives access to the GetInfo call.
PermissionEntityCustomURI = "uri"
// ErrUnknownVersion is returned when a macaroon is of an unknown
// is presented.
ErrUnknownVersion = fmt.Errorf("unknown macaroon version")
// ErrInvalidID is returned when a macaroon ID is invalid.
ErrInvalidID = fmt.Errorf("invalid ID")
)
// MacaroonValidator is an interface type that can check if macaroons are valid.
type MacaroonValidator interface {
// ValidateMacaroon extracts the macaroon from the context's gRPC
// metadata, checks its signature, makes sure all specified permissions
// for the called method are contained within and finally ensures all
// caveat conditions are met. A non-nil error is returned if any of the
// checks fail.
ValidateMacaroon(ctx context.Context,
requiredPermissions []bakery.Op, fullMethod string) error
}
// ExtendedRootKeyStore is an interface augments the existing
// macaroons.RootKeyStorage interface by adding a number of additional utility
// methods such as encrypting and decrypting the root key given a password.
type ExtendedRootKeyStore interface {
bakery.RootKeyStore
// Close closes the RKS and zeros out any in-memory encryption keys.
Close() error
// CreateUnlock calls the underlying root key store's CreateUnlock and
// returns the result.
CreateUnlock(password *[]byte) error
// ListMacaroonIDs returns all the root key ID values except the value
// of encryptedKeyID.
ListMacaroonIDs(ctxt context.Context) ([][]byte, error)
// DeleteMacaroonID removes one specific root key ID. If the root key
// ID is found and deleted, it will be returned.
DeleteMacaroonID(ctxt context.Context, rootKeyID []byte) ([]byte, error)
// ChangePassword calls the underlying root key store's ChangePassword
// and returns the result.
ChangePassword(oldPw, newPw []byte) error
// GenerateNewRootKey calls the underlying root key store's
// GenerateNewRootKey and returns the result.
GenerateNewRootKey() error
// SetRootKey calls the underlying root key store's SetRootKey and
// returns the result.
SetRootKey(rootKey []byte) error
}
// Service encapsulates bakery.Bakery and adds a Close() method that zeroes the
// root key service encryption keys, as well as utility methods to validate a
// macaroon against the bakery and gRPC middleware for macaroon-based auth.
type Service struct {
bakery.Bakery
rks bakery.RootKeyStore
// ExternalValidators is a map between an absolute gRPC URIs and the
// corresponding external macaroon validator to be used for that URI.
// If no external validator for an URI is specified, the service will
// use the internal validator.
ExternalValidators map[string]MacaroonValidator
// StatelessInit denotes if the service was initialized in the stateless
// mode where no macaroon files should be created on disk.
StatelessInit bool
}
// NewService returns a service backed by the macaroon DB backend. The `checks`
// argument can be any of the `Checker` type functions defined in this package,
// or a custom checker if desired. This constructor prevents double-registration
// of checkers to prevent panics, so listing the same checker more than once is
// not harmful. Default checkers, such as those for `allow`, `time-before`,
// `declared`, and `error` caveats are registered automatically and don't need
// to be added.
func NewService(keyStore bakery.RootKeyStore, location string,
statelessInit bool, checks ...Checker) (*Service, error) {
macaroonParams := bakery.BakeryParams{
Location: location,
RootKeyStore: keyStore,
// No third-party caveat support for now.
// TODO(aakselrod): Add third-party caveat support.
Locator: nil,
Key: nil,
}
svc := bakery.New(macaroonParams)
// Register all custom caveat checkers with the bakery's checker.
// TODO(aakselrod): Add more checks as required.
checker := svc.Checker.FirstPartyCaveatChecker.(*checkers.Checker)
for _, check := range checks {
cond, fun := check()
if !isRegistered(checker, cond) {
checker.Register(cond, "std", fun)
}
}
return &Service{
Bakery: *svc,
rks: keyStore,
ExternalValidators: make(map[string]MacaroonValidator),
StatelessInit: statelessInit,
}, nil
}
// isRegistered checks to see if the required checker has already been
// registered in order to avoid a panic caused by double registration.
func isRegistered(c *checkers.Checker, name string) bool {
if c == nil {
return false
}
for _, info := range c.Info() {
if info.Name == name &&
info.Prefix == "" &&
info.Namespace == "std" {
return true
}
}
return false
}
// RegisterExternalValidator registers a custom, external macaroon validator for
// the specified absolute gRPC URI. That validator is then fully responsible to
// make sure any macaroon passed for a request to that URI is valid and
// satisfies all conditions.
func (svc *Service) RegisterExternalValidator(fullMethod string,
validator MacaroonValidator) error {
if validator == nil {
return fmt.Errorf("validator cannot be nil")
}
_, ok := svc.ExternalValidators[fullMethod]
if ok {
return fmt.Errorf("external validator for method %s already "+
"registered", fullMethod)
}
svc.ExternalValidators[fullMethod] = validator
return nil
}
// ValidateMacaroon validates the capabilities of a given request given a
// bakery service, context, and uri. Within the passed context.Context, we
// expect a macaroon to be encoded as request metadata using the key
// "macaroon".
func (svc *Service) ValidateMacaroon(ctx context.Context,
requiredPermissions []bakery.Op, fullMethod string) error {
// Get macaroon bytes from context and unmarshal into macaroon.
macHex, err := RawMacaroonFromContext(ctx)
if err != nil {
return err
}
// With the macaroon obtained, we'll now decode the hex-string encoding.
macBytes, err := hex.DecodeString(macHex)
if err != nil {
return err
}
return svc.CheckMacAuth(
ctx, macBytes, requiredPermissions, fullMethod,
)
}
// CheckMacAuth checks that the macaroon is not disobeying any caveats and is
// authorized to perform the operation the user wants to perform.
func (svc *Service) CheckMacAuth(ctx context.Context, macBytes []byte,
requiredPermissions []bakery.Op, fullMethod string) error {
// With the macaroon obtained, we'll now unmarshal it from binary into
// its concrete struct representation.
mac := &macaroon.Macaroon{}
err := mac.UnmarshalBinary(macBytes)
if err != nil {
return err
}
// Ensure that the macaroon is using the exact same version as we
// expect. In the future, we can relax this check to phase in new
// versions.
if mac.Version() != macaroon.V2 {
return fmt.Errorf("%w: %v", ErrUnknownVersion,
mac.Version())
}
// Run a similar version check on the ID used for the macaroon as well.
const minIDLength = 1
if len(mac.Id()) < minIDLength {
return ErrInvalidID
}
if mac.Id()[0] != byte(bakery.Version3) {
return ErrInvalidID
}
// Check the method being called against the permitted operation, the
// expiration time and IP address and return the result.
authChecker := svc.Checker.Auth(macaroon.Slice{mac})
_, err = authChecker.Allow(ctx, requiredPermissions...)
// If the macaroon contains broad permissions and checks out, we're
// done.
if err == nil {
return nil
}
// To also allow the special permission of "uri:<FullMethod>" to be a
// valid permission, we need to check it manually in case there is no
// broader scope permission defined.
_, err = authChecker.Allow(ctx, bakery.Op{
Entity: PermissionEntityCustomURI,
Action: fullMethod,
})
return err
}
// Close closes the database that underlies the RootKeyStore and zeroes the
// encryption keys.
func (svc *Service) Close() error {
if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok {
return boltRKS.Close()
}
return nil
}
// CreateUnlock calls the underlying root key store's CreateUnlock and returns
// the result.
func (svc *Service) CreateUnlock(password *[]byte) error {
if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok {
return boltRKS.CreateUnlock(password)
}
return nil
}
// NewMacaroon wraps around the function Oven.NewMacaroon with the defaults,
// - version is always bakery.LatestVersion;
// - caveats is always nil.
//
// In addition, it takes a rootKeyID parameter, and puts it into the context.
// The context is passed through Oven.NewMacaroon(), in which calls the function
// RootKey(), that reads the context for rootKeyID.
func (svc *Service) NewMacaroon(
ctx context.Context, rootKeyID []byte,
ops ...bakery.Op) (*bakery.Macaroon, error) {
// Check rootKeyID is not called with nil or empty bytes. We want the
// caller to be aware the value of root key ID used, so we won't replace
// it with the DefaultRootKeyID if not specified.
if len(rootKeyID) == 0 {
return nil, ErrMissingRootKeyID
}
// Pass the root key ID to context.
ctx = ContextWithRootKeyID(ctx, rootKeyID)
return svc.Oven.NewMacaroon(ctx, bakery.LatestVersion, nil, ops...)
}
// ListMacaroonIDs returns all the root key ID values except the value of
// encryptedKeyID.
func (svc *Service) ListMacaroonIDs(ctxt context.Context) ([][]byte, error) {
if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok {
return boltRKS.ListMacaroonIDs(ctxt)
}
return nil, nil
}
// DeleteMacaroonID removes one specific root key ID. If the root key ID is
// found and deleted, it will be returned.
func (svc *Service) DeleteMacaroonID(ctxt context.Context,
rootKeyID []byte) ([]byte, error) {
if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok {
return boltRKS.DeleteMacaroonID(ctxt, rootKeyID)
}
return nil, nil
}
// GenerateNewRootKey calls the underlying root key store's GenerateNewRootKey
// and returns the result.
func (svc *Service) GenerateNewRootKey() error {
if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok {
return boltRKS.GenerateNewRootKey()
}
return nil
}
// SetRootKey calls the underlying root key store's SetRootKey and returns the
// result.
func (svc *Service) SetRootKey(rootKey []byte) error {
if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok {
return boltRKS.SetRootKey(rootKey)
}
return nil
}
// ChangePassword calls the underlying root key store's ChangePassword and
// returns the result.
func (svc *Service) ChangePassword(oldPw, newPw []byte) error {
if boltRKS, ok := svc.rks.(ExtendedRootKeyStore); ok {
return boltRKS.ChangePassword(oldPw, newPw)
}
return nil
}
// RawMacaroonFromContext is a helper function that extracts a raw macaroon
// from the given incoming gRPC request context.
func RawMacaroonFromContext(ctx context.Context) (string, error) {
// Get macaroon bytes from context and unmarshal into macaroon.
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", fmt.Errorf("unable to get metadata from context")
}
if len(md["macaroon"]) != 1 {
return "", fmt.Errorf("expected 1 macaroon, got %d",
len(md["macaroon"]))
}
return md["macaroon"][0], nil
}
// SafeCopyMacaroon creates a copy of a macaroon that is safe to be used and
// modified. This is necessary because the macaroon library's own Clone() method
// is unsafe for certain edge cases, resulting in both the cloned and the
// original macaroons to be modified.
func SafeCopyMacaroon(mac *macaroon.Macaroon) (*macaroon.Macaroon, error) {
macBytes, err := mac.MarshalBinary()
if err != nil {
return nil, err
}
newMac := &macaroon.Macaroon{}
if err := newMac.UnmarshalBinary(macBytes); err != nil {
return nil, err
}
return newMac, nil
}