Skip to content

Commit

Permalink
check for mismatch on tlv2 credential submissions across request ids (#…
Browse files Browse the repository at this point in the history
…2331)

* check for mismatch on tlv2 credential submissions across request ids

review feedback, change response time for aretimelimitedv2credssubmitted

adding a unit test for mismatch

linting issues resolved

* updates based on review feedback

* adding query test as per review feedback

* updates to datastore interface for method signature change
  • Loading branch information
husobee authored Feb 6, 2024
1 parent 62d5bd9 commit 0780c62
Show file tree
Hide file tree
Showing 7 changed files with 716 additions and 62 deletions.
6 changes: 6 additions & 0 deletions services/skus/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,9 @@ func CreateOrderCreds(svc *Service) handlers.AppHandler {

if err := svc.CreateOrderItemCredentials(ctx, *orderID.UUID(), req.ItemID, reqID, req.BlindedCreds); err != nil {
lg.Error().Err(err).Msg("failed to create the order credentials")
if errors.Is(err, errCredsAlreadySubmittedMismatch) {
return handlers.WrapError(err, "Order credentials already exist", http.StatusConflict)
}
return handlers.WrapError(err, "Error creating order creds", http.StatusBadRequest)
}

Expand Down Expand Up @@ -651,6 +654,9 @@ func createItemCreds(svc *Service) handlers.AppHandler {

if err := svc.CreateOrderItemCredentials(ctx, *orderID.UUID(), *itemID.UUID(), *reqID.UUID(), req.BlindedCreds); err != nil {
lg.Error().Err(err).Msg("failed to create the order credentials")
if errors.Is(err, errCredsAlreadySubmittedMismatch) {
return handlers.WrapError(err, "Order credentials already exist", http.StatusConflict)
}
return handlers.WrapError(err, "Error creating order creds", http.StatusBadRequest)
}

Expand Down
29 changes: 17 additions & 12 deletions services/skus/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ var (
ErrOrderHasNoItems model.Error = "order has no items"
ErrCredsAlreadyExist model.Error = "credentials already exist"

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"
errUnsupportedCredType model.Error = "unsupported credential type"
errItemDoesNotExist model.Error = "order item does not exist for order"
errCredsAlreadySubmitted model.Error = "credentials already submitted"
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"
errUnsupportedCredType model.Error = "unsupported credential type"
errItemDoesNotExist model.Error = "order item does not exist for order"
errCredsAlreadySubmitted model.Error = "credentials already submitted"
errCredsAlreadySubmittedMismatch model.Error = "credentials already submitted with a different request"

defaultExpiresAt = time.Now().Add(17532 * time.Hour) // 2 years
retryPolicy = retrypolicy.DefaultRetry
Expand Down Expand Up @@ -254,7 +255,7 @@ func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID, itemI
return errItemDoesNotExist
}

if err := s.doCredentialsExist(ctx, orderItem, blindedCreds); err != nil {
if err := s.doCredentialsExist(ctx, requestID, orderItem, blindedCreds); err != nil {
if errors.Is(err, errCredsAlreadySubmitted) {
return nil
}
Expand Down Expand Up @@ -307,7 +308,7 @@ func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID, itemI
return nil
}

func (s *Service) doCredentialsExist(ctx context.Context, item *model.OrderItem, blindedCreds []string) error {
func (s *Service) doCredentialsExist(ctx context.Context, requestID uuid.UUID, item *model.OrderItem, blindedCreds []string) error {
switch item.CredentialType {
case timeLimitedV2:
// NOTE: This creates a possible race to submit between clients.
Expand All @@ -318,27 +319,31 @@ func (s *Service) doCredentialsExist(ctx context.Context, item *model.OrderItem,
// As a result, one client will successfully unblind the credentials and
// the others will fail.

return s.doTLV2Exist(ctx, item, blindedCreds)
return s.doTLV2Exist(ctx, requestID, item, blindedCreds)
default:
return s.doCredsExist(ctx, item)
}
}

func (s *Service) doTLV2Exist(ctx context.Context, item *model.OrderItem, blindedCreds []string) error {
func (s *Service) doTLV2Exist(ctx context.Context, requestID uuid.UUID, item *model.OrderItem, blindedCreds []string) error {
if item.CredentialType != timeLimitedV2 {
return errUnsupportedCredType
}

// Check TLV2 to see if we have credentials signed that match incoming blinded tokens.
alreadySubmitted, err := s.Datastore.AreTimeLimitedV2CredsSubmitted(ctx, blindedCreds...)
credsSubmitted, err := s.Datastore.AreTimeLimitedV2CredsSubmitted(ctx, requestID, blindedCreds...)
if err != nil {
return fmt.Errorf("error validating credentials exist for order item: %w", err)
}

if alreadySubmitted {
if credsSubmitted.AlreadySubmitted {
// No need to create order credentials, since these are already submitted.
return errCredsAlreadySubmitted
}
if credsSubmitted.Mismatch {
// conflict because those credentials were submitted with a different request id
return errCredsAlreadySubmittedMismatch
}

// Check if we have signed credentials for this order item.
// If there is no order and no creds, we can submit again.
Expand Down
36 changes: 26 additions & 10 deletions services/skus/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ type Datastore interface {
GetOrderCreds(orderID uuid.UUID, isSigned bool) ([]OrderCreds, error)
SendSigningRequest(ctx context.Context, signingRequestWriter SigningRequestWriter) error
InsertSignedOrderCredentialsTx(ctx context.Context, tx *sqlx.Tx, signedOrderResult *SigningOrderResult) error
AreTimeLimitedV2CredsSubmitted(ctx context.Context, blindedCreds ...string) (bool, error)
AreTimeLimitedV2CredsSubmitted(ctx context.Context, requestID uuid.UUID, blindedCreds ...string) (AreTimeLimitedV2CredsSubmittedResult, error)
GetTimeLimitedV2OrderCredsByOrder(orderID uuid.UUID) (*TimeLimitedV2Creds, error)
GetTLV2Creds(ctx context.Context, dbi sqlx.QueryerContext, ordID, itemID, reqID uuid.UUID) (*TimeLimitedV2Creds, error)
DeleteTimeLimitedV2OrderCredsByOrderTx(ctx context.Context, tx *sqlx.Tx, orderID uuid.UUID) error
Expand Down Expand Up @@ -950,24 +950,40 @@ type TimeAwareSubIssuedCreds struct {
RequestID string `json:"-" db:"request_id"`
}

func (pg *Postgres) AreTimeLimitedV2CredsSubmitted(ctx context.Context, blindedCreds ...string) (bool, error) {
type AreTimeLimitedV2CredsSubmittedResult struct {
AlreadySubmitted bool `db:"already_submitted"`
Mismatch bool `db:"mismatch"`
}

func (pg *Postgres) AreTimeLimitedV2CredsSubmitted(ctx context.Context, requestID uuid.UUID, blindedCreds ...string) (AreTimeLimitedV2CredsSubmittedResult, error) {
return areTimeLimitedV2CredsSubmitted(ctx, pg.RawDB(), requestID, blindedCreds...)
}

type getContext interface {
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
}

func areTimeLimitedV2CredsSubmitted(ctx context.Context, dbi getContext, requestID uuid.UUID, blindedCreds ...string) (AreTimeLimitedV2CredsSubmittedResult, error) {
var result = AreTimeLimitedV2CredsSubmittedResult{}

if len(blindedCreds) < 1 {
return false, errors.New("invalid parameter to tlv2 creds signed")
return result, errors.New("invalid parameter to tlv2 creds signed")
}

query := `
const query = `
select exists(
select 1 from time_limited_v2_order_creds where blinded_creds->>0 = $1
)
) as already_submitted,
exists(
select 1 from time_limited_v2_order_creds where blinded_creds->>0 = $1 and request_id != $2
) as mismatch
`

var alreadySubmitted bool
err := pg.RawDB().Get(&alreadySubmitted, query, blindedCreds[0])
err := dbi.GetContext(ctx, &result, query, blindedCreds[0], requestID)
if err != nil {
return false, err
return result, err
}

return alreadySubmitted, nil
return result, nil
}

// GetTimeLimitedV2OrderCredsByOrder returns all the non expired time limited v2 order credentials for a given order.
Expand Down
70 changes: 70 additions & 0 deletions services/skus/datastore_noint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package skus

import (
"context"
"testing"

"github.com/golang/mock/gomock"
uuid "github.com/satori/go.uuid"
should "github.com/stretchr/testify/assert"
)

type mockGetContext struct {
getContext func(ctx context.Context, dest interface{}, query string, args ...interface{}) error
}

func (mgc *mockGetContext) GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
if mgc.getContext != nil {
return mgc.getContext(ctx, dest, query, args)
}
return nil
}

func TestAreTimeLimitedV2CredsSubmitted(t *testing.T) {
type tcExpected struct {
result map[string]bool
noErr bool
}

type testCase struct {
name string
dbi getContext
given uuid.UUID
exp tcExpected
}

ctrl := gomock.NewController(t)
defer ctrl.Finish()

tests := []testCase{
{
name: "mismatch",
dbi: &mockGetContext{
getContext: func(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
*dest.(*AreTimeLimitedV2CredsSubmittedResult) = AreTimeLimitedV2CredsSubmittedResult{
AlreadySubmitted: true,
Mismatch: true,
}
return nil
},
},
given: uuid.Must(uuid.FromString("8f51f9ca-b593-4200-9bfb-91ac34748e09")),
exp: tcExpected{
noErr: true,
result: map[string]bool{
"mismatch": true,
},
},
},
}

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

t.Run(tc.name, func(t *testing.T) {
result, err := areTimeLimitedV2CredsSubmitted(context.TODO(), tc.dbi, tc.given, "")
should.Equal(t, tc.exp.result["mismatch"], result.Mismatch)
should.Equal(t, tc.exp.noErr, err == nil)
})
}
}
95 changes: 95 additions & 0 deletions services/skus/datastore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/lib/pq"
uuid "github.com/satori/go.uuid"
"github.com/shopspring/decimal"
should "github.com/stretchr/testify/assert"
must "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"

Expand Down Expand Up @@ -664,3 +665,97 @@ func (suite *PostgresTestSuite) createOrderCreds(t *testing.T, ctx context.Conte

return orderCredentials
}

func (suite *PostgresTestSuite) TestAreTimeLimitedV2CredsSubmitted() {
type tcGiven struct {
reqID uuid.UUID
blindedCreds []string
timeAwareCrds TimeAwareSubIssuedCreds
}

type exp struct {
submittedCreds AreTimeLimitedV2CredsSubmittedResult
mustErr must.ErrorAssertionFunc
}

type testCases struct {
name string
given tcGiven
exp exp
}

tests := []testCases{
{
name: "no_creds",
exp: exp{
mustErr: func(t must.TestingT, err error, i ...interface{}) {
must.ErrorContains(t, err, "invalid parameter to tlv2 creds signed")
},
},
},
{
name: "already_submitted_true_and_mismatch_true",
given: tcGiven{
reqID: uuid.NewV4(),
timeAwareCrds: TimeAwareSubIssuedCreds{
RequestID: uuid.NewV4().String(),
BlindedCreds: []string{"test-cred"},
},
blindedCreds: []string{"test-cred"},
},
exp: exp{
submittedCreds: AreTimeLimitedV2CredsSubmittedResult{AlreadySubmitted: true, Mismatch: true},
mustErr: func(t must.TestingT, err error, i ...interface{}) {
must.NoError(t, err)
},
},
},
}

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

suite.T().Cleanup(func() {
_, err := suite.storage.RawDB().Exec("truncate table time_limited_v2_order_creds")
must.NoError(suite.T(), err)
})

suite.T().Run(tc.name, func(t *testing.T) {

ctx := context.Background()

{
ir := repository.NewIssuer()
issuer, err1 := ir.Create(ctx, suite.storage.RawDB(), model.IssuerNew{
MerchantID: test.RandomString(),
PublicKey: test.RandomString(),
})
must.NoError(t, err1)

order := &model.OrderNew{Status: OrderStatusPending}
items := []model.OrderItem{{}}

o, err2 := suite.storage.CreateOrder(ctx, suite.storage.RawDB(), order, items)
must.NoError(t, err2)

tx, err3 := suite.storage.BeginTx()
must.NoError(t, err3)

tc.given.timeAwareCrds.IssuerID = issuer.ID
tc.given.timeAwareCrds.OrderID = o.ID
tc.given.timeAwareCrds.ItemID = o.Items[0].ID

err4 := suite.storage.InsertTimeLimitedV2OrderCredsTx(ctx, tx, tc.given.timeAwareCrds)
must.NoError(t, err4)

err5 := tx.Commit()
must.NoError(t, err5)
}

actual, err := suite.storage.AreTimeLimitedV2CredsSubmitted(ctx, uuid.NewV4(), tc.given.blindedCreds...)
tc.exp.mustErr(t, err)

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

0 comments on commit 0780c62

Please sign in to comment.