Skip to content

Commit

Permalink
Revert "Revert "Production 2023-11-10_01""
Browse files Browse the repository at this point in the history
This reverts commit 252406a.
  • Loading branch information
clD11 committed Oct 12, 2023
1 parent 140e325 commit 2464609
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 126 deletions.
38 changes: 22 additions & 16 deletions services/skus/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -841,48 +841,54 @@ func MerchantTransactions(service *Service) handlers.AppHandler {

// VerifyCredentialV2 - version 2 of verify credential
func VerifyCredentialV2(service *Service) handlers.AppHandler {
return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError {
return func(w http.ResponseWriter, r *http.Request) *handlers.AppError {

ctx := r.Context()
logger := logging.Logger(ctx, "VerifyCredentialV2")
logger.Debug().Msg("starting VerifyCredentialV2 controller")
l := logging.Logger(ctx, "VerifyCredentialV2")

var req = new(VerifyCredentialRequestV2)
if err := inputs.DecodeAndValidateReader(ctx, req, r.Body); err != nil {
logger.Error().Err(err).Msg("failed to read request")
l.Error().Err(err).Msg("failed to read request")
return handlers.WrapError(err, "Error in request body", http.StatusBadRequest)
}

return service.verifyCredential(ctx, req, w)
})
appErr := service.verifyCredential(ctx, req, w)
if appErr != nil {
l.Error().Err(appErr).Msg("failed to verify credential")
}

return appErr
}
}

// VerifyCredentialV1 is the handler for verifying subscription credentials
func VerifyCredentialV1(service *Service) handlers.AppHandler {
return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError {
return func(w http.ResponseWriter, r *http.Request) *handlers.AppError {
ctx := r.Context()

logger := logging.Logger(r.Context(), "VerifyCredentialV1")
logger.Debug().Msg("starting VerifyCredentialV1 controller")
l := logging.Logger(r.Context(), "VerifyCredentialV1")

var req = new(VerifyCredentialRequestV1)

err := requestutils.ReadJSON(r.Context(), r.Body, &req)
if err != nil {
logger.Error().Err(err).Msg("failed to read request")
l.Error().Err(err).Msg("failed to read request")
return handlers.WrapError(err, "Error in request body", http.StatusBadRequest)
}
logger.Debug().Msg("read verify credential post body")
l.Debug().Msg("read verify credential post body")

_, err = govalidator.ValidateStruct(req)
if err != nil {
logger.Error().Err(err).Msg("failed to validate request")
l.Error().Err(err).Msg("failed to validate request")
return handlers.WrapError(err, "Error in request validation", http.StatusBadRequest)
}

logger.Debug().Msg("validated verify credential post body")
appErr := service.verifyCredential(ctx, req, w)
if appErr != nil {
l.Error().Err(appErr).Msg("failed to verify credential")
}

return service.verifyCredential(ctx, req, w)
})
return appErr
}
}

// WebhookRouter - handles calls from various payment method webhooks informing payments of completion
Expand Down
232 changes: 122 additions & 110 deletions services/skus/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1297,9 +1297,8 @@ type credential interface {
GetPresentation(context.Context) string
}

// TODO refactor this see issue #1502
// verifyCredential - given a credential, verify it.
func (s *Service) verifyCredential(ctx context.Context, req credential, w http.ResponseWriter) *handlers.AppError {
func (s *Service) verifyCredential(ctx context.Context, cred credential, w http.ResponseWriter) *handlers.AppError {
logger := logging.Logger(ctx, "verifyCredential")

merchant, err := GetMerchant(ctx)
Expand All @@ -1312,9 +1311,9 @@ func (s *Service) verifyCredential(ctx context.Context, req credential, w http.R

caveats := GetCaveats(ctx)

if req.GetMerchantID(ctx) != merchant {
if cred.GetMerchantID(ctx) != merchant {
logger.Warn().
Str("req.MerchantID", req.GetMerchantID(ctx)).
Str("req.MerchantID", cred.GetMerchantID(ctx)).
Str("merchant", merchant).
Msg("merchant does not match the key's merchant")
return handlers.WrapError(nil, "Verify request merchant does not match authentication", http.StatusForbidden)
Expand All @@ -1324,9 +1323,9 @@ func (s *Service) verifyCredential(ctx context.Context, req credential, w http.R

if caveats != nil {
if sku, ok := caveats["sku"]; ok {
if req.GetSku(ctx) != sku {
if cred.GetSku(ctx) != sku {
logger.Warn().
Str("req.SKU", req.GetSku(ctx)).
Str("req.SKU", cred.GetSku(ctx)).
Str("sku", sku).
Msg("sku caveat does not match")
return handlers.WrapError(nil, "Verify request sku does not match authentication", http.StatusForbidden)
Expand All @@ -1335,130 +1334,97 @@ func (s *Service) verifyCredential(ctx context.Context, req credential, w http.R
}
logger.Debug().Msg("caveats validated")

if req.GetType(ctx) == singleUse || req.GetType(ctx) == timeLimitedV2 {
var bytes []byte
bytes, err = base64.StdEncoding.DecodeString(req.GetPresentation(ctx))
if err != nil {
return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest)
}
kind := cred.GetType(ctx)
switch kind {
case singleUse, timeLimitedV2:
return s.verifyBlindedTokenCredential(ctx, cred, w)
case timeLimited:
return s.verifyTimeLimitedV1Credential(ctx, cred, w)
default:
return handlers.WrapError(nil, "Unknown credential type", http.StatusBadRequest)
}
}

var decodedCredential cbr.CredentialRedemption
err = json.Unmarshal(bytes, &decodedCredential)
if err != nil {
return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest)
}
// verifyBlindedTokenCredential verifies a single use or time limited v2 credential.
func (s *Service) verifyBlindedTokenCredential(ctx context.Context, req credential, w http.ResponseWriter) *handlers.AppError {
bytes, err := base64.StdEncoding.DecodeString(req.GetPresentation(ctx))
if err != nil {
return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest)
}

// Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details
issuerID, err := encodeIssuerID(req.GetMerchantID(ctx), req.GetSku(ctx))
if err != nil {
return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest)
}
if issuerID != decodedCredential.Issuer {
return handlers.WrapError(nil, "Error, outer merchant and sku don't match issuer", http.StatusBadRequest)
}
decodedCred := &cbr.CredentialRedemption{}
if err := json.Unmarshal(bytes, decodedCred); err != nil {
return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest)
}

switch req.GetType(ctx) {
case singleUse:
err = s.cbClient.RedeemCredential(ctx, decodedCredential.Issuer, decodedCredential.TokenPreimage,
decodedCredential.Signature, decodedCredential.Issuer)
case timeLimitedV2:
err = s.cbClient.RedeemCredentialV3(ctx, decodedCredential.Issuer, decodedCredential.TokenPreimage,
decodedCredential.Signature, decodedCredential.Issuer)
default:
return handlers.WrapError(fmt.Errorf("credential type %s not suppoted", req.GetType(ctx)),
"unknown credential type %s", http.StatusBadRequest)
}
// Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details.
issuerID, err := encodeIssuerID(req.GetMerchantID(ctx), req.GetSku(ctx))
if err != nil {
return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest)
}

if err != nil {
// if this is a duplicate redemption these are not verified
if err.Error() == cbr.ErrDupRedeem.Error() || err.Error() == cbr.ErrBadRequest.Error() {
return handlers.WrapError(err, "invalid credentials", http.StatusForbidden)
}
return handlers.WrapError(err, "Error verifying credentials", http.StatusInternalServerError)
}
if issuerID != decodedCred.Issuer {
return handlers.WrapError(nil, "Error, outer merchant and sku don't match issuer", http.StatusBadRequest)
}

return s.redeemBlindedCred(ctx, w, req.GetType(ctx), decodedCred)
}

// verifyTimeLimitedV1Credential verifies a time limited v1 credential.
func (s *Service) verifyTimeLimitedV1Credential(ctx context.Context, req credential, w http.ResponseWriter) *handlers.AppError {
data, err := base64.StdEncoding.DecodeString(req.GetPresentation(ctx))
if err != nil {
return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest)
}

return handlers.RenderContent(ctx, "Credentials successfully verified", w, http.StatusOK)
present := &tlv1CredPresentation{}
if err := json.Unmarshal(data, present); err != nil {
return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest)
}

if req.GetType(ctx) == "time-limited" {
// Presentation includes a token and token metadata test test
type Presentation struct {
IssuedAt string `json:"issuedAt"`
ExpiresAt string `json:"expiresAt"`
Token string `json:"token"`
}
merchID := req.GetMerchantID(ctx)

var bytes []byte
bytes, err = base64.StdEncoding.DecodeString(req.GetPresentation(ctx))
if err != nil {
logger.Error().Err(err).
Msg("failed to decode the request token presentation")
return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest)
}
logger.Debug().Str("presentation", string(bytes)).Msg("presentation decoded")
// Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details.
issuerID, err := encodeIssuerID(merchID, req.GetSku(ctx))
if err != nil {
return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest)
}

var presentation Presentation
err = json.Unmarshal(bytes, &presentation)
if err != nil {
logger.Error().Err(err).
Msg("failed to unmarshal the request token presentation")
return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest)
}
keys, err := s.GetCredentialSigningKeys(ctx, merchID)
if err != nil {
return handlers.WrapError(err, "failed to get merchant signing key", http.StatusInternalServerError)
}

logger.Debug().Str("presentation", string(bytes)).Msg("presentation unmarshalled")
issuedAt, err := time.Parse("2006-01-02", present.IssuedAt)
if err != nil {
return handlers.WrapError(err, "Error parsing issuedAt", http.StatusBadRequest)
}

// Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details
issuerID, err := encodeIssuerID(req.GetMerchantID(ctx), req.GetSku(ctx))
if err != nil {
logger.Error().Err(err).
Msg("failed to encode the issuer id")
return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest)
}
logger.Debug().Str("issuer", issuerID).Msg("issuer encoded")
expiresAt, err := time.Parse("2006-01-02", present.ExpiresAt)
if err != nil {
return handlers.WrapError(err, "Error parsing expiresAt", http.StatusBadRequest)
}

keys, err := s.GetCredentialSigningKeys(ctx, req.GetMerchantID(ctx))
if err != nil {
return handlers.WrapError(err, "failed to get merchant signing key", http.StatusInternalServerError)
}
for _, key := range keys {
timeLimitedSecret := cryptography.NewTimeLimitedSecret(key)

issuedAt, err := time.Parse("2006-01-02", presentation.IssuedAt)
if err != nil {
logger.Error().Err(err).
Msg("failed to parse issued at time of credential")
return handlers.WrapError(err, "Error parsing issuedAt", http.StatusBadRequest)
}
expiresAt, err := time.Parse("2006-01-02", presentation.ExpiresAt)
verified, err := timeLimitedSecret.Verify([]byte(issuerID), issuedAt, expiresAt, present.Token)
if err != nil {
logger.Error().Err(err).
Msg("failed to parse expires at time of credential")
return handlers.WrapError(err, "Error parsing expiresAt", http.StatusBadRequest)
return handlers.WrapError(err, "Error in token verification", http.StatusBadRequest)
}

for _, key := range keys {
timeLimitedSecret := cryptography.NewTimeLimitedSecret(key)
verified, err := timeLimitedSecret.Verify([]byte(issuerID), issuedAt, expiresAt, presentation.Token)
if err != nil {
logger.Error().Err(err).
Msg("failed to verify time limited credential")
return handlers.WrapError(err, "Error in token verification", http.StatusBadRequest)
if verified {
// Check against expiration time, issued time.
now := time.Now()
if now.After(expiresAt) || now.Before(issuedAt) {
return handlers.WrapError(nil, "Credentials are not valid", http.StatusForbidden)
}

if verified {
// check against expiration time, issued time
if time.Now().After(expiresAt) || time.Now().Before(issuedAt) {
logger.Error().
Msg("credentials are not valid")
return handlers.RenderContent(ctx, "Credentials are not valid", w, http.StatusForbidden)
}
logger.Debug().Msg("credentials verified")
return handlers.RenderContent(ctx, "Credentials successfully verified", w, http.StatusOK)
}
return handlers.RenderContent(ctx, "Credentials successfully verified", w, http.StatusOK)
}
logger.Error().
Msg("credentials could not be verified")
return handlers.RenderContent(ctx, "Credentials could not be verified", w, http.StatusForbidden)
}
return handlers.WrapError(nil, "Unknown credential type", http.StatusBadRequest)

return handlers.WrapError(nil, "Credentials could not be verified", http.StatusForbidden)
}

// RunSendSigningRequestJob - send the order credentials signing requests
Expand Down Expand Up @@ -1806,6 +1772,41 @@ func (s *Service) createStripeSessID(ctx context.Context, req *model.CreateOrder
return nil
}

func (s *Service) redeemBlindedCred(ctx context.Context, w http.ResponseWriter, kind string, cred *cbr.CredentialRedemption) *handlers.AppError {
var redeemFn func(ctx context.Context, issuer, preimage, signature, payload string) error

switch kind {
case singleUse:
redeemFn = s.cbClient.RedeemCredential
case timeLimitedV2:
redeemFn = s.cbClient.RedeemCredentialV3
default:
return handlers.WrapError(fmt.Errorf("credential type %s not suppoted", kind), "unknown credential type %s", http.StatusBadRequest)
}

// FIXME: we shouldn't be using the issuer as the payload, it ideally would be a unique request identifier
// to allow for more flexible idempotent behavior.
if err := redeemFn(ctx, cred.Issuer, cred.TokenPreimage, cred.Signature, cred.Issuer); err != nil {
msg := err.Error()

// Time limited v2: Expose a credential id so the caller can decide whether to allow multiple redemptions.
if kind == timeLimitedV2 && msg == cbr.ErrDupRedeem.Error() {
data := &blindedCredVrfResult{ID: cred.TokenPreimage, Duplicate: true}

return handlers.RenderContent(ctx, data, w, http.StatusOK)
}

// Duplicate redemptions are not verified.
if msg == cbr.ErrDupRedeem.Error() || msg == cbr.ErrBadRequest.Error() {
return handlers.WrapError(err, "invalid credentials", http.StatusForbidden)
}

return handlers.WrapError(err, "Error verifying credentials", http.StatusInternalServerError)
}

return handlers.RenderContent(ctx, &blindedCredVrfResult{ID: cred.TokenPreimage}, w, http.StatusOK)
}

func createOrderItems(req *model.CreateOrderRequestNew) ([]model.OrderItem, error) {
result := make([]model.OrderItem, 0)

Expand Down Expand Up @@ -1882,3 +1883,14 @@ func durationFromISO(v string) (time.Duration, error) {

return time.Until(*durt), nil
}

type blindedCredVrfResult struct {
ID string `json:"id"`
Duplicate bool `json:"duplicate"`
}

type tlv1CredPresentation struct {
Token string `json:"token"`
IssuedAt string `json:"issuedAt"`
ExpiresAt string `json:"expiresAt"`
}

0 comments on commit 2464609

Please sign in to comment.