Go強化月間らしいので、助かる人がいるか不明ですが、きっと将来の誰かの助けになるはず...🚀!
結論
AmazonPay、GoのSDK開発してくれ...!
すぐできるやろ😇
AmazonPayのComplete Checkout Sessionの電子署名
このサンプルを良い感じに調整して、フロントのボタンのsignatureとか、Update Checkout Sessionのsignatureとか生成できるはずです
電子署名の説明は書かないです。警察からコメントが来るので😗
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/pkg/errors"
)
type AmazonPayGateway struct {
}
const (
AmazonPayAlgorithm = "AMZN-PAY-RSASSA-PSS"
)
type ChargeAmount struct {
Amount int `json:"amount"`
CurrencyCode string `json:"currencyCode"` // JPY
}
type StatusDetails struct {
State string `json:"state"` // Completed
LastUpdatedTimestamp string `json:"lastUpdatedTimestamp"`
}
// パラメータはもっとありますので、適宜フィールドを追加してください
type CompleteCheckoutSessionResponse struct {
CheckoutSessionId string `json:"checkoutSessionId"`
StatusDetails *StatusDetails `json:"statusDetails"`
ChargePermissionId string `json:"chargePermissionId"`
ChargeId string `json:"chargeId"`
CreationTimestamp string `json:"creationTimestamp"`
}
func (g *AmazonPayGateway) CompleteCheckoutSession(sessionID string) error {
ca := &ChargeAmount{
Amount: 10,
CurrencyCode: "JPY",
}
payload := map[string]interface{}{
"chargeAmount": ca,
}
urlPath := fmt.Sprintf("/%s/v2/checkoutSessions/%s/complete", "sandbox", sessionID)
bodyContents, err := g.sendRequest(urlPath, "POST", payload)
if err != nil {
return errors.WithStack(err)
}
var parsedRes *CompleteCheckoutSessionResponse
err = json.Unmarshal(bodyContents, &parsedRes)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (g *AmazonPayGateway) sendRequest(urlPath string, requestMethod string, payload interface{}) ([]byte, error) {
jsonPayload, err := json.Marshal(payload)
if err != nil {
return nil, errors.WithStack(err)
}
r, err := http.NewRequest(
requestMethod,
// ここのドメイン pay-api.amazon.jp と pay-api.amazon.com があって、日本では.jpのドメインになります
// これに気づくのに数時間ハマりました
fmt.Sprintf("https://pay-api.amazon.jp/%s", urlPath),
strings.NewReader(string(jsonPayload)),
)
if err != nil {
return nil, errors.WithStack(err)
}
timestamp := time.Now().UTC().Format("20060102T150405Z")
r.Header.Add("content-type", "application/json")
r.Header.Add("x-amz-pay-date", timestamp)
// httpリクエストにヘッダーの順番という概念はないと思いますが、AmazonPayはヘッダーの追加順番と
// ↓signedHeadersの順番が一致してないとsignatureエラーになるので
// signedHeaders := "x-amz-pay-date;content-type" にするとエラーになります
// これに気づけずに、何週間も溶かしました
signedHeaders := "content-type;x-amz-pay-date"
signature, err := g.createDigitalSignForRequest(r.Header, jsonPayload, requestMethod, urlPath, signedHeaders)
if err != nil {
return nil, errors.WithStack(err)
}
authorization := fmt.Sprintf("%s PublicKeyId=%s, SignedHeaders=%s, Signature=%s", AmazonPayAlgorithm, "ここにpublickKeyID", signedHeaders, signature)
r.Header.Add("Authorization", authorization)
client := &http.Client{}
res, err := client.Do(r)
if err != nil {
return nil, errors.WithStack(err)
}
bodyContents, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, errors.WithStack(err)
}
return bodyContents, nil
}
func (g *AmazonPayGateway) createDigitalSignForRequest(header http.Header, jsonPayload []byte, requestMethod, urlPath, signedHeaders string) (string, error) {
var headers []string
for key, values := range header {
headers = append(headers, fmt.Sprintf("%s:%s", strings.ToLower(key), values[0]))
}
sort.Strings(headers) // http.Headerからheaderをrangeで取ろうとすると順不同で取ってしまい署名に影響が出てしまうので、ここでソートしている
hashedPayload := sha256.Sum256(jsonPayload)
canonicalRequest := fmt.Sprintf("%s\n%s\n\n%s\n%s\n%x", requestMethod, urlPath, headers, signedHeaders, hashedPayload)
privateKey, err := ReadRsaPrivateKey("ここにprivateKeyをbase64でエンコードしたstringを入れる")
if err != nil {
return "", errors.WithStack(err)
}
canonicalRequest = fmt.Sprintf("%s\n%x", AmazonPayAlgorithm, sha256.Sum256([]byte(canonicalRequest)))
hashedCanonicalRequest := sha256.Sum256([]byte(canonicalRequest))
pssOptions := &rsa.PSSOptions{
SaltLength: 20, // ここは20じゃないとダメなので、constにしましょう
Hash: crypto.SHA256, // これはなくても動くかも(試してない)
}
signature, err := rsa.SignPSS(rand.Reader, privateKey, crypto.SHA256, hashedCanonicalRequest[:], pssOptions)
if err != nil {
return "", errors.WithStack(err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}
func ReadRsaPrivateKey(privateKeyBase64 string) (*rsa.PrivateKey, error) {
// pemファイルをgithubにpushするわけにいかないので、pemファイルをbase64にエンコードして、envファイルに保存しています
data, err := base64.StdEncoding.DecodeString(privateKeyBase64)
if err != nil {
return nil, errors.New("invalid private key base64")
}
// ↓参考にしました https://increment.hatenablog.com/entry/2017/08/25/223915
// errorを握り潰してるわけではなく、デコードに失敗した場合、privateKeyBlockがnilになって、第二引数にデータをそのまま返却してるので第二引数必要なし
privateKeyBlock, _ := pem.Decode(data)
if privateKeyBlock == nil {
return nil, errors.Errorf("invalid private key data")
}
var privateKey *rsa.PrivateKey
if privateKeyBlock.Type == "PRIVATE KEY" {
keyInterface, err := x509.ParsePKCS8PrivateKey(privateKeyBlock.Bytes)
if err != nil {
return nil, err
}
var ok bool
privateKey, ok = keyInterface.(*rsa.PrivateKey)
if !ok {
return nil, errors.Errorf("invalid RSA private key")
}
} else if privateKeyBlock.Type == "RSA PRIVATE KEY" {
privateKey, err = x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes)
if err != nil {
return nil, err
}
} else {
return nil, errors.Errorf("invalid RSA private key type")
}
privateKey.Precompute()
if err := privateKey.Validate(); err != nil {
return nil, err
}
return privateKey, nil
}
細かい説明しないので、読んで頑張ってほしい!
AmazonPayの 良くないところ ハマりどころ
- 継続支払いと都度支払いのドキュメントがそっくりすぎる割に、微妙にパラメータが違ったりする
-
送料なし決済についてのドキュメント(APB支払い)が端っこにあって気づかない
→ AmazonPayのドキュメントで一番上にあるAPIのインテグレーションガイドは主に、送料があるサービス向けの説明で、我々のようなデジタルサービスの開発ではAPB支払い
という方法が推奨されています!!!!!!!(って一番上に書いておけ!!!!!) -
.com
のドメインと.jp
のドメインの説明が端っこに書いてあって気付きにくい
他にもハマりどころありそうですが、一旦ね、一旦これだけ