Skip to content

Commit 8b8b878

Browse files
authored
Merge pull request #109 from wpaulino/lsat-basics
lsat: introduce LSAT related utilities
2 parents edc3037 + 1eb8ed3 commit 8b8b878

File tree

7 files changed

+844
-0
lines changed

7 files changed

+844
-0
lines changed

lsat/caveat.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package lsat
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"gopkg.in/macaroon.v2"
9+
)
10+
11+
const (
12+
// PreimageKey is the key used for a payment preimage caveat.
13+
PreimageKey = "preimage"
14+
)
15+
16+
var (
17+
// ErrInvalidCaveat is an error returned when we attempt to decode a
18+
// caveat with an invalid format.
19+
ErrInvalidCaveat = errors.New("caveat must be of the form " +
20+
"\"condition=value\"")
21+
)
22+
23+
// Caveat is a predicate that can be applied to an LSAT in order to restrict its
24+
// use in some form. Caveats are evaluated during LSAT verification after the
25+
// LSAT's signature is verified. The predicate of each caveat must hold true in
26+
// order to successfully validate an LSAT.
27+
type Caveat struct {
28+
// Condition serves as a way to identify a caveat and how to satisfy it.
29+
Condition string
30+
31+
// Value is what will be used to satisfy a caveat. This can be as
32+
// flexible as needed, as long as it can be encoded into a string.
33+
Value string
34+
}
35+
36+
// NewCaveat construct a new caveat with the given condition and value.
37+
func NewCaveat(condition string, value string) Caveat {
38+
return Caveat{Condition: condition, Value: value}
39+
}
40+
41+
// String returns a user-friendly view of a caveat.
42+
func (c Caveat) String() string {
43+
return EncodeCaveat(c)
44+
}
45+
46+
// EncodeCaveat encodes a caveat into its string representation.
47+
func EncodeCaveat(c Caveat) string {
48+
return fmt.Sprintf("%v=%v", c.Condition, c.Value)
49+
}
50+
51+
// DecodeCaveat decodes a caveat from its string representation.
52+
func DecodeCaveat(s string) (Caveat, error) {
53+
parts := strings.SplitN(s, "=", 2)
54+
if len(parts) != 2 {
55+
return Caveat{}, ErrInvalidCaveat
56+
}
57+
return Caveat{Condition: parts[0], Value: parts[1]}, nil
58+
}
59+
60+
// AddFirstPartyCaveats adds a set of caveats as first-party caveats to a
61+
// macaroon.
62+
func AddFirstPartyCaveats(m *macaroon.Macaroon, caveats ...Caveat) error {
63+
for _, c := range caveats {
64+
rawCaveat := []byte(EncodeCaveat(c))
65+
if err := m.AddFirstPartyCaveat(rawCaveat); err != nil {
66+
return err
67+
}
68+
}
69+
70+
return nil
71+
}
72+
73+
// HasCaveat checks whether the given macaroon has a caveat with the given
74+
// condition, and if so, returns its value. If multiple caveats with the same
75+
// condition exist, then the value of the last one is returned.
76+
func HasCaveat(m *macaroon.Macaroon, cond string) (string, bool) {
77+
var value *string
78+
for _, rawCaveat := range m.Caveats() {
79+
caveat, err := DecodeCaveat(string(rawCaveat.Id))
80+
if err != nil {
81+
// Ignore any unknown caveats as we can't decode them.
82+
continue
83+
}
84+
if caveat.Condition == cond {
85+
value = &caveat.Value
86+
}
87+
}
88+
89+
if value == nil {
90+
return "", false
91+
}
92+
return *value, true
93+
}
94+
95+
// VerifyCaveats determines whether every relevant caveat of an LSAT holds true.
96+
// A caveat is considered relevant if a satisfier is provided for it, which is
97+
// what we'll use as their evaluation.
98+
//
99+
// NOTE: The caveats provided should be in the same order as in the LSAT to
100+
// ensure the correctness of each satisfier's SatisfyPrevious.
101+
func VerifyCaveats(caveats []Caveat, satisfiers ...Satisfier) error {
102+
// Construct a set of our satisfiers to determine which caveats we know
103+
// how to satisfy.
104+
caveatSatisfiers := make(map[string]Satisfier, len(satisfiers))
105+
for _, satisfier := range satisfiers {
106+
caveatSatisfiers[satisfier.Condition] = satisfier
107+
}
108+
relevantCaveats := make(map[string][]Caveat)
109+
for _, caveat := range caveats {
110+
if _, ok := caveatSatisfiers[caveat.Condition]; !ok {
111+
continue
112+
}
113+
relevantCaveats[caveat.Condition] = append(
114+
relevantCaveats[caveat.Condition], caveat,
115+
)
116+
}
117+
118+
for condition, caveats := range relevantCaveats {
119+
satisfier := caveatSatisfiers[condition]
120+
121+
// Since it's possible for a chain of caveat to exist for the
122+
// same condition as a way to demote privileges, we'll ensure
123+
// each one satisfies its previous.
124+
for i, j := 0, 1; j < len(caveats); i, j = i+1, j+1 {
125+
prevCaveat := caveats[i]
126+
curCaveat := caveats[j]
127+
err := satisfier.SatisfyPrevious(prevCaveat, curCaveat)
128+
if err != nil {
129+
return err
130+
}
131+
}
132+
133+
// Once we verify the previous ones, if any, we can proceed to
134+
// verify the final one, which is the decision maker.
135+
err := satisfier.SatisfyFinal(caveats[len(caveats)-1])
136+
if err != nil {
137+
return err
138+
}
139+
}
140+
141+
return nil
142+
}

lsat/caveat_test.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package lsat
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"gopkg.in/macaroon.v2"
8+
)
9+
10+
var (
11+
testMacaroon, _ = macaroon.New(nil, nil, "", macaroon.LatestVersion)
12+
)
13+
14+
// TestCaveatSerialization ensures that we can properly encode/decode valid
15+
// caveats and cannot do so for invalid ones.
16+
func TestCaveatSerialization(t *testing.T) {
17+
t.Parallel()
18+
19+
tests := []struct {
20+
name string
21+
caveatStr string
22+
err error
23+
}{
24+
{
25+
name: "valid caveat",
26+
caveatStr: "expiration=1337",
27+
err: nil,
28+
},
29+
{
30+
name: "valid caveat with separator in value",
31+
caveatStr: "expiration=1337=",
32+
err: nil,
33+
},
34+
{
35+
name: "invalid caveat",
36+
caveatStr: "expiration:1337",
37+
err: ErrInvalidCaveat,
38+
},
39+
}
40+
41+
for _, test := range tests {
42+
test := test
43+
success := t.Run(test.name, func(t *testing.T) {
44+
caveat, err := DecodeCaveat(test.caveatStr)
45+
if !errors.Is(err, test.err) {
46+
t.Fatalf("expected err \"%v\", got \"%v\"",
47+
test.err, err)
48+
}
49+
50+
if test.err != nil {
51+
return
52+
}
53+
54+
caveatStr := EncodeCaveat(caveat)
55+
if caveatStr != test.caveatStr {
56+
t.Fatalf("expected encoded caveat \"%v\", "+
57+
"got \"%v\"", test.caveatStr, caveatStr)
58+
}
59+
})
60+
if !success {
61+
return
62+
}
63+
}
64+
}
65+
66+
// TestHasCaveat ensures we can determine whether a macaroon contains a caveat
67+
// with a specific condition.
68+
func TestHasCaveat(t *testing.T) {
69+
t.Parallel()
70+
71+
const (
72+
cond = "cond"
73+
value = "value"
74+
)
75+
m := testMacaroon.Clone()
76+
77+
// The macaroon doesn't have any caveats, so we shouldn't find any.
78+
if _, ok := HasCaveat(m, cond); ok {
79+
t.Fatal("found unexpected caveat with unknown condition")
80+
}
81+
82+
// Add two caveats, one in a valid LSAT format and another invalid.
83+
// We'll test that we're still able to determine the macaroon contains
84+
// the valid caveat even though there is one that is invalid.
85+
invalidCaveat := []byte("invalid")
86+
if err := m.AddFirstPartyCaveat(invalidCaveat); err != nil {
87+
t.Fatalf("unable to add macaroon caveat: %v", err)
88+
}
89+
validCaveat1 := Caveat{Condition: cond, Value: value}
90+
if err := AddFirstPartyCaveats(m, validCaveat1); err != nil {
91+
t.Fatalf("unable to add macaroon caveat: %v", err)
92+
}
93+
94+
caveatValue, ok := HasCaveat(m, cond)
95+
if !ok {
96+
t.Fatal("expected macaroon to contain caveat")
97+
}
98+
if caveatValue != validCaveat1.Value {
99+
t.Fatalf("expected caveat value \"%v\", got \"%v\"",
100+
validCaveat1.Value, caveatValue)
101+
}
102+
103+
// If we add another caveat with the same condition, the value of the
104+
// most recently added caveat should be returned instead.
105+
validCaveat2 := validCaveat1
106+
validCaveat2.Value += value
107+
if err := AddFirstPartyCaveats(m, validCaveat2); err != nil {
108+
t.Fatalf("unable to add macaroon caveat: %v", err)
109+
}
110+
111+
caveatValue, ok = HasCaveat(m, cond)
112+
if !ok {
113+
t.Fatal("expected macaroon to contain caveat")
114+
}
115+
if caveatValue != validCaveat2.Value {
116+
t.Fatalf("expected caveat value \"%v\", got \"%v\"",
117+
validCaveat2.Value, caveatValue)
118+
}
119+
}
120+
121+
// TestVerifyCaveats ensures caveat verification only holds true for known
122+
// caveats.
123+
func TestVerifyCaveats(t *testing.T) {
124+
t.Parallel()
125+
126+
caveat1 := Caveat{Condition: "1", Value: "test"}
127+
caveat2 := Caveat{Condition: "2", Value: "test"}
128+
satisfier := Satisfier{
129+
Condition: caveat1.Condition,
130+
SatisfyPrevious: func(c Caveat, prev Caveat) error {
131+
return nil
132+
},
133+
SatisfyFinal: func(c Caveat) error {
134+
return nil
135+
},
136+
}
137+
invalidSatisfyPrevious := func(c Caveat, prev Caveat) error {
138+
return errors.New("no")
139+
}
140+
invalidSatisfyFinal := func(c Caveat) error {
141+
return errors.New("no")
142+
}
143+
144+
tests := []struct {
145+
name string
146+
caveats []Caveat
147+
satisfiers []Satisfier
148+
shouldFail bool
149+
}{
150+
{
151+
name: "simple verification",
152+
caveats: []Caveat{caveat1},
153+
satisfiers: []Satisfier{satisfier},
154+
shouldFail: false,
155+
},
156+
{
157+
name: "unknown caveat",
158+
caveats: []Caveat{caveat1, caveat2},
159+
satisfiers: []Satisfier{satisfier},
160+
shouldFail: false,
161+
},
162+
{
163+
name: "one invalid",
164+
caveats: []Caveat{caveat1, caveat2},
165+
satisfiers: []Satisfier{
166+
satisfier,
167+
{
168+
Condition: caveat2.Condition,
169+
SatisfyFinal: invalidSatisfyFinal,
170+
},
171+
},
172+
shouldFail: true,
173+
},
174+
{
175+
name: "prev invalid",
176+
caveats: []Caveat{caveat1, caveat1},
177+
satisfiers: []Satisfier{
178+
{
179+
Condition: caveat1.Condition,
180+
SatisfyPrevious: invalidSatisfyPrevious,
181+
},
182+
},
183+
shouldFail: true,
184+
},
185+
}
186+
187+
for _, test := range tests {
188+
test := test
189+
success := t.Run(test.name, func(t *testing.T) {
190+
err := VerifyCaveats(test.caveats, test.satisfiers...)
191+
if test.shouldFail && err == nil {
192+
t.Fatal("expected caveat verification to fail")
193+
}
194+
if !test.shouldFail && err != nil {
195+
t.Fatal("unexpected caveat verification failure")
196+
}
197+
})
198+
if !success {
199+
return
200+
}
201+
}
202+
}

0 commit comments

Comments
 (0)