From 662731e719299e5a189225a98efdb9917cf9b4d2 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 17 Aug 2017 19:50:15 -0600 Subject: [PATCH] macaroons: add macaroons package and update glide --- glide.lock | 14 ++++- glide.yaml | 5 ++ macaroons/auth.go | 76 +++++++++++++++++++++++ macaroons/service.go | 44 +++++++++++++ macaroons/store.go | 145 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 macaroons/auth.go create mode 100644 macaroons/service.go create mode 100644 macaroons/store.go diff --git a/glide.lock b/glide.lock index 1cb7bd594..009cfda75 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: d40636a5875b6dedd06b34514ea3430717c02153ecd16f32aa5bcedeedd6a833 -updated: 2017-08-02T21:19:34.129016558-07:00 +hash: 6569f51a7172f16b26c374cdcf3cae0abea234207ac99bfa6dc4712406bfc4d2 +updated: 2017-08-09T14:51:02.638659953-06:00 imports: - name: github.com/aead/chacha20 version: d31a916ded42d1640b9d89a26f8abd53cc96790c @@ -180,4 +180,14 @@ imports: - stats - tap - transport +- name: gopkg.in/macaroon.v1 + version: d8fd13e6951f2ce46f0964a58149cf2f103cac9a +- name: gopkg.in/macaroon-bakery.v1 + version: 8e14f8b0f5e286ad100ca8d6ce5bde0f0dfb8b21 +- name: github.com/juju/loggo + version: 8232ab8918d91c72af1a9fb94d3edbe31d88b790 +- name: github.com/rogpeppe/fastuuid + version: 6724a57986aff9bff1a1770e9347036def7c89f6 +- name: gopkg.in/errgo.v1 + version: 442357a80af5c6bf9b6d51ae791a39c3421004f3 testImports: [] diff --git a/glide.yaml b/glide.yaml index b4d6b52c0..d9a0f593b 100644 --- a/glide.yaml +++ b/glide.yaml @@ -71,3 +71,8 @@ import: - chaincfg - package: github.com/lightninglabs/neutrino version: 807d3e267a2863654e99dda39ac4daac78943f7e +- package: gopkg.in/macaroon.v1 +- package: gopkg.in/macaroon-bakery.v1 +- package: github.com/juju/loggo +- package: github.com/rogpeppe/fastuuid +- package: gopkg.in/errgo.v1 diff --git a/macaroons/auth.go b/macaroons/auth.go new file mode 100644 index 000000000..452b2a3bc --- /dev/null +++ b/macaroons/auth.go @@ -0,0 +1,76 @@ +package macaroons + +import ( + "encoding/hex" + "fmt" + + "golang.org/x/net/context" + "google.golang.org/grpc/metadata" + + "gopkg.in/macaroon-bakery.v1/bakery" + "gopkg.in/macaroon-bakery.v1/bakery/checkers" + macaroon "gopkg.in/macaroon.v1" +) + +// MacaroonCredential wraps a macaroon to implement the +// credentials.PerRPCCredentials interface. +type MacaroonCredential struct { + *macaroon.Macaroon +} + +// RequireTransportSecurity implements the PerRPCCredentials interface. +func (m MacaroonCredential) RequireTransportSecurity() bool { + return true +} + +// GetRequestMetadata implements the PerRPCCredentials interface. +func (m MacaroonCredential) GetRequestMetadata(ctx context.Context, + uri ...string) (map[string]string, error) { + macBytes, err := m.MarshalBinary() + if err != nil { + return nil, err + } + md := make(map[string]string) + md["macaroon"] = hex.EncodeToString(macBytes) + return md, nil +} + +// NewMacaroonCredential returns a copy of the passed macaroon wrapped in a +// MacaroonCredential struct which implements PerRPCCredentials. +func NewMacaroonCredential(m *macaroon.Macaroon) MacaroonCredential { + ms := MacaroonCredential{} + ms.Macaroon = m.Clone() + return ms +} + +// ValidateMacaroon validates auth given a bakery service, context, and uri. +func ValidateMacaroon(ctx context.Context, method string, + svc *bakery.Service) error { + // Get macaroon bytes from context and unmarshal into macaroon. + // TODO(aakselrod): use FromIncomingContext after grpc update in glide. + md, ok := metadata.FromContext(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"])) + } + macBytes, err := hex.DecodeString(md["macaroon"][0]) + if err != nil { + return err + } + mac := &macaroon.Macaroon{} + err = mac.UnmarshalBinary(macBytes) + if err != nil { + return err + } + + // Check the method being called against the permitted operation and the + // expiration time and return the result. + // TODO(aakselrod): Add more checks as required. + return svc.Check(macaroon.Slice{mac}, checkers.New( + checkers.OperationChecker(method), + checkers.TimeBefore, + )) +} diff --git a/macaroons/service.go b/macaroons/service.go new file mode 100644 index 000000000..b0fe7d91c --- /dev/null +++ b/macaroons/service.go @@ -0,0 +1,44 @@ +package macaroons + +import ( + "path" + + "gopkg.in/macaroon-bakery.v1/bakery" + + "github.com/boltdb/bolt" +) + +var ( + // dbFileName is the filename within the data directory which contains + // the macaroon stores. + dbFilename = "macaroons.db" +) + +// NewService returns a service backed by the macaroon Bolt DB stored in the +// passed directory. +func NewService(dir string) (*bakery.Service, error) { + // Open the database. + macaroonDB, err := bolt.Open(path.Join(dir, dbFilename), 0600, + bolt.DefaultOptions) + if err != nil { + return nil, err + } + rootKeyStore, err := NewRootKeyStorage(macaroonDB) + if err != nil { + return nil, err + } + macaroonStore, err := NewStorage(macaroonDB) + if err != nil { + return nil, err + } + macaroonParams := bakery.NewServiceParams{ + Location: "lnd", + Store: macaroonStore, + RootKeyStore: rootKeyStore, + // No third-party caveat support for now. + // TODO(aakselrod): Add third-party caveat support. + Locator: nil, + Key: nil, + } + return bakery.NewService(macaroonParams) +} diff --git a/macaroons/store.go b/macaroons/store.go new file mode 100644 index 000000000..2baebd39c --- /dev/null +++ b/macaroons/store.go @@ -0,0 +1,145 @@ +package macaroons + +import ( + "crypto/rand" + "fmt" + "io" + + "github.com/boltdb/bolt" +) + +const ( + // RootKeyLen is the length of a root key. + RootKeyLen = 32 +) + +var ( + // rootKeyBucketName is the name of the root key store bucket. + rootKeyBucketName = []byte("macrootkeys") + + // defaultRootKeyID is the ID of the default root key. The first is + // just 0, to emulate the memory storage that comes with bakery. + // TODO(aakselrod): Add support for key rotation. + defaultRootKeyID = "0" + + // macaroonBucketName is the name of the macaroon store bucket. + macaroonBucketName = []byte("macaroons") +) + +// RootKeyStorage implements the bakery.RootKeyStorage interface. +type RootKeyStorage struct { + *bolt.DB +} + +// NewRootKeyStorage creates a RootKeyStorage instance. +// TODO(aakselrod): Add support for encryption of data with passphrase. +func NewRootKeyStorage(db *bolt.DB) (*RootKeyStorage, error) { + // If the store's bucket doesn't exist, create it. + err := db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(rootKeyBucketName) + return err + }) + if err != nil { + return nil, err + } + // Return the DB wrapped in a RootKeyStorage object. + return &RootKeyStorage{db}, nil +} + +// Get implements the Get method for the bakery.RootKeyStorage interface. +func (r *RootKeyStorage) Get(id string) ([]byte, error) { + var rootKey []byte + err := r.View(func(tx *bolt.Tx) error { + rootKey = tx.Bucket(rootKeyBucketName).Get([]byte(id)) + if len(rootKey) == 0 { + return fmt.Errorf("root key with id %s doesn't exist", + id) + } + return nil + }) + if err != nil { + return nil, err + } + return rootKey, nil +} + +// RootKey implements the RootKey method for the bakery.RootKeyStorage +// interface. +// TODO(aakselrod): Add support for key rotation. +func (r *RootKeyStorage) RootKey() ([]byte, string, error) { + var rootKey []byte + id := defaultRootKeyID + err := r.Update(func(tx *bolt.Tx) error { + ns := tx.Bucket(rootKeyBucketName) + rootKey = ns.Get([]byte(id)) + + // If there's no root key stored in the bucket yet, create one. + if len(rootKey) != 0 { + return nil + } + + // Create a RootKeyLen-byte root key. + rootKey = make([]byte, RootKeyLen) + if _, err := io.ReadFull(rand.Reader, rootKey[:]); err != nil { + return err + } + return ns.Put([]byte(id), rootKey) + }) + if err != nil { + return nil, "", err + } + return rootKey, id, nil +} + +// Storage implements the bakery.Storage interface. +type Storage struct { + *bolt.DB +} + +// NewStorage creates a Storage instance. +// TODO(aakselrod): Add support for encryption of data with passphrase. +func NewStorage(db *bolt.DB) (*Storage, error) { + // If the store's bucket doesn't exist, create it. + err := db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(macaroonBucketName) + return err + }) + if err != nil { + return nil, err + } + // Return the DB wrapped in a Storage object. + return &Storage{db}, nil +} + +// Put implements the Put method for the bakery.Storage interface. +func (s *Storage) Put(location string, item string) error { + return s.Update(func(tx *bolt.Tx) error { + return tx.Bucket(macaroonBucketName).Put([]byte(location), + []byte(item)) + }) +} + +// Get implements the Get method for the bakery.Storage interface. +func (s *Storage) Get(location string) (string, error) { + var item string + err := s.View(func(tx *bolt.Tx) error { + itemBytes := tx.Bucket(macaroonBucketName).Get([]byte(location)) + if len(itemBytes) == 0 { + return fmt.Errorf("couldn't get item for location %s", + location) + } + item = string(itemBytes) + return nil + }) + if err != nil { + return "", err + } + return item, nil +} + +// Del implements the Del method for the bakery.Storage interface. +func (s *Storage) Del(location string) error { + return s.Update(func(tx *bolt.Tx) error { + return tx.Bucket(macaroonBucketName).Delete([]byte(location)) + }) +}