Skip to content

Commit

Permalink
fix: add leeway to issue key activity calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelbrm committed May 23, 2024
1 parent b66333c commit c1659b0
Show file tree
Hide file tree
Showing 8 changed files with 639 additions and 110 deletions.
13 changes: 8 additions & 5 deletions kafka/signed_token_redeem_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import (
"strings"
"time"

"github.com/brave-intl/challenge-bypass-server/model"
"github.com/rs/zerolog"
"github.com/segmentio/kafka-go"

crypto "github.com/brave-intl/challenge-bypass-ristretto-ffi"

avroSchema "github.com/brave-intl/challenge-bypass-server/avro/generated"
"github.com/brave-intl/challenge-bypass-server/btd"
"github.com/brave-intl/challenge-bypass-server/model"
cbpServer "github.com/brave-intl/challenge-bypass-server/server"
"github.com/brave-intl/challenge-bypass-server/utils"
"github.com/rs/zerolog"
"github.com/segmentio/kafka-go"
)

/*
Expand Down Expand Up @@ -94,10 +95,12 @@ func SignedTokenRedeemHandler(
)
}

// Create a lookup for issuers & signing keys based on public key
// Create a lookup for issuers & signing keys based on public key.
signedTokens := make(map[string]SignedIssuerToken)
now := time.Now()

for _, issuer := range issuers {
if !issuer.ExpiresAtTime().IsZero() && issuer.ExpiresAtTime().Before(time.Now()) {
if issuer.HasExpired(now) {
continue
}

Expand Down
52 changes: 45 additions & 7 deletions model/issuer.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package model

import (
"errors"
"time"

"github.com/google/uuid"
"github.com/lib/pq"
"time"

crypto "github.com/brave-intl/challenge-bypass-ristretto-ffi"
)

// Issuer of tokens
var (
ErrInvalidIssuerType = errors.New("model: invalid issuer type")
ErrInvalidIV3Key = errors.New("model: issuer_v3: invalid key")
ErrIssuerV3NoCryptoKey = errors.New("model: issuer_v3: no crypto signing key for period")
)

// Issuer represents an issuer of tokens.
type Issuer struct {
ID *uuid.UUID `json:"id" db:"issuer_id"`
IssuerType string `json:"issuer_type" db:"issuer_type"`
Expand All @@ -26,11 +36,39 @@ type Issuer struct {
Keys []IssuerKeys `json:"keys" db:"-"`
}

func (iss *Issuer) ExpiresAtTime() time.Time {
var t time.Time
if !iss.ExpiresAt.Valid {
return t
func (x *Issuer) ExpiresAtTime() time.Time {
if !x.ExpiresAt.Valid {
return time.Time{}
}

return x.ExpiresAt.Time
}

func (x *Issuer) HasExpired(now time.Time) bool {
expt := x.ExpiresAtTime()

return !expt.IsZero() && expt.Before(now)
}

func (x *Issuer) FindSigningKey(now time.Time) (*crypto.SigningKey, error) {
if x.Version != 3 {
return nil, ErrInvalidIssuerType
}

const leeway = 1 * time.Hour

for i := range x.Keys {
active, err := x.Keys[i].isActiveV3Leeway(now, leeway)
if err != nil {
return nil, err
}

if active {
if key := x.Keys[i].CryptoSigningKey(); key != nil {
return key, nil
}
}
}

return iss.ExpiresAt.Time
return nil, ErrIssuerV3NoCryptoKey
}
44 changes: 36 additions & 8 deletions model/issuer_keys.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package model

import (
crypto "github.com/brave-intl/challenge-bypass-ristretto-ffi"
"github.com/google/uuid"
"time"

"github.com/google/uuid"

crypto "github.com/brave-intl/challenge-bypass-ristretto-ffi"
)

// IssuerKeys - an issuer that uses time based keys
// IssuerKeys represents time-based keys.
type IssuerKeys struct {
ID *uuid.UUID `json:"id" db:"key_id"`
SigningKey []byte `json:"-" db:"signing_key"`
Expand All @@ -18,12 +20,38 @@ type IssuerKeys struct {
EndAt *time.Time `json:"end_at" db:"end_at"`
}

func (key *IssuerKeys) CryptoSigningKey() *crypto.SigningKey {
cryptoSigningKey := crypto.SigningKey{}
err := cryptoSigningKey.UnmarshalText(key.SigningKey)
if err != nil {
func (x *IssuerKeys) CryptoSigningKey() *crypto.SigningKey {
result := &crypto.SigningKey{}
if err := result.UnmarshalText(x.SigningKey); err != nil {
return nil
}

return &cryptoSigningKey
return result
}

//nolint:unused
func (x *IssuerKeys) isActiveV3(now time.Time) (bool, error) {
return x.isActiveV3Leeway(now, 0)
}

func (x *IssuerKeys) isActiveV3Leeway(now time.Time, lw time.Duration) (bool, error) {
if !x.isValidV3() {
return false, ErrInvalidIV3Key
}

start, end := *x.StartAt, *x.EndAt
if lw == 0 {
return isTimeWithin(start, end, now), nil
}

// Shift start/end earlier/later by lw, respectively.
return isTimeWithin(start.Add(-1*lw), end.Add(lw), now), nil
}

func (x *IssuerKeys) isValidV3() bool {
return x.StartAt != nil && x.EndAt != nil
}

func isTimeWithin(start, end, now time.Time) bool {
return now.After(start) && now.Before(end)
}
201 changes: 201 additions & 0 deletions model/issuer_keys_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package model

import (
"testing"
"time"

should "github.com/stretchr/testify/assert"
must "github.com/stretchr/testify/require"
)

func TestIssuerKeys_isActiveV3Leeway(t *testing.T) {
type tcGiven struct {
key *IssuerKeys
now time.Time
lw time.Duration
}

type tcExpected struct {
val bool
err error
}

type testCase struct {
name string
given tcGiven
exp tcExpected
}

tests := []testCase{
{
name: "invalid_v3",
given: tcGiven{
key: &IssuerKeys{},
now: time.Date(2024, time.January, 1, 0, 0, 1, 0, time.UTC),
lw: 1 * time.Hour,
},
exp: tcExpected{err: ErrInvalidIV3Key},
},

{
name: "zero_leeway",
given: tcGiven{
key: &IssuerKeys{
StartAt: ptrTo(time.Date(2023, time.December, 31, 0, 0, 1, 0, time.UTC)),
EndAt: ptrTo(time.Date(2024, time.January, 2, 0, 0, 1, 0, time.UTC)),
},
now: time.Date(2024, time.January, 1, 0, 0, 1, 0, time.UTC),
},
exp: tcExpected{val: true},
},

{
name: "leeway_1hour",
given: tcGiven{
key: &IssuerKeys{
StartAt: ptrTo(time.Date(2023, time.December, 31, 0, 0, 1, 0, time.UTC)),
EndAt: ptrTo(time.Date(2024, time.January, 2, 0, 0, 1, 0, time.UTC)),
},
now: time.Date(2024, time.January, 2, 0, 0, 1, 0, time.UTC),
lw: 1 * time.Hour,
},
exp: tcExpected{val: true},
},
}

for i := range tests {
tc := tests[i]

t.Run(tc.name, func(t *testing.T) {
actual, err := tc.given.key.isActiveV3Leeway(tc.given.now, tc.given.lw)
must.Equal(t, tc.exp.err, err)

should.Equal(t, tc.exp.val, actual)
})
}
}

func TestIssuerKeys_isValidV3(t *testing.T) {
type testCase struct {
name string
given *IssuerKeys
exp bool
}

tests := []testCase{
{
name: "invalid_both",
given: &IssuerKeys{},
},

{
name: "invalid_end",
given: &IssuerKeys{
StartAt: ptrTo(time.Date(2023, time.December, 31, 0, 0, 1, 0, time.UTC)),
},
},

{
name: "invalid_start",
given: &IssuerKeys{
EndAt: ptrTo(time.Date(2024, time.January, 2, 0, 0, 1, 0, time.UTC)),
},
},

{
name: "valid",
given: &IssuerKeys{
StartAt: ptrTo(time.Date(2023, time.December, 31, 0, 0, 1, 0, time.UTC)),
EndAt: ptrTo(time.Date(2024, time.January, 2, 0, 0, 1, 0, time.UTC)),
},
exp: true,
},
}

for i := range tests {
tc := tests[i]

t.Run(tc.name, func(t *testing.T) {
actual := tc.given.isValidV3()
should.Equal(t, tc.exp, actual)
})
}
}

func TestIsTimeWithin(t *testing.T) {
type tcGiven struct {
start time.Time
end time.Time
now time.Time
}

type testCase struct {
name string
given tcGiven
exp bool
}

tests := []testCase{
{
name: "zero_all",
given: tcGiven{},
},

{
name: "zero_start_end",
given: tcGiven{
now: time.Date(2024, time.January, 1, 0, 0, 1, 0, time.UTC),
},
},

{
name: "zero_start_now",
given: tcGiven{
end: time.Date(2024, time.January, 2, 0, 0, 1, 0, time.UTC),
},
},

{
name: "zero_now_end",
given: tcGiven{
start: time.Date(2023, time.December, 31, 0, 0, 1, 0, time.UTC),
},
},

{
name: "zero_now",
given: tcGiven{
start: time.Date(2023, time.December, 31, 0, 0, 1, 0, time.UTC),
end: time.Date(2024, time.January, 2, 0, 0, 1, 0, time.UTC),
},
},

{
name: "invalid_inverse",
given: tcGiven{
start: time.Date(2024, time.January, 2, 0, 0, 1, 0, time.UTC),
end: time.Date(2023, time.December, 31, 0, 0, 1, 0, time.UTC),
now: time.Date(2024, time.January, 1, 0, 0, 1, 0, time.UTC),
},
},

{
name: "valid",
given: tcGiven{
start: time.Date(2023, time.December, 31, 0, 0, 1, 0, time.UTC),
end: time.Date(2024, time.January, 2, 0, 0, 1, 0, time.UTC),
now: time.Date(2024, time.January, 1, 0, 0, 1, 0, time.UTC),
},
exp: true,
},
}

for i := range tests {
tc := tests[i]

t.Run(tc.name, func(t *testing.T) {
actual := isTimeWithin(tc.given.start, tc.given.end, tc.given.now)
should.Equal(t, tc.exp, actual)
})
}
}
Loading

0 comments on commit c1659b0

Please sign in to comment.