Skip to content

Commit

Permalink
validate blinded tokens submitted for tlv2 (#2158)
Browse files Browse the repository at this point in the history
* validation of tlv2 time limited v2 blinded tokens submitted, fixes: #2157

* use default of 2 per interval credentials in this test

* review feedback

* revert back the test to original

* adding test to excercise check

* Patches For 2158 (#2160)

* Refactor code and tests

* Remove unnecessary type conversion

* Use better names

---------

Co-authored-by: Pavel Brm <[email protected]>
  • Loading branch information
husobee and pavelbrm authored Oct 20, 2023
1 parent 48d76c5 commit cc9860c
Show file tree
Hide file tree
Showing 3 changed files with 340 additions and 5 deletions.
44 changes: 39 additions & 5 deletions services/skus/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ var (
ErrOrderUnpaid = errors.New("order not paid")
ErrOrderHasNoItems = errors.New("order has no items")

errInvalidIssuerResp model.Error = "invalid issuer response"
errInvalidIssuerResp model.Error = "invalid issuer response"
errInvalidNCredsSingleUse model.Error = "submitted more blinded creds than quantity of order item"
errInvalidNCredsTlv2 model.Error = "submitted more blinded creds than allowed for order"

defaultExpiresAt = time.Now().Add(17532 * time.Hour) // 2 years
retryPolicy = retrypolicy.DefaultRetry
Expand Down Expand Up @@ -247,10 +249,8 @@ func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID uuid.U
return errors.New("order item does not exist for order")
}

if orderItem.CredentialType == "single-use" {
if len(blindedCreds) > orderItem.Quantity {
return errors.New("submitted more blinded creds than quantity of order item")
}
if err := checkNumBlindedCreds(order, orderItem, len(blindedCreds)); err != nil {
return err
}

issuerID, err := encodeIssuerID(order.MerchantID, orderItem.SKU)
Expand Down Expand Up @@ -649,3 +649,37 @@ func (s *Service) DeleteOrderCreds(ctx context.Context, orderID uuid.UUID, isSig

return nil
}

// checkNumBlindedCreds checks the number of submitted blinded credentials.
//
// The number of submitted credentials must not exceed:
// - for single-use the quantity of the item;
// - for time-limited-v2 the product of numPerInterval and numIntervals.
func checkNumBlindedCreds(ord *model.Order, item *model.OrderItem, ncreds int) error {
switch item.CredentialType {
case singleUse:
if ncreds > item.Quantity {
return errInvalidNCredsSingleUse
}

return nil
case timeLimitedV2:
nperInterval, err := ord.NumPerInterval()
if err != nil {
return err
}

nintervals, err := ord.NumIntervals()
if err != nil {
return err
}

if ncreds > nperInterval*nintervals {
return errInvalidNCredsTlv2
}

return nil
default:
return nil
}
}
55 changes: 55 additions & 0 deletions services/skus/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"net/url"
"sort"
"strconv"
"time"

"github.com/lib/pq"
Expand All @@ -32,10 +33,17 @@ const (
ErrInvalidOrderNoCancelURL Error = "model: invalid order: no cancel url"
ErrInvalidOrderNoProductID Error = "model: invalid order: no product id"

ErrNumPerIntervalNotSet Error = "model: invalid order: numPerInterval must be set"
ErrNumIntervalsNotSet Error = "model: invalid order: numIntervals must be set"
ErrInvalidNumPerInterval Error = "model: invalid order: invalid numPerInterval"
ErrInvalidNumIntervals Error = "model: invalid order: invalid numIntervals"

// The text of the following errors is preserved as is, in case anything depends on them.
ErrInvalidSKU Error = "Invalid SKU Token provided in request"
ErrDifferentPaymentMethods Error = "all order items must have the same allowed payment methods"
ErrInvalidOrderRequest Error = "model: no items to be created"

errInvalidNumConversion Error = "model: invalid numeric conversion"
)

const (
Expand Down Expand Up @@ -246,6 +254,34 @@ func (o *Order) GetTrialDays() int64 {
return *o.TrialDays
}

func (o *Order) NumPerInterval() (int, error) {
numRaw, ok := o.Metadata["numPerInterval"]
if !ok {
return 0, ErrNumPerIntervalNotSet
}

result, err := numFromAny(numRaw)
if err != nil {
return 0, ErrInvalidNumPerInterval
}

return result, nil
}

func (o *Order) NumIntervals() (int, error) {
numRaw, ok := o.Metadata["numIntervals"]
if !ok {
return 0, ErrNumIntervalsNotSet
}

result, err := numFromAny(numRaw)
if err != nil {
return 0, ErrInvalidNumIntervals
}

return result, nil
}

// OrderItem represents a particular order item.
type OrderItem struct {
ID uuid.UUID `json:"id" db:"id"`
Expand Down Expand Up @@ -568,3 +604,22 @@ func addURLParam(src, name, val string) (string, error) {

return raw.String(), nil
}

func numFromAny(raw any) (int, error) {
switch v := raw.(type) {
case int:
return v, nil
case int64:
return int(v), nil
case int32:
return int(v), nil
case float32:
return int(v), nil
case float64:
return int(v), nil
case string:
return strconv.Atoi(v)
default:
return 0, errInvalidNumConversion
}
}
246 changes: 246 additions & 0 deletions services/skus/service_nonint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package skus

import (
"testing"

uuid "github.com/satori/go.uuid"
should "github.com/stretchr/testify/assert"

"github.com/brave-intl/bat-go/libs/datastore"

"github.com/brave-intl/bat-go/services/skus/model"
)

func TestCheckNumBlindedCreds(t *testing.T) {
type tcGiven struct {
ord *model.Order
item *model.OrderItem
ncreds int
}

type testCase struct {
name string
given tcGiven
exp error
}

tests := []testCase{
{
name: "irrelevant_credential_type",
given: tcGiven{
ord: &model.Order{
ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
},
item: &model.OrderItem{
ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")),
OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
CredentialType: timeLimited,
},
},
},

{
name: "single_use_valid_1",
given: tcGiven{
ord: &model.Order{
ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
},
item: &model.OrderItem{
ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")),
OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
CredentialType: singleUse,
Quantity: 1,
},
ncreds: 1,
},
},

{
name: "single_use_valid_2",
given: tcGiven{
ord: &model.Order{
ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
},
item: &model.OrderItem{
ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")),
OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
CredentialType: singleUse,
Quantity: 2,
},
ncreds: 1,
},
},

{
name: "single_use_invalid",
given: tcGiven{
ord: &model.Order{
ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
},
item: &model.OrderItem{
ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")),
OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
CredentialType: singleUse,
Quantity: 2,
},
ncreds: 3,
},
exp: errInvalidNCredsSingleUse,
},

{
name: "tlv2_invalid_numPerInterval_missing",
given: tcGiven{
ord: &model.Order{
ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
Metadata: datastore.Metadata{},
},
item: &model.OrderItem{
ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")),
OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
CredentialType: timeLimitedV2,
Quantity: 1,
},
ncreds: 6,
},
exp: model.ErrNumPerIntervalNotSet,
},

{
name: "tlv2_invalid_numPerInterval_invalid",
given: tcGiven{
ord: &model.Order{
ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
Metadata: datastore.Metadata{
"numPerInterval": "NaN",
},
},
item: &model.OrderItem{
ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")),
OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
CredentialType: timeLimitedV2,
Quantity: 1,
},
ncreds: 6,
},
exp: model.ErrInvalidNumPerInterval,
},

{
name: "tlv2_invalid_numIntervals_missing",
given: tcGiven{
ord: &model.Order{
ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
Metadata: datastore.Metadata{
// We get a float64 upon fetching from the database.
"numPerInterval": float64(2),
},
},
item: &model.OrderItem{
ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")),
OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
CredentialType: timeLimitedV2,
Quantity: 1,
},
ncreds: 6,
},
exp: model.ErrNumIntervalsNotSet,
},

{
name: "tlv2_invalid_numIntervals_invalid",
given: tcGiven{
ord: &model.Order{
ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
Metadata: datastore.Metadata{
// We get a float64 upon fetching from the database.
"numPerInterval": float64(2),
"numIntervals": "NaN",
},
},
item: &model.OrderItem{
ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")),
OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
CredentialType: timeLimitedV2,
Quantity: 1,
},
ncreds: 6,
},
exp: model.ErrInvalidNumIntervals,
},

{
name: "tlv2_valid_1",
given: tcGiven{
ord: &model.Order{
ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
Metadata: datastore.Metadata{
// We get a float64 upon fetching from the database.
"numPerInterval": float64(2),
"numIntervals": float64(3),
},
},
item: &model.OrderItem{
ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")),
OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
CredentialType: timeLimitedV2,
Quantity: 1,
},
ncreds: 6,
},
},

{
name: "tlv2_valid_2",
given: tcGiven{
ord: &model.Order{
ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
Metadata: datastore.Metadata{
// We get a float64 upon fetching from the database.
"numPerInterval": float64(2),
"numIntervals": float64(4),
},
},
item: &model.OrderItem{
ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")),
OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
CredentialType: timeLimitedV2,
Quantity: 1,
},
ncreds: 6,
},
},

{
name: "tlv2_invalid",
given: tcGiven{
ord: &model.Order{
ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
Metadata: datastore.Metadata{
// We get a float64 upon fetching from the database.
"numPerInterval": float64(2),
"numIntervals": float64(3),
},
},
item: &model.OrderItem{
ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")),
OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")),
CredentialType: timeLimitedV2,
Quantity: 1,
},
ncreds: 7,
},
exp: errInvalidNCredsTlv2,
},
}

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

t.Run(tc.name, func(t *testing.T) {
actual := checkNumBlindedCreds(tc.given.ord, tc.given.item, tc.given.ncreds)

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

0 comments on commit cc9860c

Please sign in to comment.