Bài này là “wiki thực chiến”: làm nút Amazon Pay chạy được với Golang, hiểu cơ chế ký, lệnh tạo khóa, code mẫu cho V1 và V2, cách dùng AWS KMS (cho V2), tích hợp FE, và checklist debug khi gặp lỗi InvalidSignatureError
.
1) Hiểu đúng “button signature”
Khi hiển thị nút, bạn phải cung cấp 3 thứ cho JS SDK của Amazon:
payloadJSON
: chuỗi JSON mô tả checkout (URL quay lại, storeId, scopes…)signature
: chữ ký của chuỗi theo quy tắc Amazonalgorithm
: phiên bản thuật toán ký
Có hai phiên bản:
Phiên bản | algorithm trên FE |
Cách ký (RSA-PSS) |
---|---|---|
V1 | AMZN-PAY-RSASSA-PSS |
SHA-256 + PSS saltLen=20 |
V2 | AMZN-PAY-RSASSA-PSS-V2 |
SHA-256 + PSS saltLen=32 |
Điểm mấu chốt (theo SDK Amazon Pay, hàm generateButtonSignature
):
signedString = "<ALGORITHM>\n" + hex( sha256( stripcslashes(payloadJSON) ) )
signature = RSASSA-PSS-SHA256( signedString ) // saltLen tùy theo V1/V2
Quan trọng: ký
signedString
, không ký trực tiếppayloadJSON
.
2) Tạo cặp khóa & PublicKeyId
Bạn cần private key PEM để ký, và public key để đăng ký với Amazon nhằm lấy PublicKeyId (ID bạn dùng ở FE).
Lệnh OpenSSL (RSA 2048)
# 1) Private key (PKCS#8, khuyến nghị)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem
# 2) Public key (PEM)
openssl rsa -in private.pem -pubout -out public.pem
Upload public.pem
lên Seller/Integration Central → nhận PublicKeyId (thường có prefix SANDBOX-...
hoặc LIVE-...
).
PublicKeyId là ID, không phải nội dung key; không thể tự “tạo ID” bằng openssl.
3) Xây payloadJSON
ổn định trong Go
Amazon so sánh theo byte. Vì vậy:
- Dùng struct để có JSON ổn định.
- Dùng
json.Encoder
+SetEscapeHTML(false)
để không biến&
thành\u0026
. - Dùng
json.Compact
để loại newline/khoảng trắng dư thừa. - FE phải dùng đúng chuỗi
payloadJSON
backend trả về (không stringify lại).
4) Golang: thư viện nhỏ ký giống hệt generateButtonSignature
(V1 & V2)
4.1. Mã nguồn (thư mục amazonpay/button_signature.go
)
package amazonpay
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"strings"
)
const (
AlgV1 = "AMZN-PAY-RSASSA-PSS" // saltLen = 20
AlgV2 = "AMZN-PAY-RSASSA-PSS-V2" // saltLen = 32
)
// GenerateButtonSignature tái hiện đúng logic PHP generateButtonSignature.
// Trả về: signature(base64), payloadJSON(đã ký), signedString(để debug).
func GenerateButtonSignature(privateKeyPEM []byte, algorithm string, payload any) (sigB64, payloadJSON, signedString string, err error) {
// 1) chọn algorithm
saltLen := 0
switch algorithm {
case AlgV1:
saltLen = 20
case AlgV2:
saltLen = 32
default:
return "", "", "", fmt.Errorf("invalid algorithm: %s", algorithm)
}
// 2) parse private key
priv, err := parseRSAPrivateKey(privateKeyPEM)
if err != nil {
return "", "", "", fmt.Errorf("parse private key: %w", err)
}
// 3) payloadJSON: JSON không escape &, compact
payloadJSON, err = toJSONNoEscape(payload)
if err != nil {
return "", "", "", fmt.Errorf("encode payload: %w", err)
}
// 4) PHP stripcslashes(payload) trước khi hash
normalized := stripcslashesLikePHP(payloadJSON)
// 5) hash payload -> hex lowercase, dựng signedString
h := sha256.Sum256([]byte(normalized))
hashHex := strings.ToLower(hex.EncodeToString(h[:]))
signedString = algorithm + "\n" + hashHex
// 6) sha256(signedString) + RSA-PSS SHA-256 (saltLen tùy phiên bản)
d := sha256.Sum256([]byte(signedString))
sig, err := rsa.SignPSS(rand.Reader, priv, crypto.SHA256, d[:],
&rsa.PSSOptions{SaltLength: saltLen, Hash: crypto.SHA256})
if err != nil {
return "", "", "", fmt.Errorf("rsa.SignPSS: %w", err)
}
return base64.StdEncoding.EncodeToString(sig), payloadJSON, signedString, nil
}
// ----------------- Helpers -----------------
func parseRSAPrivateKey(pemBytes []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("invalid PEM")
}
// PKCS#1
if k1, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
return k1, nil
}
// PKCS#8
any, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err == nil {
if k2, ok := any.(*rsa.PrivateKey); ok {
return k2, nil
}
return nil, errors.New("pkcs8 is not RSA")
}
return nil, errors.New("unsupported private key format")
}
func toJSONNoEscape(v any) (string, error) {
switch t := v.(type) {
case string:
return t, nil
case []byte:
return string(t), nil
default:
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false) // giữ nguyên '&'
if err := enc.Encode(v); err != nil {
return "", err
}
var out bytes.Buffer // compact bỏ newline/space dư
if err := json.Compact(&out, buf.Bytes()); err != nil {
return "", err
}
return out.String(), nil
}
}
// Bản rút gọn tương đương stripcslashes PHP cho các escape JSON hay gặp
func stripcslashesLikePHP(s string) string {
return strings.NewReplacer(`\/`, `/`, `\"`, `"`, `\\`, `\`).Replace(s)
}
4.2. Ví dụ dùng (V1 & V2)
// ví dụ payload typed để JSON ổn định
type WebCheckoutDetails struct {
CheckoutReviewReturnURL string `json:"checkoutReviewReturnUrl"`
CheckoutCancelURL string `json:"checkoutCancelUrl"`
}
type PaymentDetails struct {
AllowOvercharge bool `json:"allowOvercharge"`
}
type CheckoutPayload struct {
WebCheckoutDetails WebCheckoutDetails `json:"webCheckoutDetails"`
StoreID string `json:"storeId"`
Scopes []string `json:"scopes,omitempty"`
PaymentDetails *PaymentDetails `json:"paymentDetails,omitempty"`
}
// V1
pemBytes := mustRead("private.pem")
payload := CheckoutPayload{
WebCheckoutDetails: WebCheckoutDetails{
CheckoutReviewReturnURL: "https://example.com/review",
CheckoutCancelURL: "https://example.com/cancel",
},
StoreID: "amzn1.application-oa2-client.xxxxx",
Scopes: []string{"name","email","billingAddress","phoneNumber"},
PaymentDetails: &PaymentDetails{AllowOvercharge: true},
}
sigV1, payloadJSON, signedV1, err := amazonpay.GenerateButtonSignature(pemBytes, amazonpay.AlgV1, payload)
// V2 (chỉ khác tham số algorithm)
sigV2, payloadJSON2, signedV2, err := amazonpay.GenerateButtonSignature(pemBytes, amazonpay.AlgV2, payload)
Tại FE, đặt
algorithm
đúng với chữ ký (AlgV1
hoặcAlgV2
),publicKeyId
ở cấp root, và gán nguyên xipayloadJSON
từ backend.
5) Tích hợp FE (mẫu tối thiểu)
<script src="https://static-fe.payments-amazon.com/checkout.js"></script>
<script>
(async () => {
const r = await fetch('/api/amazonpay/sign'); // trả về payloadJSON, signature, algorithm, publicKeyId
const { payloadJSON, signature, algorithm, publicKeyId } = await r.json();
amazon.Pay.renderButton('#AmazonPayButton', {
merchantId: 'A2ZXXXXXXX',
publicKeyId, // ví dụ: "SANDBOX-XXXX"
ledgerCurrency: 'JPY',
sandbox: true,
productType: 'PayAndShip',
placement: 'Cart',
createCheckoutSessionConfig: {
payloadJSON, // dùng chính chuỗi từ backend
signature, // chữ ký base64
algorithm // "AMZN-PAY-RSASSA-PSS" hoặc "...-V2"
}
});
})();
</script>
Khi chạy file html trên trình duyệt, bạn sẽ thấy button hiện ra như ảnh:
Khi click vào button, nếu chữ ký đúng, thì sẽ hiện thông tin thanh toán:
6) Dùng AWS KMS (chỉ cho V2)
Nếu muốn ký bằng “thư viện AWS”, hãy dùng KMS với RSASSA_PSS_SHA_256 (tương đương V2). Quy trình:
- Tạo key
SIGN_VERIFY
(RSA_2048) trong KMS, hoặc import khóa vào KMS (nếu cần). - Lấy public key từ KMS và đăng ký với Amazon để lấy PublicKeyId.
- Ở Go: vẫn build
signedString
như mục 4, nhưng chữ ký lấy từkms.Sign
.
Code KMS (đưa signedString
vào, KMS hash hộ – MessageType RAW)
import (
"context"
"encoding/base64"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/aws/aws-sdk-go-v2/service/kms/types"
)
func SignWithKMSV2(ctx context.Context, keyID, signedString string) (string, error) {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil { return "", err }
cli := kms.NewFromConfig(cfg)
out, err := cli.Sign(ctx, &kms.SignInput{
KeyId: &keyID,
Message: []byte(signedString), // đưa signedString THÔ
MessageType: types.MessageTypeRaw, // để KMS tự SHA-256
SigningAlgorithm: types.SigningAlgorithmSpecRsassaPssSha256,
})
if err != nil { return "", err }
return base64.StdEncoding.EncodeToString(out.Signature), nil
}
Với KMS, chỉ dùng V2 (
AlgV2
). V1 yêu cầusaltLen=20
, KMS không cho đặt saltLen tùy ý.
7) Verify cục bộ (nên làm 1 lần)
Khi có public key PEM, có thể xác nhận chữ ký ngay tại backend:
func VerifyButtonSignature(pubPEM []byte, algorithm, payloadJSON, sigB64 string) error {
// parse public
block, _ := pem.Decode(pubPEM)
if block == nil { return errors.New("invalid public PEM") }
any, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil { return err }
pub, ok := any.(*rsa.PublicKey)
if !ok { return errors.New("not RSA public key") }
// stripcslashes + hash
normalized := stripcslashesLikePHP(payloadJSON)
h := sha256.Sum256([]byte(normalized))
hashHex := strings.ToLower(hex.EncodeToString(h[:]))
signedString := algorithm + "\n" + hashHex
// verify PSS
sum := sha256.Sum256([]byte(signedString))
saltLen := 20
if algorithm == AlgV2 { saltLen = 32 }
sig, _ := base64.StdEncoding.DecodeString(sigB64)
return rsa.VerifyPSS(pub, crypto.SHA256, sum[:], sig,
&rsa.PSSOptions{SaltLength: saltLen, Hash: crypto.SHA256})
}
8) Checklist debug InvalidSignatureError
-
So khớp “signedString …”
Amazon thường hiện:… signedString AMZN-PAY-… <HEX>
.
Hãy logsignedString
ở backend (mục 4 đã trả về). Phần<HEX>
phải trùng. -
Chuỗi
payloadJSON
FE phải dùng đúng chuỗi backend trả về. Tuyệt đối không stringify lại hoặc tự gõ tay khác 1 ký tự. -
Algorithm
Ký V1 thì FE setAMZN-PAY-RSASSA-PSS
. Ký V2 thì FE setAMZN-PAY-RSASSA-PSS-V2
. -
publicKeyId
Đặt ở cấp root (không đặt trongcreateCheckoutSessionConfig
), đúng môi trường (SANDBOX-
với sandbox), đúng merchant, và khớp cặp private key. -
URL phải HTTPS và đúng domain đã khai báo (không gây lỗi chữ ký, nhưng sẽ vấp ở bước sau nếu sai).
9) Câu hỏi thường gặp
-
Vì sao chữ ký trước đây luôn sai?
Thường do ký sai đối tượng (ký trực tiếppayloadJSON
), thiếu bướcsignedString
, hoặc JSON bị escape&
thành\u0026
. -
Có thể dùng aws/signer/v4 không?
Không. Đó là HMAC cho dịch vụ AWS, không phải Amazon Pay Button. -
KMS có dùng cho V1 không?
Không. KMS PSS SHA-256 tương đương V2 (saltLen=32). Nếu cần V1, ký local bằng private key PEM.
10) Kết luận
- Hãy coi hàm
GenerateButtonSignature
ở trên là “chuẩn vàng” cho Go: bám sát SDK PHP/Node, hỗ trợ V1 và V2. - Nếu muốn “thuần AWS”, dùng KMS cho V2.
- Khi lỗi, so ngay
signedString
và (nếu có) verify cục bộ để tìm đúng nguyên nhân.
Chúc bạn triển khai trơn tru!