Carpe Diem

備忘録

GoでGooglePlayの課金レシートの署名検証

概要

GooglePlayでは課金処理の実行時に署名を発行してくれるため、そのレシートが正規のレシートかどうかを検証する事が可能です。
今回はGoでその検証ロジックを実装してみます。

環境

必要な情報

以下の3つが必要になります。

1. アプリの公開鍵

以下の様なbase64文字列です。

Vm1wS01GWXlSWGhYV0d4WFlteEtWMWxVU2xOVlZsbDNXa1pPYW1KR2NIaFZWelZyWWtkS1NHVkdhRmhoTVZVeFYxWmtTMVpzV25GV2JHUnBWMFZLV0ZaVldrWlBWa0pWVFVRd1BWWnFTakJXTWtWNFYxaHNWMkpzU2xkWlZFcFRWVlpaZDFwR1RtcGlSbkI0VlZjMWEySkhTa2hsUm1oWVlURlZNVmRXWkV0V2JGcHhWbXhrYVZkRlNsaFdWVnBHVDFaQ1ZVMUVNRDFXYWtvd1ZqSkZlRmRZYkZkaWJFcFhXVlJLVTFWV1dYZGFSazVxWWtad2VGVlhOV3RpUjBwSVpVWm9XR0V4VlRGWFZtUkxWbXhhY1Zac1pHbFhSVXBZVmxWYVJrOVdRbFZOUkRBOVBRPT0=

※値はダミーです

2. 購入時のreceipt

以下の様なJSONです。クライアントから送ってもらいます。

{
  "orderId": "GPA.xxxx-xxxx-xxxx-xxxxx",
  "packageName": "com.example",
  "productId": "premium",
  "purchaseTime": 1459390336508,
  "purchaseState": 0,
  "developerPayload": "your_payload",
  "purchaseToken": "xxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxx",
  "autoRenewing": true
}

※各値はダミーです

3. 購入時のsignature

以下の様なbase64文字列です。クライアントから送ってもらいます。

Vm0xd1MwMUdXWGxTV0doWVYwZDRXRmx0ZUV0V01XeFZVMnhPVmxac2JETlhhMXBQWVcxS1IyTklhRlpXZWxaeVdXdGtTMU5IVmtkaFJtaG9UVlpWZUZZeFdtdFRNVnB6VjI1R1YySkhVbkJXTUZaTFYwWmFWbGRyV2xCV2EwcFdWRlZSZDFCV1duRlRha0pYVFd0V05GWXhhSE5XTWtwelUyeGtXbFpGY0ZSV1ZscGFaREZ3UjFSdGNHbFNia0kwVmxaak1XRXlTa2hUYTJoc1VtMW9XVmxVUmxaTlZtUlhXa1YwVjJKR2NIaFdiWGhyWVZaa1JsTnNhRmRXVm5CSFZERmFRMVpWTVVWTlJERlhZV3R2ZDFacVNrWmxSbVJaWWtaa2FXSkZjRmhYVmxKTFZURldWMWRZWkdGU2F6VnhXV3RhZDJWR1ZsaE9WM1JwVWpCd1NWcFZXbTlYUjBWNFZsUkdXRlp0VWt4V2JYaGhZMVphYzFwSGJGaFNWWEJaVm14V1lWSnJPVmRSYkZaT1VrUkJPVkJSUFQwPQ==

※値はダミーです

公開鍵、signatureは元々base64で扱われています。receipt情報は普通のJSONですが、扱いやすさのためにbase64エンコードしたものとして扱います。

公開鍵の取得

GooglePlayDeveloperConsoleにログインし、左メニューのすべてのアプリから自分のアプリをクリックし、以下のページを開きます。

f:id:quoll00:20160331142102p:plain

検証ロジック

頑張って実装すると以下のようにします。

package main

import (
    "crypto"
    "crypto/rsa"
    "crypto/sha1"
    "crypto/x509"
    "encoding/base64"
    "fmt"
    "log"
)

const (
    // you can get this public key from GooglePlayDeveloperConsole.
    base64EncodedPublicKey = "--- your app's public key ---"
)

func main() {
    receipt := "--- receipt data encoded by base64 ---"
    signature := "--- signature ---"

    isValid, err := verify(receipt, signature)
    if err != nil {
        log.Println(err)
    }
    log.Println("valid: ", isValid)
}

func verify(receipt string, signature string) (bool, error) {
    // prepare public key
    decodedPublicKey, err := base64.StdEncoding.DecodeString(base64EncodedPublicKey)
    if err != nil {
        return false, fmt.Errorf("failed to decode public key")
    }
    publicKeyInterface, err := x509.ParsePKIXPublicKey(decodedPublicKey)
    if err != nil {
        return false, fmt.Errorf("failed to parse public key")
    }
    publicKey, _ := publicKeyInterface.(*rsa.PublicKey)

    // decode signature
    decodedSignature, err := base64.StdEncoding.DecodeString(signature)
    if err != nil {
        return false, fmt.Errorf("failed to decode signature")
    }

    // generate hash value from receipt
    decodedReceipt, err := base64.StdEncoding.DecodeString(receipt)
    if err != nil {
        return false, fmt.Errorf("failed to decode receipt")
    }
    hasher := sha1.New()
    hasher.Write(decodedReceipt)
    hashedReceipt := hasher.Sum(nil)

    // verify
    err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA1, hashedReceipt, decodedSignature)
    if err != nil {
        return false, nil
    }

    return true, nil
}

動作確認

レシートと署名が正しいと以下のようにtrueが返ります。

$ go run verify.go
2016/04/01 09:02:01 valid:  true

ライブラリでもっと楽に検証

go-iapというライブラリがあるのでそれを使うともっとコードが短くなります。

github.com

import (
    "encoding/base64"
    "log"

    "github.com/dogenzaka/go-iap/playstore"
)

const (
    base64EncodedPublicKey = "--- your app's public key ---"
)

func main() {
    receipt := "--- receipt data encoded by base64 ---"
    signature := "--- signature ---"
    d, _ := base64.StdEncoding.DecodeString(receipt)

    isValid, err := playstore.VerifySignature(base64EncodedPublicKey, d, signature)
    if err != nil {
        log.Println("Failed to verify signature")
    }

    log.Println("valid: ", isValid)
}

その他注意

今回receiptはbase64文字列で受け取って処理しています。
もしレシートをstructで定義して受け取る場合はフィールドの順番に気をつけましょう。フィールド順が元のレシートと異なるとハッシュ化で違う値になるためです。
以下が正しい順番です。ただしGoogle側がフィールドを変更する可能性もあるので、Goでは定義せずまるっとJSONごと受け取って検証するほうが安全です。

type Receipt struct {
    OrderID          string `json:"orderId"`
    PackageName      string `json:"packageName"`
    ProductID        string `json:"productId"`
    PurchaseTime     int64  `json:"purchaseTime"`
    PurchaseState    int    `json:"purchaseState"`
    DeveloperPayload string `json:"developerPayload"`
    PurchaseToken    string `json:"purchaseToken"`
    AutoRenewing     bool   `json:"autoRenewing"`
}

ソース