fixed signature check
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -43,10 +45,11 @@ type callbackOperation struct {
|
||||
Currency string `json:"currency"`
|
||||
} `json:"sum_converted"`
|
||||
Provider struct {
|
||||
ID int64 `json:"id"`
|
||||
PaymentID string `json:"payment_id"`
|
||||
Date string `json:"date"`
|
||||
AuthCode string `json:"auth_code"`
|
||||
ID int64 `json:"id"`
|
||||
PaymentID string `json:"payment_id"`
|
||||
AuthCode string `json:"auth_code"`
|
||||
EndpointID int64 `json:"endpoint_id"`
|
||||
Date string `json:"date"`
|
||||
} `json:"provider"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
@@ -56,7 +59,11 @@ type monetixCallback struct {
|
||||
ProjectID int64 `json:"project_id"`
|
||||
Payment callbackPayment `json:"payment"`
|
||||
Account struct {
|
||||
Number string `json:"number"`
|
||||
Number string `json:"number"`
|
||||
Type string `json:"type"`
|
||||
CardHolder string `json:"card_holder"`
|
||||
ExpiryMonth string `json:"expiry_month"`
|
||||
ExpiryYear string `json:"expiry_year"`
|
||||
} `json:"account"`
|
||||
Customer struct {
|
||||
ID string `json:"id"`
|
||||
@@ -120,17 +127,48 @@ func fallbackProviderPaymentID(cb monetixCallback) string {
|
||||
return cb.Payment.ID
|
||||
}
|
||||
|
||||
func verifyCallbackSignature(cb monetixCallback, secret string) error {
|
||||
expected := cb.Signature
|
||||
cb.Signature = ""
|
||||
calculated, err := monetix.SignPayload(cb, secret)
|
||||
func verifyCallbackSignature(payload []byte, secret string) (string, error) {
|
||||
root, err := decodeCallbackPayload(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
if subtleConstantTimeCompare(expected, calculated) {
|
||||
return nil
|
||||
signature, ok := signatureFromPayload(root)
|
||||
if !ok || strings.TrimSpace(signature) == "" {
|
||||
return "", merrors.InvalidArgument("signature is missing")
|
||||
}
|
||||
return merrors.DataConflict("signature mismatch")
|
||||
calculated, err := monetix.SignPayload(root, secret)
|
||||
if err != nil {
|
||||
return signature, err
|
||||
}
|
||||
if subtleConstantTimeCompare(signature, calculated) {
|
||||
return signature, nil
|
||||
}
|
||||
return signature, merrors.DataConflict("signature mismatch")
|
||||
}
|
||||
|
||||
func decodeCallbackPayload(payload []byte) (any, error) {
|
||||
var root any
|
||||
decoder := json.NewDecoder(bytes.NewReader(payload))
|
||||
decoder.UseNumber()
|
||||
if err := decoder.Decode(&root); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func signatureFromPayload(root any) (string, bool) {
|
||||
payload, ok := root.(map[string]any)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
for key, value := range payload {
|
||||
if !strings.EqualFold(key, "signature") {
|
||||
continue
|
||||
}
|
||||
signature, ok := value.(string)
|
||||
return signature, ok
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func subtleConstantTimeCompare(a, b string) bool {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -38,11 +39,11 @@ func TestMapCallbackToState_StatusMapping(t *testing.T) {
|
||||
cfg := monetix.DefaultConfig()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
paymentStatus string
|
||||
name string
|
||||
paymentStatus string
|
||||
operationStatus string
|
||||
code string
|
||||
expectedStatus mntxv1.PayoutStatus
|
||||
code string
|
||||
expectedStatus mntxv1.PayoutStatus
|
||||
expectedOutcome string
|
||||
}{
|
||||
{
|
||||
@@ -119,12 +120,20 @@ func TestVerifyCallbackSignature(t *testing.T) {
|
||||
t.Fatalf("failed to sign payload: %v", err)
|
||||
}
|
||||
cb.Signature = sig
|
||||
if err := verifyCallbackSignature(cb, secret); err != nil {
|
||||
payload, err := json.Marshal(cb)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal callback: %v", err)
|
||||
}
|
||||
if _, err := verifyCallbackSignature(payload, secret); err != nil {
|
||||
t.Fatalf("expected valid signature, got %v", err)
|
||||
}
|
||||
|
||||
cb.Signature = "invalid"
|
||||
if err := verifyCallbackSignature(cb, secret); err == nil {
|
||||
payload, err = json.Marshal(cb)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal callback: %v", err)
|
||||
}
|
||||
if _, err := verifyCallbackSignature(payload, secret); err == nil {
|
||||
t.Fatalf("expected signature mismatch error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package gateway
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -338,18 +339,19 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(cb.Signature) == "" {
|
||||
p.logger.Warn("Monetix callback signature is missing", zap.String("payout_id", cb.Payment.ID))
|
||||
return http.StatusBadRequest, merrors.InvalidArgument("signature is missing")
|
||||
}
|
||||
if err := verifyCallbackSignature(cb, p.config.SecretKey); err != nil {
|
||||
signature, err := verifyCallbackSignature(payload, p.config.SecretKey)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
p.logger.Warn("Monetix callback signature check failed",
|
||||
zap.String("payout_id", cb.Payment.ID),
|
||||
zap.String("signature", cb.Signature),
|
||||
zap.String("signature", signature),
|
||||
zap.String("payload", string(payload)),
|
||||
zap.Error(err),
|
||||
)
|
||||
return http.StatusForbidden, err
|
||||
return status, err
|
||||
}
|
||||
|
||||
state, statusLabel := mapCallbackToState(p.clock, p.config, cb)
|
||||
|
||||
Reference in New Issue
Block a user