mirror of
https://github.com/lightninglabs/aperture.git
synced 2026-01-25 12:14:31 +01:00
Merge pull request #81 from bucko13/poc-creation-time-caveat
Timeout Caveat Support
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,3 +13,6 @@
|
||||
|
||||
/aperture
|
||||
cmd/aperture/aperture
|
||||
|
||||
# misc
|
||||
.vscode
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
87
lsat/satisfier_test.go
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
17
mint/mint.go
17
mint/mint.go
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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.
|
||||
|
||||
34
services.go
34
services.go
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user