Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix time drift between client and server when redeeming v3 #711

Merged
merged 1 commit into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
pavelbrm marked this conversation as resolved.
Show resolved Hide resolved
}

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()) {
pavelbrm marked this conversation as resolved.
Show resolved Hide resolved
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
Loading