Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

macaroons: reject unknown macaroon versions #8132

Merged
merged 1 commit into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/release-notes/release-notes-0.17.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
* A bug that would cause taproot channels to sometimes not display as active
[has been fixed](https://github.com/lightningnetwork/lnd/pull/8104).

* [`lnd` will now properly reject macaroons with unknown versions.](https://github.com/lightningnetwork/lnd/pull/8132)

# New Features
## Functional Enhancements

Expand Down
2 changes: 1 addition & 1 deletion itest/lnd_macaroons_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
defer cleanup()
_, err := client.GetInfo(ctxt, infoReq)
require.Error(t, err)
require.Contains(t, err.Error(), "cannot get macaroon")
require.Contains(t, err.Error(), "invalid ID")
},
}, {
// Third test: Try to access a write method with read-only
Expand Down
10 changes: 10 additions & 0 deletions macaroons/constraints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ func createDummyMacaroon(t *testing.T) *macaroon.Macaroon {
// TestAddConstraints tests that constraints can be added to an existing
// macaroon and therefore tighten its restrictions.
func TestAddConstraints(t *testing.T) {
t.Parallel()

// We need a dummy macaroon to start with. Create one without
// a bakery, because we mock everything anyway.
initialMac := createDummyMacaroon(t)
Expand All @@ -55,6 +57,8 @@ func TestAddConstraints(t *testing.T) {
// TestTimeoutConstraint tests that a caveat for the lifetime of
// a macaroon is created.
func TestTimeoutConstraint(t *testing.T) {
t.Parallel()

// Get a configured version of the constraint function.
constraintFunc := macaroons.TimeoutConstraint(3)

Expand All @@ -79,6 +83,8 @@ func TestTimeoutConstraint(t *testing.T) {
// TestTimeoutConstraint tests that a caveat for the lifetime of
// a macaroon is created.
func TestIpLockConstraint(t *testing.T) {
t.Parallel()

// Get a configured version of the constraint function.
constraintFunc := macaroons.IPLockConstraint("127.0.0.1")

Expand All @@ -99,6 +105,8 @@ func TestIpLockConstraint(t *testing.T) {
// TestIPLockBadIP tests that an IP constraint cannot be added if the
// provided string is not a valid IP address.
func TestIPLockBadIP(t *testing.T) {
t.Parallel()

constraintFunc := macaroons.IPLockConstraint("127.0.0/800")
testMacaroon := createDummyMacaroon(t)
err := constraintFunc(testMacaroon)
Expand All @@ -110,6 +118,8 @@ func TestIPLockBadIP(t *testing.T) {
// TestCustomConstraint tests that a custom constraint with a name and value can
// be added to a macaroon.
func TestCustomConstraint(t *testing.T) {
t.Parallel()

// Test a custom caveat with a value first.
constraintFunc := macaroons.CustomConstraint("unit-test", "test-value")
testMacaroon := createDummyMacaroon(t)
Expand Down
26 changes: 25 additions & 1 deletion macaroons/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ var (
// 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.
Expand Down Expand Up @@ -208,6 +215,23 @@ func (svc *Service) CheckMacAuth(ctx context.Context, macBytes []byte,
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})
Expand Down Expand Up @@ -267,7 +291,7 @@ func (svc *Service) NewMacaroon(
return nil, ErrMissingRootKeyID
}

// // Pass the root key ID to context.
// Pass the root key ID to context.
ctx = ContextWithRootKeyID(ctx, rootKeyID)

return svc.Oven.NewMacaroon(ctx, bakery.LatestVersion, nil, ops...)
Expand Down
72 changes: 72 additions & 0 deletions macaroons/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"google.golang.org/grpc/metadata"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
macaroon "gopkg.in/macaroon.v2"
)

var (
Expand Down Expand Up @@ -53,6 +54,8 @@ func setupTestRootKeyStorage(t *testing.T) kvdb.Backend {

// TestNewService tests the creation of the macaroon service.
func TestNewService(t *testing.T) {
t.Parallel()

// First, initialize a dummy DB file with a store that the service
// can read from. Make sure the file is removed in the end.
db := setupTestRootKeyStorage(t)
Expand Down Expand Up @@ -104,6 +107,8 @@ func TestNewService(t *testing.T) {
// TestValidateMacaroon tests the validation of a macaroon that is in an
// incoming context.
func TestValidateMacaroon(t *testing.T) {
t.Parallel()

// First, initialize the service and unlock it.
db := setupTestRootKeyStorage(t)
rootKeyStore, err := macaroons.NewRootKeyStorage(db)
Expand Down Expand Up @@ -149,6 +154,8 @@ func TestValidateMacaroon(t *testing.T) {

// TestListMacaroonIDs checks that ListMacaroonIDs returns the expected result.
func TestListMacaroonIDs(t *testing.T) {
t.Parallel()

// First, initialize a dummy DB file with a store that the service
// can read from. Make sure the file is removed in the end.
db := setupTestRootKeyStorage(t)
Expand Down Expand Up @@ -180,6 +187,8 @@ func TestListMacaroonIDs(t *testing.T) {

// TestDeleteMacaroonID removes the specific root key ID.
func TestDeleteMacaroonID(t *testing.T) {
t.Parallel()

ctxb := context.Background()

// First, initialize a dummy DB file with a store that the service
Expand Down Expand Up @@ -237,6 +246,8 @@ func TestDeleteMacaroonID(t *testing.T) {
// TestCloneMacaroons tests that macaroons can be cloned correctly and that
// modifications to the copy don't affect the original.
func TestCloneMacaroons(t *testing.T) {
t.Parallel()

// Get a configured version of the constraint function.
constraintFunc := macaroons.TimeoutConstraint(3)

Expand Down Expand Up @@ -288,3 +299,64 @@ func TestCloneMacaroons(t *testing.T) {
newMac.Caveats()[0].Location,
)
}

// TestMacaroonVersionDecode tests that we'll reject macaroons with an unknown
// version.
func TestMacaroonVersionDecode(t *testing.T) {
t.Parallel()

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.
db := setupTestRootKeyStorage(t)

// Second, create the new service instance, unlock it and pass in a
// checker that we expect it to add to the bakery.
rootKeyStore, err := macaroons.NewRootKeyStorage(db)
require.NoError(t, err)

service, err := macaroons.NewService(
rootKeyStore, "lnd", false, macaroons.IPLockChecker,
)
require.NoError(t, err, "Error creating new service")

defer service.Close()

t.Run("invalid_version", func(t *testing.T) {
// Now that we have our sample service, we'll make a new custom
// macaroon with an unknown version.
testMac, err := macaroon.New(
testRootKey, testID, testLocation, 1,
)
require.NoError(t, err)

// Next, we'll serialize the macaroon to the binary form,
// modifying the first byte to signal an unknown version.
testMacBytes, err := testMac.MarshalBinary()
require.NoError(t, err)

// If we attempt to check the mac auth, then we should get a
// failure that the version is unknown.
err = service.CheckMacAuth(ctxb, testMacBytes, nil, "")
require.ErrorIs(t, err, macaroons.ErrUnknownVersion)
})

t.Run("invalid_id", func(t *testing.T) {
// We'll now make a macaroon with a valid version, but modify
// the ID to be rejected.
badID := []byte{}
testMac, err := macaroon.New(
testRootKey, badID, testLocation, testVersion,
)
require.NoError(t, err)

testMacBytes, err := testMac.MarshalBinary()
require.NoError(t, err)

// If we attempt to check the mac auth, then we should get a
// failure that the ID is bad.
err = service.CheckMacAuth(ctxb, testMacBytes, nil, "")
require.ErrorIs(t, err, macaroons.ErrInvalidID)
})
}
Loading