Skip to content

Commit

Permalink
fix: allow leeway to reduce errors during key rotation
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelbrm committed May 28, 2024
1 parent b66333c commit 69f1854
Show file tree
Hide file tree
Showing 9 changed files with 920 additions and 125 deletions.
31 changes: 19 additions & 12 deletions btd/issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,32 +113,39 @@ func ApproveTokens(blindedTokens []*crypto.BlindedToken, key *crypto.SigningKey)
}

// VerifyTokenRedemption checks a redemption request against the observed request data
// and MAC according a set of keys. keys keeps a set of private keys that
// are ever used to sign the token so we can rotate private key easily
// Returns nil on success and an error on failure.
// and MAC according a set of keys.
// Keys keeps a set of private keys that are ever used to sign the token so we can rotate private key easily.
func VerifyTokenRedemption(preimage *crypto.TokenPreimage, signature *crypto.VerificationSignature, payload string, keys []*crypto.SigningKey) error {
var valid bool
var err error
for _, key := range keys {

for i := range keys {
verifyTokenRedemptionCounter.Add(1)
// server derives the unblinded token using its key and the clients token preimage
unblindedToken := key.RederiveUnblindedToken(preimage)

// server derives the shared key from the unblinded token
timer := prometheus.NewTimer(verifyTokenDeriveKeyDuration)
// Derive the unblinded token using a server's key and the client's preimage.
unblindedToken := keys[i].RederiveUnblindedToken(preimage)

timerUT := prometheus.NewTimer(verifyTokenDeriveKeyDuration)

// Derive the shared key from the unblinded token.
sharedKey := unblindedToken.DeriveVerificationKey()
timer.ObserveDuration()
_ = timerUT.ObserveDuration()

timerVrf := prometheus.NewTimer(verifyTokenSignatureDuration)

// server signs the same message using the shared key and compares the client signature to its own
timer = prometheus.NewTimer(verifyTokenSignatureDuration)
// Sign the same message using the shared key and compare the client's signature with the server's.
valid, err = sharedKey.Verify(signature, payload)
if err != nil {
_ = timerVrf.ObserveDuration()

return err
}

_ = timerVrf.ObserveDuration()

if valid {
break
}
timer.ObserveDuration()
}

if !valid {
Expand Down
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
92 changes: 85 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,79 @@ 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) FindSigningKeys(now time.Time) ([]*crypto.SigningKey, error) {
if x.Version != 3 {
return nil, ErrInvalidIssuerType
}

const leeway = 1 * time.Hour

keys, err := x.findActiveKeys(now, leeway)
if err != nil {
return nil, err
}

if len(keys) == 0 {
return nil, nil
}

return parseSigningKeys(keys), nil
}

// findActiveKeys finds active keys in x.Keys that are active for time now.
//
// It searches for strictly matching key first, and places it at the first position of the result.
// Then it searches for keys that match with leeway lw.
// The strictly matching key is excluded from search with lw.
func (x *Issuer) findActiveKeys(now time.Time, lw time.Duration) ([]*IssuerKeys, error) {
var result []*IssuerKeys

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

if active {
result = append([]*IssuerKeys{&x.Keys[i]}, result...)
continue
}

activeLw, err := x.Keys[i].isActiveV3(now, lw)
if err != nil {
return nil, err
}

if activeLw {
result = append(result, &x.Keys[i])
}
}

return result, nil
}

func parseSigningKeys(keys []*IssuerKeys) []*crypto.SigningKey {
result := make([]*crypto.SigningKey, 0, len(keys))

for i := range keys {
if key := keys[i].CryptoSigningKey(); key != nil {
result = append(result, key)
}
}

return iss.ExpiresAt.Time
return result
}
39 changes: 31 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,33 @@ 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
}

func (x *IssuerKeys) isActiveV3(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)
}
Loading

0 comments on commit 69f1854

Please sign in to comment.