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
review feedback, change response time for aretimelimitedv2credssubmitted

adding a unit test for mismatch
  • Loading branch information
husobee committed Feb 5, 2024
1 parent 6b09228 commit bdfe3a0
Show file tree
Hide file tree
Showing 6 changed files with 579 additions and 61 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
34 changes: 25 additions & 9 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) {
if len(blindedCreds) < 1 {
return false, errors.New("invalid parameter to tlv2 creds signed")
return nil, errors.New("invalid parameter to tlv2 creds signed")
}

var result = AreTimeLimitedV2CredsSubmittedResult{}

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 nil, err
}

return alreadySubmitted, nil
return &result, nil
}

// GetTimeLimitedV2OrderCredsByOrder returns all the non expired time limited v2 order credentials for a given order.
Expand Down
71 changes: 71 additions & 0 deletions services/skus/datastore_noint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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
ok bool

Check failure on line 26 in services/skus/datastore_noint_test.go

View workflow job for this annotation

GitHub Actions / lint

field `ok` is unused (unused)
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)
})
}
}
32 changes: 16 additions & 16 deletions services/skus/instrumented_datastore.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit bdfe3a0

Please sign in to comment.