Skip to content

Commit 6d76e22

Browse files
guggeroellemouton
authored andcommitted
firewall: add macaroon caveat logic
1 parent 8f0bc2f commit 6d76e22

File tree

2 files changed

+321
-0
lines changed

2 files changed

+321
-0
lines changed

firewall/caveats.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package firewall
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/lightningnetwork/lnd/macaroons"
9+
)
10+
11+
const (
12+
// RuleEnforcerCaveat is the name of the custom caveat that contains all
13+
// the rules that need to be enforced in the firewall.
14+
RuleEnforcerCaveat = "lit-mac-fw"
15+
16+
// MetaInfoValuePrefix is the static prefix a macaroon caveat value has
17+
// to mark the beginning of the meta information JSON data.
18+
MetaInfoValuePrefix = "meta"
19+
20+
// MetaRulesValuePrefix is the static prefix a macaroon caveat value has
21+
// to mark the beginning of the rules list JSON data.
22+
MetaRulesValuePrefix = "rules"
23+
)
24+
25+
var (
26+
// MetaInfoFullCaveatPrefix is the full prefix a caveat needs to have to
27+
// be recognized as a meta information caveat.
28+
MetaInfoFullCaveatPrefix = fmt.Sprintf("%s %s %s",
29+
macaroons.CondLndCustom, RuleEnforcerCaveat,
30+
MetaInfoValuePrefix)
31+
32+
// MetaRulesFullCaveatPrefix is the full prefix a caveat needs to have
33+
// to be recognized as a rules list caveat.
34+
MetaRulesFullCaveatPrefix = fmt.Sprintf("%s %s %s",
35+
macaroons.CondLndCustom, RuleEnforcerCaveat,
36+
MetaRulesValuePrefix)
37+
38+
// ErrNoMetaInfoCaveat is the error that is returned if a caveat doesn't
39+
// have the prefix to be recognized as a meta information caveat.
40+
ErrNoMetaInfoCaveat = fmt.Errorf("not a meta info caveat")
41+
42+
// ErrNoRulesCaveat is the error that is returned if a caveat doesn't
43+
// have the prefix to be recognized as a rules list caveat.
44+
ErrNoRulesCaveat = fmt.Errorf("not a rules list caveat")
45+
)
46+
47+
// InterceptMetaInfo is the JSON serializable struct containing meta information
48+
// about a request made by an automated node management software against LiT.
49+
// The meta information is added as a macaroon caveat.
50+
type InterceptMetaInfo struct {
51+
// ActorName is the name of the actor service (=management software)
52+
// that is issuing this request.
53+
ActorName string `json:"actor_name"`
54+
55+
// Trigger is the action or condition that triggered this intercepted
56+
// request to be made.
57+
Trigger string `json:"trigger"`
58+
59+
// Intent is the desired outcome or end condition this request aims to
60+
// arrive at.
61+
Intent string `json:"intent"`
62+
}
63+
64+
// ToCaveat returns the full custom caveat string representation of the
65+
// interception meta information in this format:
66+
// lnd-custom lit-mac-fw meta:<JSON_encoded_meta_information>
67+
func (i *InterceptMetaInfo) ToCaveat() (string, error) {
68+
jsonBytes, err := json.Marshal(i)
69+
if err != nil {
70+
return "", fmt.Errorf("error JSON marshaling: %v", err)
71+
}
72+
73+
return fmt.Sprintf("%s:%s", MetaInfoFullCaveatPrefix, jsonBytes), nil
74+
}
75+
76+
// ParseMetaInfoCaveat tries to parse the given caveat string as a meta
77+
// information struct.
78+
func ParseMetaInfoCaveat(caveat string) (*InterceptMetaInfo, error) {
79+
if !strings.HasPrefix(caveat, MetaInfoFullCaveatPrefix) {
80+
return nil, ErrNoMetaInfoCaveat
81+
}
82+
83+
// Only the prefix isn't enough.
84+
if len(caveat) <= len(MetaInfoFullCaveatPrefix)+1 {
85+
return nil, ErrNoMetaInfoCaveat
86+
}
87+
88+
// There's a colon after the prefix that we need to skip as well.
89+
jsonData := caveat[len(MetaInfoFullCaveatPrefix)+1:]
90+
i := &InterceptMetaInfo{}
91+
92+
if err := json.Unmarshal([]byte(jsonData), i); err != nil {
93+
return nil, fmt.Errorf("error unmarshaling JSON: %v", err)
94+
}
95+
96+
return i, nil
97+
}
98+
99+
// InterceptRule is the JSON serializable struct containing all the rules and
100+
// their limits/settings that need to be enforced on a request made by an
101+
// automated node management software against LiT. The rule information is added
102+
// as a custom macaroon caveat.
103+
type InterceptRule struct {
104+
// Name is the name of the rule. It must correspond to a
105+
Name string `json:"name"`
106+
107+
// Restrictions is a key/value map of all the parameters that apply to
108+
// this rule.
109+
Restrictions map[string]string `json:"restrictions"`
110+
}
111+
112+
// RulesToCaveat encodes a list of rules as a full custom caveat string
113+
// representation in this format:
114+
// lnd-custom lit-mac-fw rules:[<array_of_JSON_encoded_rules>]
115+
func RulesToCaveat(rules []*InterceptRule) (string, error) {
116+
jsonBytes, err := json.Marshal(rules)
117+
if err != nil {
118+
return "", fmt.Errorf("error JSON marshaling: %v", err)
119+
}
120+
121+
return fmt.Sprintf("%s:%s", MetaRulesFullCaveatPrefix, jsonBytes), nil
122+
}
123+
124+
// ParseRuleCaveat tries to parse the given caveat string as a rule struct.
125+
func ParseRuleCaveat(caveat string) ([]*InterceptRule, error) {
126+
if !strings.HasPrefix(caveat, MetaRulesFullCaveatPrefix) {
127+
return nil, ErrNoRulesCaveat
128+
}
129+
130+
// Only the prefix isn't enough.
131+
if len(caveat) <= len(MetaRulesFullCaveatPrefix)+1 {
132+
return nil, ErrNoRulesCaveat
133+
}
134+
135+
// There's a colon after the prefix that we need to skip as well.
136+
jsonData := caveat[len(MetaRulesFullCaveatPrefix)+1:]
137+
var rules []*InterceptRule
138+
139+
if err := json.Unmarshal([]byte(jsonData), &rules); err != nil {
140+
return nil, fmt.Errorf("error unmarshaling JSON: %v", err)
141+
}
142+
143+
return rules, nil
144+
}

firewall/caveats_test.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package firewall
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
const (
11+
testMetaCaveat = "lnd-custom lit-mac-fw meta:{\"actor_name\":" +
12+
"\"re-balancer\",\"trigger\":\"channel 7413345453234435345 " +
13+
"depleted\",\"intent\":\"increase outbound liquidity by " +
14+
"2000000 sats\"}"
15+
16+
testRulesCaveat = "lnd-custom lit-mac-fw rules:[{\"name\":" +
17+
"\"re-balance-limits\",\"restrictions\":" +
18+
"{\"first-hop-ignore-list\":\"03abcd...,02badb01...\"," +
19+
"\"max-hops\":\"4\",\"off-chain-fees-sats\":\"10\"}}," +
20+
"{\"name\":\"time-limits\",\"restrictions\":" +
21+
"{\"re-balance-min-interval-seconds\":\"3600\"}}]"
22+
)
23+
24+
// TestInterceptMetaInfo makes sure that a meta information struct can be
25+
// formatted as a caveat and then parsed again successfully.
26+
func TestInterceptMetaInfo(t *testing.T) {
27+
info := &InterceptMetaInfo{
28+
ActorName: "re-balancer",
29+
Trigger: "channel 7413345453234435345 depleted",
30+
Intent: "increase outbound liquidity by 2000000 sats",
31+
}
32+
33+
caveat, err := info.ToCaveat()
34+
require.NoError(t, err)
35+
36+
require.Equal(t, testMetaCaveat, caveat)
37+
38+
parsedInfo, err := ParseMetaInfoCaveat(caveat)
39+
require.NoError(t, err)
40+
41+
require.Equal(t, info, parsedInfo)
42+
}
43+
44+
// TestParseMetaInfoCaveat makes sure the meta information caveat parsing works
45+
// as expected.
46+
func TestParseMetaInfoCaveat(t *testing.T) {
47+
testCases := []struct {
48+
name string
49+
input string
50+
err error
51+
result *InterceptMetaInfo
52+
}{{
53+
name: "empty string",
54+
input: "",
55+
err: ErrNoMetaInfoCaveat,
56+
}, {
57+
name: "prefix only",
58+
input: "lnd-custom lit-mac-fw meta:",
59+
err: ErrNoMetaInfoCaveat,
60+
}, {
61+
name: "invalid JSON",
62+
input: "lnd-custom lit-mac-fw meta:bar",
63+
err: fmt.Errorf("error unmarshaling JSON: invalid character " +
64+
"'b' looking for beginning of value"),
65+
}, {
66+
name: "empty JSON",
67+
input: "lnd-custom lit-mac-fw meta:{}",
68+
result: &InterceptMetaInfo{},
69+
}}
70+
71+
for _, tc := range testCases {
72+
tc := tc
73+
74+
t.Run(tc.name, func(tt *testing.T) {
75+
i, err := ParseMetaInfoCaveat(tc.input)
76+
77+
if tc.err != nil {
78+
require.Error(tt, err)
79+
require.Equal(tt, tc.err, err)
80+
81+
return
82+
}
83+
84+
require.NoError(tt, err)
85+
require.Equal(tt, tc.result, i)
86+
})
87+
}
88+
}
89+
90+
// TestInterceptRule makes sure that a rules list struct can be formatted as a
91+
// caveat and then parsed again successfully.
92+
func TestInterceptRule(t *testing.T) {
93+
rules := []*InterceptRule{{
94+
Name: "re-balance-limits",
95+
Restrictions: map[string]string{
96+
"off-chain-fees-sats": "10",
97+
"max-hops": "4",
98+
"first-hop-ignore-list": "03abcd...,02badb01...",
99+
},
100+
}, {
101+
Name: "time-limits",
102+
Restrictions: map[string]string{
103+
"re-balance-min-interval-seconds": "3600",
104+
},
105+
}}
106+
107+
caveat, err := RulesToCaveat(rules)
108+
require.NoError(t, err)
109+
110+
require.Equal(t, testRulesCaveat, caveat)
111+
112+
parsedRules, err := ParseRuleCaveat(caveat)
113+
require.NoError(t, err)
114+
115+
require.Equal(t, rules, parsedRules)
116+
}
117+
118+
// TestParseRulesCaveat makes sure the rule list caveat parsing works as
119+
// expected.
120+
func TestParseRulesCaveat(t *testing.T) {
121+
testCases := []struct {
122+
name string
123+
input string
124+
err error
125+
result []*InterceptRule
126+
}{{
127+
name: "empty string",
128+
input: "",
129+
err: ErrNoRulesCaveat,
130+
}, {
131+
name: "prefix only",
132+
input: "lnd-custom lit-mac-fw rules:",
133+
err: ErrNoRulesCaveat,
134+
}, {
135+
name: "invalid JSON",
136+
input: "lnd-custom lit-mac-fw rules:bar",
137+
err: fmt.Errorf("error unmarshaling JSON: invalid character " +
138+
"'b' looking for beginning of value"),
139+
}, {
140+
name: "empty JSON",
141+
input: "lnd-custom lit-mac-fw rules:[]",
142+
result: []*InterceptRule{},
143+
}, {
144+
name: "empty rules",
145+
input: "lnd-custom lit-mac-fw rules:[{}, {}]",
146+
result: []*InterceptRule{{}, {}},
147+
}, {
148+
name: "valid rules",
149+
input: "lnd-custom lit-mac-fw rules:[{\"name\":\"foo\"}, " +
150+
"{\"restrictions\":{\"foo\":\"bar\"}}]",
151+
result: []*InterceptRule{{
152+
Name: "foo",
153+
}, {
154+
Restrictions: map[string]string{
155+
"foo": "bar",
156+
},
157+
}},
158+
}}
159+
160+
for _, tc := range testCases {
161+
tc := tc
162+
163+
t.Run(tc.name, func(tt *testing.T) {
164+
i, err := ParseRuleCaveat(tc.input)
165+
166+
if tc.err != nil {
167+
require.Error(tt, err)
168+
require.Equal(tt, tc.err, err)
169+
170+
return
171+
}
172+
173+
require.NoError(tt, err)
174+
require.Equal(tt, tc.result, i)
175+
})
176+
}
177+
}

0 commit comments

Comments
 (0)