Merge pull request #81 from bucko13/poc-creation-time-caveat

Timeout Caveat Support
This commit is contained in:
Oliver Gugger
2023-04-26 08:55:32 +02:00
committed by GitHub
11 changed files with 325 additions and 5 deletions

3
.gitignore vendored
View File

@@ -13,3 +13,6 @@
/aperture
cmd/aperture/aperture
# misc
.vscode

View File

@@ -662,6 +662,7 @@ func createProxy(cfg *Config, challenger *LndChallenger,
Challenger: challenger,
Secrets: newSecretStore(etcdClient),
ServiceLimiter: newStaticServiceLimiter(cfg.Services),
Now: time.Now,
})
authenticator := auth.NewLsatAuthenticator(minter, challenger)

View File

@@ -2,7 +2,9 @@ package lsat
import (
"fmt"
"strconv"
"strings"
"time"
)
// Satisfier provides a generic interface to satisfy a caveat based on its
@@ -79,7 +81,9 @@ func NewServicesSatisfier(targetService string) Satisfier {
// NewCapabilitiesSatisfier implements a satisfier to determine whether the
// target capability for a service is authorized for a given LSAT.
func NewCapabilitiesSatisfier(service string, targetCapability string) Satisfier {
func NewCapabilitiesSatisfier(service string,
targetCapability string) Satisfier {
return Satisfier{
Condition: service + CondCapabilitiesSuffix,
SatisfyPrevious: func(prev, cur Caveat) error {
@@ -115,3 +119,62 @@ func NewCapabilitiesSatisfier(service string, targetCapability string) Satisfier
},
}
}
// NewTimeoutSatisfier checks if an LSAT is expired or not. The Satisfier takes
// a service name to set as the condition prefix and currentTimestamp to
// compare against the expiration(s) in the caveats. The expiration time is
// retrieved from the caveat values themselves. The satisfier will also make
// sure that each subsequent caveat of the same condition only has increasingly
// strict expirations.
func NewTimeoutSatisfier(service string, now func() time.Time) Satisfier {
return Satisfier{
Condition: service + CondTimeoutSuffix,
SatisfyPrevious: func(prev, cur Caveat) error {
prevValue, err := strconv.ParseInt(prev.Value, 10, 64)
if err != nil {
return fmt.Errorf("error parsing previous "+
"caveat value: %w", err)
}
currValue, err := strconv.ParseInt(cur.Value, 10, 64)
if err != nil {
return fmt.Errorf("error parsing caveat "+
"value: %w", err)
}
prevTime := time.Unix(prevValue, 0)
currTime := time.Unix(currValue, 0)
// Satisfier should fail if a previous timestamp in the
// list is earlier than ones after it b/c that means
// they are getting more permissive.
if prevTime.Before(currTime) {
return fmt.Errorf("%s caveat violates "+
"increasing restrictiveness",
service+CondTimeoutSuffix)
}
return nil
},
SatisfyFinal: func(c Caveat) error {
expirationTimestamp, err := strconv.ParseInt(
c.Value, 10, 64,
)
if err != nil {
return fmt.Errorf("caveat value not a valid "+
"integer: %v", err)
}
expirationTime := time.Unix(expirationTimestamp, 0)
// Make sure that the final relevant caveat is not
// passed the current date/time.
if now().Before(expirationTime) {
return nil
}
return fmt.Errorf("not authorized to access " +
"service. LSAT has expired")
},
}
}

87
lsat/satisfier_test.go Normal file
View File

@@ -0,0 +1,87 @@
package lsat
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestTimeoutSatisfier tests that the Timeout Satisfier implementation behaves
// as expected and correctly accepts or rejects calls based on if the
// timeout has been reached or not.
func TestTimeoutSatisfier(t *testing.T) {
t.Parallel()
now := int64(0)
var tests = []struct {
name string
timeouts []int64
expectFinalErr bool
expectPrevErr bool
}{
{
name: "current time is before expiration",
timeouts: []int64{now + 1000},
},
{
name: "time passed is greater than " +
"expiration",
timeouts: []int64{now - 1000},
expectFinalErr: true,
},
{
name: "successive caveats are increasingly " +
"restrictive and not yet expired",
timeouts: []int64{now + 1000, now + 500},
},
{
name: "latter caveat is less restrictive " +
"then previous",
timeouts: []int64{now + 500, now + 1000},
expectPrevErr: true,
},
}
var (
service = "restricted"
condition = service + CondTimeoutSuffix
satisfier = NewTimeoutSatisfier(service, func() time.Time {
return time.Unix(now, 0)
})
)
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
var prev *Caveat
for _, timeout := range test.timeouts {
caveat := NewCaveat(
condition, fmt.Sprintf("%d", timeout),
)
if prev != nil {
err := satisfier.SatisfyPrevious(
*prev, caveat,
)
if test.expectPrevErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
}
err := satisfier.SatisfyFinal(caveat)
if test.expectFinalErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
prev = &caveat
}
})
}
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"strconv"
"strings"
"time"
)
const (
@@ -15,6 +16,10 @@ const (
// capabilities caveat. For example, the condition of a capabilities
// caveat for a service named `loop` would be `loop_capabilities`.
CondCapabilitiesSuffix = "_capabilities"
// CondTimeoutSuffix is the condition suffix used for a service's
// timeout caveat.
CondTimeoutSuffix = "_valid_until"
)
var (
@@ -129,3 +134,19 @@ func NewCapabilitiesCaveat(serviceName string, capabilities string) Caveat {
Value: capabilities,
}
}
// NewTimeoutCaveat creates a new caveat that will result in a macaroon being
// valid for numSeconds after the current time.
func NewTimeoutCaveat(serviceName string, numSeconds int64,
now func() time.Time) Caveat {
var (
macaroonTimeout = time.Duration(numSeconds) * time.Second
requestTimeout = now().Add(macaroonTimeout)
)
return Caveat{
Condition: serviceName + CondTimeoutSuffix,
Value: strconv.FormatInt(requestTimeout.Unix(), 10),
}
}

View File

@@ -7,6 +7,7 @@ import (
"crypto/sha256"
"errors"
"fmt"
"time"
"github.com/lightninglabs/aperture/lsat"
"github.com/lightningnetwork/lnd/lntypes"
@@ -60,6 +61,10 @@ type ServiceLimiter interface {
// enforces additional constraints on a particular service/service
// capability.
ServiceConstraints(context.Context, ...lsat.Service) ([]lsat.Caveat, error)
// ServiceTimeouts returns the timeout caveat for each service. This
// will determine if and when service access can expire.
ServiceTimeouts(context.Context, ...lsat.Service) ([]lsat.Caveat, error)
}
// Config packages all of the required dependencies to instantiate a new LSAT
@@ -76,6 +81,9 @@ type Config struct {
// ServiceLimiter provides us with how we should limit a new LSAT based
// on its target services.
ServiceLimiter ServiceLimiter
// Now returns the current time.
Now func() time.Time
}
// Mint is an entity that is able to mint and verify LSATs for a set of
@@ -210,10 +218,15 @@ func (m *Mint) caveatsForServices(ctx context.Context,
if err != nil {
return nil, err
}
timeouts, err := m.cfg.ServiceLimiter.ServiceTimeouts(ctx, services...)
if err != nil {
return nil, err
}
caveats := []lsat.Caveat{servicesCaveat}
caveats = append(caveats, capabilities...)
caveats = append(caveats, constraints...)
caveats = append(caveats, timeouts...)
return caveats, nil
}
@@ -269,6 +282,8 @@ func (m *Mint) VerifyLSAT(ctx context.Context, params *VerificationParams) error
caveats = append(caveats, caveat)
}
return lsat.VerifyCaveats(
caveats, lsat.NewServicesSatisfier(params.TargetService),
caveats,
lsat.NewServicesSatisfier(params.TargetService),
lsat.NewTimeoutSatisfier(params.TargetService, m.cfg.Now),
)
}

View File

@@ -5,8 +5,10 @@ import (
"crypto/sha256"
"strings"
"testing"
"time"
"github.com/lightninglabs/aperture/lsat"
"github.com/stretchr/testify/require"
"gopkg.in/macaroon.v2"
)
@@ -27,6 +29,7 @@ func TestBasicLSAT(t *testing.T) {
Secrets: newMockSecretStore(),
Challenger: newMockChallenger(),
ServiceLimiter: newMockServiceLimiter(),
Now: time.Now,
})
// Mint a basic LSAT which is only able to access the given service.
@@ -46,7 +49,7 @@ func TestBasicLSAT(t *testing.T) {
// It should not be able to access an unknown service.
unknownParams := params
unknownParams.TargetService = "uknown"
unknownParams.TargetService = "unknown"
err = mint.VerifyLSAT(ctx, &unknownParams)
if !strings.Contains(err.Error(), "not authorized") {
t.Fatal("expected LSAT to not be authorized")
@@ -63,6 +66,7 @@ func TestAdminLSAT(t *testing.T) {
Secrets: newMockSecretStore(),
Challenger: newMockChallenger(),
ServiceLimiter: newMockServiceLimiter(),
Now: time.Now,
})
// Mint an admin LSAT by not including any services.
@@ -92,6 +96,7 @@ func TestRevokedLSAT(t *testing.T) {
Secrets: newMockSecretStore(),
Challenger: newMockChallenger(),
ServiceLimiter: newMockServiceLimiter(),
Now: time.Now,
})
// Mint an LSAT and verify it.
@@ -128,6 +133,7 @@ func TestTamperedLSAT(t *testing.T) {
Secrets: newMockSecretStore(),
Challenger: newMockChallenger(),
ServiceLimiter: newMockServiceLimiter(),
Now: time.Now,
})
// Mint a new LSAT and verify it is valid.
@@ -175,6 +181,7 @@ func TestDemotedServicesLSAT(t *testing.T) {
Secrets: newMockSecretStore(),
Challenger: newMockChallenger(),
ServiceLimiter: newMockServiceLimiter(),
Now: time.Now,
})
unauthorizedService := testService
@@ -225,3 +232,66 @@ func TestDemotedServicesLSAT(t *testing.T) {
t.Fatal("expected macaroon to be invalid")
}
}
// TestExpiredServicesLSAT asserts the behavior of the Timeout caveat.
func TestExpiredServicesLSAT(t *testing.T) {
t.Parallel()
initialTime := int64(1000)
mockTime := newMockTime(initialTime)
ctx := context.Background()
mint := New(&Config{
Secrets: newMockSecretStore(),
Challenger: newMockChallenger(),
ServiceLimiter: newMockServiceLimiter(),
Now: mockTime.now,
})
// Mint a new lsat for accessing a test service.
mac, _, err := mint.MintLSAT(ctx, testService)
require.NoError(t, err)
authorizedParams := VerificationParams{
Macaroon: mac,
Preimage: testPreimage,
TargetService: testService.Name,
}
// It should be able to access the service if no timeout caveat added.
require.NoError(t, mint.VerifyLSAT(ctx, &authorizedParams))
// Add a timeout caveat that expires in the future.
timeout := lsat.NewTimeoutCaveat(testService.Name, 1000, mockTime.now)
require.NoError(t, lsat.AddFirstPartyCaveats(mac, timeout))
// Make sure that the LSAT is still valid after timeout is added since
// the timeout has not yet been reached.
require.NoError(t, mint.VerifyLSAT(ctx, &authorizedParams))
// Force time to pass such that the LSAT should no longer be valid.
mockTime.setTime(initialTime + 1001)
// Assert that the LSAT is no longer valid due to the timeout being
// reached.
err = mint.VerifyLSAT(ctx, &authorizedParams)
require.Contains(t, err.Error(), "not authorized")
}
type mockTime struct {
time time.Time
}
func newMockTime(initialTime int64) *mockTime {
return &mockTime{
time: time.Unix(initialTime, 0),
}
}
func (mt *mockTime) now() time.Time {
return mt.time
}
func (mt *mockTime) setTime(timestamp int64) {
mt.time = time.Unix(timestamp, 0)
}

View File

@@ -73,6 +73,7 @@ func newMockSecretStore() *mockSecretStore {
type mockServiceLimiter struct {
capabilities map[lsat.Service]lsat.Caveat
constraints map[lsat.Service][]lsat.Caveat
timeouts map[lsat.Service]lsat.Caveat
}
var _ ServiceLimiter = (*mockServiceLimiter)(nil)
@@ -81,6 +82,7 @@ func newMockServiceLimiter() *mockServiceLimiter {
return &mockServiceLimiter{
capabilities: make(map[lsat.Service]lsat.Caveat),
constraints: make(map[lsat.Service][]lsat.Caveat),
timeouts: make(map[lsat.Service]lsat.Caveat),
}
}
@@ -111,3 +113,17 @@ func (l *mockServiceLimiter) ServiceConstraints(ctx context.Context,
}
return res, nil
}
func (l *mockServiceLimiter) ServiceTimeouts(ctx context.Context,
services ...lsat.Service) ([]lsat.Caveat, error) {
res := make([]lsat.Caveat, 0, len(services))
for _, service := range services {
timeouts, ok := l.timeouts[service]
if !ok {
continue
}
res = append(res, timeouts)
}
return res, nil
}

View File

@@ -72,6 +72,12 @@ type Service struct {
// the file is sent encoded as base64.
Headers map[string]string `long:"headers" description:"Header fields to always pass to the service"`
// Timeout is an optional value that indicates in how many seconds the
// service's caveat should time out relative to the time of creation. So
// if a value of 100 is set, then the timeout will be 100 seconds
// after creation of the LSAT.
Timeout int64 `long:"timeout" description:"An integer value that indicates the number of seconds until the service access expires"`
// Capabilities is the list of capabilities authorized for the service
// at the base tier.
Capabilities string `long:"capabilities" description:"A comma-separated list of the service capabilities authorized for the base tier"`

View File

@@ -84,7 +84,13 @@ services:
# The set of constraints that are applied to tokens of the service at the
# base tier.
constraints:
"valid_until": "2020-01-01"
# This is just an example of how aperture could be extended
# but would not have any effect without additional support added.
"valid_until": 1682483169
# a caveat will be added that expires the LSAT after this many seconds,
# 31557600 = 1 year.
timeout: 31557600
# The LSAT value in satoshis for the service. It is ignored if
# dynamicprice.enabled is set to true.

View File

@@ -2,6 +2,7 @@ package aperture
import (
"context"
"time"
"github.com/lightninglabs/aperture/lsat"
"github.com/lightninglabs/aperture/mint"
@@ -14,6 +15,7 @@ import (
type staticServiceLimiter struct {
capabilities map[lsat.Service]lsat.Caveat
constraints map[lsat.Service][]lsat.Caveat
timeouts map[lsat.Service]lsat.Caveat
}
// A compile-time constraint to ensure staticServiceLimiter implements
@@ -22,9 +24,12 @@ var _ mint.ServiceLimiter = (*staticServiceLimiter)(nil)
// newStaticServiceLimiter instantiates a new static service limiter backed by
// the given restrictions.
func newStaticServiceLimiter(proxyServices []*proxy.Service) *staticServiceLimiter {
func newStaticServiceLimiter(
proxyServices []*proxy.Service) *staticServiceLimiter {
capabilities := make(map[lsat.Service]lsat.Caveat)
constraints := make(map[lsat.Service][]lsat.Caveat)
timeouts := make(map[lsat.Service]lsat.Caveat)
for _, proxyService := range proxyServices {
s := lsat.Service{
@@ -32,6 +37,15 @@ func newStaticServiceLimiter(proxyServices []*proxy.Service) *staticServiceLimit
Tier: lsat.BaseTier,
Price: proxyService.Price,
}
if proxyService.Timeout > 0 {
timeouts[s] = lsat.NewTimeoutCaveat(
proxyService.Name,
proxyService.Timeout,
time.Now,
)
}
capabilities[s] = lsat.NewCapabilitiesCaveat(
proxyService.Name, proxyService.Capabilities,
)
@@ -44,6 +58,7 @@ func newStaticServiceLimiter(proxyServices []*proxy.Service) *staticServiceLimit
return &staticServiceLimiter{
capabilities: capabilities,
constraints: constraints,
timeouts: timeouts,
}
}
@@ -80,3 +95,20 @@ func (l *staticServiceLimiter) ServiceConstraints(ctx context.Context,
return res, nil
}
// ServiceTimeouts returns the timeout caveat for each service. This enforces
// an expiration time for service access if enabled.
func (l *staticServiceLimiter) ServiceTimeouts(ctx context.Context,
services ...lsat.Service) ([]lsat.Caveat, error) {
res := make([]lsat.Caveat, 0, len(services))
for _, service := range services {
timeout, ok := l.timeouts[service]
if !ok {
continue
}
res = append(res, timeout)
}
return res, nil
}