macaroons: specify root key ID in bakery
This commit is contained in:
parent
37a29b4869
commit
f362f7670b
@ -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
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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user