mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-24 06:47:44 +01:00
In this commit, we modify the `macaroons.NewService` consturctor to accept the main interface rather than the raw DB. This allows us to use other backends other than bolt or the kvdb interface to store the macaroon root keys. We also create a new ExtendedRootKeyStore interface that implements some of the more advanced features we use such as macaroon encryption and password rotation.
345 lines
11 KiB
Go
345 lines
11 KiB
Go
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"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|