Skip to content

Commit 03712e6

Browse files
committed
accounts: add an IDFromCaveates helper
And use that from the existing accountFromMacaroon helper (which will then test the new helper by proxy). We add this helper so that we can use it later on from the sessions package where we want to extract an account ID from a caveat (we wont have a full macaroon available).
1 parent 2717aa0 commit 03712e6

File tree

3 files changed

+142
-15
lines changed

3 files changed

+142
-15
lines changed

accounts/interceptor.go

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import (
55
"encoding/hex"
66
"errors"
77
"fmt"
8+
"strings"
89

910
mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware"
11+
"github.com/lightningnetwork/lnd/fn"
1012
"github.com/lightningnetwork/lnd/lnrpc"
1113
"github.com/lightningnetwork/lnd/macaroons"
1214
"google.golang.org/protobuf/proto"
15+
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
1316
"gopkg.in/macaroon.v2"
1417
)
1518

@@ -23,6 +26,15 @@ const (
2326
accountMiddlewareName = "lit-account"
2427
)
2528

29+
var (
30+
// caveatPrefix is the prefix that is used for custom caveats that are
31+
// used by the account system. This prefix is used to identify the
32+
// custom caveat and extract the condition (the AccountID) from it.
33+
caveatPrefix = []byte(fmt.Sprintf(
34+
"%s %s ", macaroons.CondLndCustom, CondAccount,
35+
))
36+
)
37+
2638
// Name returns the name of the interceptor.
2739
func (s *InterceptorService) Name() string {
2840
return accountMiddlewareName
@@ -199,22 +211,64 @@ func parseRPCMessage(msg *lnrpc.RPCMessage) (proto.Message, error) {
199211
// accountFromMacaroon attempts to extract an account ID from the custom account
200212
// caveat in the macaroon.
201213
func accountFromMacaroon(mac *macaroon.Macaroon) (*AccountID, error) {
202-
// Extract the account caveat from the macaroon.
203-
macaroonAccount := macaroons.GetCustomCaveatCondition(mac, CondAccount)
204-
if macaroonAccount == "" {
205-
// There is no condition that locks the macaroon to an account,
206-
// so there is nothing to check.
214+
if mac == nil {
207215
return nil, nil
208216
}
209217

210-
// The macaroon is indeed locked to an account. Fetch the account and
211-
// validate its balance.
212-
accountIDBytes, err := hex.DecodeString(macaroonAccount)
218+
// Extract the account caveat from the macaroon.
219+
accountID, err := IDFromCaveats(mac.Caveats())
213220
if err != nil {
214221
return nil, err
215222
}
216223

224+
var id *AccountID
225+
accountID.WhenSome(func(aID AccountID) {
226+
id = &aID
227+
})
228+
229+
return id, nil
230+
}
231+
232+
// CaveatFromID creates a custom caveat that can be used to bind a macaroon to
233+
// a certain account.
234+
func CaveatFromID(id AccountID) macaroon.Caveat {
235+
condition := checkers.Condition(macaroons.CondLndCustom, fmt.Sprintf(
236+
"%s %x", CondAccount, id[:],
237+
))
238+
239+
return macaroon.Caveat{Id: []byte(condition)}
240+
}
241+
242+
// IDFromCaveats attempts to extract an AccountID from the given set of caveats
243+
// by looking for the custom caveat that binds a macaroon to a certain account.
244+
func IDFromCaveats(caveats []macaroon.Caveat) (fn.Option[AccountID], error) {
245+
var accountIDStr string
246+
for _, caveat := range caveats {
247+
// The caveat id has a format of
248+
// "lnd-custom [custom-caveat-name] [custom-caveat-condition]"
249+
// and we only want the condition part. If we match the prefix
250+
// part we return the condition that comes after the prefix.
251+
_, after, found := strings.Cut(
252+
string(caveat.Id), string(caveatPrefix),
253+
)
254+
if !found {
255+
continue
256+
}
257+
258+
accountIDStr = after
259+
}
260+
261+
if accountIDStr == "" {
262+
return fn.None[AccountID](), nil
263+
}
264+
217265
var accountID AccountID
266+
accountIDBytes, err := hex.DecodeString(accountIDStr)
267+
if err != nil {
268+
return fn.None[AccountID](), err
269+
}
270+
218271
copy(accountID[:], accountIDBytes)
219-
return &accountID, nil
272+
273+
return fn.Some(accountID), nil
220274
}

accounts/interceptor_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package accounts
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/lightningnetwork/lnd/fn"
8+
"github.com/lightningnetwork/lnd/macaroons"
9+
"github.com/stretchr/testify/require"
10+
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
11+
"gopkg.in/macaroon.v2"
12+
)
13+
14+
// TestAccountIDCaveatEmbedding tests that the account ID can be embedded in a
15+
// macaroon caveat and extracted from it.
16+
func TestAccountIDCaveatEmbedding(t *testing.T) {
17+
badCondition := checkers.Condition(macaroons.CondLndCustom, fmt.Sprintf(
18+
"%s %s", CondAccount, "invalid hex",
19+
))
20+
21+
tests := []struct {
22+
name string
23+
caveats []macaroon.Caveat
24+
expectedErr string
25+
expectedAcct fn.Option[AccountID]
26+
}{
27+
{
28+
name: "valid account ID, single caveat",
29+
caveats: []macaroon.Caveat{
30+
CaveatFromID(AccountID{1, 2, 3, 4, 5}),
31+
},
32+
expectedAcct: fn.Some(AccountID{1, 2, 3, 4, 5}),
33+
},
34+
{
35+
name: "valid account ID, single multiple caveats",
36+
caveats: []macaroon.Caveat{
37+
{Id: []byte("some other caveat")},
38+
CaveatFromID(AccountID{1, 2, 3, 4, 5}),
39+
{Id: []byte("another one")},
40+
},
41+
expectedAcct: fn.Some(AccountID{1, 2, 3, 4, 5}),
42+
},
43+
{
44+
name: "invalid account ID",
45+
caveats: []macaroon.Caveat{
46+
{Id: []byte(badCondition)},
47+
},
48+
expectedErr: "encoding/hex: invalid",
49+
},
50+
}
51+
52+
for _, test := range tests {
53+
t.Run(test.name, func(t *testing.T) {
54+
t.Parallel()
55+
56+
acct, err := IDFromCaveats(test.caveats)
57+
if test.expectedErr != "" {
58+
require.ErrorContains(t, err, test.expectedErr)
59+
60+
return
61+
}
62+
require.NoError(t, err)
63+
64+
if test.expectedAcct.IsNone() {
65+
require.True(t, acct.IsNone())
66+
67+
return
68+
}
69+
require.True(t, acct.IsSome())
70+
71+
test.expectedAcct.WhenSome(func(id AccountID) {
72+
acct.WhenSome(func(acct AccountID) {
73+
require.Equal(t, id, acct)
74+
})
75+
})
76+
})
77+
}
78+
}

session_rpcserver.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,7 @@ func (s *sessionRpcServer) AddSession(ctx context.Context,
235235
return nil, fmt.Errorf("invalid account ID: %v", err)
236236
}
237237

238-
cav := checkers.Condition(macaroons.CondLndCustom, fmt.Sprintf(
239-
"%s %x", accounts.CondAccount, id[:],
240-
))
241-
caveats = append(caveats, macaroon.Caveat{
242-
Id: []byte(cav),
243-
})
238+
caveats = append(caveats, accounts.CaveatFromID(*id))
244239

245240
// For the custom macaroon type, we use the custom permissions specified
246241
// in the request. For the time being, the caveats list will be empty

0 commit comments

Comments
 (0)