macaroons: specify root key ID in bakery

This commit is contained in:
yyforyongyu 2020-07-24 00:26:59 +08:00
parent 37a29b4869
commit f362f7670b
No known key found for this signature in database
GPG Key ID: 9BCD95C4FF296868
6 changed files with 341 additions and 21 deletions

@ -95,8 +95,8 @@ command line.
Users can create their own macaroons with custom permissions if the provided
default macaroons (`admin`, `invoice` and `readonly`) are not sufficient.
For example, a macaroon that is only allowed to manage peers would be created
with the following command:
For example, a macaroon that is only allowed to manage peers with a default root
key `0` would be created with the following command:
`lncli bakemacaroon peers:read peers:write`
@ -114,3 +114,19 @@ removing all three default macaroons (`admin.macaroon`, `invoice.macaroon` and
`readonly.macaroon`, **NOT** the `macaroons.db`!) from their
`data/chain/<chain>/<network>/` directory inside the lnd data directory and
restarting lnd.
## Root key rotation
To manage the root keys used by macaroons, there are `listmacaroonids` and
`deletemacaroonid` available through gPRC and command line.
Users can view a list of all macaroon root key IDs that are in use using:
`lncli listmacaroonids`
And remove a specific macaroon root key ID using command:
`lncli deletemacaroonid root_key_id`
Be careful with the `deletemacaroonid` command as when a root key is deleted,
**all the macaroons created from it are invalidated**.

44
macaroons/context.go Normal file

@ -0,0 +1,44 @@
package macaroons
import (
"context"
"fmt"
)
var (
// RootKeyIDContextKey is the key to get rootKeyID from context.
RootKeyIDContextKey = contextKey{"rootkeyid"}
// ErrContextRootKeyID is used when the supplied context doesn't have
// a root key ID.
ErrContextRootKeyID = fmt.Errorf("failed to read root key ID " +
"from context")
)
// contextKey is the type we use to identify values in the context.
type contextKey struct {
Name string
}
// ContextWithRootKeyID passes the root key ID value to context.
func ContextWithRootKeyID(ctx context.Context,
value interface{}) context.Context {
return context.WithValue(ctx, RootKeyIDContextKey, value)
}
// RootKeyIDFromContext retrieves the root key ID from context using the key
// RootKeyIDContextKey.
func RootKeyIDFromContext(ctx context.Context) ([]byte, error) {
id, ok := ctx.Value(RootKeyIDContextKey).([]byte)
if !ok {
return nil, ErrContextRootKeyID
}
// Check that the id is not empty.
if len(id) == 0 {
return nil, ErrMissingRootKeyID
}
return id, nil
}

@ -20,6 +20,13 @@ var (
// DBFilename is the filename within the data directory which contains
// the macaroon stores.
DBFilename = "macaroons.db"
// 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")
)
// Service encapsulates bakery.Bakery and adds a Close() method that zeroes the
@ -197,3 +204,39 @@ func (svc *Service) Close() error {
func (svc *Service) CreateUnlock(password *[]byte) error {
return svc.rks.CreateUnlock(password)
}
// 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) {
return svc.rks.ListMacaroonIDs(ctxt)
}
// 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) {
return svc.rks.DeleteMacaroonID(ctxt, rootKeyID)
}

@ -10,6 +10,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb/kvdb"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
@ -72,8 +73,13 @@ func TestNewService(t *testing.T) {
}
// Third, check if the created service can bake macaroons.
macaroon, err := service.Oven.NewMacaroon(
context.TODO(), bakery.LatestVersion, nil, testOperation,
_, err = service.NewMacaroon(context.TODO(), nil, testOperation)
if err != macaroons.ErrMissingRootKeyID {
t.Fatalf("Received %v instead of ErrMissingRootKeyID", err)
}
macaroon, err := service.NewMacaroon(
context.TODO(), macaroons.DefaultRootKeyID, testOperation,
)
if err != nil {
t.Fatalf("Error creating macaroon from service: %v", err)
@ -117,8 +123,8 @@ func TestValidateMacaroon(t *testing.T) {
}
// Then, create a new macaroon that we can serialize.
macaroon, err := service.Oven.NewMacaroon(
context.TODO(), bakery.LatestVersion, nil, testOperation,
macaroon, err := service.NewMacaroon(
context.TODO(), macaroons.DefaultRootKeyID, testOperation,
)
if err != nil {
t.Fatalf("Error creating macaroon from service: %v", err)
@ -141,3 +147,84 @@ func TestValidateMacaroon(t *testing.T) {
t.Fatalf("Error validating the macaroon: %v", err)
}
}
// TestListMacaroonIDs checks that ListMacaroonIDs returns the expected result.
func TestListMacaroonIDs(t *testing.T) {
// First, initialize a dummy DB file with a store that the service
// can read from. Make sure the file is removed in the end.
tempDir := setupTestRootKeyStorage(t)
defer os.RemoveAll(tempDir)
// Second, create the new service instance, unlock it and pass in a
// checker that we expect it to add to the bakery.
service, err := macaroons.NewService(tempDir, macaroons.IPLockChecker)
require.NoError(t, err, "Error creating new service")
defer service.Close()
err = service.CreateUnlock(&defaultPw)
require.NoError(t, err, "Error unlocking root key storage")
// Third, make 3 new macaroons with different root key IDs.
expectedIDs := [][]byte{{1}, {2}, {3}}
for _, v := range expectedIDs {
_, err := service.NewMacaroon(context.TODO(), v, testOperation)
require.NoError(t, err, "Error creating macaroon from service")
}
// Finally, check that calling List return the expected values.
ids, _ := service.ListMacaroonIDs(context.TODO())
require.Equal(t, expectedIDs, ids, "root key IDs mismatch")
}
// TestDeleteMacaroonID removes the specific root key ID.
func TestDeleteMacaroonID(t *testing.T) {
ctxb := context.Background()
// First, initialize a dummy DB file with a store that the service
// can read from. Make sure the file is removed in the end.
tempDir := setupTestRootKeyStorage(t)
defer os.RemoveAll(tempDir)
// Second, create the new service instance, unlock it and pass in a
// checker that we expect it to add to the bakery.
service, err := macaroons.NewService(tempDir, macaroons.IPLockChecker)
require.NoError(t, err, "Error creating new service")
defer service.Close()
err = service.CreateUnlock(&defaultPw)
require.NoError(t, err, "Error unlocking root key storage")
// Third, checks that removing encryptedKeyID returns an error.
encryptedKeyID := []byte("enckey")
_, err = service.DeleteMacaroonID(ctxb, encryptedKeyID)
require.Equal(t, macaroons.ErrDeletionForbidden, err)
// Fourth, checks that removing DefaultKeyID returns an error.
_, err = service.DeleteMacaroonID(ctxb, macaroons.DefaultRootKeyID)
require.Equal(t, macaroons.ErrDeletionForbidden, err)
// Fifth, checks that removing empty key id returns an error.
_, err = service.DeleteMacaroonID(ctxb, []byte{})
require.Equal(t, macaroons.ErrMissingRootKeyID, err)
// Sixth, checks that removing a non-existed key id returns nil.
nonExistedID := []byte("test-non-existed")
deletedID, err := service.DeleteMacaroonID(ctxb, nonExistedID)
require.NoError(t, err, "deleting macaroon ID got an error")
require.Nil(t, deletedID, "deleting non-existed ID should return nil")
// Seventh, make 3 new macaroons with different root key IDs, and delete
// one.
expectedIDs := [][]byte{{1}, {2}, {3}}
for _, v := range expectedIDs {
_, err := service.NewMacaroon(ctxb, v, testOperation)
require.NoError(t, err, "Error creating macaroon from service")
}
deletedID, err = service.DeleteMacaroonID(ctxb, expectedIDs[0])
require.NoError(t, err, "deleting macaroon ID got an error")
// Finally, check that the ID is deleted.
require.Equal(t, expectedIDs[0], deletedID, "expected ID to be removed")
ids, _ := service.ListMacaroonIDs(ctxb)
require.Equal(t, expectedIDs[1:], ids, "root key IDs mismatch")
}

@ -1,6 +1,7 @@
package macaroons
import (
"bytes"
"context"
"crypto/rand"
"fmt"
@ -21,11 +22,9 @@ 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
// 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 = []byte("0")
DefaultRootKeyID = []byte("0")
// encryptedKeyID is the name of the database key that stores the
// encryption key, encrypted with a salted + hashed password. The
@ -42,6 +41,10 @@ var (
// ErrPasswordRequired specifies that a nil password has been passed.
ErrPasswordRequired = fmt.Errorf("a non-nil password is required")
// ErrKeyValueForbidden is used when the root key ID uses encryptedKeyID as
// its value.
ErrKeyValueForbidden = fmt.Errorf("root key ID value is not allowed")
)
// RootKeyStorage implements the bakery.RootKeyStorage interface.
@ -157,8 +160,7 @@ func (r *RootKeyStorage) Get(_ context.Context, id []byte) ([]byte, error) {
// RootKey implements the RootKey method for the bakery.RootKeyStorage
// interface.
// TODO(aakselrod): Add support for key rotation.
func (r *RootKeyStorage) RootKey(_ context.Context) ([]byte, []byte, error) {
func (r *RootKeyStorage) RootKey(ctx context.Context) ([]byte, []byte, error) {
r.encKeyMtx.RLock()
defer r.encKeyMtx.RUnlock()
@ -166,8 +168,19 @@ func (r *RootKeyStorage) RootKey(_ context.Context) ([]byte, []byte, error) {
return nil, nil, ErrStoreLocked
}
var rootKey []byte
id := defaultRootKeyID
err := kvdb.Update(r, func(tx kvdb.RwTx) error {
// Read the root key ID from the context. If no key is specified in the
// context, an error will be returned.
id, err := RootKeyIDFromContext(ctx)
if err != nil {
return nil, nil, err
}
if bytes.Equal(id, encryptedKeyID) {
return nil, nil, ErrKeyValueForbidden
}
err = kvdb.Update(r, func(tx kvdb.RwTx) error {
ns := tx.ReadWriteBucket(rootKeyBucketName)
dbKey := ns.Get(id)
@ -215,3 +228,88 @@ func (r *RootKeyStorage) Close() error {
}
return r.Backend.Close()
}
// ListMacaroonIDs returns all the root key ID values except the value of
// encryptedKeyID.
func (r *RootKeyStorage) ListMacaroonIDs(_ context.Context) ([][]byte, error) {
r.encKeyMtx.RLock()
defer r.encKeyMtx.RUnlock()
// Check it's unlocked.
if r.encKey == nil {
return nil, ErrStoreLocked
}
var rootKeySlice [][]byte
// Read all the items in the bucket and append the keys, which are the
// root key IDs we want.
err := kvdb.View(r, func(tx kvdb.RTx) error {
// appendRootKey is a function closure that appends root key ID
// to rootKeySlice.
appendRootKey := func(k, _ []byte) error {
// Only append when the key value is not encryptedKeyID.
if !bytes.Equal(k, encryptedKeyID) {
rootKeySlice = append(rootKeySlice, k)
}
return nil
}
return tx.ReadBucket(rootKeyBucketName).ForEach(appendRootKey)
})
if err != nil {
return nil, err
}
return rootKeySlice, nil
}
// DeleteMacaroonID removes one specific root key ID. If the root key ID is
// found and deleted, it will be returned.
func (r *RootKeyStorage) DeleteMacaroonID(
_ context.Context, rootKeyID []byte) ([]byte, error) {
r.encKeyMtx.RLock()
defer r.encKeyMtx.RUnlock()
// Check it's unlocked.
if r.encKey == nil {
return nil, ErrStoreLocked
}
// Check the rootKeyID is not empty.
if len(rootKeyID) == 0 {
return nil, ErrMissingRootKeyID
}
// Deleting encryptedKeyID or DefaultRootKeyID is not allowed.
if bytes.Equal(rootKeyID, encryptedKeyID) ||
bytes.Equal(rootKeyID, DefaultRootKeyID) {
return nil, ErrDeletionForbidden
}
var rootKeyIDDeleted []byte
err := kvdb.Update(r, func(tx kvdb.RwTx) error {
bucket := tx.ReadWriteBucket(rootKeyBucketName)
// Check the key can be found. If not, return nil.
if bucket.Get(rootKeyID) == nil {
return nil
}
// Once the key is found, we do the deletion.
if err := bucket.Delete(rootKeyID); err != nil {
return err
}
rootKeyIDDeleted = rootKeyID
return nil
})
if err != nil {
return nil, err
}
return rootKeyIDDeleted, nil
}

@ -51,13 +51,45 @@ func TestStore(t *testing.T) {
t.Fatalf("Error creating store encryption key: %v", err)
}
key, id, err := store.RootKey(context.TODO())
// Check ErrContextRootKeyID is returned when no root key ID found in
// context.
_, _, err = store.RootKey(context.TODO())
if err != macaroons.ErrContextRootKeyID {
t.Fatalf("Received %v instead of ErrContextRootKeyID", err)
}
// Check ErrMissingRootKeyID is returned when empty root key ID is used.
emptyKeyID := []byte{}
badCtx := macaroons.ContextWithRootKeyID(context.TODO(), emptyKeyID)
_, _, err = store.RootKey(badCtx)
if err != macaroons.ErrMissingRootKeyID {
t.Fatalf("Received %v instead of ErrMissingRootKeyID", err)
}
// Create a context with illegal root key ID value.
encryptedKeyID := []byte("enckey")
badCtx = macaroons.ContextWithRootKeyID(context.TODO(), encryptedKeyID)
_, _, err = store.RootKey(badCtx)
if err != macaroons.ErrKeyValueForbidden {
t.Fatalf("Received %v instead of ErrKeyValueForbidden", err)
}
// Create a context with root key ID value.
ctx := macaroons.ContextWithRootKeyID(
context.TODO(), macaroons.DefaultRootKeyID,
)
key, id, err := store.RootKey(ctx)
if err != nil {
t.Fatalf("Error getting root key from store: %v", err)
}
rootID := id
key2, err := store.Get(context.TODO(), id)
rootID := id
if !bytes.Equal(rootID, macaroons.DefaultRootKeyID) {
t.Fatalf("Root key ID doesn't match: expected %v, got %v",
macaroons.DefaultRootKeyID, rootID)
}
key2, err := store.Get(ctx, id)
if err != nil {
t.Fatalf("Error getting key with ID %s: %v", string(id), err)
}
@ -100,12 +132,12 @@ func TestStore(t *testing.T) {
t.Fatalf("Received %v instead of ErrPasswordRequired", err)
}
_, _, err = store.RootKey(context.TODO())
_, _, err = store.RootKey(ctx)
if err != macaroons.ErrStoreLocked {
t.Fatalf("Received %v instead of ErrStoreLocked", err)
}
_, err = store.Get(context.TODO(), nil)
_, err = store.Get(ctx, nil)
if err != macaroons.ErrStoreLocked {
t.Fatalf("Received %v instead of ErrStoreLocked", err)
}
@ -115,7 +147,7 @@ func TestStore(t *testing.T) {
t.Fatalf("Error unlocking root key store: %v", err)
}
key, err = store.Get(context.TODO(), rootID)
key, err = store.Get(ctx, rootID)
if err != nil {
t.Fatalf("Error getting key with ID %s: %v",
string(rootID), err)
@ -125,7 +157,7 @@ func TestStore(t *testing.T) {
key2, key)
}
key, id, err = store.RootKey(context.TODO())
key, id, err = store.RootKey(ctx)
if err != nil {
t.Fatalf("Error getting root key from store: %v", err)
}