fixed signature check

This commit is contained in:
Stephan D
2025-12-30 00:22:49 +01:00
parent a6374d1136
commit 6a2efd3d22
4 changed files with 138 additions and 26 deletions

View File

@@ -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 {

View File

@@ -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")
}
}

View File

@@ -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)

View File

@@ -146,3 +146,66 @@ func TestSignPayload_EthEstimateGasExample(t *testing.T) {
t.Fatalf("unexpected signature\nexpected: %s\ngot: %s", expected, got)
}
}
func TestSignPayload_MonetixCallbackExample(t *testing.T) {
payload := map[string]any{
"customer": map[string]any{
"id": "694ece88df756c2672dc6ce8",
},
"account": map[string]any{
"number": "220070******0161",
"type": "mir",
"card_holder": "STEPHAN",
"expiry_month": "03",
"expiry_year": "2030",
},
"project_id": 157432,
"payment": map[string]any{
"id": "6952d0b307d2916aba87d4e8",
"type": "payout",
"status": "success",
"date": "2025-12-29T19:04:24+0000",
"method": "card",
"sum": map[string]any{
"amount": 10849,
"currency": "RUB",
},
"description": "",
},
"operation": map[string]any{
"sum_initial": map[string]any{
"amount": 10849,
"currency": "RUB",
},
"sum_converted": map[string]any{
"amount": 10849,
"currency": "RUB",
},
"code": "0",
"message": "Success",
"provider": map[string]any{
"id": 26226,
"payment_id": "a3761838-eabc-4c65-aa36-c854c47a226b",
"auth_code": "",
"endpoint_id": 26226,
"date": "2025-12-29T19:04:23+0000",
},
"id": int64(5089807000008124),
"type": "payout",
"status": "success",
"date": "2025-12-29T19:04:24+0000",
"created_date": "2025-12-29T19:04:21+0000",
"request_id": "7c3032f00629c94ad78e399c87da936f1cdc30de-2559ba11d6958d558a9f8ab8c20474d33061c654-05089808",
},
"signature": "IBgtwCoxhMUxF15q8DLc7orYOIJomeiaNpWs8JHHsdDYPKJsIKn4T+kYavPnKTO+yibhCLNKeL+hk2oWg9wPCQ==",
}
got, err := SignPayload(payload, "1")
if err != nil {
t.Fatalf("failed to sign payload: %v", err)
}
expected := "IBgtwCoxhMUxF15q8DLc7orYOIJomeiaNpWs8JHHsdDYPKJsIKn4T+kYavPnKTO+yibhCLNKeL+hk2oWg9wPCQ=="
if got != expected {
t.Fatalf("unexpected signature\nexpected: %s\ngot: %s", expected, got)
}
}