Skip to content

Commit fdec369

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 03cc9ff commit fdec369

File tree

3 files changed

+146
-15
lines changed

3 files changed

+146
-15
lines changed

accounts/interceptor.go

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package accounts
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/hex"
67
"errors"
78
"fmt"
9+
"strings"
810

911
mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware"
12+
"github.com/lightningnetwork/lnd/fn"
1013
"github.com/lightningnetwork/lnd/lnrpc"
1114
"github.com/lightningnetwork/lnd/macaroons"
1215
"google.golang.org/protobuf/proto"
16+
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
1317
"gopkg.in/macaroon.v2"
1418
)
1519

@@ -23,6 +27,15 @@ const (
2327
accountMiddlewareName = "lit-account"
2428
)
2529

30+
var (
31+
// caveatPrefix is the prefix that is used for custom caveats that are
32+
// used by the account system. This prefix is used to identify the
33+
// custom caveat and extract the condition (the AccountID) from it.
34+
caveatPrefix = []byte(fmt.Sprintf(
35+
"%s %s ", macaroons.CondLndCustom, CondAccount,
36+
))
37+
)
38+
2639
// Name returns the name of the interceptor.
2740
func (s *InterceptorService) Name() string {
2841
return accountMiddlewareName
@@ -199,22 +212,68 @@ func parseRPCMessage(msg *lnrpc.RPCMessage) (proto.Message, error) {
199212
// accountFromMacaroon attempts to extract an account ID from the custom account
200213
// caveat in the macaroon.
201214
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.
215+
if mac == nil {
207216
return nil, nil
208217
}
209218

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

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

accounts/interceptor_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
70+
test.expectedAcct.WhenSome(func(id AccountID) {
71+
acct.WhenSome(func(acct AccountID) {
72+
require.Equal(t, id, acct)
73+
})
74+
})
75+
})
76+
}
77+
}

session_rpcserver.go

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

228-
cav := checkers.Condition(macaroons.CondLndCustom, fmt.Sprintf(
229-
"%s %x", accounts.CondAccount, id[:],
230-
))
231-
caveats = append(caveats, macaroon.Caveat{
232-
Id: []byte(cav),
233-
})
228+
caveats = append(caveats, accounts.CaveatFromID(*id))
234229

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

0 commit comments

Comments
 (0)