84 lines
3.1 KiB
Go
84 lines
3.1 KiB
Go
package payments
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestVerifiedNotificationRejectsReplayAndTampering(t *testing.T) {
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pubDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
privDER, err := x509.MarshalPKCS8PrivateKey(key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
config := Config{MchID: "mch", AppID: "app", NotifyURL: "https://example.com/notify", MerchantSerial: "serial",
|
|
PrivateKeyPEM: string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER})),
|
|
WechatPublicKeyID: "PUB_KEY_ID_123", WechatPublicKeyPEM: string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})),
|
|
APIv3Key: "12345678901234567890123456789012"}
|
|
provider, err := NewWeChatProvider(config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wp := provider.(*wechatProvider)
|
|
txn := map[string]any{"out_trade_no": "order1", "mchid": "mch", "appid": "app", "transaction_id": "wx1",
|
|
"trade_state": "SUCCESS", "amount": map[string]any{"total": 10000, "currency": "CNY"}, "success_time": time.Now().UTC().Format(time.RFC3339)}
|
|
plain, _ := json.Marshal(txn)
|
|
block, _ := aes.NewCipher([]byte(config.APIv3Key))
|
|
aead, _ := cipher.NewGCM(block)
|
|
nonce := "123456789012"
|
|
ciphertext := aead.Seal(nil, []byte(nonce), plain, []byte("associated"))
|
|
body, _ := json.Marshal(map[string]any{"id": "event1", "event_type": "TRANSACTION.SUCCESS", "resource_type": "encrypt-resource",
|
|
"resource": map[string]any{"algorithm": "AEAD_AES_256_GCM", "ciphertext": base64.StdEncoding.EncodeToString(ciphertext), "associated_data": "associated", "nonce": nonce}})
|
|
makeRequest := func(timestamp int64, tamper bool) *http.Request {
|
|
ts := strconv.FormatInt(timestamp, 10)
|
|
signed := fmt.Sprintf("%s\n%s\n%s\n", ts, "nonce1", body)
|
|
hash := sha256.Sum256([]byte(signed))
|
|
signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, hash[:])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
actualBody := body
|
|
if tamper {
|
|
actualBody = append(bytes.Clone(body), ' ')
|
|
}
|
|
req, _ := http.NewRequest(http.MethodPost, "https://example.com/notify", bytes.NewReader(actualBody))
|
|
req.Header.Set("Wechatpay-Serial", config.WechatPublicKeyID)
|
|
req.Header.Set("Wechatpay-Timestamp", ts)
|
|
req.Header.Set("Wechatpay-Nonce", "nonce1")
|
|
req.Header.Set("Wechatpay-Signature", base64.StdEncoding.EncodeToString(signature))
|
|
return req
|
|
}
|
|
valid, err := wp.ParseNotification(context.Background(), makeRequest(time.Now().Unix(), false))
|
|
if err != nil || valid.AmountFen != 10000 || valid.OrderID != "order1" {
|
|
t.Fatalf("valid notification: %v %+v", err, valid)
|
|
}
|
|
if _, err := wp.ParseNotification(context.Background(), makeRequest(time.Now().Add(-10*time.Minute).Unix(), false)); err == nil {
|
|
t.Fatal("accepted stale timestamp")
|
|
}
|
|
if _, err := wp.ParseNotification(context.Background(), makeRequest(time.Now().Unix(), true)); err == nil {
|
|
t.Fatal("accepted tampered body")
|
|
}
|
|
}
|